diff --git a/.github/workflows/xctest-nightly.yml b/.github/workflows/xctest-nightly.yml index 91ba7a5648..39ae0d1783 100644 --- a/.github/workflows/xctest-nightly.yml +++ b/.github/workflows/xctest-nightly.yml @@ -126,10 +126,8 @@ jobs: # (packages/platform-apple/src/runner/runner-session.ts always passes it as the sole # `-only-testing:`). It compiles unconditionally — the `#if AGENT_DEVICE_RUNNER_UNIT_TESTS` # block ends well above it — so an unfiltered run reaches it in alphabetical order and - # hangs the job until `timeout-minutes`. Its one escape hatch, - # AGENT_DEVICE_RUNNER_NOOP_STARTUP, is an environment variable, and the comment above - # the flag records that env plumbing into a simulator test process is not dependable; - # `-skip-testing:` is the lever that works from here. + # hangs the job until `timeout-minutes`. The method has no in-process escape hatch, so + # `-skip-testing:` is the lever that keeps it out of this run. # # A typo in that identifier silently re-arms the hang, so # `pnpm check:xctest-selection` validates `-skip-testing:` exactly like `-only-testing:`. diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift index 9d41034d68..354ffba76c 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift @@ -55,14 +55,10 @@ extension RunnerTests { static func privateAXCustomActionCoverage(_ raw: Any?) -> SnapshotCustomActionCoverage? { guard let coverage = raw as? [String: Any], let read = (coverage[RunnerAXSnapshotCustomActionsReadKey] as? NSNumber)?.intValue, - let candidates = (coverage[RunnerAXSnapshotCustomActionsCandidatesKey] as? NSNumber)?.intValue + let candidates = (coverage[RunnerAXSnapshotCustomActionsCandidatesKey] as? NSNumber)?.intValue, + let truncated = (coverage[RunnerAXSnapshotCustomActionsTruncatedKey] as? NSNumber)?.intValue, + let blocked = (coverage[RunnerAXSnapshotCustomActionsBlockedKey] as? NSNumber)?.boolValue else { return nil } - // read/candidates are the ratio and must both be present; the truncation - // count is additive, so an older bridge that omits it reads as zero rather - // than voiding the whole coverage. - let truncated = - (coverage[RunnerAXSnapshotCustomActionsTruncatedKey] as? NSNumber)?.intValue ?? 0 - let blocked = (coverage[RunnerAXSnapshotCustomActionsBlockedKey] as? NSNumber)?.boolValue ?? false return SnapshotCustomActionCoverage( read: read, candidates: candidates, truncated: truncated, blocked: blocked) } @@ -513,19 +509,28 @@ extension RunnerTests { /// The disclosure only exists if the counts survive the bridge boundary, and /// "did not ask" must stay distinguishable from "read none". func testCustomActionCoverageParsesOnlyCompletePairs() { - let coverage = Self.privateAXCustomActionCoverage([ + let complete: [String: Any] = [ RunnerAXSnapshotCustomActionsReadKey: 12, RunnerAXSnapshotCustomActionsCandidatesKey: 19, - ]) + RunnerAXSnapshotCustomActionsTruncatedKey: 2, + RunnerAXSnapshotCustomActionsBlockedKey: true, + ] + let coverage = Self.privateAXCustomActionCoverage(complete) XCTAssertEqual(coverage?.read, 12) XCTAssertEqual(coverage?.candidates, 19) + XCTAssertEqual(coverage?.truncated, 2) + XCTAssertEqual(coverage?.blocked, true) // Absent key = the capture never asked; it must not read as (0, 0), which // would warn "0 of 0" on every default capture. XCTAssertNil(Self.privateAXCustomActionCoverage(nil)) - // A half-present pair cannot express a ratio, so it is dropped whole. - XCTAssertNil( - Self.privateAXCustomActionCoverage([RunnerAXSnapshotCustomActionsReadKey: 12])) + // The bridge in this target always writes all four keys, so a partial + // dictionary is malformed and is dropped whole. + for key in complete.keys { + var partial = complete + partial.removeValue(forKey: key) + XCTAssertNil(Self.privateAXCustomActionCoverage(partial), "missing \(key)") + } } /// The AX call cannot be cancelled once issued, so the read deadline frees diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift index b4e50a3ab5..1f8477482b 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift @@ -824,7 +824,7 @@ extension RunnerTests { } #endif - func testExecuteDispatchedReturnsBusyBeforeMainThreadFastPath() throws { + func testDispatchReturnsBusyBeforeQueueingMainThreadWork() throws { let command = try runnerCommandFixture(#"{"command":"snapshot","commandId":"snapshot-busy"}"#) abandonedMainThreadWorkCount = 1 abandonedMainThreadWorkSince = Date(timeIntervalSinceNow: -2) @@ -833,14 +833,14 @@ extension RunnerTests { abandonedMainThreadWorkSince = nil } - let response = try executeDispatched(command: command) + let response = try execute(command: command) XCTAssertFalse(response.ok) XCTAssertEqual(response.error?.code, "RUNNER_BUSY") XCTAssertTrue(response.error?.message.contains("previous command") == true) } - func testExecuteDispatchedReturnsWedgedBeforeMainThreadFastPath() throws { + func testDispatchReturnsWedgedBeforeQueueingMainThreadWork() throws { let command = try runnerCommandFixture(#"{"command":"snapshot","commandId":"snapshot-wedged"}"#) abandonedMainThreadWorkCount = 1 abandonedMainThreadWorkSince = Date(timeIntervalSinceNow: -(mainThreadWedgeThreshold + 1)) @@ -849,7 +849,7 @@ extension RunnerTests { abandonedMainThreadWorkSince = nil } - let response = try executeDispatched(command: command) + let response = try execute(command: command) XCTAssertFalse(response.ok) XCTAssertEqual(response.error?.code, "RUNNER_WEDGED") @@ -936,15 +936,32 @@ extension RunnerTests { #endif #if AGENT_DEVICE_RUNNER_UNIT_TESTS + /// Routes `command` through the transport's inline and queued paths. The calling test's main + /// thread serves the command's main-thread work while it waits. func execute(command: Command) throws -> Response { - if command.command == .status { - return executeStatus(command: command) + dispatchPrecondition(condition: .onQueue(.main)) + if let response = inlineResponse(for: command) { + return response } - if command.command == .uptime { - return executeUptime() + final class ResultBox { + var result: Result? } - commandJournal.accept(command: command) - return try executeAccepted(command: command) + let box = ResultBox() + let executed = XCTestExpectation(description: "\(command.command.rawValue) executed off main") + enqueueAccepted(command: command) { result in + box.result = result + executed.fulfill() + } + guard XCTWaiter.wait(for: [executed], timeout: mainThreadExecutionTimeout + 5) == .completed, + let result = box.result + else { + throw NSError( + domain: RunnerErrorDomain.general, + code: RunnerErrorCode.commandReturnedNoResponse, + userInfo: [NSLocalizedDescriptionKey: "command did not finish on the command queue"] + ) + } + return try result.get() } #endif @@ -1063,14 +1080,6 @@ extension RunnerTests { let alertDeadline = command.command == .alert ? Date().addingTimeInterval(Self.alertCommandTimeout(timeoutMs: command.timeoutMs)) : nil - if Thread.isMainThread { - let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(command) - return try executeOnMainSafely( - command: command, - alertDeadline: alertDeadline, - routeToSpringboard: routeToSpringboard - ) - } // Resolve this before the command's outer main-thread block. If the bounded probe abandons // slow XCTest enumeration, return the established recoverable response instead of queueing // command preparation behind work that may outlive the 30-second command watchdog. @@ -1300,7 +1309,7 @@ extension RunnerTests { private func executeSnapshotPrepared( command: Command, activeApp: XCUIApplication, - systemSurface: SystemSurfaceHost? = nil + systemSurface: SystemSurfaceHost? ) throws -> Response { let options = Self.presentationOptions(from: command) do { @@ -1332,10 +1341,6 @@ extension RunnerTests { } private func setNeedsPostSnapshotInteractionDelay() { - if Thread.isMainThread { - needsPostSnapshotInteractionDelay = true - return - } guard !hasAbandonedMainThreadWork() else { NSLog("AGENT_DEVICE_RUNNER_POST_SNAPSHOT_DELAY_MARK_SKIPPED_XCTEST_OCCUPIED") return @@ -1354,10 +1359,6 @@ extension RunnerTests { } private func invalidateCachedTargetAfterSnapshotFailure() { - if Thread.isMainThread { - invalidateCachedTarget(reason: "ax_snapshot_failure") - return - } // Abandoned work ahead of this hop cannot be cancelled: queue the drop behind it without // waiting, so the failed capture answers now and the next command still finds the target gone. guard !hasAbandonedMainThreadWork() else { @@ -1633,7 +1634,8 @@ extension RunnerTests { clearRememberedTextEntryTap() } switch command.command { - case .status, .activate, .terminate, .targetReset, .shutdown, .recordStart, .recordStop, .uptime: + case .status, .activate, .terminate, .targetReset, .shutdown, .recordStart, .recordStop, .uptime, + .snapshot: return Response( ok: false, error: ErrorPayload( @@ -2049,8 +2051,6 @@ extension RunnerTests { return Response(ok: false, error: ErrorPayload(message: "readText did not resolve text")) } return Response(ok: true, data: DataPayload(text: text)) - case .snapshot: - return try executeSnapshotPrepared(command: command, activeApp: activeApp) case .screenshot: #if os(macOS) // macOS keeps the app-targeted capture behavior for window-level screenshots. @@ -2116,11 +2116,10 @@ extension RunnerTests { inlineScreenshot: command.inlineScreenshot == true ) #endif - case .back, .backInApp: + case .backInApp: switch tapInAppBackControl(app: activeApp) { case .performed: - let message = command.command == .back ? "back" : "backInApp" - return Response(ok: true, data: DataPayload(message: message)) + return Response(ok: true, data: DataPayload(message: "backInApp")) case .unavailable: return Response( ok: false, @@ -2612,15 +2611,6 @@ extension RunnerTests { } #endif let probeDeadline = Date().addingTimeInterval(systemModalProbeBudget) - // `runMainThreadWork` executes inline for a main-thread caller, so that path cannot use its - // timeout machinery. Direct main-thread dispatch keeps the prior synchronous modal check; - // normal off-main command dispatch uses the bounded probe and post-probe busy recovery. - if Thread.isMainThread { - return firstBlockingSystemModal( - in: springboard, - deadline: probeDeadline - ) != nil - } return boundedBlockingSystemAlertSnapshot( deadline: probeDeadline ) != nil @@ -2668,15 +2658,12 @@ extension RunnerTests { let textEntryMode = resolveTextEntryMode(command) let target: TextEntryTarget var resolvedCoordinateContext: SynthesizedCoordinateContext? - var maestroNonHittableCoordinateFallbackUsed: Bool? - if command.allowNonHittableCoordinateFallback == true, - command.x != nil, - command.y != nil - { - // The shared runtime has already resolved this node as non-hittable and - // deliberately selected Maestro's coordinate compatibility route. - maestroNonHittableCoordinateFallbackUsed = true - } + // The shared runtime has already resolved this node as non-hittable and + // deliberately selected Maestro's coordinate compatibility route. + let maestroNonHittableCoordinateFallbackUsed: Bool? = + command.allowNonHittableCoordinateFallback == true && command.x != nil && command.y != nil + ? true + : nil let focusStartedAt = Date() #if os(iOS) let xCTestChannelPenalized = isSnapshotXCTestChannelPenalized(bundleId: currentBundleId) @@ -2721,28 +2708,6 @@ extension RunnerTests { #endif if let resolvedCoordinateTarget { target = resolvedCoordinateTarget - } else if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue { - // Released daemons may still send selector-keyed type commands even though current - // daemons resolve fill selectors through the runtime tree before reaching the runner. - let match = findElement( - app: activeApp, - selectorKey: selectorKey, - selectorValue: selectorValue, - allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true - ) - if match.isAmbiguous { - return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements")) - } - guard let element = match.element else { - return Response(ok: false, error: ErrorPayload(code: "NO_MATCH", message: "selector did not match an element")) - } - guard isTextEntryElement(element) else { - return Response(ok: false, error: ErrorPayload(code: "INVALID_TARGET", message: "selector did not match a text input")) - } - if command.allowNonHittableCoordinateFallback == true { - maestroNonHittableCoordinateFallbackUsed = match.usedNonHittableFallback - } - target = focusTextInputForTextEntry(app: activeApp, element: element) } else { target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y) } diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift index f15acf8f9b..9ed3b9f51f 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift @@ -136,7 +136,7 @@ final class RunnerCommandJournal { case .snapshot, .screenshot: return false case .tap, .mouseClick, .longPress, .drag, - .remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back, + .remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .backInApp, .backSystem, .home, .rotate, .appSwitcher, .actionButton, .keyboardDismiss, .keyboardReturn, .alert, .sequence, .gesture, .gestureViewport, .recordStart, .recordStop, .status, .uptime, .activate, .terminate, .targetReset, .shutdown: diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift index 610c3384c6..0ae4f1f1c9 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift @@ -15,16 +15,4 @@ enum RunnerEnv { } return 0 } - - static func isTruthy(_ name: String) -> Bool { - guard let raw = ProcessInfo.processInfo.environment[name] else { - return false - } - switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { - case "1", "true", "yes", "on": - return true - default: - return false - } - } } diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift index 991b554053..57bde60d26 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift @@ -426,10 +426,7 @@ extension RunnerTests { } func shouldRetryCommand(_ command: Command) -> Bool { - if RunnerEnv.isTruthy("AGENT_DEVICE_RUNNER_DISABLE_READONLY_RETRY") { - return false - } - return isReadOnlyCommand(command) + isReadOnlyCommand(command) } func shouldRetryException(_ command: Command, message: String) -> Bool { diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift index 81068ab004..4ddd306b16 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift @@ -17,7 +17,6 @@ enum CommandType: String, Codable { case readText case snapshot case screenshot - case back case backInApp case backSystem case home @@ -78,7 +77,7 @@ extension CommandType { // classification as .drag. .desktopScroll is the macOS frame-resolve + wheel event sibling. // .sequence is the fused multi-step gesture batch. case .tap, .longPress, .drag, .remotePress, .type, .swipe, .scroll, .desktopScroll, - .back, .backInApp, .backSystem, .rotate, .appSwitcher, + .backInApp, .backSystem, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn, .sequence, .gesture: return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false) diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift index 5618d78358..c05d3fb2f9 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift @@ -69,15 +69,7 @@ extension RunnerTests { in springboard: XCUIApplication, deadline: Date = .distantFuture ) -> XCUIElement? { - let disableSafeProbe = RunnerEnv.isTruthy("AGENT_DEVICE_RUNNER_DISABLE_SAFE_MODAL_PROBE") - let queryElements: (() -> [XCUIElement]) -> [XCUIElement] = { fetch in - if disableSafeProbe { - return fetch() - } - return self.safeElementsQuery(fetch) - } - - let alerts = queryElements { + let alerts = safeElementsQuery { springboard.alerts.allElementsBoundByIndex } for alert in alerts { @@ -92,7 +84,7 @@ extension RunnerTests { return nil } - let sheets = queryElements { + let sheets = safeElementsQuery { springboard.sheets.allElementsBoundByIndex } for sheet in sheets { diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift index da780c8270..6d49503938 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift @@ -1,8 +1,8 @@ import XCTest // Text-entry target acquisition: choosing the element a `type`/`fill` will address and getting -// focus onto it — the one-shot tap witness, the post-tap stabilization, and the two -// `focusTextInputForTextEntry` entry points the command layer calls. Whether that target is ready +// focus onto it — the one-shot tap witness, the post-tap stabilization, and the +// `focusTextInputForTextEntry` entry point the command layer calls. Whether that target is ready // is RunnerTests+TextEntryReadiness.swift's question, and this file asks it rather than answering // it. extension RunnerTests { @@ -152,41 +152,6 @@ extension RunnerTests { ) } - func focusTextInputForTextEntry(app: XCUIApplication, element: XCUIElement) -> TextEntryTarget { - let point = textEntryRefreshPoint(for: element) - let keyboardVisibleBeforeTap = isKeyboardVisible(app: app) - if let point { - _ = tapAt(app: app, x: point.x, y: point.y) - } - // See the coordinate-target path above: direct element typing keeps this scoped to the - // tapped target, while the first-character warmup and final verify still catch dropped input. - if keyboardVisibleBeforeTap { - return TextEntryTarget( - element: element, - refreshPoint: textEntryRefreshPoint(for: element) ?? point, - prefersFocusedElement: false - ) - } - let stabilized = stabilizeTextInputBeforeTyping( - app: app, - target: element, - keyboardVisibleBeforeTap: keyboardVisibleBeforeTap - ) - let readyTarget = TextEntryTarget( - element: stabilized.element ?? element, - refreshPoint: point, - prefersFocusedElement: false - ) - let resolved = stabilized.focusConfirmed - ? (stabilized.element ?? element) - : (waitForTextEntryReadiness(app: app, target: readyTarget) ?? stabilized.element ?? element) - return TextEntryTarget( - element: resolved, - refreshPoint: textEntryRefreshPoint(for: resolved) ?? point, - prefersFocusedElement: false - ) - } - private func textEntryRefreshPoint(for element: XCUIElement?) -> CGPoint? { guard let element else { return nil diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift index 5545ab6b9c..e694f41baa 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift @@ -108,12 +108,8 @@ extension RunnerTests { do { let command = try JSONDecoder().decode(Command.self, from: body) - if command.command == .status { - completion((jsonResponse(status: 200, response: executeStatus(command: command)), false)) - return - } - if command.command == .uptime { - completion((jsonResponse(status: 200, response: executeUptime()), false)) + if let response = inlineResponse(for: command) { + completion((jsonResponse(status: 200, response: response), false)) return } // Re-sends of a still-executing commandId (the daemon's transport retry loop) attach to @@ -127,10 +123,9 @@ extension RunnerTests { command.command.rawValue, command.commandId ?? "" ) - commandJournal.accept(command: command) - commandExecutionQueue.async { - do { - let response = try self.executeAccepted(command: command) + enqueueAccepted(command: command) { result in + switch result { + case .success(let response): NSLog( "AGENT_DEVICE_RUNNER_COMMAND_COMPLETED command=%@ commandId=%@ ok=%d", command.command.rawValue, @@ -142,7 +137,7 @@ extension RunnerTests { result: (self.jsonResponse(status: 200, response: response), command.command == .shutdown), completion: completion ) - } catch { + case .failure(let error): NSLog( "AGENT_DEVICE_RUNNER_COMMAND_FAILED command=%@ commandId=%@ error=%@", command.command.rawValue, @@ -174,6 +169,32 @@ extension RunnerTests { } } + // MARK: - Command Routing + + /// Status and uptime read runner state without entering the journal or the command queue. + func inlineResponse(for command: Command) -> Response? { + switch command.command { + case .status: + return executeStatus(command: command) + case .uptime: + return executeUptime() + default: + return nil + } + } + + /// Journal-accepts `command` and executes it on `commandExecutionQueue`; `completion` runs on that + /// queue. + func enqueueAccepted( + command: Command, + completion: @escaping (Result) -> Void + ) { + commandJournal.accept(command: command) + commandExecutionQueue.async { + completion(Result { try self.executeAccepted(command: command) }) + } + } + // MARK: - In-Flight Command Coalescing /// Returns true when this send duplicated a still-executing commandId and was attached as a diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift index afee02d03b..0785df2b96 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift @@ -270,11 +270,6 @@ final class RunnerTests: XCTestCase { @MainActor func testCommand() throws { - if RunnerEnv.isTruthy("AGENT_DEVICE_RUNNER_NOOP_STARTUP") { - NSLog("AGENT_DEVICE_RUNNER_NOOP_STARTUP=1") - return - } - doneExpectation = expectation(description: "agent-device command handled") NSLog("AGENT_DEVICE_RUNNER_HEADLESS_STARTUP=1") let desiredPort = RunnerEnv.resolvePort() diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+AlertDispatchTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+AlertDispatchTests.swift index 7cbca88ed1..0fe213c883 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+AlertDispatchTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+AlertDispatchTests.swift @@ -6,11 +6,8 @@ extension RunnerTests { final class ResultBox { var routingProbeCount = 0 var resolutionCount = 0 - var response: Response? - var error: Error? } let box = ResultBox() - let finished = expectation(description: "alert dispatch finished") currentApp = springboard currentBundleId = Self.springboardBundleId systemModalProbeOverrideForTesting = { _ in @@ -30,31 +27,19 @@ extension RunnerTests { let command = try runnerCommandFixture( #"{"command":"alert","commandId":"alert-routing-once","appBundleId":"com.apple.springboard","action":"get","timeoutMs":1000}"# ) - DispatchQueue(label: "agent-device.runner.tests.alert-routing").async { - do { - box.response = try self.execute(command: command) - } catch { - box.error = error - } - finished.fulfill() - } - wait(for: [finished], timeout: 2) - XCTAssertNil(box.error) - XCTAssertEqual(box.response?.error?.code, "ALERT_NOT_FOUND") + let response = try execute(command: command) + XCTAssertEqual(response.error?.code, "ALERT_NOT_FOUND") XCTAssertEqual(box.resolutionCount, 1) XCTAssertEqual(box.routingProbeCount, 0) } func testAlertResolutionCannotBypassRequestedDeadline() throws { final class ResultBox { - var error: Error? var observedDeadline: Date? - var commandStartedAt: Date? } let box = ResultBox() let releaseResolution = DispatchSemaphore(value: 0) let resolutionExited = expectation(description: "bounded alert resolution exited") - let commandFinished = expectation(description: "alert command respected its deadline") let command = try runnerCommandFixture( #"{"command":"alert","commandId":"alert-deadline","appBundleId":"com.apple.springboard","action":"get","timeoutMs":500}"# ) @@ -73,26 +58,16 @@ extension RunnerTests { currentBundleId = nil } - DispatchQueue(label: "agent-device.runner.tests.alert-deadline").async { - box.commandStartedAt = Date() - do { - _ = try self.execute(command: command) - } catch { - box.error = error - } - commandFinished.fulfill() - } - - wait(for: [commandFinished], timeout: 1) - let error = box.error as NSError? - XCTAssertEqual(error?.domain, RunnerErrorDomain.general) - XCTAssertEqual(error?.code, RunnerErrorCode.mainThreadExecutionTimedOut) - XCTAssertNotNil(box.observedDeadline) - if let observedDeadline = box.observedDeadline, - let commandStartedAt = box.commandStartedAt - { - XCTAssertEqual(observedDeadline.timeIntervalSince(commandStartedAt), 0.5, accuracy: 0.05) + let commandStartedAt = Date() + // The resolution outlives the 500 ms request, so only the deadline-bounded dispatch can throw + // the main-thread timeout; a bypassed deadline answers ALERT_NOT_FOUND once it returns. + XCTAssertThrowsError(try execute(command: command)) { error in + let error = error as NSError + XCTAssertEqual(error.domain, RunnerErrorDomain.general) + XCTAssertEqual(error.code, RunnerErrorCode.mainThreadExecutionTimedOut) } + let observedDeadline = try XCTUnwrap(box.observedDeadline) + XCTAssertEqual(observedDeadline.timeIntervalSince(commandStartedAt), 0.5, accuracy: 0.05) releaseResolution.signal() wait(for: [resolutionExited], timeout: 1) diff --git a/packages/platform-apple/src/core/tool-provider.ts b/packages/platform-apple/src/core/tool-provider.ts index 0f6f0bd7cc..02d3190073 100644 --- a/packages/platform-apple/src/core/tool-provider.ts +++ b/packages/platform-apple/src/core/tool-provider.ts @@ -29,8 +29,8 @@ export type { export type AppleToolProvider = { runCommand: AppleToolCommandExecutor; - simctl?: AppleXcrunToolProvider; - devicectl?: AppleXcrunToolProvider; + simctl: AppleXcrunToolProvider; + devicectl: AppleXcrunToolProvider; macosHelper?: AppleMacOsHelperProvider; macosHost?: AppleMacOsHostProvider; plist?: ApplePlistProvider; @@ -118,12 +118,10 @@ export async function runXcrun(args: string[], options?: ExecOptions): Promise default: [ 'actionButton', 'appSwitcher', - 'back', 'backInApp', 'backSystem', 'home', diff --git a/packages/platform-apple/src/runner/runner-command-traits.ts b/packages/platform-apple/src/runner/runner-command-traits.ts index 5e3bcce382..75307908c5 100644 --- a/packages/platform-apple/src/runner/runner-command-traits.ts +++ b/packages/platform-apple/src/runner/runner-command-traits.ts @@ -66,7 +66,6 @@ export const RUNNER_COMMAND_TRAITS = { readText: READ_ONLY_TRAITS, snapshot: READ_ONLY_TRAITS, screenshot: READ_ONLY_TRAITS, - back: DEFAULT_TRAITS, backInApp: DEFAULT_TRAITS, backSystem: DEFAULT_TRAITS, home: DEFAULT_TRAITS, diff --git a/packages/platform-apple/src/runner/runner-contract.ts b/packages/platform-apple/src/runner/runner-contract.ts index 2dd42692b7..b7ecaafd14 100644 --- a/packages/platform-apple/src/runner/runner-contract.ts +++ b/packages/platform-apple/src/runner/runner-contract.ts @@ -48,7 +48,6 @@ export type RunnerCommand = { | 'readText' | 'snapshot' | 'screenshot' - | 'back' | 'backInApp' | 'backSystem' | 'home' diff --git a/src/platform-runtime-apple-tool-host.test.ts b/src/platform-runtime-apple-tool-host.test.ts index 55da2a86dd..0f59de4daa 100644 --- a/src/platform-runtime-apple-tool-host.test.ts +++ b/src/platform-runtime-apple-tool-host.test.ts @@ -14,6 +14,11 @@ test('Apple tool host uses a full scoped provider when local xcrun is unavailabl throw new Error('local command fallback is unavailable'); }, whichCommand, + simctl: { + run: async () => { + throw new Error('simctl is unscripted'); + }, + }, devicectl: { run }, }; const host = createAppleToolHost();