mobile-debug-mcp 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +10 -0
  2. package/agent-plugin/.antigravity-plugin/plugin.json +1 -1
  3. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  4. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  5. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  6. package/agent-plugin/.mcp.json +1 -1
  7. package/agent-plugin/cursor/.cursor-plugin/plugin.json +2 -2
  8. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +10 -2
  9. package/agent-plugin/mcp.json +1 -1
  10. package/agent-plugin/mcp_config.json +1 -1
  11. package/agent-plugin/plugin.json +1 -1
  12. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +10 -2
  13. package/dist/contracts/device-readiness.js +1 -0
  14. package/dist/interact/waits/ui-change-wait.js +9 -2
  15. package/dist/interact/waits/ui-wait.js +7 -3
  16. package/dist/manage/index.js +3 -0
  17. package/dist/observe/device-readiness.js +116 -0
  18. package/dist/observe/evidence.js +31 -0
  19. package/dist/observe/ios-trace/sessions.js +5 -3
  20. package/dist/observe/readiness-service.js +21 -9
  21. package/dist/observe/readiness.js +30 -0
  22. package/dist/observe/screenshot-encoding.js +4 -0
  23. package/dist/observe/snapshot-collection.js +7 -2
  24. package/dist/server/definitions/manage.js +3 -2
  25. package/dist/server/handlers/action-execution.js +23 -14
  26. package/dist/server/handlers/compact-interaction.js +106 -0
  27. package/dist/server/handlers/compact-state.js +47 -0
  28. package/dist/server/handlers/input.js +2 -0
  29. package/dist/server/handlers/interact.js +2 -0
  30. package/dist/server/handlers/interaction-diagnostics.js +62 -0
  31. package/dist/server/handlers/journey.js +18 -20
  32. package/dist/server/handlers/observe.js +2 -1
  33. package/dist/server/handlers/tool-handlers.js +9 -5
  34. package/dist/server/schemas/interaction-diagnostics.js +9 -0
  35. package/dist/server/tool-definitions.js +9 -4
  36. package/dist/server-core.js +5 -2
  37. package/dist/server.js +1 -1
  38. package/dist/system/android.js +19 -15
  39. package/dist/system/index.js +2 -1
  40. package/dist/utils/android/utils.js +7 -16
  41. package/dist/utils/app-installation.js +77 -0
  42. package/dist/utils/benchmark-metrics.js +43 -0
  43. package/dist/utils/exec.js +4 -0
  44. package/dist/utils/ios/utils.js +4 -11
  45. package/dist/utils/resolve-device.js +3 -2
  46. package/dist/utils/timing.js +3 -3
  47. package/docs/CHANGELOG.md +25 -0
  48. package/docs/benchmarks/README.md +1 -0
  49. package/docs/benchmarks/end-to-end-latency/README.md +98 -0
  50. package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
  51. package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
  52. package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
  53. package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
  54. package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
  55. package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
  56. package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
  57. package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
  58. package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
  59. package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
  60. package/docs/benchmarks/end-to-end-latency/smoke.md +21 -0
  61. package/docs/specs/031-ios-system-traces/plan.md +28 -0
  62. package/docs/specs/033-device-readiness-blockers/android-evidence.json +437 -0
  63. package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
  64. package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
  65. package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
  66. package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
  67. package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
  68. package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
  69. package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
  70. package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
  71. package/docs/specs/034-app-installation-status/intent.md +41 -0
  72. package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
  73. package/docs/specs/034-app-installation-status/plan.md +73 -0
  74. package/docs/specs/034-app-installation-status/spec.md +114 -0
  75. package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
  76. package/docs/specs/035-compact-interaction-results/intent.md +38 -0
  77. package/docs/specs/035-compact-interaction-results/plan.md +43 -0
  78. package/docs/specs/035-compact-interaction-results/spec.md +118 -0
  79. package/docs/specs/035-compact-interaction-results/verification.md +111 -0
  80. package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
  81. package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
  82. package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
  83. package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
  84. package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
  85. package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
  86. package/docs/tools/TOOLS.md +1 -0
  87. package/docs/tools/compact-interactions.md +56 -0
  88. package/docs/tools/correlated-evidence.md +6 -0
  89. package/docs/tools/interact.md +17 -8
  90. package/docs/tools/manage.md +10 -2
  91. package/docs/tools/observe.md +42 -2
  92. package/docs/tools/system.md +6 -0
  93. package/package.json +1 -1
  94. package/src/contracts/device-readiness.ts +8 -0
  95. package/src/interact/waits/ui-change-wait.ts +7 -2
  96. package/src/interact/waits/ui-wait.ts +6 -3
  97. package/src/manage/index.ts +2 -0
  98. package/src/observe/device-readiness.ts +96 -0
  99. package/src/observe/evidence.ts +30 -1
  100. package/src/observe/ios-trace/sessions.ts +4 -2
  101. package/src/observe/readiness-service.ts +21 -9
  102. package/src/observe/readiness.ts +33 -0
  103. package/src/observe/screenshot-encoding.ts +4 -0
  104. package/src/observe/snapshot-collection.ts +7 -2
  105. package/src/server/common.ts +2 -2
  106. package/src/server/definitions/manage.ts +3 -2
  107. package/src/server/handlers/action-execution.ts +22 -14
  108. package/src/server/handlers/compact-interaction.ts +91 -0
  109. package/src/server/handlers/compact-state.ts +43 -0
  110. package/src/server/handlers/input.ts +2 -0
  111. package/src/server/handlers/interact.ts +2 -0
  112. package/src/server/handlers/interaction-diagnostics.ts +50 -0
  113. package/src/server/handlers/journey.ts +15 -17
  114. package/src/server/handlers/observe.ts +2 -1
  115. package/src/server/handlers/payload.ts +2 -1
  116. package/src/server/handlers/tool-handlers.ts +9 -5
  117. package/src/server/schemas/interaction-diagnostics.ts +10 -0
  118. package/src/server/tool-definitions.ts +8 -4
  119. package/src/server-core.ts +5 -2
  120. package/src/server.ts +1 -1
  121. package/src/system/android.ts +18 -11
  122. package/src/system/index.ts +2 -1
  123. package/src/types.ts +2 -0
  124. package/src/utils/android/utils.ts +8 -10
  125. package/src/utils/app-installation.ts +73 -0
  126. package/src/utils/benchmark-metrics.ts +42 -0
  127. package/src/utils/exec.ts +5 -0
  128. package/src/utils/ios/utils.ts +4 -10
  129. package/src/utils/resolve-device.ts +4 -2
  130. package/src/utils/timing.ts +3 -2
  131. package/test/device/automated/debugger/runtime.android.test.ts +5 -0
  132. package/test/device/automated/debugger/runtime.ios.test.ts +7 -1
  133. package/test/device/automated/interact/compact-results.smoke.ts +58 -0
  134. package/test/device/automated/latency/end-to-end-environment.ts +61 -0
  135. package/test/device/automated/latency/end-to-end-report.ts +90 -0
  136. package/test/device/automated/latency/end-to-end-session.ts +98 -0
  137. package/test/device/automated/latency/end-to-end.ts +149 -0
  138. package/test/device/automated/latency/owned-transport.ts +81 -0
  139. package/test/device/automated/latency/snapshot-pair.ts +143 -0
  140. package/test/device/automated/latency-ios/benchmark.ts +126 -0
  141. package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
  142. package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
  143. package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
  144. package/test/device/automated/observe/ios-system-trace.smoke.ts +2 -2
  145. package/test/device/automated/observe/system-trace.smoke.ts +2 -2
  146. package/test/fixtures/contracts/tool-definitions.json +51 -15
  147. package/test/fixtures/latency-journey-app/build.ts +3 -3
  148. package/test/unit/debugger/handle-guards.test.ts +2 -1
  149. package/test/unit/interact/ui-change-readiness-interruption.test.ts +37 -0
  150. package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
  151. package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
  152. package/test/unit/observe/device-readiness.test.ts +97 -0
  153. package/test/unit/observe/ios-trace-sessions.test.ts +23 -0
  154. package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
  155. package/test/unit/server/compact-interaction.test.ts +87 -0
  156. package/test/unit/server/compact-journey-evidence.test.ts +40 -0
  157. package/test/unit/server/compact-review-regressions.test.ts +78 -0
  158. package/test/unit/server/conditional_actions.test.ts +7 -3
  159. package/test/unit/server/correlated-evidence.test.ts +18 -6
  160. package/test/unit/server/end-to-end-report.test.ts +46 -0
  161. package/test/unit/server/input-device-readiness.test.ts +55 -0
  162. package/test/unit/server/interaction-diagnostics.test.ts +45 -0
  163. package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
  164. package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
  165. package/test/unit/server/readiness.test.ts +24 -1
  166. package/test/unit/server/run_journey.test.ts +2 -1
  167. package/test/unit/server/selector_tap_target.test.ts +2 -1
  168. package/test/unit/server/system-trace.test.ts +6 -4
  169. package/test/unit/utils/app-installation.test.ts +78 -0
  170. package/test/unit/utils/benchmark-metrics.test.ts +36 -0
  171. package/test/unit/utils/resolve_device.test.ts +11 -0
@@ -883,9 +883,40 @@
883
883
  ]
884
884
  }
885
885
  },
886
+ {
887
+ "name": "get_interaction_diagnostics",
888
+ "description": "Read the retained original interaction or journey result without replaying input or observing the device. Pass diagnostics.session_id/result_id as sessionId/resultId. Session-local memory: ten minutes, 100 results, 16 MiB total, 1 MiB per result. Expired, evicted or restarted results are unavailable. May contain private UI text and errors; compact output is not redaction.",
889
+ "inputSchema": {
890
+ "type": "object",
891
+ "additionalProperties": false,
892
+ "required": [
893
+ "sessionId",
894
+ "resultId"
895
+ ],
896
+ "properties": {
897
+ "sessionId": {
898
+ "type": "string",
899
+ "format": "uuid"
900
+ },
901
+ "resultId": {
902
+ "type": "string",
903
+ "format": "uuid"
904
+ }
905
+ },
906
+ "allOf": [
907
+ {
908
+ "not": {
909
+ "required": [
910
+ "expectedState"
911
+ ]
912
+ }
913
+ }
914
+ ]
915
+ }
916
+ },
886
917
  {
887
918
  "name": "get_app_readiness",
888
- "description": "Observe readiness with at most two fresh UI-tree samples. Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.",
919
+ "description": "Observe readiness with at most two fresh UI-tree samples. Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android and supported iOS simulator debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
889
920
  "inputSchema": {
890
921
  "type": "object",
891
922
  "additionalProperties": false,
@@ -930,7 +961,7 @@
930
961
  },
931
962
  {
932
963
  "name": "wait_for_app_ready",
933
- "description": "Wait within a single bounded budget for settled UI evidence. Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.",
964
+ "description": "Wait within a single bounded budget for settled UI evidence. Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android and supported iOS simulator debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
934
965
  "inputSchema": {
935
966
  "type": "object",
936
967
  "additionalProperties": false,
@@ -981,7 +1012,7 @@
981
1012
  },
982
1013
  {
983
1014
  "name": "start_app",
984
- "description": "Purpose:\nLaunch a mobile app on Android or iOS.\n\nInputs:\n- platform\n- appId\n- deviceId (optional)\n\nOutput Structure:\n - action_id, timestamp (ISO 8601), action_type\n - lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n - source_module: runtime source of the action envelope\n - target.selector = { appId }\n- success = true when launch was dispatched successfully\n- failure_code/retryable when launch dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Define the expected landing screen when it is known\n2. Call start_app\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen\n5. If verification fails, retry once or capture a snapshot\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- expect_screen is the authoritative verification step when the landing screen is known\n- Do not treat timing or screen change alone as final verification\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → inspect snapshot/logs before retrying",
1015
+ "description": "Purpose:\nLaunch a mobile app on Android or iOS.\n\nInputs:\n- platform\n- appId\n- deviceId (optional)\n\nOutput Structure:\n - action_id, timestamp (ISO 8601), action_type\n - lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n - source_module: runtime source of the action envelope\n - target.selector = { appId }\n- success = true when launch was dispatched successfully\n- failure_code/retryable when launch dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Define the expected landing screen when it is known\n2. Call start_app\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen\n5. If verification fails, retry once or capture a snapshot\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- expect_screen is the authoritative verification step when the landing screen is known\n- Do not treat timing or screen change alone as final verification\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → inspect snapshot/logs before retrying\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
985
1016
  "inputSchema": {
986
1017
  "type": "object",
987
1018
  "properties": {
@@ -1080,7 +1111,7 @@
1080
1111
  },
1081
1112
  {
1082
1113
  "name": "restart_app",
1083
- "description": "Purpose:\nRestart a mobile app on Android or iOS.\n\nInputs:\n- platform\n- appId\n- deviceId (optional)\n\nOutput Structure:\n - action_id, timestamp (ISO 8601), action_type\n - lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n - source_module: runtime source of the action envelope\n - target.selector = { appId }\n- success = true when the restart command completed\n- failure_code/retryable when restart dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Define the expected landing screen when it is known\n2. Call restart_app\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen\n5. If verification fails, retry once or capture a snapshot\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- expect_screen is the authoritative verification step when the reopened screen is known\n- Do not treat timing or screen change alone as final verification\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → inspect snapshot/logs before retrying",
1114
+ "description": "Purpose:\nRestart a mobile app on Android or iOS.\n\nInputs:\n- platform\n- appId\n- deviceId (optional)\n\nOutput Structure:\n - action_id, timestamp (ISO 8601), action_type\n - lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n - source_module: runtime source of the action envelope\n - target.selector = { appId }\n- success = true when the restart command completed\n- failure_code/retryable when restart dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Define the expected landing screen when it is known\n2. Call restart_app\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen\n5. If verification fails, retry once or capture a snapshot\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- expect_screen is the authoritative verification step when the reopened screen is known\n- Do not treat timing or screen change alone as final verification\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → inspect snapshot/logs before retrying\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
1084
1115
  "inputSchema": {
1085
1116
  "type": "object",
1086
1117
  "properties": {
@@ -1361,7 +1392,7 @@
1361
1392
  },
1362
1393
  {
1363
1394
  "name": "list_devices",
1364
- "description": "List connected devices and their metadata (android + ios).",
1395
+ "description": "List Android devices and iOS simulators. Supply appId for device-specific installation status; unknown checks do not imply absence.",
1365
1396
  "inputSchema": {
1366
1397
  "type": "object",
1367
1398
  "properties": {
@@ -1371,6 +1402,11 @@
1371
1402
  "android",
1372
1403
  "ios"
1373
1404
  ]
1405
+ },
1406
+ "appId": {
1407
+ "type": "string",
1408
+ "pattern": "^[A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)*$",
1409
+ "description": "Optional package or bundle identifier to check on each device."
1374
1410
  }
1375
1411
  },
1376
1412
  "allOf": [
@@ -1829,7 +1865,7 @@
1829
1865
  },
1830
1866
  {
1831
1867
  "name": "wait_for_ui_change",
1832
- "description": "Purpose:\nWait for a non-navigation UI mutation or in-place update to become stable.\n\nInputs:\n- expected_change (optional): hierarchy_diff, text_change, or state_change\n- timeout_ms (optional)\n- stability_window_ms (optional)\n- scope (optional): screen or subtree\n- target (optional): element_id when scope=subtree\n\nGuidance:\n- Prefer wait_for_screen_change for navigation transitions.\n- Prefer wait_for_ui_change for in-place mutations and non-navigation updates.\n- Use the returned snapshot_revision as the observed synchronization point when available.\n- Scoped waits return scope-aware stability metadata and a lightweight change summary.\n\nFailure Handling:\n- TIMEOUT means the UI did not change in a stable way within the allotted time.\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.",
1868
+ "description": "Purpose:\nWait for a non-navigation UI mutation or in-place update to become stable.\n\nInputs:\n- expected_change (optional): hierarchy_diff, text_change, or state_change\n- timeout_ms (optional)\n- stability_window_ms (optional)\n- scope (optional): screen or subtree\n- target (optional): element_id when scope=subtree\n\nGuidance:\n- Prefer wait_for_screen_change for navigation transitions.\n- Prefer wait_for_ui_change for in-place mutations and non-navigation updates.\n- Use the returned snapshot_revision as the observed synchronization point when available.\n- Scoped waits return scope-aware stability metadata and a lightweight change summary.\n\nFailure Handling:\n- TIMEOUT means the UI did not change in a stable way within the allotted time.\nDevice-readiness blockers prevent readiness success; unknown/unsupported device signals remain explicit.\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.",
1833
1869
  "inputSchema": {
1834
1870
  "type": "object",
1835
1871
  "properties": {
@@ -2073,7 +2109,7 @@
2073
2109
  },
2074
2110
  {
2075
2111
  "name": "adjust_control",
2076
- "description": "Purpose:\nAdjust a numeric control value with verification.\n\nThis is the initial adjustable-control surface for slider-like controls and other controls that expose a numeric value or value_range.\n\nInputs:\n- exactly one of selector or element_id\n- property (defaults to \"value\")\n- targetValue\n- tolerance (optional; discrete default is 0, continuous default is max(step / 2, 1% of range) when omitted)\n- maxAttempts (optional; defaults to 5)\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target_state / actual_state / within_tolerance / converged / attempts / adjustment_mode\n- target.selector = original selector or element handle\n- success = true when the control converges within tolerance\n\nVerification Guidance:\n- Prefer direct platform adjustment when the runtime exposes it; then verified increment/decrement actions; use coordinate fallback only as degraded mode\n- Use expect_state for the control value readback\n- Do not use derived semantic metadata as proof of success\n\nFailure Handling:\n- ELEMENT_NOT_FOUND → re-resolve the control\n- ELEMENT_NOT_INTERACTABLE → the control cannot be adjusted through the current runtime\n- CONTROL_CONVERGENCE_FAILED → the control did not converge within bounded retries or the requested value is out of range\n- UNKNOWN → capture a snapshot and stop",
2112
+ "description": "Purpose:\nAdjust a numeric control value with verification.\n\nThis is the initial adjustable-control surface for slider-like controls and other controls that expose a numeric value or value_range.\n\nInputs:\n- exactly one of selector or element_id\n- property (defaults to \"value\")\n- targetValue\n- tolerance (optional; discrete default is 0, continuous default is max(step / 2, 1% of range) when omitted)\n- maxAttempts (optional; defaults to 5)\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target_state / actual_state / within_tolerance / converged / attempts / adjustment_mode\n- target.selector = original selector or element handle\n- success = true when the control converges within tolerance\n\nVerification Guidance:\n- Prefer direct platform adjustment when the runtime exposes it; then verified increment/decrement actions; use coordinate fallback only as degraded mode\n- Use expect_state for the control value readback\n- Do not use derived semantic metadata as proof of success\n\nFailure Handling:\n- ELEMENT_NOT_FOUND → re-resolve the control\n- ELEMENT_NOT_INTERACTABLE → the control cannot be adjusted through the current runtime\n- CONTROL_CONVERGENCE_FAILED → the control did not converge within bounded retries or the requested value is out of range\n- UNKNOWN → capture a snapshot and stop\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2077
2113
  "inputSchema": {
2078
2114
  "type": "object",
2079
2115
  "properties": {
@@ -2158,7 +2194,7 @@
2158
2194
  },
2159
2195
  {
2160
2196
  "name": "wait_for_ui",
2161
- "description": "Purpose:\nResolve elements and/or detect that a UI transition or availability condition has occurred.\n\nCapabilities:\n- Deterministic element resolution\n- Synchronization when element timing or availability is uncertain\n\nConstraints:\n- Does not verify correctness of the resulting state\n- Must not be used alone to confirm action success when an applicable expect_* tool exists\n\nRecommended Usage:\n1. Use wait_for_ui to resolve an element before acting or to wait for UI readiness\n2. Call the action tool\n3. If the expected outcome is known, follow with expect_* as final verification\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.",
2197
+ "description": "Purpose:\nResolve elements and/or detect that a UI transition or availability condition has occurred.\n\nCapabilities:\n- Deterministic element resolution\n- Synchronization when element timing or availability is uncertain\n\nConstraints:\n- Does not verify correctness of the resulting state\n- Must not be used alone to confirm action success when an applicable expect_* tool exists\n\nRecommended Usage:\n1. Use wait_for_ui to resolve an element before acting or to wait for UI readiness\n2. Call the action tool\n3. If the expected outcome is known, follow with expect_* as final verification\nDevice-readiness blockers prevent readiness success; unknown/unsupported device signals remain explicit.\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.",
2162
2198
  "inputSchema": {
2163
2199
  "type": "object",
2164
2200
  "properties": {
@@ -2297,7 +2333,7 @@
2297
2333
  },
2298
2334
  {
2299
2335
  "name": "tap",
2300
- "description": "Purpose:\nDispatch a tap at specific screen coordinates.\n\nInputs:\n- x, y coordinates\n- platform (optional)\n- deviceId (optional)\n\nOutput Structure:\n - action_id, timestamp (ISO 8601), action_type\n - lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n - source_module: runtime source of the action envelope\n - target.selector = { x, y }\n- success = true when the tap was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Resolve coordinates deterministically\n2. Call tap\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen or expect_element_visible depending on the intended outcome\n\nVerification Guidance:\n- Prefer tap_element over tap when an element can be resolved\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Use expect_screen for navigation and expect_element_visible for local UI changes\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → capture a snapshot and stop\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures include the fresh UI tree. This is not atomic with OS input delivery.",
2336
+ "description": "Purpose:\nDispatch a tap at specific screen coordinates.\n\nInputs:\n- x, y coordinates\n- platform (optional)\n- deviceId (optional)\n\nOutput Structure:\n - action_id, timestamp (ISO 8601), action_type\n - lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n - source_module: runtime source of the action envelope\n - target.selector = { x, y }\n- success = true when the tap was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Resolve coordinates deterministically\n2. Call tap\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen or expect_element_visible depending on the intended outcome\n\nVerification Guidance:\n- Prefer tap_element over tap when an element can be resolved\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Use expect_screen for navigation and expect_element_visible for local UI changes\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → capture a snapshot and stop\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2301
2337
  "inputSchema": {
2302
2338
  "type": "object",
2303
2339
  "properties": {
@@ -2414,7 +2450,7 @@
2414
2450
  },
2415
2451
  {
2416
2452
  "name": "tap_element",
2417
- "description": "Purpose:\nTap a previously resolved UI element using its elementId.\n\nInputs:\n- elementId: a resolved UI element identifier returned by wait_for_ui\n\nOutput Structure:\n- action_id: unique timestamp-based action identifier\n- timestamp: ISO 8601 timestamp for the action attempt\n- action_type: \"tap_element\"\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector: original target handle ({ elementId })\n- target.resolved: minimal resolved element info used for the tap\n- success: true when the tap was dispatched\n- failure_code: present when success=false\n- retryable: present when failure_code exists\n- ui_fingerprint_before/ui_fingerprint_after: optional fingerprints captured around the action\n\nRecommended Usage:\n1. Resolve the target with wait_for_ui or another deterministic resolver\n2. Call tap_element\n3. If needed, wait for transition using wait_for_*\n4. Verify outcome using expect_*\n - use expect_screen when navigation is expected\n - use expect_element_visible when the UI change is local\n5. If verification fails, inspect failure_code and follow the retry strategy below\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Prefer expect_screen for navigation or modal transitions\n- Prefer expect_element_visible when the tap should reveal or update a specific element\n- wait_for_* may be used for resolution and synchronization, but not as the final verification step when an applicable expect_* tool exists\n- Do not treat tap_element.success as outcome success; it only means the tap was executed\n\nFailure Handling:\n- STALE_REFERENCE → re-resolve the element, then retry\n- ELEMENT_NOT_INTERACTABLE → wait or refine the target, then retry\n- UNKNOWN → capture a snapshot and stop\n\nThis tool reports execution success only. Verification must be done with a separate expect_* tool.\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures include the fresh UI tree. This is not atomic with OS input delivery.",
2453
+ "description": "Purpose:\nTap a previously resolved UI element using its elementId.\n\nInputs:\n- elementId: a resolved UI element identifier returned by wait_for_ui\n\nOutput Structure:\n- action_id: unique timestamp-based action identifier\n- timestamp: ISO 8601 timestamp for the action attempt\n- action_type: \"tap_element\"\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector: original target handle ({ elementId })\n- target.resolved: minimal resolved element info used for the tap\n- success: true when the tap was dispatched\n- failure_code: present when success=false\n- retryable: present when failure_code exists\n- ui_fingerprint_before/ui_fingerprint_after: optional fingerprints captured around the action\n\nRecommended Usage:\n1. Resolve the target with wait_for_ui or another deterministic resolver\n2. Call tap_element\n3. If needed, wait for transition using wait_for_*\n4. Verify outcome using expect_*\n - use expect_screen when navigation is expected\n - use expect_element_visible when the UI change is local\n5. If verification fails, inspect failure_code and follow the retry strategy below\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Prefer expect_screen for navigation or modal transitions\n- Prefer expect_element_visible when the tap should reveal or update a specific element\n- wait_for_* may be used for resolution and synchronization, but not as the final verification step when an applicable expect_* tool exists\n- Do not treat tap_element.success as outcome success; it only means the tap was executed\n\nFailure Handling:\n- STALE_REFERENCE → re-resolve the element, then retry\n- ELEMENT_NOT_INTERACTABLE → wait or refine the target, then retry\n- UNKNOWN → capture a snapshot and stop\n\nThis tool reports execution success only. Verification must be done with a separate expect_* tool.\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2418
2454
  "inputSchema": {
2419
2455
  "type": "object",
2420
2456
  "properties": {
@@ -2591,7 +2627,7 @@
2591
2627
  },
2592
2628
  {
2593
2629
  "name": "swipe",
2594
- "description": "Purpose:\nDispatch a swipe gesture on Android or iOS.\n\nInputs:\n- start and end coordinates\n- duration\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = { x1, y1, x2, y2, duration }\n- success = true when the swipe was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Determine swipe coordinates\n2. Call swipe\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen or expect_element_visible when a deterministic outcome is expected\n\nVerification Guidance:\n- Swipe outcomes are less predictable; choose the most specific verifier available for the intended effect\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → capture a snapshot and stop\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures include the fresh UI tree. This is not atomic with OS input delivery.",
2630
+ "description": "Purpose:\nDispatch a swipe gesture on Android or iOS.\n\nInputs:\n- start and end coordinates\n- duration\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = { x1, y1, x2, y2, duration }\n- success = true when the swipe was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Determine swipe coordinates\n2. Call swipe\n3. If needed, wait for transition using wait_for_*\n4. Verify with expect_screen or expect_element_visible when a deterministic outcome is expected\n\nVerification Guidance:\n- Swipe outcomes are less predictable; choose the most specific verifier available for the intended effect\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → capture a snapshot and stop\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2595
2631
  "inputSchema": {
2596
2632
  "type": "object",
2597
2633
  "properties": {
@@ -2723,7 +2759,7 @@
2723
2759
  },
2724
2760
  {
2725
2761
  "name": "scroll_to_element",
2726
- "description": "Purpose:\nScroll until a target element becomes visible.\n\nInputs:\n- platform\n- selector\n- direction, maxScrolls, scrollAmount, deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = original selector\n- target.resolved = minimal resolved element info when found\n- success = true when scrolling produced a visible target element\n- failure_code/retryable when the target was not reached\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Resolve the target selector\n2. Call scroll_to_element\n3. If needed, wait for UI stabilization using wait_for_*\n4. Verify with expect_element_visible when the expected element visibility is known\n5. If success=false, follow failure handling before retrying\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Use expect_element_visible when you need an explicit post-scroll confirmation\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- NAVIGATION_NO_CHANGE → adjust scroll direction or stop\n- TIMEOUT → retry with refined selector or larger scroll budget\n- UNKNOWN → capture a snapshot and stop",
2762
+ "description": "Purpose:\nScroll until a target element becomes visible.\n\nInputs:\n- platform\n- selector\n- direction, maxScrolls, scrollAmount, deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = original selector\n- target.resolved = minimal resolved element info when found\n- success = true when scrolling produced a visible target element\n- failure_code/retryable when the target was not reached\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Resolve the target selector\n2. Call scroll_to_element\n3. If needed, wait for UI stabilization using wait_for_*\n4. Verify with expect_element_visible when the expected element visibility is known\n5. If success=false, follow failure handling before retrying\n\nVerification Guidance:\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Use expect_element_visible when you need an explicit post-scroll confirmation\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- NAVIGATION_NO_CHANGE → adjust scroll direction or stop\n- TIMEOUT → retry with refined selector or larger scroll budget\n- UNKNOWN → capture a snapshot and stop\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2727
2763
  "inputSchema": {
2728
2764
  "type": "object",
2729
2765
  "properties": {
@@ -2815,7 +2851,7 @@
2815
2851
  },
2816
2852
  {
2817
2853
  "name": "type_text",
2818
- "description": "Purpose:\nType text into the currently focused Android input field.\n\nInputs:\n- text\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = { text }\n- success = true when text input was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Resolve or focus the target input first\n2. Call type_text\n3. If needed, wait for UI stabilization using wait_for_*\n4. Verify with expect_element_visible or expect_screen, depending on the intended outcome\n\nVerification Guidance:\n- Prefer verifying the next expected element or screen state instead of inferring success from the text action alone\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → re-focus the input or capture a snapshot\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures include the fresh UI tree. This is not atomic with OS input delivery.",
2854
+ "description": "Purpose:\nType text into the currently focused Android input field.\n\nInputs:\n- text\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = { text }\n- success = true when text input was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Resolve or focus the target input first\n2. Call type_text\n3. If needed, wait for UI stabilization using wait_for_*\n4. Verify with expect_element_visible or expect_screen, depending on the intended outcome\n\nVerification Guidance:\n- Prefer verifying the next expected element or screen state instead of inferring success from the text action alone\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → re-focus the input or capture a snapshot\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2819
2855
  "inputSchema": {
2820
2856
  "type": "object",
2821
2857
  "properties": {
@@ -2925,7 +2961,7 @@
2925
2961
  },
2926
2962
  {
2927
2963
  "name": "press_back",
2928
- "description": "Purpose:\nDispatch the Android Back action.\n\nInputs:\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = { key: \"back\" }\n- success = true when the back action was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Call press_back\n2. If needed, wait for transition using wait_for_*\n3. Verify with expect_screen when a known destination is expected\n4. If verification fails, retry once or recover explicitly\n\nVerification Guidance:\n- Back outcomes can vary by screen, so verify against the intended destination when possible\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → capture a snapshot and stop\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures include the fresh UI tree. This is not atomic with OS input delivery.",
2964
+ "description": "Purpose:\nDispatch the Android Back action.\n\nInputs:\n- platform/deviceId (optional)\n\nOutput Structure:\n- action_id, timestamp (ISO 8601), action_type\n- lifecycle_state: post-dispatch lifecycle state (pending_verification or failed)\n- source_module: runtime source of the action envelope\n- target.selector = { key: \"back\" }\n- success = true when the back action was dispatched\n- failure_code/retryable when dispatch fails\n- ui_fingerprint_before/ui_fingerprint_after when available\n\nRecommended Usage:\n1. Call press_back\n2. If needed, wait for transition using wait_for_*\n3. Verify with expect_screen when a known destination is expected\n4. If verification fails, retry once or recover explicitly\n\nVerification Guidance:\n- Back outcomes can vary by screen, so verify against the intended destination when possible\n- Follow RESOLVE → ACT → WAIT (if needed) → EXPECT\n- Do not use wait_for_* alone as final verification when an applicable expect_* tool exists\n\nFailure Handling:\n- TIMEOUT → retry once\n- UNKNOWN → capture a snapshot and stop\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.",
2929
2965
  "inputSchema": {
2930
2966
  "type": "object",
2931
2967
  "properties": {
@@ -3091,7 +3127,7 @@
3091
3127
  },
3092
3128
  {
3093
3129
  "name": "run_journey",
3094
- "description": "Run an ordered, bounded sequence of start_app, tap, wait, and deterministic assertion steps against one device. Stops after the first failed step.\nTap steps may include expectedState from a successful same-device get_ui_tree. A failed condition blocks that input, retains fresh condition evidence even in compact mode, and stops subsequent steps. Conditions are not journey defaults or supported on other step types.",
3130
+ "description": "Run an ordered, bounded sequence of start_app, tap, wait, and deterministic assertion steps against one device. Stops after the first failed step.\nTap steps may include expectedState from a successful same-device get_ui_tree. A failed condition blocks that input, retains fresh condition evidence through a diagnostics reference in compact mode, and stops subsequent steps. Conditions are not journey defaults or supported on other step types.",
3095
3131
  "inputSchema": {
3096
3132
  "type": "object",
3097
3133
  "properties": {
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process'
2
- import { mkdirSync } from 'node:fs'
2
+ import { existsSync, mkdirSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
 
@@ -11,7 +11,7 @@ export function buildFixture() {
11
11
  const androidJar = path.join(sdk, 'platforms', 'android-36', 'android.jar')
12
12
  const java = process.env.JAVA_HOME
13
13
  const tool = (name: string) => java ? path.join(java, 'bin', name) : name
14
- const run = (cmd: string, args: string[]) => execFileSync(cmd, args, { cwd: out, stdio: 'pipe' })
14
+ const run = (cmd: string, args: string[]) => execFileSync(cmd, args, { cwd: out, stdio: 'pipe', timeout: 60000, killSignal: 'SIGKILL' })
15
15
  mkdirSync(path.join(out, 'classes'), { recursive: true })
16
16
  run(tool('javac'), ['--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(root, 'src/MainActivity.java')])
17
17
  run(tool('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
@@ -20,7 +20,7 @@ export function buildFixture() {
20
20
  run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
21
21
  run(path.join(buildTools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
22
22
  // Ephemeral signing material is generated outside version control.
23
- try { run(tool('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Latency Fixture', '-keyalg', 'RSA', '-validity', '3650']) } catch { /* Existing local fixture key. */ }
23
+ if (!existsSync(path.join(out, 'fixture.jks'))) run(tool('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Latency Fixture', '-keyalg', 'RSA', '-validity', '3650'])
24
24
  run(path.join(buildTools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', 'fixture.apk', 'aligned.apk'])
25
25
  return path.join(out, 'fixture.apk')
26
26
  }
@@ -52,7 +52,8 @@ try {
52
52
  for (const hints of [{}, { deviceId: 'ios-a' }, { platform: 'ios' }]) {
53
53
  const blocked = await call(ios, hints)
54
54
  assert.equal(blocked.failure_code, 'DEBUGGER_PAUSED')
55
- assert.equal(blocked.debugger.debug_session_id, 'ios-session')
55
+ const details = payload(await handleToolCall('get_interaction_diagnostics', { sessionId: blocked.diagnostics.session_id, resultId: blocked.diagnostics.result_id }))
56
+ assert.equal(details.result.debugger.debug_session_id, 'ios-session')
56
57
  assert.equal(blocked.delivery_status, 'not_delivered'); assert.equal(blocked.dispatch_started, false)
57
58
  }
58
59
  assert.equal(reads, 0, 'A known pause must block before hierarchy reads')
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict'
2
+ import { ReadinessTracker, withReadinessTracker } from '../../../src/observe/readiness.js'
3
+ import { withDeviceReadinessProbe } from '../../../src/observe/device-readiness.js'
4
+ import { getSnapshotSessionId } from '../../../src/observe/snapshot-metadata.js'
5
+ import { waitForUIChange } from '../../../src/interact/waits/ui-change-wait.js'
6
+ import { resolveUiChangeScope } from '../../../src/interact/targeting/target-scope.js'
7
+ import type { Clock } from '../../../src/runtime/clock.js'
8
+
9
+ async function run() {
10
+ let now = 10000
11
+ const clock: Clock = { now: () => now, sleep: async ms => { now += ms } }
12
+ class Tracker extends ReadinessTracker {
13
+ override observe(tree: unknown, started: number) { return super.observe(tree, started, now) }
14
+ }
15
+ const tracker = new Tracker('android', 'fixture', 100)
16
+ const readTimes: number[] = []
17
+ const result = await withDeviceReadinessProbe(async (_command, args) => ({
18
+ exitCode: 0, stderr: '', stdout: args.includes('power')
19
+ ? `mWakefulness=${now >= 10600 && now < 11200 ? 'Asleep' : 'Awake'}\nmWakefulnessChanging=false`
20
+ : 'KeyguardServiceDelegate\n showing=false\n mIsShowing=false'
21
+ }), () => withReadinessTracker(tracker, () => waitForUIChange({ platform: 'android', deviceId: 'fixture', timeout_ms: 3000, stability_window_ms: 1200, expected_change: 'text_change' }, {
22
+ clock,
23
+ observe: async () => {
24
+ const text = readTimes.length ? 'After' : 'Before'
25
+ readTimes.push(now)
26
+ return { device: { platform: 'android', id: 'fixture' }, screen: '', resolution: { width: 100, height: 100 },
27
+ elements: [{ text, type: 'TextView', stable_id: 'label', visible: true, bounds: [0, 0, 100, 40] }],
28
+ captured_at_ms: now, snapshot_revision: readTimes.length, snapshot_session_id: getSnapshotSessionId() }
29
+ },
30
+ scope: (tree, scope, target) => resolveUiChangeScope(tree, scope, target, () => null)
31
+ })))
32
+ assert.equal(result.success, true)
33
+ assert.deepEqual(readTimes, [10000, 10300, 11200, 11500, 11800, 12100, 12400])
34
+ assert.equal(result.elapsed_ms, 2400, 'the full change stability window starts again after the blocker')
35
+ console.log('PASS readiness interruption resets UI-change stability, excluding blocked time')
36
+ }
37
+ run().catch(error => { console.error(error); process.exitCode = 1 })
@@ -1,6 +1,8 @@
1
1
  import { ToolsObserve } from '../../../src/observe/index.js'
2
2
  import assert from 'assert'
3
3
  import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
4
+ import { computeScreenFingerprint } from '../../../src/utils/ui/index.js'
5
+ import type { DeviceInfo } from '../../../src/types.js'
4
6
 
5
7
  async function run() {
6
8
  console.log('Starting capture_debug_snapshot unit tests...')
@@ -13,7 +15,7 @@ async function run() {
13
15
  const origReadLogStreamHandler = (ToolsObserve as any).readLogStreamHandler
14
16
  const origGetLogsHandler = (ToolsObserve as any).getLogsHandler
15
17
 
16
- const device = { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }
18
+ const device: DeviceInfo = { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }
17
19
  let resolutions = 0
18
20
  _setDeviceListersForTests({
19
21
  listAndroidDevices: async () => { resolutions++; return [device] },
@@ -30,17 +32,23 @@ async function run() {
30
32
  assertTarget(args)
31
33
  return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screenshot: 'BASE64PNG', resolution: { width: 1080, height: 1920 } }
32
34
  }
35
+ const current = { device, package: 'com.example', activity: 'com.example.Main', shortActivity: 'Main' }
36
+ const postTreeActivity = { ...current, activity: 'com.example.After' }
37
+ let activityCalls = 0
33
38
  ;(ToolsObserve as any).getCurrentScreenHandler = async function(args: any) {
34
39
  assert.strictEqual(args.deviceId, device.id)
35
- return { device: { platform: 'android', id: 'mock' }, package: 'com.example', activity: 'com.example.Main', shortActivity: 'Main' }
40
+ return ++activityCalls === 1 ? current : postTreeActivity
36
41
  }
37
- ;(ToolsObserve as any).getScreenFingerprintHandler = async function(args: any) {
38
- assertTarget(args)
39
- return { fingerprint: 'abc123', activity: 'Main' }
42
+ let independentFingerprints = 0
43
+ ;(ToolsObserve as any).getScreenFingerprintHandler = async function() {
44
+ independentFingerprints++
45
+ return { fingerprint: 'independent' }
40
46
  }
47
+ let treeReads = 0
41
48
  ;(ToolsObserve as any).getUITreeHandler = async function(args: any) {
42
49
  assertTarget(args)
43
- return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] }
50
+ treeReads++
51
+ return { device, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] }
44
52
  }
45
53
  ;(ToolsObserve as any).readLogStreamHandler = async function(args: any) {
46
54
  assertTarget(args)
@@ -53,8 +61,11 @@ async function run() {
53
61
 
54
62
  const res1: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 50, sessionId: 's1' })
55
63
  console.log('res1:', JSON.stringify(res1, null, 2))
56
- const pass1 = res1 && res1.raw && res1.raw.screenshot === 'BASE64PNG' && res1.raw.activity && res1.raw.fingerprint === 'abc123' && Array.isArray(res1.raw.logs) && res1.raw.logs.length === 1
64
+ const pass1 = res1 && res1.raw && res1.raw.screenshot === 'BASE64PNG' && res1.raw.activity === current.activity && Array.isArray(res1.raw.logs) && res1.raw.logs.length === 1
57
65
  assert.ok(pass1, 'captureDebugSnapshot should aggregate successful handler results')
66
+ assert.equal(treeReads, 1)
67
+ assert.equal(independentFingerprints, 0)
68
+ assert.equal(res1.raw.fingerprint, computeScreenFingerprint(res1.raw.ui_tree, postTreeActivity, 'android').fingerprint)
58
69
  assert.strictEqual(res1.semantic.screen, 'Main')
59
70
  assert.strictEqual(res1.semantic.confidence >= 0.7, true)
60
71
  assert.deepStrictEqual(res1.semantic.actions_available, null)
@@ -63,7 +74,7 @@ async function run() {
63
74
 
64
75
  // --- Test 2: screenshot and ui tree fail; logs fallback to getLogs ---
65
76
  ;(ToolsObserve as any).captureScreenshotHandler = async function(args: any) { assertTarget(args); throw new Error('screencap failed') }
66
- ;(ToolsObserve as any).getUITreeHandler = async function(args: any) { assertTarget(args); throw new Error('uie_error') }
77
+ ;(ToolsObserve as any).getUITreeHandler = async function(args: any) { assertTarget(args); treeReads++; throw new Error('ACTION_TIMEOUT') }
67
78
  ;(ToolsObserve as any).readLogStreamHandler = async function(args: any) { assertTarget(args); return { entries: [] } }
68
79
  ;(ToolsObserve as any).getLogsHandler = async function(args: any) { assertTarget(args); return { device: { platform: 'android', id: 'mock' }, logs: ['INFO starting','ERROR crash here'], logCount: 2 } }
69
80
 
@@ -71,33 +82,76 @@ async function run() {
71
82
  console.log('res2:', JSON.stringify(res2, null, 2))
72
83
  const pass2 = res2 && res2.raw && res2.raw.screenshot_error && res2.raw.ui_tree_error && Array.isArray(res2.raw.logs) && res2.raw.logs.length === 2
73
84
  assert.ok(pass2, 'captureDebugSnapshot should surface partial failures and fallback logs')
85
+ assert.equal(res2.raw.ui_tree_error, 'ACTION_TIMEOUT')
86
+ assert.equal(res2.raw.fingerprint_error, 'ACTION_TIMEOUT')
87
+ assert.equal(res2.raw.fingerprint, null)
88
+ assert.equal(treeReads, 2)
74
89
  console.log('Test 2:', pass2 ? 'PASS' : 'FAIL')
75
90
 
76
91
  // --- Test 3: includeLogs=false should omit logs ---
77
92
  ;(ToolsObserve as any).captureScreenshotHandler = async function(args: any) { assertTarget(args); return { device: { platform: 'android', id: 'mock' }, screenshot: null } }
78
- ;(ToolsObserve as any).getCurrentScreenHandler = async function(args: any) { assert.strictEqual(args.deviceId, device.id); return { device: { platform: 'android', id: 'mock' }, package: '', activity: '', shortActivity: '' } }
79
- ;(ToolsObserve as any).getScreenFingerprintHandler = async function(args: any) { assertTarget(args); return { fingerprint: null } }
80
- ;(ToolsObserve as any).getUITreeHandler = async function(args: any) { assertTarget(args); return { device: { platform: 'android', id: 'mock' }, screen: '', resolution: { width: 0, height: 0 }, elements: [] } }
93
+ ;(ToolsObserve as any).getCurrentScreenHandler = async function(args: any) { assert.strictEqual(args.deviceId, device.id); throw new Error('activity unavailable') }
94
+ ;(ToolsObserve as any).getUITreeHandler = async function(args: any) {
95
+ assertTarget(args)
96
+ const revision = ++treeReads
97
+ return { device, screen: '', snapshot_revision: revision, captured_at_ms: 1000 + revision,
98
+ resolution: { width: 1080, height: 1920 }, elements: [{ visible: true, text: `State ${revision}` }] }
99
+ }
81
100
  ;(ToolsObserve as any).readLogStreamHandler = async function(args: any) { assertTarget(args); return { entries: [] } }
82
101
 
83
102
  const res3: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: false })
84
103
  console.log('res3:', JSON.stringify(res3, null, 2))
85
104
  const pass3 = res3 && res3.raw && typeof res3.raw.logs !== 'undefined' && res3.raw.logs.length === 0
86
105
  assert.ok(pass3, 'captureDebugSnapshot should return an empty logs array when includeLogs is false')
106
+ assert.equal(res3.raw.activity_error, 'activity unavailable')
107
+ assert.equal(res3.raw.fingerprint, computeScreenFingerprint(res3.raw.ui_tree, null, 'android').fingerprint)
87
108
  console.log('Test 3:', pass3 ? 'PASS' : 'FAIL')
88
109
 
89
110
  let screenshotCalls = 0
90
111
  ;(ToolsObserve as any).captureScreenshotHandler = async function(args: any) { assertTarget(args); screenshotCalls++; return { screenshot: 'unexpected' } }
91
- const res4: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: false, includeScreenshot: false })
112
+ const res4: any = await ToolsObserve.captureDebugSnapshotHandler({ includeLogs: false, includeScreenshot: false })
92
113
  assert.strictEqual(screenshotCalls, 0, 'includeScreenshot=false must not invoke screenshot capture')
93
114
  assert.strictEqual(res4.raw.screenshot, null)
94
115
  assert.strictEqual(resolutions, 4, 'Each snapshot resolves its own target once')
116
+ assert.equal(treeReads, 4)
117
+ assert.notEqual(res4.raw.fingerprint, res3.raw.fingerprint)
118
+ assert.equal(res4.raw.snapshot_revision, 4)
119
+ assert.equal(res4.raw.captured_at_ms, 1004)
120
+ assert.equal(independentFingerprints, 0)
95
121
 
96
122
  // Discovery failure must not collect from an arbitrary/default device.
97
123
  _setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('fixture discovery unavailable') } })
98
124
  await assert.rejects(() => ToolsObserve.captureDebugSnapshotHandler({ platform: 'android' }), /fixture discovery unavailable/)
99
125
  assert.strictEqual(screenshotCalls, 0, 'No component capture should run without a resolved target')
100
126
 
127
+ const other: DeviceInfo = { ...device, id: 'other-fixture' }
128
+ _setDeviceListersForTests({ listAndroidDevices: async () => [device, other] })
129
+ ;(ToolsObserve as any).getUITreeHandler = async ({ deviceId }: { deviceId: string }) => ({
130
+ device: deviceId === device.id ? device : other, screen: '',
131
+ resolution: { width: 100, height: 200 }, elements: [{ visible: true, text: deviceId }]
132
+ })
133
+ ;(ToolsObserve as any).getCurrentScreenHandler = async ({ deviceId }: { deviceId: string }) => ({
134
+ activity: 'com.example.Main', device: deviceId === device.id ? device : other
135
+ })
136
+ const [first, second] = await Promise.all([device, other].map(target => ToolsObserve.captureDebugSnapshotHandler({
137
+ platform: 'android', deviceId: target.id, includeLogs: false, includeScreenshot: false
138
+ })))
139
+ assert.equal(first.raw.ui_tree?.device.id, device.id)
140
+ assert.equal(second.raw.ui_tree?.device.id, other.id)
141
+ assert.notEqual(first.raw.fingerprint, second.raw.fingerprint)
142
+ assert.equal(independentFingerprints, 0)
143
+
144
+ const ios: DeviceInfo = { ...device, platform: 'ios', id: 'ios-fixture' }
145
+ _setDeviceListersForTests({ listIOSDevices: async () => [ios] })
146
+ ;(ToolsObserve as any).getUITreeHandler = async ({ platform, deviceId }: { platform: string; deviceId: string }) => {
147
+ assert.equal(platform, 'ios')
148
+ assert.equal(deviceId, ios.id)
149
+ return { device: ios, screen: '', resolution: { width: 100, height: 200 }, elements: [] }
150
+ }
151
+ const iosResult = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'ios', deviceId: ios.id, includeLogs: false, includeScreenshot: false })
152
+ assert.equal(iosResult.raw.fingerprint, 'independent')
153
+ assert.equal(independentFingerprints, 1)
154
+
101
155
  } finally {
102
156
  _resetDeviceListersForTests()
103
157
  ;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict'
2
+ import { cleanupReadinessDevice } from '../../device/automated/observe/device-readiness-cleanup.js'
3
+
4
+ async function run() {
5
+ for (const failedStep of [0, 1, 2]) {
6
+ const commands: string[][] = []
7
+ let closed = 0
8
+ const commandFailure = new Error('disconnected')
9
+ const closeFailure = new Error('close failed')
10
+ const failures = await cleanupReadinessDevice(true, (...args) => {
11
+ commands.push(args)
12
+ if (commands.length - 1 === failedStep) throw commandFailure
13
+ }, async () => { closed++; throw closeFailure })
14
+ assert.equal(commands.length, 3, 'a failed restoration must not skip later steps')
15
+ assert.equal(commands[2].at(-1), '3', 'Home is attempted')
16
+ assert.equal(closed, 1, 'client close always attempted')
17
+ assert.deepEqual(failures, [commandFailure, closeFailure])
18
+ }
19
+ let closed = false
20
+ assert.deepEqual(await cleanupReadinessDevice(false, () => { assert.fail('restoration disabled') }, async () => { closed = true }), [])
21
+ assert.equal(closed, true)
22
+ console.log('PASS readiness cleanup continues after each ADB failure and closes the client')
23
+ }
24
+ run().catch(error => { console.error(error); process.exitCode = 1 })
@@ -0,0 +1,97 @@
1
+ import { handleAdjustControl } from '../../../src/server/handlers/interact.js'
2
+ import { ToolsInteract } from '../../../src/interact/index.js'
3
+ import type { AdjustControlResponse } from '../../../src/types.js'
4
+ import { ReadinessTracker } from '../../../src/observe/readiness.js'
5
+ import { getSnapshotSessionId } from '../../../src/observe/snapshot-metadata.js'
6
+ import assert from 'node:assert/strict'
7
+ import { parseAndroidPower, parseAndroidLock, observeDeviceReadiness, parseIosLock, withDeviceReadinessProbe } from '../../../src/observe/device-readiness.js'
8
+ import { registerDebuggerPauseProvider } from '../../../src/debugger/guard.js'
9
+ import { runWithBudget } from '../../../src/utils/operation-budget.js'
10
+ import { compactAction, actionHandler } from '../../../src/server/handlers/action-execution.js'
11
+ import { wrapResponse } from '../../../src/server/common.js'
12
+ import { withResolvedDevice } from '../../../src/utils/resolve-device.js'
13
+
14
+ const power = (state: string, changing = 'false') => `mWakefulness=${state}\nmWakefulnessChanging=${changing}`
15
+ const lock = (state: string) => `KeyguardServiceDelegate\n showing=${state}\n KeyguardStateMonitor\n mIsShowing=${state}`
16
+ for (const [text, expected] of [[power('Awake'), 'awake'], [power('Asleep'), 'asleep'], [power('Dozing'), 'asleep'], [power('Awake', 'true'), 'transitioning'], [power('Dreaming'), 'unknown'], ['mWakefulness=Awake', 'unknown'], [power('Awake') + '\nmWakefulness=Asleep', 'unknown']]) assert.equal(parseAndroidPower(text), expected)
17
+ assert.equal(parseAndroidPower('Power Manager State:\n' + power('Dozing') + '\nSettings and Configuration:\nmWakefulness=3'), 'asleep', 'vendor debug fields outside current-state section cannot conflict with it')
18
+ assert.equal(parseAndroidLock(lock('true')), 'locked')
19
+ assert.equal(parseAndroidLock(lock('false')), 'unlocked')
20
+ for (const text of ['', 'deviceHasKeyguard=true', 'showing=false', lock('true').replace('mIsShowing=true', 'mIsShowing=false')]) assert.equal(parseAndroidLock(text), 'unknown')
21
+ const unsupported = JSON.stringify({ error: { domain: 'com.apple.dt.CoreDeviceError', code: 1001, userInfo: { CapabilityFeatureIdentifier: { string: 'com.apple.coredevice.feature.getlockstate' } } } })
22
+ assert.equal(parseIosLock(unsupported), 'unsupported')
23
+ assert.equal(parseIosLock(unsupported.replace('getlockstate', 'other')), 'unknown')
24
+ assert.equal(parseIosLock('{"result":{"passcodeRequired":false}}'), 'unknown', 'unverified success semantics cannot establish unlocked')
25
+ const device = { platform: 'android', id: 'fixture', model: 'Fixture', osVersion: '16', simulator: true }
26
+ async function run() {
27
+ const calls: string[][] = []
28
+ const probe = async (_command: string, args: string[], timeout: number) => {
29
+ calls.push(args)
30
+ assert.ok(timeout > 0 && timeout <= 1000)
31
+ return { exitCode: 0, stdout: args.includes('power') ? power('Asleep') : lock('true'), stderr: '' }
32
+ }
33
+ const started = Date.now()
34
+ const state = await runWithBudget(500, () => observeDeviceReadiness(device, probe))
35
+ assert.deepEqual(state.blockers, ['DEVICE_ASLEEP', 'DEVICE_LOCKED'])
36
+ assert.equal(state.input_ready, false)
37
+ assert.ok(state.evidence.every(item => item.captured_at_ms >= started))
38
+ assert.ok(calls.every(args => args[0] === '-s' && args[1] === device.id))
39
+ assert.ok(!JSON.stringify(state).includes('mWakefulness'))
40
+ const partial = await observeDeviceReadiness(device, async (...args) => {
41
+ if (args[1].includes('power')) throw new Error('private diagnostic')
42
+ return probe(...args)
43
+ })
44
+ assert.equal(partial.wakefulness, 'unknown')
45
+ assert.equal(partial.lock_state, 'locked')
46
+ assert.ok(!JSON.stringify(partial).includes('private diagnostic'))
47
+ const recovered = await observeDeviceReadiness(device, async (_cmd, args) => ({ exitCode: 0, stdout: args.includes('power') ? power('Awake') : lock('false'), stderr: '' }))
48
+ assert.equal(recovered.input_ready, null)
49
+ assert.deepEqual(recovered.blockers, [])
50
+ const tracker = new ReadinessTracker(device.platform, device.id, 100)
51
+ const tree = { device, captured_at_ms: Date.now(), snapshot_revision: 1, snapshot_session_id: getSnapshotSessionId(), resolution: { width: 100, height: 100 }, elements: [{ type: 'Button', visible: true, text: 'Ready' }] }
52
+ tracker.observe(tree, tree.captured_at_ms, 0)
53
+ assert.equal(tracker.observe(tree, tree.captured_at_ms, 100).state, 'settled')
54
+ await withDeviceReadinessProbe(probe, () => tracker.observeDevice())
55
+ assert.equal(tracker.ready, false)
56
+ await withDeviceReadinessProbe(async (_cmd, args) => ({ exitCode: 0, stdout: args.includes('power') ? power('Awake') : lock('false'), stderr: '' }), () => tracker.observeDevice())
57
+ assert.equal(tracker.observe(tree, tree.captured_at_ms, 500).state, 'unknown', 'blocker breaks UI stability continuity')
58
+ assert.equal(tracker.observe(tree, tree.captured_at_ms, 600).state, 'settled')
59
+ const before = calls.length
60
+ const expired = await runWithBudget(0, () => observeDeviceReadiness(device, probe))
61
+ assert.equal(calls.length, before, 'no probe starts after expiry')
62
+ assert.equal(expired.wakefulness, 'unknown')
63
+ assert.equal((await observeDeviceReadiness(null, probe)).limitations[0], 'DEVICE_TARGET_UNAVAILABLE')
64
+ const ios = await observeDeviceReadiness({ platform: 'ios', id: 'ios-fixture' }, async (_cmd, args, timeout) => { assert.equal(args[args.indexOf('--timeout') + 1], '5'); assert.ok(timeout <= 1000); return { exitCode: 1, stdout: unsupported, stderr: '' } })
65
+ assert.equal(ios.lock_state, 'unsupported')
66
+ assert.equal(ios.wakefulness, 'unknown')
67
+ registerDebuggerPauseProvider(() => ({ debug_session_id: 'paused', captured_at_ms: started, target: {} }))
68
+ try {
69
+ const paused = await observeDeviceReadiness(device, probe)
70
+ assert.deepEqual(paused.blockers, ['DEBUGGER_PAUSED'])
71
+ assert.equal(calls.length, before)
72
+ } finally { registerDebuggerPauseProvider(() => null) }
73
+ assert.deepEqual(compactAction({ success: true, device_readiness: state }, 'none').device_readiness, state)
74
+ for (const responseMode of ['debug', 'compact']) {
75
+ const response = await withDeviceReadinessProbe(probe, () => withResolvedDevice(device, () => actionHandler('start_app', async () => wrapResponse({ success: true, device }))( { platform: 'android', deviceId: device.id, responseMode, verificationMode: 'none', actionTimeoutMs: 500 })))
76
+ const result = JSON.parse(response.content[0].text!)
77
+ assert.equal(result.success, true, 'blocker observation preserves dispatch outcome')
78
+ assert.equal(result.verification.status, 'not_requested')
79
+ assert.deepEqual(result.device_readiness.blockers, ['DEVICE_ASLEEP', 'DEVICE_LOCKED'])
80
+ }
81
+ const failedProbe = await withDeviceReadinessProbe(async () => { throw new Error('private'); }, () => withResolvedDevice(device, () => actionHandler('tap', async () => wrapResponse({ success: true, device }))({ responseMode: 'compact', verificationMode: 'none', actionTimeoutMs: 200 })))
82
+ const unknown = JSON.parse(failedProbe.content[0].text!)
83
+ assert.equal(unknown.success, true)
84
+ assert.equal(unknown.device_readiness.wakefulness, 'unknown')
85
+ assert.equal(unknown.device_readiness.lock_state, 'unknown')
86
+ assert.ok(!JSON.stringify(unknown.device_readiness).includes('private'))
87
+ const adjust = ToolsInteract.adjustControlHandler
88
+ ToolsInteract.adjustControlHandler = async () => ({ success: true, device } as AdjustControlResponse)
89
+ try {
90
+ const response = await withDeviceReadinessProbe(probe, () => handleAdjustControl({ selector: { text: 'Volume' }, targetValue: 50 }))
91
+ const result = JSON.parse(response.content[0].text!)
92
+ assert.equal(result.success, true)
93
+ assert.equal(result.device_readiness.input_ready, false, 'adjust_control carries the same blocker contract')
94
+ } finally { ToolsInteract.adjustControlHandler = adjust }
95
+ console.log('Device readiness: parsing, probes, budgets, recovery, debugger and action contracts PASS')
96
+ }
97
+ run().catch(error => { console.error(error); process.exitCode = 1 })