Skip to content

fix(tvos): skip SpringBoard system-modal probe on tvOS (#1351)#1353

Merged
thymikee merged 1 commit into
mainfrom
worktree-tvos-springboard-probe-1351
Jul 21, 2026
Merged

fix(tvos): skip SpringBoard system-modal probe on tvOS (#1351)#1353
thymikee merged 1 commit into
mainfrom
worktree-tvos-springboard-probe-1351

Conversation

@thymikee

@thymikee thymikee commented Jul 20, 2026

Copy link
Copy Markdown
Member

Fixes #1351

Problem

On tvOS, every snapshot and alert resolution failed. The runner probes com.apple.springboard for blocking system modals on each snapshot/alert resolution, but tvOS has no SpringBoard (it uses PineBoard/HeadBoard). The query raises an XCTest failure — Failed to resolve query: Application com.apple.springboard is not running — that safely() cannot trap (it only catches Obj-C exceptions), so the whole runner test dies. The CLI then misreported this as the screen "overwhelming the iOS accessibility capture".

Fix

The root problem is that resolveBlockingSystemModal — the single chokepoint every snapshot/alert path funnels through — implicitly assumed a SpringBoard host always exists. Rather than special-casing tvOS at each call site (the issue suggested flipping #if os(macOS) in two files), I made that assumption explicit and let the resolution fall out of it:

// SpringBoard hosts blocking system modals on iOS/visionOS; tvOS (PineBoard/HeadBoard)
// and macOS have no such host, so there is nothing to probe there.
static let hasSpringBoardSystemModalHost: Bool = {  }()

func resolveBlockingSystemModal(deadline: Date) -> BlockingSystemModalResolution {
  guard Self.hasSpringBoardSystemModalHost else { return .absent }
   existing SpringBoard probe 
}

Guarding the one chokepoint covers all three modal-resolution sites and is future-proof against a new caller re-introducing the probe. Returning .absent (rather than short-circuiting the callers) is what preserves app-owned alert queries: resolveAlert(app:) sees .absent, falls through, and still queries the foreground app's own alerts.

The fourth SpringBoard probe site, shouldRouteToSpringboardBlockingSystemModal, is already #if os(iOS) and needs no change.

Out of scope (per triage)

  • PineBoard/HeadBoard system-modal support on tvOS — a separate enhancement; today the probe is skipped, not re-hosted.
  • Error-classification follow-up — the misleading "overwhelming accessibility capture" message. It no longer appears here because the runner no longer terminates; if it surfaces for other reasons it should be a focused follow-up.

Regression tests

Added two tvOS-gated unit tests (RunnerTests+BlockingSystemModalResolution.swift) covering the snapshot and alert-resolution paths. They are revert-sensitive: with the guard removed, both reproduce the exact issue signature on a tvOS build.

Test Case '…testBlockingSystemAlertSnapshotIsNilOnTvOS' passed (0.019 seconds).
Test Case '…testResolveBlockingSystemModalIsAbsentWithoutSpringBoardOnTvOS' passed (0.008 seconds).

Revert-sensitivity check (guard temporarily disabled → SpringBoard probed on tvOS):

…testResolveBlockingSystemModalIsAbsentWithoutSpringBoardOnTvOS :
    Failed to resolve query: Application com.apple.springboard is not running   ← reproduces #1351
** TEST EXECUTE FAILED **

Verification

Environment: tvOS Simulator — Apple TV 4K (3rd gen), tvOS 26.2 (matches the report). Runner built from this branch's source via the real CLI.

  • pnpm build:xcuitest (iOS + macOS) — both ** TEST BUILD SUCCEEDED ** (no regression on the shared runner).
  • ✅ tvOS runner builds (build:xcuitest:tvos, with unit tests) — no warnings.
  • ✅ Live snapshot -i on com.apple.TVSettings now returns a real tree instead of the "overwhelming capture" error:
    Snapshot: 47 nodes
    @e1 [application] "Settings"
    @e6 [table] "General"
    @e7 [cell] "General"
    …
    
  • ✅ Live alert get completes cleanly (alert not found — no alert present) instead of crashing the runner; runner.log shows 0 com.apple.springboard is not running failures.
  • ✅ Live press @e7 --settle navigates (General → About/Appearance/…) with a proper settled diff — repeated probes stay clean.
  • ✅ npm-package source stripper ships the production predicate/guard and strips the nested unit-test block (apple-runner-package-source.test.ts passes).

Note: there is no tvOS CI lane, so the tvOS regression tests run on build:xcuitest:tvos (exercised locally here), not in the default iOS/macOS CI.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.8 MB 1.8 MB 0 B
JS gzip 574.2 kB 574.2 kB 0 B
npm tarball 688.4 kB 688.7 kB +380 B
npm unpacked 2.4 MB 2.4 MB +1.2 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.4 ms 26.8 ms +0.4 ms
CLI --help 54.1 ms 54.2 ms +0.1 ms

Top changed chunks: no changes in the largest emitted chunks.

tvOS has no SpringBoard (it uses PineBoard/HeadBoard), so probing
com.apple.springboard for blocking system modals raises an XCTest failure
("Application com.apple.springboard is not running") that safely() cannot
trap, terminating the whole runner test. Every snapshot and alert
resolution then failed, and the CLI misreported it as the screen
"overwhelming the accessibility capture".

resolveBlockingSystemModal — the single chokepoint every snapshot/alert
path funnels through — assumed a SpringBoard host always exists. Model that
assumption explicitly with `hasSpringBoardSystemModalHost` and bail to
`.absent` when no host exists, so app-owned alert queries still run. The
fourth probe site (shouldRouteToSpringboardBlockingSystemModal) is already
`#if os(iOS)` and needs no change.

Add tvOS regression tests covering the snapshot and alert-resolution paths.
@thymikee
thymikee force-pushed the worktree-tvos-springboard-probe-1351 branch from 37558df to bca6697 Compare July 20, 2026 18:39
@thymikee

Copy link
Copy Markdown
Member Author

Code review is clean, but one required tvOS validation remains before readiness. The live alert get result shown here is alert not found with no alert present; that proves the SpringBoard crash is gone, but it does not prove the .absent path successfully falls through to a real app-owned alert. The two tvOS tests also cover only blocking-modal absence/snapshot behavior. Please provide a live tvOS app alert with alert get and preferably accept/dismiss, plus runner-log evidence showing no SpringBoard probe. All current CI is green, but there is no tvOS CI lane.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed — I now have positive proof that the .absent fallthrough resolves a real app-owned alert on tvOS, including accept/dismiss, with runner-log evidence of no SpringBoard probe.

Why a purpose-built app

The tvOS 26.2 simulator ships only com.apple.TVSettings, and its system actions don't use UIAlertController — I confirmed live that Power Off and Restart act directly (power-off overlay / device reboot), not alerts, so Settings can't produce an app-owned alert to resolve. To get a deterministic app-owned alert I built a ~40-line tvOS app (com.callstack.adtvalert) that presents a two-button UIAlertController on launch, installed it on the sim, and drove it through agent-device.

Evidence (tvOS Simulator — Apple TV 4K, tvOS 26.2)

Real app-owned alert on screen (Cancel / Confirm), then:

1. alert get --json — the .absent path falls through to activeApp.alerts and resolves it:

{
  "message": "Agent Device Test Alert.",
  "items": [ "Cancel", "Confirm" ]
}

2. alert accept — taps Confirm; alert dismisses:

accepted

A follow-up alert get then returns alert not found, and the screen is back to the app background (alert gone).

3. runner.log — the alert commands succeed and never probe SpringBoard:

AGENT_DEVICE_RUNNER_COMMAND_COMPLETED command=alert ... ok=1   # alert get
AGENT_DEVICE_RUNNER_COMMAND_COMPLETED command=alert ... ok=1   # alert get --json
AGENT_DEVICE_RUNNER_COMMAND_COMPLETED command=alert ... ok=1   # alert accept
AGENT_DEVICE_RUNNER_COMMAND_COMPLETED command=alert ... ok=0   # follow-up get: alert not found

grep -c "com.apple.springboard is not running|Failed to resolve query" over the full session log = 0.

Test coverage note

The two Swift regression tests stay as the compile-time-guard coverage (blocking-modal absence + snapshot), since positively presenting a UIAlertController inside the XCUITest host isn't feasible in the unit target — the app-owned fallthrough is the standard activeApp.alerts query shared with iOS and is now proven live above. If you'd like, I can land the throwaway adtvalert app as a committed tvOS alert fixture so this path can be exercised in a future tvOS CI lane; say the word and I'll add it.

@thymikee

Copy link
Copy Markdown
Member Author

Re-review is clean and the prior tvOS device-evidence blocker is resolved. The exact-head live run exercises the missing path with a real app-owned UIAlertController: alert get resolves both actions, alert accept dismisses it, the follow-up reports absence, and the session log shows zero SpringBoard resolution failures. Head bca6697 remains conflict-free with all 23 checks green. Residual risk: tvOS still has no CI lane, but the committed guard tests plus this live simulator evidence are sufficient for this scoped fix. Ready for human review.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 20, 2026
@thymikee
thymikee merged commit c6a8e78 into main Jul 21, 2026
23 checks passed
@thymikee
thymikee deleted the worktree-tvos-springboard-probe-1351 branch July 21, 2026 05:57
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-21 05:58 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tvOS: every snapshot/alert resolution fails because the runner probes com.apple.springboard (no SpringBoard on tvOS)

1 participant