Skip to content

Commit f01a3b1

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor/core-lease-scope-contracts
* origin/main: (25 commits) fix(ios): avoid duplicate alert routing queries (#2398) docs(adr): record the #2278 coupling audit and re-trace entry-to-platform hops (#2355) chore(gates): classify #2278 daemon-platform-runtime edges and ratchet handler session authority (#2354) test(capture): shrink the 3x crop fixture to avoid coverage timeouts (#2399) perf(ios): settle the first interaction on a deadline, not a fixed sleep (#2395) feat: support standalone Maestro clearState command (#2366) refactor(capture-kit): complete ADR 0019 end state — relocate snapshot and recording zones (#2385) refactor(ios): delete the unused snapshot plan interface (#2392) fix(conformance): isolate post-tap settling from app launch (#2390) test(remote): assert the proxied snapshot's whole wire conversation (#2387) fix(daemon): stop an attested tenant from downgrading its own isolation (#2386) refactor(ios): prune converged snapshot paths (#2383) refactor(move): move the batch runner and batch policy into @agent-device/command-registry (#2388) fix(remote): let a plain-session client read its own failure record (#2382) refactor(move): move replay divergence vocabulary into @agent-device/ad-replay (#2384) refactor(cli): take the CLI's vocabulary off runtime barrels (#2379) perf(cli): keep scroll vocabulary off gesture runtime (#2378) chore: drop the stale planned-operations closure approval (#2373) fix(gates): stop an eager-closure approval from turning main red on merge (#2375) fix(ios): honor the startup budget through a cold Simulator boot (#2325) ...
2 parents 54d5057 + 78cfc45 commit f01a3b1

358 files changed

Lines changed: 7502 additions & 1275 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.fallowrc.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@
9191
"file": "packages/platform-apple/src/app-resolution-facade.ts",
9292
"exports": ["buildAppNotInstalledError"]
9393
},
94+
{
95+
"comment": "Runner operations reach the daemon only through the memoized loader in src/platform-runtime-apple-application-tools.ts (one dynamic import per specifier, #2314). Fallow cannot connect a member read off that loader's promise to these re-exports; keep the list to the facade members no static import consumes.",
96+
"file": "packages/platform-apple/src/runner-operations-facade.ts",
97+
"exports": [
98+
"detachIosSimulatorRunnerSessionsForShutdown",
99+
"hasLiveIosRunnerSession",
100+
"releaseSpeculativeIosRunnerSessionFor",
101+
"stopAllIosRunnerSessions"
102+
]
103+
},
94104
{
95105
"comment": "Apple install mechanics are reached through the named install-artifact façade. Fallow cannot connect workspace package exports to these source exports; keep this list limited to the actual facade re-exports.",
96106
"file": "packages/platform-apple/src/core/install-artifact.ts",

‎.github/workflows/ios.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ on:
2929
- 'packages/*/src/**/*.test.ts'
3030
- 'packages/*/src/**/__tests__/**/*.ts'
3131
- 'packages/platform-android/src/**'
32-
- 'src/snapshot/snapshot-presentation/android/**'
32+
- 'packages/capture-kit/src/snapshot/snapshot-presentation/android/**'
3333
- 'packages/platform-harmonyos/src/**'
3434
- 'packages/platform-linux/src/**'
3535
- 'packages/platform-vega/src/**'
@@ -153,6 +153,8 @@ jobs:
153153
-xctestrun "$XCTESTRUN_PATH" \
154154
-destination "platform=iOS Simulator,id=${{ steps.ios-simulator.outputs.simulator-udid }}" \
155155
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testSinglePointerFlingFallsBackToXCTestCoordinateDragWhenPrivateSynthesisFails \
156+
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testAlertDispatchResolvesItsOwnModalWithoutCoordinateTapRoutingProbe \
157+
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testAlertResolutionCannotBypassRequestedDeadline \
156158
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testTypeWithoutResolvedInputReturnsTypedFailureBeforeDispatchingText \
157159
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden \
158160
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testHardwareKeyboardResponderConfirmsItsOwnKeyboardFocus \

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
opening the request log. Long waits keep the first five and last twenty-five polls. The replay
1010
landmark-mismatch refusal carries the same poll evidence next to its mismatch details; `wait
1111
--stable` timeouts and a never-readable strict absence keep their existing diagnostics.
12+
- Fixed: Android `orientation` now returns once the display reports the requested rotation
13+
(polling `dumpsys display`, up to 15s) instead of right after writing the settings. On a loaded
14+
emulator the rotation takes seconds, during which accessibility reads hang, so the next command
15+
paid for the transition; a `wait` issued right after `orientation` could spend its whole budget
16+
there. A display that never reaches the requested rotation now fails the command with the
17+
observed rotation instead of reporting success; a display that reports no rotation is left to
18+
the setting as before.
1219
- Fixed: the iOS Simulator AX snapshot route bounds how long a capture waits for app discovery
1320
and stops starting a discovery per capture. Discovery (`simctl launchctl list` through xcrun)
1421
takes seconds on a loaded host; a capture now waits at most 1.5s for the one in-flight

‎CONTEXT.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,25 @@ A device-scoped pause on agent mutations during human operation.
110110
### Commands and routing
111111

112112
**Command surface**:
113-
The catalog of public command identity, interface exposure, adapter policy, and metadata across
114-
entrypoints.
113+
The catalog of public command identity, exposure, adapter policy, and metadata across entrypoints.
115114

116115
**Runtime use**:
117-
A command's platform-neutral declaration of required operations and optional preferred fast paths.
116+
A command's platform-neutral declaration of required operations and preferred fast paths.
118117

119118
**Inventory use**:
120-
An inventory command's platform-neutral declaration for composing device sources without binding one.
119+
An inventory command's platform-neutral declaration for composing device sources unbound.
121120

122121
**Daemon command registry**:
123122
The daemon-side truth for route ownership and request-policy traits.
124123

125124
**Runner command traits**:
126-
Per-command classifications controlling Apple runner lifecycle and recovery behavior
127-
independently of the public command surface.
125+
Per-command classes steering Apple runner lifecycle and recovery, independent of the public surface.
126+
127+
**Runner demand**:
128+
What a Simulator open prepares, or releases unused, of the XCTest runner for remaining steps.
128129

129130
**Daemon RPC protocol version**:
130-
The integer used to detect breaking compatibility across the remote daemon boundary.
131+
The integer that detects breaking compatibility across the remote daemon boundary.
131132

132133
**Version-skew invariant**:
133134
Local client and daemon versions must match; only remote daemons, separately versioned helpers,

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift‎

Lines changed: 5 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -825,70 +825,6 @@ extension RunnerTests {
825825
XCTAssertTrue(response.error?.hint?.contains("runner session will be restarted") == true)
826826
}
827827

828-
#if os(iOS)
829-
func testAlertResolutionCannotBypassRequestedDeadline() throws {
830-
final class ResultBox {
831-
var error: Error?
832-
var probeDeadline: Date?
833-
var observedDeadline: Date?
834-
var commandStartedAt: Date?
835-
}
836-
let box = ResultBox()
837-
let releaseResolution = DispatchSemaphore(value: 0)
838-
let resolutionExited = expectation(description: "bounded alert resolution exited")
839-
let commandFinished = expectation(description: "alert command respected its deadline")
840-
let command = try runnerCommandFixture(
841-
#"{"command":"alert","commandId":"alert-deadline","appBundleId":"com.apple.springboard","action":"get","timeoutMs":500}"#
842-
)
843-
currentApp = springboard
844-
currentBundleId = Self.springboardBundleId
845-
systemModalProbeOverrideForTesting = { deadline in
846-
box.probeDeadline = deadline
847-
return nil
848-
}
849-
alertResolutionOverrideForTesting = { deadline in
850-
box.observedDeadline = deadline
851-
_ = releaseResolution.wait(timeout: .now() + 1)
852-
resolutionExited.fulfill()
853-
return nil
854-
}
855-
defer {
856-
releaseResolution.signal()
857-
systemModalProbeOverrideForTesting = nil
858-
alertResolutionOverrideForTesting = nil
859-
currentApp = nil
860-
currentBundleId = nil
861-
}
862-
863-
DispatchQueue(label: "agent-device.runner.tests.alert-deadline").async {
864-
box.commandStartedAt = Date()
865-
do {
866-
_ = try self.executeDispatched(command: command)
867-
} catch {
868-
box.error = error
869-
}
870-
commandFinished.fulfill()
871-
}
872-
873-
wait(for: [commandFinished], timeout: 1)
874-
let error = box.error as NSError?
875-
XCTAssertEqual(error?.domain, RunnerErrorDomain.general)
876-
XCTAssertEqual(error?.code, RunnerErrorCode.mainThreadExecutionTimedOut)
877-
XCTAssertNotNil(box.probeDeadline)
878-
XCTAssertNotNil(box.observedDeadline)
879-
if let probeDeadline = box.probeDeadline,
880-
let observedDeadline = box.observedDeadline,
881-
let commandStartedAt = box.commandStartedAt
882-
{
883-
XCTAssertEqual(probeDeadline.timeIntervalSince(observedDeadline), 0, accuracy: 0.01)
884-
XCTAssertEqual(observedDeadline.timeIntervalSince(commandStartedAt), 0.5, accuracy: 0.05)
885-
}
886-
887-
releaseResolution.signal()
888-
wait(for: [resolutionExited], timeout: 1)
889-
}
890-
#endif
891-
892828
func testRunMainThreadWorkExecutesOffMainCallerOnMainThread() {
893829
final class ResultBox {
894830
var observedMainThread: Bool?
@@ -1135,10 +1071,7 @@ extension RunnerTests {
11351071
? Date().addingTimeInterval(Self.alertCommandTimeout(timeoutMs: command.timeoutMs))
11361072
: nil
11371073
if Thread.isMainThread {
1138-
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(
1139-
command,
1140-
deadline: alertDeadline
1141-
)
1074+
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(command)
11421075
return try executeOnMainSafely(
11431076
command: command,
11441077
alertDeadline: alertDeadline,
@@ -1148,10 +1081,7 @@ extension RunnerTests {
11481081
// Resolve this before the command's outer main-thread block. If the bounded probe abandons
11491082
// slow XCTest enumeration, return the established recoverable response instead of queueing
11501083
// command preparation behind work that may outlive the 30-second command watchdog.
1151-
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(
1152-
command,
1153-
deadline: alertDeadline
1154-
)
1084+
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(command)
11551085
if let unavailable = runnerUnavailableResponse(command: command) {
11561086
return unavailable
11571087
}
@@ -2652,20 +2582,18 @@ extension RunnerTests {
26522582
}
26532583

26542584
private func shouldRouteToSpringboardBlockingSystemModal(
2655-
_ command: Command,
2656-
deadline: Date? = nil
2585+
_ command: Command
26572586
) -> Bool {
26582587
#if os(iOS)
2659-
guard command.command == .alert || isCoordinateOnlyTap(command) else {
2588+
guard isCoordinateOnlyTap(command) else {
26602589
return false
26612590
}
26622591
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
26632592
if let override = blockingSystemModalPresenceOverrideForTesting {
26642593
return override
26652594
}
26662595
#endif
2667-
let budgetDeadline = Date().addingTimeInterval(systemModalProbeBudget)
2668-
let probeDeadline = deadline.map { min($0, budgetDeadline) } ?? budgetDeadline
2596+
let probeDeadline = Date().addingTimeInterval(systemModalProbeBudget)
26692597
// `runMainThreadWork` executes inline for a main-thread caller, so that path cannot use its
26702598
// timeout machinery. Direct main-thread dispatch keeps the prior synchronous modal check;
26712599
// normal off-main command dispatch uses the bounded probe and post-probe busy recovery.

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ extension RunnerTests {
108108
// old PID before refreshCachedTargetIfProcessChanged can observe it.
109109
clearSnapshotXCTestChannelPenalty(reason: "external_app_relaunch")
110110
clearPrivateAXAcceptedDepth(reason: "external_app_relaunch")
111-
needsFirstInteractionDelay = true
111+
beginFirstInteractionStabilization()
112112
return Response(ok: true, data: DataPayload(message: "target reset"))
113113
}
114114

@@ -132,7 +132,7 @@ extension RunnerTests {
132132
clearSnapshotXCTestChannelPenalty(reason: "target_process_changed")
133133
clearPrivateAXAcceptedDepth(reason: "target_process_changed")
134134
snapshotXCTestPenaltyWarmupExemptionPending = true
135-
needsFirstInteractionDelay = true
135+
beginFirstInteractionStabilization()
136136
}
137137

138138
static func processIdentifier(of target: XCUIApplication) -> Int? {
@@ -205,7 +205,7 @@ extension RunnerTests {
205205
currentAppProcessIdentifier = Self.processIdentifier(of: target)
206206
clearRememberedTextEntryTap()
207207
snapshotXCTestPenaltyWarmupExemptionPending = false
208-
needsFirstInteractionDelay = true
208+
beginFirstInteractionStabilization()
209209
return target
210210
}
211211

@@ -320,12 +320,19 @@ extension RunnerTests {
320320
sleepFor(postSnapshotInteractionDelay)
321321
needsPostSnapshotInteractionDelay = false
322322
}
323-
if needsFirstInteractionDelay {
324-
sleepFor(firstInteractionAfterActivateDelay)
325-
needsFirstInteractionDelay = false
323+
if let readyUptime = firstInteractionReadyUptime {
324+
sleepFor(readyUptime - ProcessInfo.processInfo.systemUptime)
325+
firstInteractionReadyUptime = nil
326326
}
327327
}
328328

329+
/// Start the post-activation settling window. Measured from now, so the time the caller spends
330+
/// getting back to us counts towards it instead of being charged twice.
331+
func beginFirstInteractionStabilization() {
332+
firstInteractionReadyUptime =
333+
ProcessInfo.processInfo.systemUptime + firstInteractionAfterActivateDelay
334+
}
335+
329336
func sleepFor(_ delay: TimeInterval) {
330337
guard delay > 0 else { return }
331338
// Keep XCTest/UI sources moving during command-local pauses such as delayed typing.

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ final class RunnerTests: XCTestCase {
6666
let minRecordingFps = 1
6767
let maxRecordingFps = 120
6868
var needsPostSnapshotInteractionDelay = false
69-
var needsFirstInteractionDelay = false
69+
/// When the first interaction after an activation may run, on the monotonic uptime clock.
70+
/// The guarantee is a minimum gap *since the activation*, not a pause at the interaction:
71+
/// a caller that already spent that gap elsewhere (an agent's round trip is 190-260 ms)
72+
/// has satisfied it and waits for nothing. `nil` = no activation is pending stabilization.
73+
var firstInteractionReadyUptime: TimeInterval?
7074
var runnerAccessibilityHealth: RunnerAccessibilityHealth = .unknown
7175
var activeRecording: ScreenRecorder?
7276
let commandJournal = RunnerCommandJournal()
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import XCTest
2+
3+
#if AGENT_DEVICE_RUNNER_UNIT_TESTS && os(iOS)
4+
extension RunnerTests {
5+
func testAlertDispatchResolvesItsOwnModalWithoutCoordinateTapRoutingProbe() throws {
6+
final class ResultBox {
7+
var routingProbeCount = 0
8+
var resolutionCount = 0
9+
var response: Response?
10+
var error: Error?
11+
}
12+
let box = ResultBox()
13+
let finished = expectation(description: "alert dispatch finished")
14+
currentApp = springboard
15+
currentBundleId = Self.springboardBundleId
16+
systemModalProbeOverrideForTesting = { _ in
17+
box.routingProbeCount += 1
18+
return nil
19+
}
20+
alertResolutionOverrideForTesting = { _ in
21+
box.resolutionCount += 1
22+
return nil
23+
}
24+
defer {
25+
systemModalProbeOverrideForTesting = nil
26+
alertResolutionOverrideForTesting = nil
27+
currentApp = nil
28+
currentBundleId = nil
29+
}
30+
let command = try runnerCommandFixture(
31+
#"{"command":"alert","commandId":"alert-routing-once","appBundleId":"com.apple.springboard","action":"get","timeoutMs":1000}"#
32+
)
33+
DispatchQueue(label: "agent-device.runner.tests.alert-routing").async {
34+
do {
35+
box.response = try self.execute(command: command)
36+
} catch {
37+
box.error = error
38+
}
39+
finished.fulfill()
40+
}
41+
wait(for: [finished], timeout: 2)
42+
XCTAssertNil(box.error)
43+
XCTAssertEqual(box.response?.error?.code, "ALERT_NOT_FOUND")
44+
XCTAssertEqual(box.resolutionCount, 1)
45+
XCTAssertEqual(box.routingProbeCount, 0)
46+
}
47+
48+
func testAlertResolutionCannotBypassRequestedDeadline() throws {
49+
final class ResultBox {
50+
var error: Error?
51+
var observedDeadline: Date?
52+
var commandStartedAt: Date?
53+
}
54+
let box = ResultBox()
55+
let releaseResolution = DispatchSemaphore(value: 0)
56+
let resolutionExited = expectation(description: "bounded alert resolution exited")
57+
let commandFinished = expectation(description: "alert command respected its deadline")
58+
let command = try runnerCommandFixture(
59+
#"{"command":"alert","commandId":"alert-deadline","appBundleId":"com.apple.springboard","action":"get","timeoutMs":500}"#
60+
)
61+
currentApp = springboard
62+
currentBundleId = Self.springboardBundleId
63+
alertResolutionOverrideForTesting = { deadline in
64+
box.observedDeadline = deadline
65+
_ = releaseResolution.wait(timeout: .now() + 1)
66+
resolutionExited.fulfill()
67+
return nil
68+
}
69+
defer {
70+
releaseResolution.signal()
71+
alertResolutionOverrideForTesting = nil
72+
currentApp = nil
73+
currentBundleId = nil
74+
}
75+
76+
DispatchQueue(label: "agent-device.runner.tests.alert-deadline").async {
77+
box.commandStartedAt = Date()
78+
do {
79+
_ = try self.execute(command: command)
80+
} catch {
81+
box.error = error
82+
}
83+
commandFinished.fulfill()
84+
}
85+
86+
wait(for: [commandFinished], timeout: 1)
87+
let error = box.error as NSError?
88+
XCTAssertEqual(error?.domain, RunnerErrorDomain.general)
89+
XCTAssertEqual(error?.code, RunnerErrorCode.mainThreadExecutionTimedOut)
90+
XCTAssertNotNil(box.observedDeadline)
91+
if let observedDeadline = box.observedDeadline,
92+
let commandStartedAt = box.commandStartedAt
93+
{
94+
XCTAssertEqual(observedDeadline.timeIntervalSince(commandStartedAt), 0.5, accuracy: 0.05)
95+
}
96+
97+
releaseResolution.signal()
98+
wait(for: [resolutionExited], timeout: 1)
99+
}
100+
}
101+
#endif

0 commit comments

Comments
 (0)