refactor(ios): typed back-fallback sample, app-only, one tap route

Merged
TH
2h ago
+129-228

Summary

Deletes the shape churn in the in-app back fallback, apple/runner/.../RunnerTests+Navigation.swift (2 files, net −99 lines).

captureNavigationFallbackVisualState rebuilt the typed Result<CapturedAppScreen, RunnerAppScreenCaptureFailure> into a three-optional NavigationVisualSample and back into a typed error. The loose carrier made an unreachable "no data and no reason" state constructible, so the payload needed a default code and a test for it. The before/after pair now travels as Result<Data, RunnerAppScreenCaptureFailure>, and an uncomparable pair carries the typed refusal in NavigationVisualObservation.unobserved(refusal:) — later refusal wins, same precedence.

Deleted with it: NavigationVisualSample, the navigationFallbackSample seam whose systemSurface closure was only ever discarded (_ = systemSurface), the default-code branch, the standalone payload mapper, and one of the two copies of "synthesized tap, else XCTest tap" (dispatchNavigationTap). App-only sampling stays enforced by the boundary: captureNavigationFallbackVisualState takes only the target app and samples it — no system-surface seam exists. Tests consolidate to one observation table and one unknown-outcome test plus the kept encoding-mapping test; the duplicated fixture launch folds into launchBackFixture; the showsAppsOnlyFullScreen alias goes.

Kept: app-only sampling (never SpringBoard), later-refusal-wins, the measured topLeadingNavigationFallbackPoint, the unknown-outcome semantics (#3894/#2728), and the AGENT_DEVICE_RUNNER_IN_APP_BACK_VISUAL_VERIFICATION log fields. Non-iOS #else returns .failure(.unresolvedScreen), the same code the old default produced.

No wire, CLI, ADR, or public-type changes.

Validation

Head 0cf6e7e30a (base 581357bc72); git diff --stat origin/main...HEAD: 2 files, +129/−228.

  • Runner unit-test build (AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1), navigation suite (19 tests): iPhone 18 Pro Max 19/19, iPad Pro 11-inch (M5) 19/19.
  • Mutants, each in a disposable checkout, baseline 19/19 green: precedence swap (earlier refusal wins) → testNavigationVisualObservationSeparatesChangeFromRefusal fails (1 failed, 18 passed); .unobserved → .unavailable laundering → testNavigationFallbackReportsARefusalAsAnUnknownOutcome fails (1 failed, 18 passed); encoding failure loses its reason → mapping test fails; helper loses .performed → 3 live-route tests fail. An earlier iteration also mutated in SpringBoard sampling via selectObservedScreenCapture (recorded at 49f6d311): only the then-present source-scan pin caught it; review removed that pin, so app-only sampling is held by the boundary (the capture helper takes only the target app), not by a test.
  • pnpm check:xctest-selection: 0 methods unreachable, nightly-iPad selection intact.
  • pnpm format, then pnpm check:affected --run --head 0cf6e7e30a: all runnable checks passed, exactCommit=true.
  • CI obligations: XCTest Nightly at this head — run 40401996410 — both legs green: iPhone full suite 498/498, iPad sole-screen leg 4/4 including the four testInAppBack* route tests. Evidence comment on the PR.

Size delta (Size report at this head): −10.5 kB unpacked, −2.5 kB download.

🤖 Generated with Claude Code