refactor(ios): typed back-fallback sample, app-only, one tap route
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) →
testNavigationVisualObservationSeparatesChangeFromRefusalfails (1 failed, 18 passed);.unobserved → .unavailablelaundering →testNavigationFallbackReportsARefusalAsAnUnknownOutcomefails (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 viaselectObservedScreenCapture(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, thenpnpm 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