fix(ios): report capture refusals as typed reasons in back-fallback and recording - #2766
Conversation
Live iPhone Duo verification handoff — #2728 capture failure contracts
Toolchain: Xcode 27.1 beta + booted iPhone Duo (iOS 27.1). Build with unit tests: 0. Runner unit tests — these own the two failure contracts (automatic)The app-only decision for the
1.
|
Size Report
Startup median (7 runs, lower is better):
|
cd758bd to
7b5b504
Compare
|
At 7b5b504, the typed refusals aren't pinned to the code paths that produce them. There's no live run at this head. The handoff comment in Is there a smaller shape here? Net production is around 235 lines, well under the usual size line, but it spans the runner and the TS contract doc and adds three new types. Not blocking: the SpringBoard fallback in the ADR ( I didn't build or run the Swift runner — the claim that the current tests can't catch these mutations comes from reading the test bodies, not from executing one. I also didn't measure what CI is green across all 18 checks, but the Swift runner's device lanes don't exercise the live back or record-start refusal routes, so that green doesn't cover the behavior this PR changes. Pin both fixes with tests that go through |
Duo-lane live verification — iPhone Duo, iOS 27.1Ran this PR's runner (built from Proven live
Two live checks from the handoff's recipe do NOT reach their expected codes on an iOS simulator
Net: on an iOS simulator the only failure-contract evidence available here is the unchanged no-change contract (above) plus the runner unit tests ( Also (not this PR)The iOS 27.1 beta's CoreDevice VerdictCI is green and the branch is |
|
Thanks for the Duo run at 7b5b504. It confirms the unchanged path: It also confirms that the two changed routes can't be reached live on a simulator: the activation guard relaunches the app before The next step is the one from the earlier comment. Can you drive |
|
Thanks for the updated handoff. I checked the pushed head 7b5b504, and it does not contain The earlier finding stays open until those tests are on the branch. Once they are pushed, I will check that each one fails when its route is reverted, as the handoff describes. |
|
At 16b39d2, the fix still has a gap: the regression test does not exercise the production sampler, so it cannot catch a revert of the fix. This is a follow-up on the earlier review (7b5b504, #2766 (comment)); the typed-reason plumbing for capture refusals is in place, but the test-coverage finding from that review still applies. RunnerTests+Navigation.swift:395 builds Not blocking: the Does this need the new Smoke Tests fails in Before merge, the Navigation.swift:395 test needs to go through the production sampler so that reverting the fallback to SpringBoard sampling turns it red, and then Smoke Tests should be re-run to confirm the capture-plan timing failure is a flake and not tied to this change. |
…nd recording #2741 routed screenshots through the resolved app display but left two failure contracts papered over (#2728): - The in-app `back` coordinate fallback read a nil screenshot sample as "no visual change" and sampled SpringBoard when the app resolved no window, so a two-home-screen comparison reported "in-app back control is not available" after a real leading tap had already run. The check now observes only the app's own resolved window and reports the display refusal as an unverified outcome; an observable no-change still reads as unavailable. - `record start` collapsed every no-frame bootstrap into an untyped NSError. It now surfaces the shared APP_SCREEN_* reason, so a runtime with no resolvable window fails closed with a typed code. Refuse a zero-pixel capture at the capture type so a required consumer cannot mistake it for a usable frame. macOS/tvOS keep their host-display capture and generic bootstrap error. Focused runner unit tests pin the observation split, the refusal-selection, and the typed-vs-generic error mapping.
…duces them - testRecordStartThrowsTheCaptureRefusalItReceived drives ScreenRecorder.start with an always-refusing capture and maps the actual thrown error through recordingStartErrorPayload, so the bootstrap forwarding is covered (iOS PR lane). - navigationFallbackSample(resolvingApp:systemSurface:encoding:) now holds the back fallback's app-only decision; the test hands it a system surface that fails if consulted, so a revert to SpringBoard sampling turns it red. - navigationVisualSample + inAppBackOutcome keep the refusal->code mapping and the unknown-outcome decision testable on the macOS host lane.
16b39d2 to
e5ad866
Compare
|
Addressed in Blocking — the test now goes through the production sampler. Dropped Recording test now maps the actual thrown error through Smoke Tests ( |
|
This looks ready for review at e5ad866. The earlier findings on 16b39d2 (#2766 (comment)) are addressed: the regression test now calls the production Not blocking: I read the code and did not run the Swift tests. All checks were still queued or running with no failures; the iOS Smoke Tests and runner unit lanes exercise the changed Swift, so they need to finish green on e5ad866. |
|

Summary
iOS
backfallback andrecord startnow report a capture that did not happen as a typed reason instead of a false verdict, finishing the failure contracts #2741 left papered over (#2728).backcoordinate fallback: anilscreenshot sample was read as "no visual change", and when the app resolved no window it sampled SpringBoard, so two identical home-screen captures reportedin-app back control is not availableafter a real leading tap had already run. It now observes only the app's own resolved window and returns the display refusal as an unverified outcome carryingAPP_SCREEN_*. An observable no-change is unchanged.record start: every no-frame bootstrap collapsed to an untypedNSError; it now throws the sharedAPP_SCREEN_*reason, so a runtime with no resolvable window fails closed with a typed code.macOS/tvOS keep their host-display capture and generic bootstrap error. 6 Swift files, one host doc comment, one ADR paragraph. Builds on #2741; addresses #2728 (parent #2725).
Validation
Commit
7b5b5043, rebased onmain(0.21.12).pnpm check:affected --run: all runnable checks passed (Swift runner builds and replay lanes are GitHub/device-lane only).build:xcuitest:ios→** TEST BUILD SUCCEEDED **;check:packaged-runner-swiftparses + line parity;xctest-selection276 declared, 0 unreachable;typecheck,lint,format,check:layering,check:fallowclean.NSErrorand the mapping test fails.Outstanding: live iPhone Duo re-verification of these paths (this host has no iPhone Duo / Xcode 27.1). A device handoff runbook is provided for the Duo lane before merge-ready.