Conversation
There was a problem hiding this comment.
10 issues found across 46 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="website/docs/docs/commands.md">
<violation number="1" location="website/docs/docs/commands.md:100">
P2: The documented result shape omits the required `message` field. Include `message: "Screen locked"` so typed and CLI/MCP consumers see the complete `ScreenLockCommandResult` contract.</violation>
</file>
<file name="src/daemon/system-button-runtime.ts">
<violation number="1" location="src/daemon/system-button-runtime.ts:32">
P3: `state` makes the system-button response no longer text-only, but the surrounding comments still claim otherwise. Update the row and resolver documentation to describe the screen-lock state field.</violation>
</file>
<file name="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift">
<violation number="1" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift:61">
P3: The loop checks `shouldContinue()` before each `readState()` and exits without a final read, so a transition that completes during the last `wait()` (within one poll interval of the deadline) is reported as COMMAND_FAILED even though SpringBoard is now locked. Do one final `readState()` after the loop — returning the success path when it reports locked — before giving up on the timeout.</violation>
<violation number="2" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift:83">
P2: `screenLockVisibleResponse` samples visibility only once, including the already-locked path. During boot or lock-screen animation, SpringBoard can report locked before its accessibility frame is populated, so this command fails instead of waiting for both verification conditions to agree; poll visibility until the transition deadline.</violation>
<violation number="3" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift:113">
P2: `verifyLockScreenSurface` only proves that SpringBoard has a non-empty root frame; it never identifies the Lock Screen. After the Darwin state flips, this can report success while another SpringBoard surface is visible or before the Lock Screen is rendered; verify lock-screen-specific window or descendant state.</violation>
</file>
<file name="packages/platform-apple/src/runtime.test.ts">
<violation number="1" location="packages/platform-apple/src/runtime.test.ts:377">
P3: This helper now asserts the `screenLock` fact for every leaf, but the `test.each` title at line 227 still reads 'classifies back/home/app-switcher/orientation/tv-remote/keyboard facts ...', so a `screenLock` failure surfaces under a title that does not name it. Add screen-lock (and the availability names) to the title.</violation>
</file>
<file name="packages/platform-apple/src/navigation/runtime.ts">
<violation number="1" location="packages/platform-apple/src/navigation/runtime.ts:162">
P3: `appleScreenLockFact` duplicates the existing `isHandheldAppleSimulator` predicate, including its simulator and iOS/iPadOS checks. Reuse that helper after the kind-specific refusal so this support rule cannot drift.</violation>
</file>
<file name="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+ScreenLockTests.swift">
<violation number="1" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+ScreenLockTests.swift:41">
P3: These transition tests never exercise the `readState` `.failure(Response)` branch of `executeScreenLockTransition` (initial `switch` and in-loop `switch` both return the wrapped failure verbatim). In production this branch is reachable when `notify_register_check` or `notify_get_state` returns `NOTIFY_STATUS_OK` failure, and the response code it propagates is the same `COMMAND_FAILED` that tests 4 and 5 assert, so a regression in the failure branch (or the wrong message/status) would go undetected. Add one deterministic test injecting a `.failure` read to cover the propagation path and assert dispatch is skipped.</violation>
</file>
<file name="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift">
<violation number="1" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift:257">
P2: Classifying `screenLock` as `.presentedSurfaceMutation` causes unsupported macOS/tvOS requests to activate the session app before `executeScreenLockCommand` returns `UNSUPPORTED_OPERATION`. Keep this command on a no-activation policy so unsupported platform leaves fail without changing app state.</violation>
</file>
<file name="packages/contracts/src/system-button-runtime.ts">
<violation number="1" location="packages/contracts/src/system-button-runtime.ts:12">
P3: The module doc comment still enumerates the buttons as "`home` and `appSwitcher` reach a springboard or recents surface; `actionButton` presses iPhone/iPad hardware", leaving the newly added `screenLock` out of the very list this comment is meant to describe. Extend the comment to cover `screenLock` while it is fresh.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| - A simulator scoped to a non-default set with `--ios-simulator-device-set` is refused before any hinge is touched with `UNSUPPORTED_OPERATION` and `details.reason: "unsupported-device-scope"`. The HID send accepts `--set`, but `devicectl device info displays` and `devicectl device motion hinge-angle` accept only `--device` and resolve a scoped simulator as not found, so the pose could not be read back (ADR 0025). Run `fold` against a simulator in the default set. | ||
| - `fold` costs one bounded hinge stream per read, and devicectl's smallest stream is five seconds: `closed` and `open` take about ten seconds, `half-open` about sixteen, because the hinge animates and the command waits for it to stop. A hinge whose last reading is some other pose fails with `COMMAND_FAILED` and `reason: fold-pose-unverified`, naming the angle CoreDevice still reports. A hinge seen `half-open` but never at rest fails with `reason: fold-pose-unsettled`, naming the observed and previous angles: the requested category was observed, and what is missing is a pose the hinge holds (#2730). | ||
| - `action-button` is not a cheap command to loop. On an iPhone 17 Pro Simulator the press itself spent about five seconds inside XCUITest, while `home` and `app-switcher` on the same session took under two seconds each. | ||
| - `screen-lock` transitions an iPhone or iPad Simulator to its Lock Screen. It is idempotent and returns `{ action: "screen-lock", state: "locked" }` only after both SpringBoard's lock state and a visible, non-empty SpringBoard surface agree. It never means a process mutex, device claim, or runner lease. |
There was a problem hiding this comment.
P2: The documented result shape omits the required message field. Include message: "Screen locked" so typed and CLI/MCP consumers see the complete ScreenLockCommandResult contract.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/docs/docs/commands.md, line 100:
<comment>The documented result shape omits the required `message` field. Include `message: "Screen locked"` so typed and CLI/MCP consumers see the complete `ScreenLockCommandResult` contract.</comment>
<file context>
@@ -96,6 +97,8 @@ agent-device fold open
- A simulator scoped to a non-default set with `--ios-simulator-device-set` is refused before any hinge is touched with `UNSUPPORTED_OPERATION` and `details.reason: "unsupported-device-scope"`. The HID send accepts `--set`, but `devicectl device info displays` and `devicectl device motion hinge-angle` accept only `--device` and resolve a scoped simulator as not found, so the pose could not be read back (ADR 0025). Run `fold` against a simulator in the default set.
- `fold` costs one bounded hinge stream per read, and devicectl's smallest stream is five seconds: `closed` and `open` take about ten seconds, `half-open` about sixteen, because the hinge animates and the command waits for it to stop. A hinge whose last reading is some other pose fails with `COMMAND_FAILED` and `reason: fold-pose-unverified`, naming the angle CoreDevice still reports. A hinge seen `half-open` but never at rest fails with `reason: fold-pose-unsettled`, naming the observed and previous angles: the requested category was observed, and what is missing is a pose the hinge holds (#2730).
- `action-button` is not a cheap command to loop. On an iPhone 17 Pro Simulator the press itself spent about five seconds inside XCUITest, while `home` and `app-switcher` on the same session took under two seconds each.
+- `screen-lock` transitions an iPhone or iPad Simulator to its Lock Screen. It is idempotent and returns `{ action: "screen-lock", state: "locked" }` only after both SpringBoard's lock state and a visible, non-empty SpringBoard surface agree. It never means a process mutex, device claim, or runner lease.
+- `screen-lock` is unsupported on physical devices, macOS, tvOS, watchOS, visionOS, Android, web, Linux, HarmonyOS, and Vega. Unsupported targets fail with `UNSUPPORTED_OPERATION` before dispatch.
- On iOS devices, `http(s)://` URLs open in Safari when no app is active. Custom scheme URLs require an active app in the session.
</file context>
| } | ||
|
|
||
| private func screenLockVisibleResponse(_ verifyVisibleSurface: () -> Bool) -> Response { | ||
| guard verifyVisibleSurface() else { |
There was a problem hiding this comment.
P2: screenLockVisibleResponse samples visibility only once, including the already-locked path. During boot or lock-screen animation, SpringBoard can report locked before its accessibility frame is populated, so this command fails instead of waiting for both verification conditions to agree; poll visibility until the transition deadline.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift, line 83:
<comment>`screenLockVisibleResponse` samples visibility only once, including the already-locked path. During boot or lock-screen animation, SpringBoard can report locked before its accessibility frame is populated, so this command fails instead of waiting for both verification conditions to agree; poll visibility until the transition deadline.</comment>
<file context>
@@ -0,0 +1,142 @@
+ }
+
+ private func screenLockVisibleResponse(_ verifyVisibleSurface: () -> Bool) -> Response {
+ guard verifyVisibleSurface() else {
+ return Response(
+ ok: false,
</file context>
|
|
||
| private func verifyLockScreenSurface() -> Bool { | ||
| let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") | ||
| return springboard.exists && !springboard.frame.isEmpty |
There was a problem hiding this comment.
P2: verifyLockScreenSurface only proves that SpringBoard has a non-empty root frame; it never identifies the Lock Screen. After the Darwin state flips, this can report success while another SpringBoard surface is visible or before the Lock Screen is rendered; verify lock-screen-specific window or descendant state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift, line 113:
<comment>`verifyLockScreenSurface` only proves that SpringBoard has a non-empty root frame; it never identifies the Lock Screen. After the Darwin state flips, this can report success while another SpringBoard surface is visible or before the Lock Screen is rendered; verify lock-screen-specific window or descendant state.</comment>
<file context>
@@ -0,0 +1,142 @@
+
+ private func verifyLockScreenSurface() -> Bool {
+ let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
+ return springboard.exists && !springboard.frame.isEmpty
+ }
+
</file context>
| case .actionButton, .screenLock: | ||
| return .presentedSurfaceMutation |
There was a problem hiding this comment.
P2: Classifying screenLock as .presentedSurfaceMutation causes unsupported macOS/tvOS requests to activate the session app before executeScreenLockCommand returns UNSUPPORTED_OPERATION. Keep this command on a no-activation policy so unsupported platform leaves fail without changing app state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift, line 257:
<comment>Classifying `screenLock` as `.presentedSurfaceMutation` causes unsupported macOS/tvOS requests to activate the session app before `executeScreenLockCommand` returns `UNSUPPORTED_OPERATION`. Keep this command on a no-activation policy so unsupported platform leaves fail without changing app state.</comment>
<file context>
@@ -253,7 +254,7 @@ extension Command {
return .runnerLifecycle
- case .actionButton:
+ case .actionButton, .screenLock:
return .presentedSurfaceMutation
</file context>
| case .actionButton, .screenLock: | |
| return .presentedSurfaceMutation | |
| case .actionButton: | |
| return .presentedSurfaceMutation | |
| case .screenLock: | |
| return CommandTraits(launchPolicy: .noApp, convertsRecordedFailure: true) |
| use: SystemButtonUse; | ||
| /** The success text the press reports; the response carries nothing else by design. */ | ||
| message: string; | ||
| state?: 'locked'; |
There was a problem hiding this comment.
P3: state makes the system-button response no longer text-only, but the surrounding comments still claim otherwise. Update the row and resolver documentation to describe the screen-lock state field.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/daemon/system-button-runtime.ts, line 32:
<comment>`state` makes the system-button response no longer text-only, but the surrounding comments still claim otherwise. Update the row and resolver documentation to describe the screen-lock state field.</comment>
<file context>
@@ -28,6 +29,7 @@ type SystemButtonCommandRow = Readonly<{
use: SystemButtonUse;
/** The success text the press reports; the response carries nothing else by design. */
message: string;
+ state?: 'locked';
}>;
</file context>
| expectOperationAvailability(binding, 'home', springboard); | ||
| expectOperationAvailability(binding, 'appSwitcher', springboard); | ||
|
|
||
| // Screen locking is a simulator-only host transition on iPhone and iPad. Physical devices and |
There was a problem hiding this comment.
P3: This helper now asserts the screenLock fact for every leaf, but the test.each title at line 227 still reads 'classifies back/home/app-switcher/orientation/tv-remote/keyboard facts ...', so a screenLock failure surfaces under a title that does not name it. Add screen-lock (and the availability names) to the title.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/runtime.test.ts, line 377:
<comment>This helper now asserts the `screenLock` fact for every leaf, but the `test.each` title at line 227 still reads 'classifies back/home/app-switcher/orientation/tv-remote/keyboard facts ...', so a `screenLock` failure surfaces under a title that does not name it. Add screen-lock (and the availability names) to the title.</comment>
<file context>
@@ -374,6 +374,18 @@ function expectNavigationAndKeyboardFacts(
expectOperationAvailability(binding, 'home', springboard);
expectOperationAvailability(binding, 'appSwitcher', springboard);
+ // Screen locking is a simulator-only host transition on iPhone and iPad. Physical devices and
+ // every other Apple platform leaf must refuse it before runner dispatch.
+ const screenLock =
</file context>
| } as const); | ||
| function appleScreenLockFact(device: DeviceInfo): RuntimeOperationFact { | ||
| if (device.kind !== 'simulator') return screenLockKindUnavailable; | ||
| const os = resolveDeviceAppleOs(device); |
There was a problem hiding this comment.
P3: appleScreenLockFact duplicates the existing isHandheldAppleSimulator predicate, including its simulator and iOS/iPadOS checks. Reuse that helper after the kind-specific refusal so this support rule cannot drift.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/navigation/runtime.ts, line 162:
<comment>`appleScreenLockFact` duplicates the existing `isHandheldAppleSimulator` predicate, including its simulator and iOS/iPadOS checks. Reuse that helper after the kind-specific refusal so this support rule cannot drift.</comment>
<file context>
@@ -147,6 +147,21 @@ const actionButtonOsUnavailable = Object.freeze({
+} as const);
+function appleScreenLockFact(device: DeviceInfo): RuntimeOperationFact {
+ if (device.kind !== 'simulator') return screenLockKindUnavailable;
+ const os = resolveDeviceAppleOs(device);
+ return os === 'ios' || os === 'ipados' ? available : screenLockOsUnavailable;
+}
</file context>
| XCTAssertEqual(waits, 1) | ||
| } | ||
|
|
||
| func testScreenLockPropagatesUnavailableHidWithoutPolling() { |
There was a problem hiding this comment.
P3: These transition tests never exercise the readState .failure(Response) branch of executeScreenLockTransition (initial switch and in-loop switch both return the wrapped failure verbatim). In production this branch is reachable when notify_register_check or notify_get_state returns NOTIFY_STATUS_OK failure, and the response code it propagates is the same COMMAND_FAILED that tests 4 and 5 assert, so a regression in the failure branch (or the wrong message/status) would go undetected. Add one deterministic test injecting a .failure read to cover the propagation path and assert dispatch is skipped.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+ScreenLockTests.swift, line 41:
<comment>These transition tests never exercise the `readState` `.failure(Response)` branch of `executeScreenLockTransition` (initial `switch` and in-loop `switch` both return the wrapped failure verbatim). In production this branch is reachable when `notify_register_check` or `notify_get_state` returns `NOTIFY_STATUS_OK` failure, and the response code it propagates is the same `COMMAND_FAILED` that tests 4 and 5 assert, so a regression in the failure branch (or the wrong message/status) would go undetected. Add one deterministic test injecting a `.failure` read to cover the propagation path and assert dispatch is skipped.</comment>
<file context>
@@ -0,0 +1,93 @@
+ XCTAssertEqual(waits, 1)
+ }
+
+ func testScreenLockPropagatesUnavailableHidWithoutPolling() {
+ var polled = false
+ let response = executeScreenLockTransition(
</file context>
| * a per-button module's: a button joins this list and its owners state a cell. | ||
| */ | ||
| export const SYSTEM_BUTTONS = ['home', 'appSwitcher', 'actionButton'] as const; | ||
| export const SYSTEM_BUTTONS = ['home', 'appSwitcher', 'actionButton', 'screenLock'] as const; |
There was a problem hiding this comment.
P3: The module doc comment still enumerates the buttons as "home and appSwitcher reach a springboard or recents surface; actionButton presses iPhone/iPad hardware", leaving the newly added screenLock out of the very list this comment is meant to describe. Extend the comment to cover screenLock while it is fresh.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/contracts/src/system-button-runtime.ts, line 12:
<comment>The module doc comment still enumerates the buttons as "`home` and `appSwitcher` reach a springboard or recents surface; `actionButton` presses iPhone/iPad hardware", leaving the newly added `screenLock` out of the very list this comment is meant to describe. Extend the comment to cover `screenLock` while it is fresh.</comment>
<file context>
@@ -9,7 +9,7 @@ import type { SnapshotRuntimeExecution } from './snapshot-runtime.ts';
* a per-button module's: a button joins this list and its owners state a cell.
*/
-export const SYSTEM_BUTTONS = ['home', 'appSwitcher', 'actionButton'] as const;
+export const SYSTEM_BUTTONS = ['home', 'appSwitcher', 'actionButton', 'screenLock'] as const;
export type SystemButton = (typeof SYSTEM_BUTTONS)[number];
</file context>
| while shouldContinue() { | ||
| switch readState() { | ||
| case .success(true): | ||
| return screenLockVisibleResponse(verifyVisibleSurface) | ||
| case .failure(let response): | ||
| return response | ||
| case .success(false): | ||
| wait() | ||
| } | ||
| } |
There was a problem hiding this comment.
P3: The loop checks shouldContinue() before each readState() and exits without a final read, so a transition that completes during the last wait() (within one poll interval of the deadline) is reported as COMMAND_FAILED even though SpringBoard is now locked. Do one final readState() after the loop — returning the success path when it reports locked — before giving up on the timeout.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.swift, line 61:
<comment>The loop checks `shouldContinue()` before each `readState()` and exits without a final read, so a transition that completes during the last `wait()` (within one poll interval of the deadline) is reported as COMMAND_FAILED even though SpringBoard is now locked. Do one final `readState()` after the loop — returning the success path when it reports locked — before giving up on the timeout.</comment>
<file context>
@@ -0,0 +1,142 @@
+
+ if let dispatchFailure = dispatch() { return dispatchFailure }
+
+ while shouldContinue() {
+ switch readState() {
+ case .success(true):
</file context>
| while shouldContinue() { | |
| switch readState() { | |
| case .success(true): | |
| return screenLockVisibleResponse(verifyVisibleSurface) | |
| case .failure(let response): | |
| return response | |
| case .success(false): | |
| wait() | |
| } | |
| } | |
| while shouldContinue() { | |
| switch readState() { | |
| case .success(true): | |
| return screenLockVisibleResponse(verifyVisibleSurface) | |
| case .failure(let response): | |
| return response | |
| case .success(false): | |
| wait() | |
| } | |
| } | |
| // Read once more after the deadline so a transition that landed during the | |
| // final wait is still reported as success while SpringBoard is already locked. | |
| switch readState() { | |
| case .success(true): | |
| return screenLockVisibleResponse(verifyVisibleSurface) | |
| case .failure(let response): | |
| return response | |
| case .success(false): | |
| break | |
| } |
|
Findings on 768c193. The 5s The iosSimulator coverage row in declarations.ts:1109 is a runtime-facts contract test, so nothing here exercises the actual runner route: daemon → runAppleRunnerCommand → pressLockButton → notify lockstate. The PR body says no live simulator run was done, so the one supported leaf is unproven — we don't know whether Not blocking, take or leave: Given the PR stays under the 700-line net production threshold (381 lines) and mostly adds rows to existing owners (SYSTEM_BUTTONS, the facts table, runner traits, registry descriptor), is the vacuous surface check in ScreenLock.swift:111 the only thing worth cutting, or is there another owner here that could absorb more? I did not run the Swift or TS tests in this review, and the Swift transition tests inject One CI check was reported and it's green, but it doesn't run the iOS runner route this PR changes, so it doesn't prove screen-lock works on a simulator. Two things need to happen before this is ready to merge: move the verification deadline so at least one read happens after dispatch (with a regression test for the shouldContinue-false-then-locked case), and provide the live simulator screen-lock run described above. |
Summary
screen-lockcommand through the typed Node client, CLI, MCP, daemon registry, runtime facts, and structured result schemaUNSUPPORTED_OPERATIONrefusals on other platform leavesImplementation
The Apple runner uses XCTest's simulator-only
pressLockButtonselector, the same audited route used by Appium WebDriverAgent, and independently verifiescom.apple.springboard.lockstatebefore checking the visible SpringBoard surface. This avoids Simulator.app menu/keyboard automation and does not conflate screen state with process mutexes, device claims, or runner leases.Reference: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Categories/XCUIDevice%2BFBHelpers.m
Verification
pnpm typecheckpnpm check:packaged-runner-swift(58 packaged Swift files parsed)pnpm check:xctest-selection(all declared tests reachable by a configured lane)pnpm check:affected --run: formatting, lint, typecheck, layering, fallow gate, build, integration coverage, and 5,019 related tests reached green after classification fixes; the full rerun still hit the unrelated timing-sensitiveweb shutdown cleanup reaps the exact daemonassertion under load. The exact test passed standalone. GitHub's Apple/XCTest and device lanes remain authoritative.No npm package was published and no live device/simulator green is claimed locally.