Follow-up to #3108 (merged 2026-10-08), from the #3106 decision.
Problem
snapshot --observe-only stamps observation (an app observation made without activating the app) on a payload that may not come from the app.
RunnerTests+SnapshotExecution.swift runs the ordinary capture first (snapshotFast / snapshotRaw), then checks only that target.app.state == .runningForeground and that the process identifier is unchanged, and then sets payload.observation. But snapshotFast (RunnerTests+Snapshot.swift) first calls boundedBlockingSystemAlertSnapshot(...) and returns the SpringBoard alert tree in place of the app's tree when one is presented. The post-capture check can still pass, because a system alert doesn't change the app's process and #2696 shows XCUIApplication.state can report foreground while another surface is in front. The result is a system-alert tree labelled as an observe-only capture of the app.
The default (non-observe-only) path is fine: it discloses the substitution through systemSurface provenance. The defect is only that the observe-only contract asserts app provenance without checking which tier produced the payload.
Required behavior
Pick one, and encode it in the observe-only branch:
- Refuse: when the capture tier substituted a system-alert or system-surface payload, return the typed observe-only-unavailable response (the existing
observeOnlyUnavailableResponse()), or
- Disclose: keep the payload but omit
observation and carry the substitution provenance, so a caller can't read the payload as an app observation.
Key the decision on the tier or provenance the capture returns, not on payload contents or message text.
Done when
- A runner unit test using
systemModalProbeOverrideForTesting shows an observe-only snapshot with a presented system alert never carries observation alongside the alert tree.
- The interaction or observation guarantee wording for observe-only states the chosen behavior.
Follow-up to #3108 (merged 2026-10-08), from the #3106 decision.
Problem
snapshot --observe-onlystampsobservation(an app observation made without activating the app) on a payload that may not come from the app.RunnerTests+SnapshotExecution.swiftruns the ordinary capture first (snapshotFast/snapshotRaw), then checks only thattarget.app.state == .runningForegroundand that the process identifier is unchanged, and then setspayload.observation. ButsnapshotFast(RunnerTests+Snapshot.swift) first callsboundedBlockingSystemAlertSnapshot(...)and returns the SpringBoard alert tree in place of the app's tree when one is presented. The post-capture check can still pass, because a system alert doesn't change the app's process and #2696 showsXCUIApplication.statecan report foreground while another surface is in front. The result is a system-alert tree labelled as an observe-only capture of the app.The default (non-observe-only) path is fine: it discloses the substitution through
systemSurfaceprovenance. The defect is only that the observe-only contract asserts app provenance without checking which tier produced the payload.Required behavior
Pick one, and encode it in the observe-only branch:
observeOnlyUnavailableResponse()), orobservationand carry the substitution provenance, so a caller can't read the payload as an app observation.Key the decision on the tier or provenance the capture returns, not on payload contents or message text.
Done when
systemModalProbeOverrideForTestingshows an observe-only snapshot with a presented system alert never carriesobservationalongside the alert tree.