Skip to content

ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) - #1789

Merged
thymikee merged 6 commits into
mainfrom
ci/1781-a7-full-xctest-nightly
Aug 18, 2026
Merged

ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7)#1789
thymikee merged 6 commits into
mainfrom
ci/1781-a7-full-xctest-nightly

Conversation

@thymikee

@thymikee thymikee commented Aug 17, 2026

Copy link
Copy Markdown
Member

Wave 1 of #1781 A7 — iOS XCTest, the "run" half only. No tests are deleted, moved, or classified here; that is A7's wave-3 work.

Status: draft on purpose. workflow_dispatch cannot reach a workflow that is not yet on the default branch (gh workflow run xctest-nightly.yml --ref ci/1781-a7-full-xctest-nightlyHTTP 404: workflow xctest-nightly.yml not found on the default branch). So the live macOS path is still unmeasured. Details and options at the bottom.

The gap

.github/workflows/ios.yml runs xcodebuild test-without-building with a hand-written list of -only-testing:AgentDeviceRunnerUITests/RunnerTests/… entries naming a subset of the target's methods. Everything outside that list ran nowhere — not on PRs, not nightly, not in any gate.

Figures at this head (pnpm check:xctest-selection derives them; nothing hand-copies them any more): 157 declared, 42 selected on every PR, 1 skipped by the nightly, 114 reached only by the nightly. They were 154 / 37 / 1 / 116 one day earlier — see the drift note below.

The list is also unverifiable by construction: xcodebuild treats a test identifier that matches nothing as an empty set, not an error. A renamed or deleted test silently stops being tested and the lane stays green.

1. Nightly full-suite lane

New .github/workflows/xctest-nightly.yml — schedule 30 4 * * * + workflow_dispatch, one macos-26 job.

Reuses ./.github/actions/setup-apple-runner-build and ./.github/actions/boot-ios-test-simulator with byte-identical inputs to ios.yml (same IOS_RUNTIME_VERSION: '26.2', same cache-key-prefix/cache-key-suffix, same gate/platform/destination, same AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS: '1'), then the same xcodebuild test-without-building minus every -only-testing: flag, with -resultBundlePath. Result bundle + xcresulttool summary uploaded if: always().

-skip-testing:AgentDeviceRunnerUITests/RunnerTests/testCommand is the one filter it keeps, and it is load-bearing. testCommand is not a test: it is the runner's server entry point (RunnerTests.swift:245) — it opens an NWListener and blocks in XCTWaiter.wait(timeout: 24 * 60 * 60), which is exactly how production drives it (runner-session.ts:219 always passes it as the sole -only-testing:). It compiles unconditionally; the #if AGENT_DEVICE_RUNNER_UNIT_TESTS block ends at line 167, well above it. Alphabetically it is test #16, so the first draft of this lane would have run ~16 tests and then hung to timeout-minutes, red every night, measuring nothing. Its AGENT_DEVICE_RUNNER_NOOP_STARTUP escape hatch is an env var, and the comment at RunnerTests.swift:126-129 records that env plumbing into a simulator test process is not dependable — -skip-testing: is the lever that works from CI.

AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS is load-bearing twice over: it feeds the derived-data cache's build-variant hash and it is the -D AGENT_DEVICE_RUNNER_UNIT_TESTS compile flag that makes the RunnerTests methods exist. Dropping it builds a bundle with no tests in it, which xcodebuild reports as success. The lane therefore asserts the run executed at least one test.

Under the #1781 lane rule (also in the workflow header):

  • Catches — regressions in the runner-semantics tests the PR list omits (selector matching, snapshot traversal, command journal, sequence execution, tv remote, keyboard, scroll/gesture policy). Nothing else executes them.
  • Evidence — the list has never been re-derived since it was hand-written; the omitted tests have been dark their whole life. The first run is the evidence this lane exists to produce.
  • Costpublic repo, so $0 billed. The real cost is one macOS concurrency slot for N minutes per night, N unknown until the first run. Two caveats worth stating plainly: (a) scheduled runs only see default-branch caches, so the derived-data hit depends on ios.yml's push: main run having populated the cache at the same source hash — a miss means the lane also pays a full runner build; (b) concurrency is keyed on github.ref, so a manual dispatch on the same ref cancels an in-flight scheduled run rather than queueing behind it. It does not affect PRs: schedule + manual only, no pull_request trigger, no new required check, zero minutes added to any PR.
  • Kill criterion — when A7's classification lands, this lane goes if the surviving set is small enough to run on every PR.

Expected hazards on the first run (disclosed, not yet measured)

The reachable set on this destination is 154, not 157: testCommand is skipped, and two tests (testResolveBlockingSystemModalIsAbsentWithoutSpringBoardOnTvOS, testBlockingSystemAlertSnapshotIsNilOnTvOS) sit behind #if os(tvOS) and cannot execute on an iOS Simulator. Of the rest, these have never executed anywhere and may well be red on first contact:

  • testTapPointPolicyMatchesGoldenParityTable — resolves its fixture through #filePath from inside the simulator, i.e. a host path the test process may not be able to read.
  • testExecuteDispatchedReturnsBusyBeforeBlockingSystemModalProbeDrains — does real app.launch/terminate against 15s deadlines.
  • Two Snapshot tests carry intentional 15s / 6s waits, so wall time is not dominated by the fast decision tests.

timeout-minutes is currently 120. That is the hang's ceiling, not an estimate; it should be tightened to ~2× the measured duration once a run exists.

2. Discovery check, so neither list can silently drift

pnpm check:xctest-selection (scripts/check-xctest-selection.ts, parse-only — no Xcode, no simulator) enumerates every Target/Class/method the Swift sources declare, parses both -only-testing: and -skip-testing: identifiers out of both workflows, and fails if any names a method no source declares. Both directions fail silently in their own way, and the skip direction is the expensive one — a typo there re-arms the 24-hour hang with no signal at all.

It does not fail on tests missing from the PR list; that is the nightly's job. It does fail if either scan comes back empty, or if a guarded workflow no longer exists, so a broken parser cannot report a healthy list forever.

Output today:

xctest selection: 157 declared AgentDeviceRunnerUITests methods — 42 selected on every PR (.github/workflows/ios.yml), 1 skipped by the nightly (.github/workflows/xctest-nightly.yml), 114 reached only by the nightly.

Drift note. Those were 154 / 37 / 1 / 116 when this PR opened; merging main a day later moved them to 157 / 42 / 1 / 114 (three methods added, five PR entries added). Every entry still resolves, so the check passes — but the counts I had hand-copied into comments were already wrong. 8e54e29e therefore removes hardcoded counts from the workflow header, the ci.yml comment, and the docs row, leaving them to describe the relationship and pointing at this command for the live split. A count in a comment is the same class of defect this PR exists to close.

Registered the way the repo registers checks: CheckId + ALL_CHECKS + CHECK_CATALOG (xctest-selection), a BUILD_OWNERSHIP rule so a change under AgentDeviceRunnerUITests/ selects it, a run-gate step in CI / Affected-check Selector (next to the gate manifest — same family of failure: a CI selection that stopped selecting what it claims), and check:tooling. pnpm check:gate-manifest: 49 checks wired across 34 lanes.

Tests — 28 cases across two files, both in unit-core:

  • scripts/__tests__/xctest-selection.test.ts (18): the real tree passes; a typo planted into the real ios.yml text fails with the right line; a typo planted into the real nightly -skip-testing: entry fails too; the declared-set count is derived independently of the check's own file filter; plus class func, a final class ResultBox nested in a test body, comment lines, and both blind-parse guards.
  • scripts/__tests__/xctest-run-summary.test.ts (10): the lane's reporter/liveness check, which otherwise only ever executes at 04:30 on a macOS runner — zero-test detection, the failure-list cap, message truncation, and newline flattening so a stack trace cannot forge markdown headings in the job summary.

Review fixes in 353c8276

  1. testCommand would have hung the lane. Added -skip-testing:, and extended the check to validate skip identifiers with the same rigour as only identifiers (+ a planted-typo test for it).
  2. Off-by-one in the file filter. RunnerTests*.swift missed RunnerTapPointPolicy.swift, which declares a real addressable test — the Xcode project uses a PBXFileSystemSynchronizedRootGroup, so membership is the directory, not the name. Now globs *.swift. That moved the declared count from 153 to 154 at the time; 8e54e29e later removed the hardcoded counts from the durable files entirely, for the reason in the drift note above.
  3. Tautological test removed. The count is now derived in the test by globbing the directory with its own regex, so it cannot agree with the check by construction.
  4. Stale-claim guard. Both guarded workflows are read via fs.existsSync; a missing one is a named failure rather than a claim nothing backs.
  5. [ ! -s ] not [ ! -f ], so a truncated summary hits the crafted message; --compact on xcresulttool with a plain-form fallback; and the job-summary rendering moved out of node -e into a tested script with an explicit failure-list cap, for the 1 MiB limit.
  6. Focused ownership test (scripts/check-affected/model.test.ts): a change under AgentDeviceRunnerUITests/** selects exactly swift-runner-ios, swift-runner-macos, xctest-selection; RunnerTapPointPolicy.swift selects it too; apple/runner/Sources/** does not.

Two defects the review process surfaced in my own work, both now regression-tested: the scan counted its own explanatory comments as flags (inflating 37→39 and inventing a second skip), and the per-lane counts were aggregated by flag rather than by workflow.

On the optional explicit .github/workflows/ios.yml ownership: deliberately not added. selectChecks classifies workflow paths as fail-open, so a change to ios.yml already selects the full check set including xctest-selection (verified). An explicit rule would be dead code.

Validation

  • pnpm check:tooling — green (includes the new check:xctest-selection, plus format/lint/typecheck/layering/depgraph/gate-manifest/production-exports/build/package)
  • pnpm check:gate-manifestok — 49 checks wired across 34 lanes, 1 declared unprovable
  • pnpm check:gate-manifest:test (28), pnpm check:affected:test (36, incl. the new ownership case)
  • pnpm exec vitest run --project unit-core scripts/__tests__/xctest-selection.test.ts scripts/__tests__/xctest-run-summary.test.ts (28)
  • pnpm check:fallow --base origin/main — clean
  • actionlint -shellcheck=shellcheck .github/workflows/xctest-nightly.yml .github/workflows/ci.yml — clean

Not validated: the live macOS run. workflow_dispatch is refused pre-merge because GitHub only exposes it for workflows present on the default branch. Two ways forward, your call:

  1. Merge, then dispatchgh workflow run xctest-nightly.yml, and tighten timeout-minutes from 120 to ~2× measured in a follow-up. Risk is bounded: worst case is one red night with a result bundle attached, and the hang that made this dangerous is now skipped.
  2. Say the word and I'll add a temporary push: trigger scoped to this branch (push triggers have no default-branch requirement), get the numbers, then strip it before merge. Costs one macOS slot now and means the merged trigger block differs from the measured one.

I have not done (2) unprompted — it spends a shared macOS slot and changes the artifact under review.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-18 10:01 UTC

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.26 MB 2.26 MB 0 B
JS gzip 744.6 kB 744.6 kB 0 B
npm tarball 864.0 kB 864.0 kB +22 B
npm unpacked 3.01 MB 3.01 MB +130 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.3 ms 25.9 ms -0.4 ms
CLI --help 62.0 ms 61.5 ms -0.5 ms

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

@thymikee

Copy link
Copy Markdown
Member Author

The new selection checker itself is valid: it passes at the exact head and fails with the correct workflow line after a planted renamed -only-testing entry. Before this is ready: (1) run the new XCTest Nightly workflow through workflow_dispatch and attach its xcresult summary, confirming the full suite executes; the PR currently calls this live macOS path unvalidated. (2) Add a focused check-affected model test proving a change under apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/** selects xctest-selection; the existing Swift ownership test exercises apple/runner/Sources/**, not this new rule. (3) Wait for the remaining required CI checks to complete successfully.

@thymikee

Copy link
Copy Markdown
Member Author

Thanks — all three addressed in 353c8276, and an adversarial pass caught a blocker that would have made the dispatch you asked for fail in the worst way. Taking your points in order, with the blocker first because it changes the lane.

Blocker found before dispatch: the unfiltered run would have hung. RunnerTests/testCommand (RunnerTests.swift:245) is not a test — it is the runner's server entry point: NWListener + XCTWaiter.wait(timeout: 24 * 60 * 60), which is precisely how runner-session.ts:219 drives it in production. It compiles unconditionally (the #if AGENT_DEVICE_RUNNER_UNIT_TESTS block ends at line 167), and alphabetically it is test #16. So v1 of this lane would have run ~16 tests, hung, and gone red at timeout-minutes: 120 every night while measuring nothing — and pinned a macOS slot for two hours doing it. Fixed with -skip-testing: (the AGENT_DEVICE_RUNNER_NOOP_STARTUP escape hatch is an env var, and the comment at RunnerTests.swift:126-129 records that env plumbing into a simulator test process is not dependable). The checker now validates -skip-testing: identifiers exactly like -only-testing: ones, because a typo there silently re-arms the hang; there is a planted-typo test for that direction too.

(1) The dispatch — GitHub refuses it pre-merge. gh workflow run xctest-nightly.yml --ref ci/1781-a7-full-xctest-nightlyHTTP 404: workflow xctest-nightly.yml not found on the default branch. workflow_dispatch is only exposed for workflows already on the default branch, so there is no way to run this from the branch as-is. You're right that the live macOS path is unvalidated and I have not pretended otherwise — the PR stays draft, and the body now leads with that. Two ways to close it, your call:

  • merge and then gh workflow run xctest-nightly.yml, with a follow-up tightening timeout-minutes from 120 (which is the hang's ceiling, not an estimate) to ~2× measured; or
  • say the word and I'll add a temporary push: trigger scoped to this branch, which has no default-branch requirement, get the numbers, and strip it before merge.

I didn't do the second unprompted — it spends a shared macOS slot and makes the merged trigger block differ from the measured one.

Meanwhile I've disclosed in the body what I expect the first run to hit, rather than leaving it to be discovered: reachable set is 151, not 154 (testCommand skipped, two …OnTvOS tests behind #if os(tvOS)), and three specific hazards among the never-executed tests — testTapPointPolicyMatchesGoldenParityTable reads a host path via #filePath from inside the simulator, testExecuteDispatchedReturnsBusyBeforeBlockingSystemModalProbeDrains does real app.launch/terminate against 15s deadlines, and two Snapshot tests carry intentional 15s/6s waits.

(2) Ownership test addedscripts/check-affected/model.test.ts: a change under AgentDeviceRunnerUITests/** selects exactly swift-runner-ios, swift-runner-macos, xctest-selection; RunnerTapPointPolicy.swift selects it too; apple/runner/Sources/** still does not. That last assertion earns its keep — see below.

(3) Waiting on required checks before undrafting, and not undrafting at all until the dispatch question above is settled.

Two more corrections worth flagging, both mine:

  • The count was wrong: 154, not 153. My file filter was RunnerTests*.swift, which misses RunnerTapPointPolicy.swift — and that file declares a real addressable test inside extension RunnerTests. The project uses a PBXFileSystemSynchronizedRootGroup, so membership is the directory, not the filename. Now globs *.swift; every quoted number updated. The parser test that "verified" the count was also tautological (it re-used the check's own filter to compute the expected value), so it is now derived independently by globbing the directory with its own regex.
  • The scan was counting its own comments. Both workflows explain -only-testing:/-skip-testing: in prose, and the first version read that prose as configuration — reporting 39 PR selections and 2 skips instead of 37 and 1. Fixed with a comment guard plus an identifier-shape requirement, and regression-tested.

Also picked up from the adversarial pass: [ ! -s ] instead of [ ! -f ] so a truncated summary hits the crafted message, --compact on xcresulttool with a plain-form fallback, and the job-summary rendering moved out of an inline node -e into scripts/xctest-run-summary.ts with a real failure-list cap — the night this lane matters most is the night the failure list is longest, and the job summary is capped at 1 MiB. It has its own 10-case test file, since otherwise it would only ever execute at 04:30 on a macOS runner.

On your optional suggestion of explicit ios.yml ownership in BUILD_OWNERSHIP: I checked and left it out deliberately — selectChecks already classifies workflow paths as fail-open, so a change to ios.yml selects the full set including xctest-selection. An explicit rule would be dead code.

Re-validated: pnpm check:tooling green, check:gate-manifest (49 checks / 34 lanes), check:gate-manifest:test 28, check:affected:test 36, the two new test files 28, check:fallow --base origin/main clean, actionlint -shellcheck=shellcheck clean.

@thymikee

Copy link
Copy Markdown
Member Author

CI note so the red marks aren't misread: GitHub had a platform incident while this branch was building, and every failure on it so far is that incident, not this change.

Same 503 No server is currently available to service your request from unrelated places within a few minutes:

  • Analyze (actions) / Analyze (java-kotlin) — died in Initialize CodeQL (Encountered an error while trying to determine feature enablement: HttpError: No server is currently available…), i.e. before any analysis ran, so it cannot be a finding about the new workflow. This PR touches no Java or Kotlin at all. Analyze (python) and Analyze (javascript-typescript) passed on the same run.
  • Bundle Size — the measurement succeeded; scripts/size-report.mjs then failed posting its comment (Failed to update PR comment: 503). Re-run, now green.
  • deploy-preview and Resolve native fingerprint — 503/500 on the first attempt, re-run, both now green.

The two CodeQL jobs cannot be re-run from the API (This workflow run cannot be retried — default setup runs with event: dynamic), so they will clear on the next push or a maintainer re-trigger rather than by anything I can do from here. Everything this PR actually adds is green, including the new gate step:

Affected-check Selector
  ✓ Check affected-selector model
  ✓ Check the gate manifest model
  ✓ Check every gate is owned, wired, and reachable
  ✓ Check the PR XCTest selection still names real tests   ← new

Coverage and the remaining macOS Smoke Tests are still running.

Separately, I filed the size-report papercut as its own task rather than fixing it here: a transient 503 on a cosmetic comment write should not fail a lane whose real work already succeeded.

@thymikee

Copy link
Copy Markdown
Member Author

Follow-up review at bfdda197: the focused affected-selector model test is now present, and the expanded checker / -skip-testing coverage is sound. The remaining readiness blocker is a workflow_dispatch run of XCTest Nightly at this exact head, with its xcresult summary attached. That run must demonstrate the macOS build, testCommand skip, xcresult-summary fallback, and full-suite execution. All required PR CI checks are green. The separate CodeQL workflow failure is GitHub HTTP 503 during status/upload reporting, not a source finding.

@thymikee

Copy link
Copy Markdown
Member Author

The remaining readiness blocker is a workflow_dispatch run of XCTest Nightly at this exact head

That specific run is not obtainable pre-merge — not by me, and not by you either. GitHub only exposes workflow_dispatch for workflows that already exist on the default branch, and this one exists only here:

$ gh workflow run xctest-nightly.yml --ref ci/1781-a7-full-xctest-nightly
HTTP 404: workflow xctest-nightly.yml not found on the default branch
  (https://api.github.com/repos/callstack/agent-device/actions/workflows/xctest-nightly.yml)

So the blocker as written cannot be cleared. Flagging that rather than quietly substituting a different mechanism, because the substitute costs a shared macOS slot and changes what you're reviewing.

Two paths actually produce a run that demonstrates all four things you listed (macOS build, testCommand skip, xcresult-summary fallback, full-suite execution). Reply "A" or "B" and I'll execute it.

A — temporary branch-scoped push: trigger. Push triggers have no default-branch requirement, so this yields a real run on this branch now. I add the trigger, push, wait, attach the xcresult summary (totalTestCount, pass/fail, duration, and which of the 116 newly-reached tests failed), tighten timeout-minutes from the placeholder 120 to ~2× measured, remove the trigger, push, then undraft.

One honesty caveat, since you asked for this exact head: the measured run would be at the head with the temporary trigger, and removing it changes the SHA. The delta between what ran and what merges is exactly that trigger block — nothing in the build, the skip, the summary path, or the test selection. No pre-merge scheme avoids this; a dispatch would have had the same property in reverse.

B — merge, then dispatch on main. gh workflow run xctest-nightly.yml works the moment the file is on the default branch. I follow up with the numbers and the timeout-minutes tightening in a second PR. Risk is bounded: the worst case is one red night with a result bundle attached, and the hang that made an unfiltered run genuinely dangerous (testCommand, 24h XCTWaiter) is skipped now.

My read: A if you want the evidence before this merges, B if you'd rather not spend the slot on a branch. I lean B — the lane is cheap to run once it's on main, and A's artifact-vs-measured gap is real even if small.

On the two red CodeQL checks: agreed they're the 503s, and worth knowing they are stuck. gh run rerun 32050840985 returns This workflow run cannot be retried (default setup runs as event: dynamic), so they clear only on the next push to this branch, not by a re-trigger. Under A they'd clear on their own; under B they'd need an admin override or a no-op push, which I won't make on my own.

Branch is MERGEABLE against current main (now including #1787), and all 27 required checks are green.

@thymikee

Copy link
Copy Markdown
Member Author

Merged current main (through #1794) and pushed 8e54e29e. Two things came out of it worth reporting, and the first one validates the check better than anything I could have written.

The counts drifted within a day, and the check caught it

Against merged main, pnpm check:xctest-selection now reports:

xctest selection: 157 declared AgentDeviceRunnerUITests methods — 42 selected on every PR
(.github/workflows/ios.yml), 1 skipped by the nightly (.github/workflows/xctest-nightly.yml),
114 reached only by the nightly.

Was 154 / 37 / 1 / 116 yesterday. #1798 and its siblings added three methods (RunnerTests+PrivateAXPresentation.swift plus edits to Snapshot / AXSnapshotFallback / FlatSnapshotFiltering), and ios.yml gained five -only-testing: entries. Every one of the 42 still resolves, so the check passes — but the numbers I had hand-copied into comments were already wrong, one day after writing them.

So I removed them. 8e54e29e strips the hardcoded counts from the workflow header, the ci.yml step comment, and the docs/agents/testing.md row, and points each at pnpm check:xctest-selection for the live split. Those files now describe the relationship — a hand-written subset, one skipped non-test, the rest reachable only nightly — which does not rot. A count in a comment is the same class of defect this PR exists to close; it was a bad look to ship one inside the fix.

Numbers still appear in this PR description, which is fine — it is a point-in-time artifact, and I have updated it to the current figures.

Gate manifest still models correctly after #1794

You asked me to re-check this if #1794 landed first. It did, and it changed the manifest's vocabulary — declared unprovable became a manual-only category:

gate manifest: ok — 47 checks wired across 33 lanes, manual-only: replay-android, replay-ios, replay-ios-device

xctest-selection is among the 47 and is still owned by CI / Affected-check Selector. xctest-nightly.yml keeps its schedule: trigger, so it stays a qualifying lane — it is deliberately not part of #1794's move to replays-manual.yml, since A1 parked lanes that were red 25 nights running, while this one has never run at all. Different problem, opposite remedy.

Re-validated on the merged tree: check:tooling green, check:gate-manifest as above, check:affected:test 36, the two new test files 28, check:fallow --base origin/main clean, actionlint -shellcheck=shellcheck clean.

Unrelated papercut, filed separately

pnpm format rewrites 48 files under scripts/maestro-conformance/corpus/ (flipping " to ' in the YAML), even though .oxfmtrc.json lists that path in ignorePatterns — while pnpm format:check says everything is fine both before and after. --write and --check disagree about scope. I reverted them here rather than carrying the churn, and filed it on its own; it is a live trap for anyone who formats and then commits everything, and the corpus is the conformance oracle.

Still blocked on the same question

The A-or-B decision is unchanged and unanswered: A temporary branch-scoped push: trigger (run now on this branch), or B merge then dispatch on main. The push above will have cleared the two stuck CodeQL reds as a side effect.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 8e54e29e. The stale-count cleanup is correct and exact-head PR CI is fully green, but the prior readiness blocker remains: there is still no XCTest Nightly/workflow_dispatch run or xcresult evidence at this head; Actions history contains only PR-triggered/dynamic runs. Keep this draft unlabelled until the documented temporary branch trigger is authorized and run, or until post-merge dispatch evidence exists.

@thymikee

Copy link
Copy Markdown
Member Author

Status update at 8e54e29e, since two things moved.

Exact-head CI is now fully green

The push cleared the stuck CodeQL jobs, as expected — they were unrerunnable only because the previous run was event: dynamic, and a new push starts a fresh one. Current state: 0 failing checks, mergeStateStatus: CLEAN.

So the only outstanding item is the run evidence.

On authorization

To be precise about where this stands, since I don't want to act on an ambiguity: "until the documented temporary branch trigger is authorized and run" states the condition but doesn't grant it. I've read it as not an authorization and have not touched the trigger. If you do want it, one word — "authorized" — and I'll add it, run it, attach the summary, tighten timeout-minutes to ~2× measured, remove the trigger, push, and undraft.

I'm not going to keep re-asking beyond this; if the answer is "merge first", that's fine too and I'll follow up with the numbers in a second PR.

Meanwhile: running the suite locally, at this head

Rather than wait, I'm running the full suite on this exact tree on local hardware that matches the CI image — Xcode 26.2 / iOS 26.2 simulator runtime, the same versions macos-26 and IOS_RUNTIME_VERSION: '26.2' pin. Same build (AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1, generic/platform=iOS Simulator), same test-without-building invocation, same -skip-testing: entry, same -resultBundlePath and xcresulttool summary path.

This is not a substitute for the GitHub Actions run you asked for, and I won't present it as one — it proves nothing about the runner image, the derived-data cache restore, or the boot-ios-test-simulator action. What it does answer is the question actually sitting under all of this: are the ~114 never-executed tests red, and how long does the suite take? That is what decides whether merging is safe and what timeout-minutes should be.

I'll post the numbers when it finishes, clearly labelled as local evidence: totalTestCount, pass/fail/skip, duration, and the name of every failure among the newly-reached set. It also exercises scripts/xctest-run-summary.ts against real xcresulttool output for the first time, including the --compact path — one of the four things you listed.

@thymikee

Copy link
Copy Markdown
Member Author

Full-suite evidence — and the lane found two real bugs on its first run

Ran the complete suite against this exact tree on Xcode 26.2 / iOS 26.2 Simulator — the same versions macos-26 and IOS_RUNTIME_VERSION: '26.2' pin. Same build (AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1, generic/platform=iOS Simulator), same test-without-building invocation, same -skip-testing: entry, same -resultBundlePath.

This is local evidence, not the Actions run you asked for. It says nothing about the runner image, the derived-data cache restore, or boot-ios-test-simulator. It does answer what those tests actually do.

Executed 154 tests, with 2 failures (0 unexpected) in 98.241 (98.316) seconds

Rendered through scripts/xctest-run-summary.ts from real xcresulttool ... --compact output — first time that path has run against a genuine result bundle:

### iOS runner full XCTest suite

- result: **Failed**
- executed: **154** (passed 152, failed 2, skipped 0, expected failures 0)
- duration: 120s

#### Failures
- `testTopLeadingNavigationFallbackPointRejectsInvalidFrame()` — XCTAssertNil failed: "(-8.988465674311579e+307, -8.988465674311579e+307)"
- `testTopNavigationControlFrameAcceptsOnlyHeaderBand()` — XCTAssertFalse failed

Device: iPhone 16, iOS 26.2, arm64. (ios.yml prefers iPhone 17 Pro; these are geometry and policy tests, so the model is not load-bearing.)

The count confirms the arithmetic exactly

157 declared − 1 skipped (testCommand) − 2 behind #if os(tvOS) = 154 executed. That is the predicted reachable set, landing on the nose, which is a decent check on the selection logic itself.

The two failures were both in the dark set

Neither appears in ios.yml's 42-entry -only-testing: list. Both live in RunnerTests+Navigation.swift (lines 185 and 205) and had never executed anywhere — which is precisely the case this lane was built to expose.

They are product defects, not test bugs. Both are the same root cause: the navigation helpers do not reject CGRect.infinite.

  • topLeadingNavigationFallbackPoint(in: .infinite) returns a point instead of nil. The value is CGRect.infinite's origin, so the helper computes an offset off a sentinel rect — a navigation fallback can therefore produce a tap coordinate around -9e307. The .zero case in the same test passes, so the existing guard covers empty but not infinite.
  • isTopNavigationControlFrame(.infinite, in: window) returns true — an unresolved frame is classified as sitting in the header band.

There is already a correct precedent in the same target: TapPointPolicy.isAllowed guards isNull || isEmpty || isInfinite. The navigation helpers check a subset.

I have not fixed them here — one issue per PR, and A7's "run" half is explicitly "get them running, no deleting or fixing yet". Filed separately with the repro. Worth noting the consequence for this PR: the nightly will land red on night one, which is the outcome I disclosed up front as the evidence the lane exists to produce.

timeout-minutes tightened: 120 → 45 (ec11c8f1)

The suite is ~2 minutes, not the bulk of the job — what dominates is the runner build on a cache miss, plus checkout and boot. ios.yml carries 80 minutes while doing strictly more than this lane (same build and boot, plus replays, prepare, and the fixture E2E), so 45 is generous headroom for a cache-miss night and still caps a wedged run at 45 minutes rather than two hours of held macOS slot.

I want to be straight about the derivation: "2× measured" isn't literally applicable, because what I could measure locally is the suite, not the job. 2× the suite would be 4 minutes, which would fail every cache-miss night. 45 is reasoned from ios.yml's envelope instead, and the first real run should be used to tighten it further.

What this does and does not settle

Settled: the suite runs to completion, testCommand is correctly skipped (no hang), the xcresult-summary path works on real output including --compact, the liveness assertion passes, and we now know exactly what is red and why.

Not settled: everything about the CI environment specifically. That still needs either the temporary branch trigger or a post-merge dispatch — unchanged, and still your call.

@thymikee

Copy link
Copy Markdown
Member Author

Filed the two failures as #1812 — not fixed here, so this PR stays one issue.

Digging into the helpers sharpened the diagnosis from "doesn't reject .infinite" to something more specific and more likely to recur: both guards validate the rect's size and never its origin.

CGRect.infinite is { {-CGFLOAT_MAX/2, -CGFLOAT_MAX/2}, {CGFLOAT_MAX, CGFLOAT_MAX} }, so its width and height are finite and positive and sail through width.isFinite, width > 0. The pathological part is the origin, which neither helper inspects:

  • topLeadingNavigationFallbackPoint clamps xOffset to 44, then returns frame.minX + 44 — and at that magnitude the 44 vanishes into floating-point precision, yielding exactly the -8.988465674311579e+307 in the assertion output.
  • isTopNavigationControlFrame compares candidate.midY, and CGRect.infinite.midY is -CGFLOAT_MAX/2 + CGFLOAT_MAX/2 = exactly 0.0, which lands inside the [0, 180] header band.

I reproduced both arithmetic paths independently; they match the failure output byte for byte. The .zero half of the first test passes only because width > 0 happens to catch it — .infinite is the single invalid rect that gets through.

TapPointPolicy.isAllowed in the same target already guards isNull || isEmpty || isInfinite, so the fix is to use that predicate rather than a fourth hand-rolled spelling of "is this rect usable". Suggested in the issue.

Nothing changes for this PR: the nightly will report both until they're fixed, which is the disclosed outcome.

thymikee added a commit that referenced this pull request Aug 18, 2026
…1795)

* ci: keep Bundle Size job green on transient GitHub comment failures

The size measurement and job summary had already succeeded on PR #1789
(run 32050847506) when the PR comment write got a 503 during a GitHub
incident and failed the whole lane.

--post-comment now retries 5xx / 429 / network errors (4 attempts,
1s/2s/4s backoff) on both the list and write calls. If it still fails,
it prints a ::warning::, appends a note to $GITHUB_STEP_SUMMARY, and
exits 0. Other 4xx (bad token, missing permissions) stay fatal.

* refactor: split GitHub response classification to satisfy fallow complexity gate

* fix: reconcile uncertain comment creates instead of re-POSTing; add regressions

Retry now wraps the whole list -> write cycle rather than each request, so a
create whose response was lost (network error / 5xx) is re-listed on the next
attempt and turned into a PATCH of the marker comment instead of a duplicate
POST. Splits the retry/classify helpers under the fallow complexity gate.

Adds scripts/__tests__/size-report-post-comment.test.ts (unit-core): spawns the
real script against a stubbed fetch and pins uncertain-create reconciliation,
transient exhaustion (warn + exit 0), and fatal 4xx (nonzero, no retry).
SIZE_REPORT_RETRY_BASE_MS lets the tests skip real backoff.
@thymikee
thymikee marked this pull request as ready for review August 18, 2026 10:00
@thymikee
thymikee merged commit 142d156 into main Aug 18, 2026
30 checks passed
@thymikee
thymikee deleted the ci/1781-a7-full-xctest-nightly branch August 18, 2026 10:00
@thymikee

Copy link
Copy Markdown
Member Author

Post-merge dispatch evidence — run 32124687738

gh workflow run xctest-nightly.yml --ref main succeeded immediately, no 404, confirming the default-branch requirement was the only thing blocking this pre-merge.

### iOS runner full XCTest suite

- result: **Failed**
- executed: **155** (passed 153, failed 2, skipped 0, expected failures 0)
- duration: 241s

#### Failures
- `testTopLeadingNavigationFallbackPointRejectsInvalidFrame()` — XCTAssertNil failed: "(-8.988465674311579e+307, -8.988465674311579e+307)"
- `testTopNavigationControlFrameAcceptsOnlyHeaderBand()` — XCTAssertFalse failed

xcodebuild's own line: Executed 155 tests, with 2 failures (0 unexpected) in 168.567 (170.375) seconds. Device: iPhone 17 Pro, iOS 26.2, arm64.

Timings

Step Duration
Set up job 1s
Checkout 4s
Setup toolchain 29s
Restore and build iOS XCTest runner 58s — cache HIT
Boot iOS test simulator 84s
Run the full suite 5m21s
Extract the test-results summary 3s
Report the run and assert it executed tests <1s
Upload result bundle 8s
Job total 8m50s (9m32s incl. queue)

The cache sharing works as designed

58 seconds means the derived-data cache hit — ios.yml's push: main run on merge commit 142d1563 populated it and this lane restored it, rather than paying a build-for-testing. That was the entire point of giving this workflow byte-identical cache-key-prefix, cache-key-suffix, gate, platform, destination and AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS inputs, and it is now confirmed empirically rather than argued from the action source.

Both new steps behaved on the real runner

  • Extract: succeeded in 3s, and the artifact's test-results-summary.json is a single line — so xcresulttool --compact was accepted on Xcode 26.2 and the plain-form fallback was never needed.
  • Report + assert: scripts/xctest-run-summary.ts rendered the block above into the job summary and printed Executed 155 test(s)., exiting 0. The liveness assertion passed on real data, and the job is still correctly red — the failure comes from the xcodebuild step propagating a non-zero exit under set -euo pipefail, exactly as intended. Green-with-zero-tests and red-with-real-failures are distinguishable.
  • Artifact: both RunnerTests.xcresult and test-results-summary.json uploaded.

The arithmetic holds at a different count

Merged main declares 158 methods (up from 157 — more Swift tests landed between my local run and the merge). 158 − 1 skipped (testCommand) − 2 behind #if os(tvOS) = 155 executed. Exactly what ran. The selection model reproduces on a moving tree, which is a better check than matching a number I'd pinned.

testCommand was skipped correctly: an unfiltered run would have wedged on it alphabetically and burned the full timeout instead of finishing in under nine minutes.

Failures: exactly the two expected, nothing new

Both are #1812, both in the dark set, both reproducing identically to my local run — same assertion text, same -8.988465674311579e+307. No new failures among the ~114 tests that had never executed anywhere. That is a better outcome than I expected: the dark set was almost entirely healthy, and the two genuine defects it hid are now filed.

timeout-minutes: 45 stays

On this cache-hit run the job took 8m50s. A cache-miss night adds a full build-for-testing in place of the 58s restore — on the order of 15-20 minutes — putting the worst realistic case around 25-30 minutes. 45 leaves headroom for that without letting a wedged run sit for hours. I would not drop it to 15 or 20: that would fail every night the cache misses, which is precisely the night you most want the result. No change recommended.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant