Skip to content

feat(apple): add verified simulator screen lock - #3001

Open
csark0812 wants to merge 2 commits into
callstack:mainfrom
csark0812:chris/agent/screen-lock
Open

csark0812 wants to merge 2 commits into
callstack:mainfrom
csark0812:chris/agent/screen-lock

Conversation

@csark0812

@csark0812 csark0812 commented Sep 27, 2026 •

Copy link
Copy Markdown

Summary

  • add the public screen-lock command through the typed Node client, CLI, MCP, daemon registry, runtime facts, and structured result schema
  • admit only iOS/iPadOS Simulators and return structured UNSUPPORTED_OPERATION refusals on other platform leaves
  • make the transition idempotent and report success only after SpringBoard lock state and a visible non-empty SpringBoard surface agree
  • add deterministic Swift transition tests for already-locked, delayed/booting, unavailable HID, timeout, and unverified-surface cases

Implementation

The Apple runner uses XCTest's simulator-only pressLockButton selector, the same audited route used by Appium WebDriverAgent, and independently verifies com.apple.springboard.lockstate before 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 typecheck
  • focused TypeScript suite: 139 tests passed
  • focused web/Linux runtime and coverage contracts passed
  • pnpm 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-sensitive web shutdown cleanup reaps the exact daemon assertion 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.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

}

private func screenLockVisibleResponse(_ verifyVisibleSurface: () -> Bool) -> Response {
guard verifyVisibleSurface() else {

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic


private func verifyLockScreenSurface() -> Bool {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
return springboard.exists && !springboard.frame.isEmpty

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

Comment on lines +257 to 258
case .actionButton, .screenLock:
return .presentedSurfaceMutation

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
case .actionButton, .screenLock:
return .presentedSurfaceMutation
case .actionButton:
return .presentedSurfaceMutation
case .screenLock:
return CommandTraits(launchPolicy: .noApp, convertsRecordedFailure: true)
Fix with cubic

use: SystemButtonUse;
/** The success text the press reports; the response carries nothing else by design. */
message: string;
state?: 'locked';

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

expectOperationAvailability(binding, 'home', springboard);
expectOperationAvailability(binding, 'appSwitcher', springboard);

// Screen locking is a simulator-only host transition on iPhone and iPad. Physical devices and

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

} as const);
function appleScreenLockFact(device: DeviceInfo): RuntimeOperationFact {
if (device.kind !== 'simulator') return screenLockKindUnavailable;
const os = resolveDeviceAppleOs(device);

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

XCTAssertEqual(waits, 1)
}

func testScreenLockPropagatesUnavailableHidWithoutPolling() {

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

* 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;

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

Comment on lines +61 to +70
while shouldContinue() {
switch readState() {
case .success(true):
return screenLockVisibleResponse(verifyVisibleSurface)
case .failure(let response):
return response
case .success(false):
wait()
}
}

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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
}
Fix with cubic

@thymikee

Copy link
Copy Markdown
Member

Findings on 768c193.

The 5s deadline in RunnerTests+ScreenLock.swift:14 is computed before dispatch(), and the loop checks shouldContinue() before each read but never does a final read after the loop ends. commands.md already notes the sibling action-button press takes about 5s inside XCUITest, so if XCUIDevice.perform(pressLockButton) takes close to that, the first shouldContinue() check after dispatch can already be false, and no read ever happens to catch the lock. A lock that actually succeeded would then be reported as COMMAND_FAILED with "SpringBoard did not report a locked screen", leaving the Lock Screen up while the agent thinks the command failed. The rule should be: at least one lock-state read must happen after dispatch returns, and every poll must fit inside the verification window — start the deadline after dispatch, or add a final readState after the loop, with a Swift test where shouldContinue is false immediately after dispatch and the read still reports locked and asserts ok.

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 notify_get_state on com.apple.springboard.lockstate from inside the XCTest runner process actually sees the simulator's SpringBoard lockstate, how long pressLockButton takes, or whether the Lock Screen visibly appears. Please run the CLI against a booted iOS Simulator with an app session open and screen unlocked (agent-device screen-lock --platform ios --device <udid> --json) and paste the ok response with state 'locked' and its wall time, a post-lock screenshot showing the Lock Screen, a second call returning ok with no new dispatch in runner.log to show the idempotent path, the runner.log timing for pressLockButton (this bears directly on the deadline issue above), and one refusal on a physical device or macOS target showing UNSUPPORTED_OPERATION with details.reason.

Not blocking, take or leave: verifyLockScreenSurface in RunnerTests+ScreenLock.swift:111 only checks that SpringBoard exists with a non-empty frame, which is true on the Home Screen too, so this check can't actually fail and its "Lock Screen surface was not visible" text and the commands.md claim of two agreeing conditions describe verification the code doesn't do — either drop the check (and its test and docs clause) and rely on lockstate alone, or make it Lock-Screen-specific; and appleScreenLockFact in runtime.ts:279 reimplements kernel isHandheldAppleSimulator (packages/kernel/src/device.ts:120) instead of calling it the way settings-leaf.ts and system/runtime.ts already do.

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 shouldContinue, so they can't show where the production deadline actually sits relative to dispatch. I also did not confirm that notify_get_state from the XCTest runner process reflects the simulator's SpringBoard state, did not measure pressLockButton latency, and did not check the WebDriverAgent reference — only a live run resolves these, which is why the deadline finding is rated likely rather than confirmed.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants