Repository navigation
feat(apple): serve the Simulator AX bridge from the XCTest runner - #4182
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Size Report
Startup median (7 runs, lower is better):
|
c2841b2 to
5445f27
Compare
|
At 5445f27 I found one likely defect in the cancel path and one missing live run. CI is green (17 checks, 0 not passing), and there are no conflicts. Before merge, I found this by reading the code, not by running it, so please confirm it. If a caller cancels a runner-served describe (a cancelled request, or a wait/batch step that aborts its capture), On Limrun, Not blocking, take or leave: the runner budget at runner-snapshot-bridge.ts:152 is computed before What I did not check: whether a timed-out or cancelled read-only runner exchange on the Limrun session triggers runner recovery above |
1a5ca97 to
f165d9c
Compare
5445f27 to
7e0f29c
Compare
|
Thanks. Both points are fixed at 1. 2. Live system-surface run on Limrun at this head. No repo app presents a web sign-in sheet, so I used a 60-line UIKit probe app with two buttons:
Each Limrun sheet capture sent 2 Also changed: Xcode 27 is not expected on Limrun. An Xcode 27 simulator build embeds no XCTest frameworks, so it cannot start on Limrun's iOS 26 simulators. The release workflow now selects Xcode 26, the package script refuses a runner without Non-blocking items: the observe-only comment is corrected; Gate: Still not checked: the native abandoned-read path, and whether a timed-out read-only runner exchange on Limrun triggers runner recovery above |
|
The code looks good at 7e0f29c. The earlier review issues are addressed, and the Limrun system-surface run you posted covers the occluded case: supplied signal and deadline aborts now become request cancellation without triggering ready-session recovery, while unrelated transport failures retain recovery. All 17 checks passed. There are no conflicts. Not blocking: The cancellation assertion can accept an unrelated error; you can take or leave an assertion against cancelled.signal.reason, or a named typed cancellation reason, in the cancellation test: https://github.com/callstack/agent-device/blob/7e0f29c/packages/platform-apple/src/runner-snapshot-bridge.test.ts#L324. |
f165d9c to
00d77a6
Compare
7e0f29c to
fd56bac
Compare
|
The code looks good at 3a4eb6d. The surface proof is now fenced to one runner session, the dead limrun-ios-tree producer is gone, and the refusal codes are guarded against drift. All 16 checks pass and there are no conflicts. The native sources have not changed since fd56bac, so the earlier Limrun and local sheet runs still cover them. Not blocking: You can take or leave extending the emitter guard to enumerate deadline-helper and native-build forwarding paths and classify their codes in the fixture, while keeping bridgeFailureDisposition as the runtime policy owner; the current guard recognizes only literal TypeScript snapshotSourceError calls. Guard |
|
The code at c56942e looks good: a tree that changes during a bridge read is now refused for that capture only. This new native path still needs a live run. CI reports 16 passing checks and no failures. There are no known conflicts. No recorded live run reaches the new refusal on either production channel, so device validation remains incomplete despite green CI (SnapshotBridgeRuntime.m:50). Please rebuild c56942e and record |
c56942e to
bacf32e
Compare
00d77a6 to
24be243
Compare
|
Live evidence for the Local Simulator
Commands (worktree CLI at AD="env AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH=/private/tmp/ad-4182-local2/derived node bin/agent-device.mjs --state-dir /private/tmp/ad-4182-local2/state --session ax4182b --platform ios --udid 1B89356B-961C-4993-B0BE-4C0F3917D995"
$AD open com.callstack.agentdevicelab --relaunch
$AD open com.callstack.agentdevicelab 'agent-device-test-app://heavy-feed'
# repeated until the refusal appeared (iteration 2):
$AD swipe 210 300 210 880
$AD snapshot -i --json --debug
# then, after the scroll came to rest:
$AD snapshot -i --json --debug
1–2. Refusal, then XCTest serves that capture (request 3. Next capture: The refusal had capture scope: the generation was not retired ( Limrun: earlier Xcode 27 attemptAt the time of this attempt, this host had only Xcode 27.1 and iOS 27 runtimes. The local toolchain could not build the release-shaped Limrun runner from this head, and the release runner (v0.21.36) did not contain this PR's native change. The CI build below now clears the toolchain blocker. As a fallback (at AGENT_DEVICE_SESSION=ax4182-limrun AGENT_DEVICE_LIMRUN_IOS_RUNNER_APP=<that .app> \
node bin/agent-device.mjs --state-dir /private/tmp/ad-4182-limrun/state connect limrun --platform ios
… install com.callstack.agentdevicelab ./AgentDeviceTester.app # Installed
… open com.callstack.agentdevicelab # Opened
… snapshot -i --json --debugThe runner never accepted a connection on the Limrun iOS 26 simulator: This matches the Xcode 27 limit that the PR documents. That runner shape is not the shipped one, so this run says nothing about the PR code on Limrun. The Limrun instance I started (lease Update: Xcode 26 CI runner availableBuilt exact source The temporary CI branch was removed after the successful build. No release was created. This clears the build blocker. It does not provide the requested live Limrun evidence: 🤖 Generated with Claude Code |
|
At bacf32e, local live evidence now covers guest-process transport, but the Limrun run could not start the Xcode 27-built runner on its iOS 26 simulator. Could you run this head with an Xcode 26 runner on Limrun and capture CI reports 18 passing checks and no non-passing checks at head. No conflicts are known. Please post the head-specific Limrun evidence before merge. |
|
Update on the remaining Limrun request: the Xcode build blocker is cleared. CI run 41849063216 built exact head I updated the evidence comment to remove the stale toolchain blocker. The requested live Limrun |
24be243 to
0f95de0
Compare
bacf32e to
3f4a65b
Compare
|
Limrun evidence for the
The refusal fires inside the post-swipe stabilization of one The next request ( A second occurrence happened on Bluesky's feed: request A full pass on this head ran Bluesky (signed in) on Limrun and on a local iOS 26.2 Simulator: the full flow, 30 scroll-and-capture cycles, busy/cancel, and the web-sheet probe. Every warm app capture was bridge-served, except the expected typed fallbacks:
No app tree was served under a sheet. There was no circuit retirement and no runner restart. The typical capture took 0.65 s on Limrun and 0.19 s locally. |
There was a problem hiding this comment.
All reported issues were addressed across 84 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
|
At 3f4a65b, the code changes since the earlier review look right, and all 15 checks pass. No conflicts are known. The remaining gap is live evidence for the runner-served bridge. The runner-served bridge (https://github.com/callstack/agent-device/blob/3f4a65b/packages/platform-apple/src/runner-snapshot-bridge.ts#L137) is the main device route this PR changes, and no live run on Limrun reaches it yet. The runner also now scans the Simulator process table itself (SnapshotBridgeSurfaceHosts.m inside the XCTest runner), and no run shows what that scan returns inside the runner sandbox. So the snapshot-tree-changed refusal, the XCTest fallback, the bridge recovery and the new runner-side surface host check are all unverified on a real provider Simulator. On Limrun at 3f4a65b with the Xcode 26 runner artifact, please record Not blocking, and fine to take or leave: in apple/snapshot-bridge/SnapshotBridgeSurfaceHosts.m (https://github.com/callstack/agent-device/blob/3f4a65b/apple/snapshot-bridge/SnapshotBridgeSurfaceHosts.m#L117) an empty candidate set counts as proven absence, so a filtered KERN_PROC_ALL table that returns no error would let the reader serve the occluded app tree, where before this change the runner channel always sent that capture to XCTest; the rule could be that the scan claims absence only from a table it can show is complete, checked in the shared reader for both channels, for example by treating the table as unverified unless it contains the capture target's own pid, which the request already carries as expectedPid. No review threads are open. |
Co-Authored-By: Claude <noreply@anthropic.com>
…drift Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The bridge reader re-reads a node whose children the first read withheld. When a scrolling list recycles its cells between the two reads, the re-read returns fewer children, and the runtime reported every such reader error as snapshot-tree-malformed. The route then retired the app generation, so one scroll moved every later capture of the app to XCTest (seen on Bluesky's feed). The reader errors are now a typed enum, and the runtime maps each one to its refusal: a changed tree is snapshot-tree-changed with capture scope, and a foreground change during a continuation is foreground-owner-changed. Co-Authored-By: Claude <noreply@anthropic.com>
…e guest channel #4172 moved the guest channel's presence check into the bridge request: when no runner can answer the foreground read, the route names the surface host executables, and the reader checks the process table in place. The runner channel now takes the same path instead of a constant 'unknown' presence probe. The runner forwards the executables to the shared reader and names the Simulator it runs in. A sandbox that refuses the process table answers system-surface-host-unverified, so the capture uses XCTest and says why. The surface-host refusal codes join the refusal fixture, and the runner request fixture pins a describe that carries the executables. Co-Authored-By: Claude <noreply@anthropic.com>
…lformed A continuation whose children were not an array shared the tree-changed code, so its capture-scoped refusal never opened the generation circuit. Only a child-count shrink is tree-changed now. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…able rejection Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
… header The guest bridge and the host decoder count the 4-byte length prefix against maxResponseBytes; the runner did not, so an envelope could pass the runner and fail the host. The default bound is the reader's own frame bound, and the reader request adds only the fields the host sent. Co-Authored-By: Claude <noreply@anthropic.com>
A describe request cannot omit the identity it is checked against, and the foreground read always carries the runner session it expects. Adds the observe-only admission and runner budget regression tests. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ips it Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The runner compiles the shared reader for the iOS Simulator only, so the macOS runner unit-test build could not link SnapshotBridgeHandleRequest. Co-Authored-By: Claude <noreply@anthropic.com>
The host reads still go out together, but a decisive first host now answers without waiting for the reads after it. Co-Authored-By: Claude <noreply@anthropic.com>
Off the iOS Simulator the default envelope bound was 0, so the runner's runner-bridge-unavailable refusal came back as response-too-large when a request named no bound. Co-Authored-By: Claude <noreply@anthropic.com>
The wire-vocabulary guard matched SNAPSHOT_BRIDGE_PROTOCOL_VERSION 1 as a substring, so a native protocol version of 10 still passed. It now matches each whole #define line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner sent an absent SIMULATOR_UDID as an empty string, and its bridge handler re-detected that with a second copy of the reader's 64-character UDID bound. The request builder is the only place that knows the UDID is absent, so it now leaves simulatorUdid out, and the handler refuses the surface proof with system-surface-host-unverified when the key is missing. The reader keeps the only UDID validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aeb439d to
da8e78e
Compare
|
Live on the new base:
|
|
At da8e78e, the code looks good. The rebase onto main (with #4224's runner launcher) keeps the bridge on the same The two inline threads on the For the record: the absent- |
|
Post-merge record for Base and rebase. I retargeted the PR from
Review follow-ups (one push):
Gate.
The passing run was at a load average of about 60. Live run at
Threads. All 4 open threads are answered and resolved: the macro anchors and literal 64 are fixed; the runner restart and wedged reader are deferred to #4241. No open threads remain. 🤖 Generated with Claude Code |
Summary
Limrun iOS snapshots read the same Simulator AX bridge as a local Simulator. Limrun cannot start the guest bridge process, so the XCTest runner compiles the shared
apple/snapshot-bridgereader and servessnapshotBridgeoff its XCTest main thread. Both channels share one route: generation circuit, surface-absence proof (runner foreground read, then the reader's own host check), and one typed XCTest fallback.snapshot-tree-changed(a list scrolling between two reads) costs one capture, not the generation. A refusal-code fixture and guard keep Objective-C, Swift and TypeScript in step. The Limrun runner must be built with Xcode 26. ADR 0004 is amended.84 files against
main(which now has #4224's runner switch). Follow-ups: #4205, #4241.Validation
Head
da8e78ee7c, basemain.pnpm check:affected --run --head da8e78ee7c: passed, exactCommit=true (run-dbxS07).appstatebefore and after the runner starts (evidence).snapshot-tree-changed→ XCTest → bridge on one generation on Limrun (evidence); release-shaped runner, Xcode 26.6 (evidence).Risks: the abandoned-read path has unit evidence only (#4241).