Encapsulate the runner's bounded main-thread work and occupancy
Deletion-first acceptance
This is part of #2803's simplify-and-shrink initiative. Before production simplification, name the production mechanism, duplicated decision, state, fallback, forwarding layer, or public/internal interface this change removes. Compare deletion and inlining against extraction. Moving the same state into another file is not a deletion case.
Every PR reports baseline/final SHAs; added, deleted and net production lines; test/fixture/documentation changes separately; and the exact mechanisms removed. Use a rename-aware diff (git diff --numstat -M BASE...HEAD) and account for all affected production paths, including destination packages. Moving tests out of production files, generated output, formatting churn, and moving code elsewhere are not production-code deletion. Preserve readable code; no compressed formatting to hit a number.
Default for simplification work: net production reduction. A required behavior-preserving size split may be line-neutral. Growth needs an explicit, quantified explanation of the correctness or type guarantee it buys, why deletion/inlining cannot achieve it, and what old mechanism is removed; it is not automatically acceptable because ownership looks cleaner. If the only benefit is relocating fields or adding a wrapper, defer the change. Required regression tests and a minimal correctness fix are allowed to grow without inventing unrelated deletion to offset them.
Priority and scope gate
Readiness: deferred; remove ready-for-agent. No scheduling defect has been demonstrated here. Resume only when a concrete design deletes redundant state/coordination or a measured problem makes this extraction necessary; merely moving locks and counters from XCTestCase is insufficient. Compare a smaller in-place private-state solution first. The preserved design below is a candidate, not an implementation commitment. Run shared runner Swift work one issue at a time.
Parent: #2803. Readiness: deferred pending a concrete deletion or correctness case. Evidence baseline: 5712a3552e209c9c853bc1f42cb3e007f1213139. Recheck current main before starting. #2911 already supplies actor isolation; #2882 owns further actor source guards.
Problem and evidence
The scheduler already has the right behavior, but its state is owned by the entire XCTest case. RunnerTests.swift:105–113 exposes its lock, in-flight and abandoned counts, abandonment timestamp and wedge threshold to every extension. RunnerTests+MainThreadWork.swift:12–197 contains the cohesive execution mechanism. Snapshot capture, command dispatch, transport stamping and recording consume it, so accidentally changing its accounting affects multiple command families.
The issue is ownership, not a demonstrated scheduling failure. #2837 already fixed the timeout-boundary race; #2911 already made submitted work main-actor isolated. Preserve both.
Implementation contract
Create one concrete RunnerMainThreadWork module and matching tests. Move the lock, counters, abandonment timestamp, wedge threshold and per-dispatch completion state into private storage. Keep one instance on RunnerTests; remove the old fields and superseded extension implementation in the same change.
The small interface is bounded run, optional runIfIdle, and an occupancy query returning idle/busy/wedged with elapsed abandonment information. Preserve @MainActor work closures, timeout-error construction and the operation-specific abandonment callback. A derived abandoned-work predicate may use the same snapshot. Do not expose lock/counter setters or introduce a protocol with one implementation. Logging and existing error codes remain unchanged. Keep the existing test-only timeout-boundary synchronization hook private to the module's test build rather than exposing production mutable state.
The module owns dispatch admission and drain accounting only. The command journal, transport coalescing, target state, snapshot penalty and SnapshotXCTestPenaltyWarmupExemption keep their existing owners. applyMainOwnedSnapshotState retains its deferred-write policy and calls this interface; it is not silently changed into optional work.
Invariants and measurable completion
- Before extraction, record the existing tests and counter fields. Afterwards, no caller reads or writes the scheduler's lock/counters directly; every dispatch and optional-frame admission crosses the new interface.
- Preserve ordinary in-flight versus watchdog-abandoned work. Optional work refuses either; wire busy/wedged reports abandonment only. An inline status response is not proof that a command completed.
- Timeout cannot cancel XCTest. An abandoned block remains counted until actual drain; the last drain clears the abandonment timestamp.
- A result published before the timeout decision acquires its lock is returned, including an error result. It must not trigger abandonment or replay a landed action.
- Keep abandonment callbacks outside the lock and exactly once. Preserve synchronous main-thread entry behavior, shared admission locking and existing deadlines; make no latency-improvement claim.
Validation and limits
Move UnitTests/RunnerTests+MainThreadWorkTests.swift with the implementation, retaining timeout-boundary, busy/drain and optional-admission behavior. Keep integration coverage in CommandDispatchTests, SnapshotCapturePlanOccupancyTests, SnapshotCaptureTargetTests and RecordingTests. Plant the completed-at-timeout regression and an optional-admission violation and observe the owning tests fail before restoring the implementation.
Run pnpm check:xctest-selection, unit-enabled AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1 pnpm build:xcuitest:ios and the corresponding macOS build, then the selected XCTest cases through the repository's existing host/iOS lanes. Run pnpm check:packaged-runner-swift and pnpm check:affected --run on the final head. Preserve source-guard lane discovery; report unavailable native/device evidence explicitly. No async/actor scheduler rewrite, wire change, retry-policy change or whole-runner refactor belongs here.
Source paths in this issue are rooted at apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/; extension names abbreviate the RunnerTests+ prefix where stated. Tests live in its UnitTests/ directory. For a fresh worktree, run pnpm install --frozen-lockfile && pnpm build before the focused checks. Read docs/agents/testing.md and docs/agents/device-verification.md for native lane selection and build freshness.
Encapsulate the runner's bounded main-thread work and occupancy
Deletion-first acceptance
This is part of #2803's simplify-and-shrink initiative. Before production simplification, name the production mechanism, duplicated decision, state, fallback, forwarding layer, or public/internal interface this change removes. Compare deletion and inlining against extraction. Moving the same state into another file is not a deletion case.
Every PR reports baseline/final SHAs; added, deleted and net production lines; test/fixture/documentation changes separately; and the exact mechanisms removed. Use a rename-aware diff (
git diff --numstat -M BASE...HEAD) and account for all affected production paths, including destination packages. Moving tests out of production files, generated output, formatting churn, and moving code elsewhere are not production-code deletion. Preserve readable code; no compressed formatting to hit a number.Default for simplification work: net production reduction. A required behavior-preserving size split may be line-neutral. Growth needs an explicit, quantified explanation of the correctness or type guarantee it buys, why deletion/inlining cannot achieve it, and what old mechanism is removed; it is not automatically acceptable because ownership looks cleaner. If the only benefit is relocating fields or adding a wrapper, defer the change. Required regression tests and a minimal correctness fix are allowed to grow without inventing unrelated deletion to offset them.
Priority and scope gate
Readiness: deferred; remove
ready-for-agent. No scheduling defect has been demonstrated here. Resume only when a concrete design deletes redundant state/coordination or a measured problem makes this extraction necessary; merely moving locks and counters from XCTestCase is insufficient. Compare a smaller in-place private-state solution first. The preserved design below is a candidate, not an implementation commitment. Run shared runner Swift work one issue at a time.Parent: #2803. Readiness: deferred pending a concrete deletion or correctness case. Evidence baseline:
5712a3552e209c9c853bc1f42cb3e007f1213139. Recheck current main before starting. #2911 already supplies actor isolation; #2882 owns further actor source guards.Problem and evidence
The scheduler already has the right behavior, but its state is owned by the entire XCTest case.
RunnerTests.swift:105–113exposes its lock, in-flight and abandoned counts, abandonment timestamp and wedge threshold to every extension.RunnerTests+MainThreadWork.swift:12–197contains the cohesive execution mechanism. Snapshot capture, command dispatch, transport stamping and recording consume it, so accidentally changing its accounting affects multiple command families.The issue is ownership, not a demonstrated scheduling failure. #2837 already fixed the timeout-boundary race; #2911 already made submitted work main-actor isolated. Preserve both.
Implementation contract
Create one concrete
RunnerMainThreadWorkmodule and matching tests. Move the lock, counters, abandonment timestamp, wedge threshold and per-dispatch completion state into private storage. Keep one instance onRunnerTests; remove the old fields and superseded extension implementation in the same change.The small interface is bounded
run, optionalrunIfIdle, and an occupancy query returning idle/busy/wedged with elapsed abandonment information. Preserve@MainActorwork closures, timeout-error construction and the operation-specific abandonment callback. A derived abandoned-work predicate may use the same snapshot. Do not expose lock/counter setters or introduce a protocol with one implementation. Logging and existing error codes remain unchanged. Keep the existing test-only timeout-boundary synchronization hook private to the module's test build rather than exposing production mutable state.The module owns dispatch admission and drain accounting only. The command journal, transport coalescing, target state, snapshot penalty and
SnapshotXCTestPenaltyWarmupExemptionkeep their existing owners.applyMainOwnedSnapshotStateretains its deferred-write policy and calls this interface; it is not silently changed into optional work.Invariants and measurable completion
Validation and limits
Move
UnitTests/RunnerTests+MainThreadWorkTests.swiftwith the implementation, retaining timeout-boundary, busy/drain and optional-admission behavior. Keep integration coverage inCommandDispatchTests,SnapshotCapturePlanOccupancyTests,SnapshotCaptureTargetTestsandRecordingTests. Plant the completed-at-timeout regression and an optional-admission violation and observe the owning tests fail before restoring the implementation.Run
pnpm check:xctest-selection, unit-enabledAGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1 pnpm build:xcuitest:iosand the corresponding macOS build, then the selected XCTest cases through the repository's existing host/iOS lanes. Runpnpm check:packaged-runner-swiftandpnpm check:affected --runon the final head. Preserve source-guard lane discovery; report unavailable native/device evidence explicitly. No async/actor scheduler rewrite, wire change, retry-policy change or whole-runner refactor belongs here.Source paths in this issue are rooted at
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/; extension names abbreviate theRunnerTests+prefix where stated. Tests live in itsUnitTests/directory. For a fresh worktree, runpnpm install --frozen-lockfile && pnpm buildbefore the focused checks. Readdocs/agents/testing.mdanddocs/agents/device-verification.mdfor native lane selection and build freshness.