Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

refactor(ios-runner): acquire first, present after — one geometry normalization pass instead of per-walker threading

Closed
#2,661 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
28/100
Issue type
Refactor
Clarity
Clearly specified
Activity status
Active
Tech stack
ios, swift

Research direction

Start with captureWithBackend, the recursive acquisition code in apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift, and SnapshotCoordinateSpace.swift. Run the deep-tree benchmark and existing rotated-output tests before changing the acquisition flow. Done means one post-acquisition normalized pass, no geometry threading in walkers, updated ADR 0004, passing package and runner suites, and benchmark results within the stated limit.

Written by the indexing model from the issue text.

Description

ready-for-agent refactor

Why

In the XCTest runner, acquisition and presentation are interleaved in the recursive tier, so every geometric fact has to be threaded through every walker by hand.

Evidence from PR #2653 (fixes #2612), which added one fact, "which coordinate space is this node in":

  • It had to touch three walkers (recursiveTreeSnapshotAcquisition, rawTreeSnapshotAcquisition, the private-AX appendPrivateAXNode) plus the collapsed-tab side path (collapsedTabFallbackNodes / collapsedTabCandidateNode) in apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift and RunnerTests+PrivateAXPresentation.swift, adding geometrySpace / parentIsWindow parameters to each frame of each walk.
  • The query-sweep tier (querySweepSnapshotAcquisition) got nothing, because it has no window ancestry inside its walk, so that tier still publishes unnormalized geometry (ADR 0004, "What stays unnormalized").
  • The collapsed-tab expansion needed a special guard (guard geometrySpace == .appOrientation else { return [] }) because it reads live element frames that are already in the app's space.

The reason a post-acquisition pass could not do this in one place is at RunnerTests+Snapshot.swift around line 244: the recursive walk calls SnapshotPresentation.regularTraversalTransition(for: node, …) mid-walk, consulting the visibility fold to decide the presented depth and whether to descend. The fold reads node geometry, so geometry must be final before the fold sees it, so it must be computed inside the walk. The private-AX tier does not have this coupling: it acquires the whole raw tree and presents afterwards through SnapshotPresentation.present(acquisition, options:) (RunnerTests+SnapshotCapturePlan.swift, captureWithBackend).

Wider counts on main (same files): 9 production RawAXNode( construction sites across 5 files; 5 call sites reading RunnerSynthesizedGesture.interfaceOrientation(forApplication:); 13 sites reading the app viewport (app.frame, safeSnapshotViewport, app.windows.firstMatch.frame). The 2026-08-31 presenter-convergence review already flagged "presented-depth frontier coupling (acquisition consults fold decisions mid-traversal) must be redesigned before any runner demotion".

Task

Make the recursive tier acquire first and present after, like the private-AX tier already does, so that geometry normalization becomes one pass over the flat [RawAXNode] array.

  1. Establish what the mid-walk fold buys. The recursive tier walks XCUIElementSnapshot.children, which XCTest materialized in one bulk snapshot; the frontier does not save AX round trips, it saves walk time and node construction on subtrees the fold would clip. Measure on the deep-tree benchmark (scripts/ bench harness used for #2424 / the deep-tree work; see docs/ for the recipe) and on the test app: raw node count walked with and without the frontier, and wall time. Report the numbers before changing anything. If the frontier is load-bearing for a known hostile screen (Bluesky feed, #1105/#1156), keep a bound that does not consult presentation: a raw depth / node cap is acceptable, a fold decision is not.
  2. Acquire raw, then normalize, then present. Walkers produce RawAXNode with reported frames only. Add SnapshotGeometrySpace.normalized(nodes:viewport:interfaceOrientation:) -> [RawAXNode] next to the existing rule in apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift: walk the array once, derive each node's space from type, parentIndex and rect with the existing space(reportedBySurfaceHost:…) rule, rewrite rect through orientedFrame(of:), and recompute hittable with SnapshotGeometry.isGeometricallyActionable. Run it once in captureWithBackend between acquisition and SnapshotPresentation.present. Delete the geometrySpace / parentIsWindow threading from every walker and the collapsed-tab guard (collapsed tab containers sit under the app's own window, so a post-pass keyed on ancestry never touches them).
  3. Bring the query-sweep tier under the same pass. Its flat nodes hang off the application root; with no window ancestry they declare no space and the pass leaves them alone, which is the current behaviour, but now for a structural reason rather than an omission. Say so in ADR 0004.
  4. Keep the disclosure. unplacedSurfaceHostCount (added after #2653) already runs post-acquisition; it should now run after normalization and be zero whenever the orientation was readable.

Acceptance criteria

  • SnapshotGeometrySpace is referenced from exactly one production site in apple/runner/** (the normalization call in captureWithBackend); no walker takes a geometrySpace or parentIsWindow parameter.
  • Every existing runner unit test that asserts rotated output still passes unchanged: testPrivateAXAcquisitionPublishesATurnedSurfaceHostInAppOrientationSpace, the RunnerTests+Snapshot.swift tests added in #2653, testStampedPayloadDisclosesUnplacedSurfaceHosts. The package suite (swift test --package-path apple/snapshot-presentation) gains a test for normalized(…) that replays the rotationCases of contracts/fixtures/window-coordinate-space.json through a two-window tree.
  • contracts/fixtures/snapshot-presentation-conformance.json, ios-snapshot-backend-conformance.json and ios-ax-recovery-conformance.json are unchanged and green in both languages.
  • Live: snapshot -i --json on the test app in portrait is byte-identical to main on the runner path (confirm the producer is apple-runner, not the bridge: the bridge fingerprint is the "does not provide hittability evidence" warning). In landscape with the keyboard up, the band and key rects match the #2653 numbers ((75,198,724,202) band, key q at (77,203,72,45)).
  • Benchmark: capture p50 and p95 on the deep-tree harness and on the Bluesky feed not regressed by more than 5 % against main, with the numbers in the PR. If the frontier had to be replaced by a raw cap, the cap and the screens it was calibrated on are named in ADR 0004.
  • pnpm check:affected --run green; runner units green under AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1; pnpm test:ios-snapshot-differential green.

Non-goals

Runner demotion or moving presentation to the host: that is a separate, measurement-gated decision (ADR 0004 host-side ownership boundary; two presenters was the measured optimum on 2026-08-31). Touching the Android or bridge producers.

Related: #2612, #2653, #1797 (acquire/present split), #2188 / #2199 (iOS snapshot convergence), ADR 0004.

Dominant language
TypeScript
Stars
4.8k
Forks
315
Avg merge
11h 17m
Merged PRs (30d)
536

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from callstack/agent-device

All issues in callstack/agent-device

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.