mobile-debug-mcp 0.33.0 → 0.35.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.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +23 -2
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +158 -124
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +20 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +79 -0
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +304 -0
- package/dist/debugger/worker.js +176 -0
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3074
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +122 -76
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +13 -2
- package/dist/observe/readiness.js +44 -18
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +294 -0
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/handlers/debugger-handlers.js +48 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/schemas/debugger-schema.js +47 -0
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
- package/dist/server/schemas/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +62 -1134
- package/dist/server-core.js +4 -4
- package/dist/server.js +34 -0
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -438
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +75 -57
- package/dist/utils/operation-budget.js +2 -0
- package/dist/utils/resolve-device.js +6 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +48 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +41 -0
- package/docs/specs/023-android-system-traces/plan.md +115 -0
- package/docs/specs/023-android-system-traces/spec.md +122 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -16
- package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
- package/docs/tools/debugger.md +180 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +64 -25
- package/docs/tools/system-traces.md +134 -0
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +66 -0
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +31 -0
- package/src/debugger/java/DebuggerWorker.java +166 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +327 -0
- package/src/debugger/worker.ts +135 -0
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3491
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +130 -71
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +10 -3
- package/src/observe/readiness.ts +49 -22
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +227 -0
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +43 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
- package/src/server/schemas/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +69 -1140
- package/src/server-core.ts +4 -4
- package/src/server.ts +25 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -523
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +71 -51
- package/src/utils/operation-budget.ts +2 -0
- package/src/utils/resolve-device.ts +11 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +219 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +67 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +65 -0
- package/test/fixtures/debugger-app/build.ts +39 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +50 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/readiness.test.ts +3 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -893
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/tool-handlers.ts +0 -923
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { UiElement } from '../model.js'
|
|
2
|
+
import { _normalize } from '../targeting/element-helpers.js'
|
|
3
|
+
|
|
4
|
+
export function _buildControlPoint(bounds: [number, number, number, number], ratio: number, axis: 'horizontal' | 'vertical') {
|
|
5
|
+
const clampedRatio = Math.max(0, Math.min(1, ratio))
|
|
6
|
+
const [left, top, right, bottom] = bounds
|
|
7
|
+
const width = Math.max(1, right - left)
|
|
8
|
+
const height = Math.max(1, bottom - top)
|
|
9
|
+
const insetX = Math.max(8, Math.floor(width * 0.08))
|
|
10
|
+
const insetY = Math.max(8, Math.floor(height * 0.08))
|
|
11
|
+
if (axis === 'vertical') {
|
|
12
|
+
const usableHeight = Math.max(1, height - (insetY * 2))
|
|
13
|
+
return {
|
|
14
|
+
x: Math.floor((left + right) / 2),
|
|
15
|
+
y: Math.floor(bottom - insetY - (usableHeight * clampedRatio))
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const usableWidth = Math.max(1, width - (insetX * 2))
|
|
19
|
+
return {
|
|
20
|
+
x: Math.floor(left + insetX + (usableWidth * clampedRatio)),
|
|
21
|
+
y: Math.floor((top + bottom) / 2)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function _buildConservativeControlPoint(
|
|
26
|
+
bounds: [number, number, number, number],
|
|
27
|
+
targetValue: number,
|
|
28
|
+
currentValue: number | null,
|
|
29
|
+
min: number,
|
|
30
|
+
max: number,
|
|
31
|
+
axis: 'horizontal' | 'vertical'
|
|
32
|
+
) {
|
|
33
|
+
const range = Math.max(1, max - min)
|
|
34
|
+
const targetRatio = (targetValue - min) / range
|
|
35
|
+
const stepRatio = 1 / range
|
|
36
|
+
const centerBias = stepRatio / 2
|
|
37
|
+
const direction = currentValue === null ? 0 : Math.sign(targetValue - currentValue)
|
|
38
|
+
const controlLengthPx = axis === 'vertical' ? Math.max(1, bounds[3] - bounds[1]) : Math.max(1, bounds[2] - bounds[0])
|
|
39
|
+
const edgeWindow = Math.max(3, Math.floor(range * 0.1))
|
|
40
|
+
const isNearLowEdge = targetValue - min <= edgeWindow
|
|
41
|
+
const isNearHighEdge = max - targetValue <= edgeWindow
|
|
42
|
+
const directionBias = direction > 0
|
|
43
|
+
? -stepRatio * 0.15
|
|
44
|
+
: direction < 0
|
|
45
|
+
? stepRatio * 0.65
|
|
46
|
+
: 0
|
|
47
|
+
const pixelBasedMargin = Math.min(0.03, Math.max(0.005, 2 / controlLengthPx))
|
|
48
|
+
const endpointMargin = Math.max(stepRatio * 0.5, pixelBasedMargin)
|
|
49
|
+
const edgeBias = isNearLowEdge
|
|
50
|
+
? endpointMargin
|
|
51
|
+
: isNearHighEdge
|
|
52
|
+
? Math.max(stepRatio * 0.4, endpointMargin * 0.75)
|
|
53
|
+
: 0
|
|
54
|
+
const safeRatio = Math.min(
|
|
55
|
+
1 - (endpointMargin * 0.25),
|
|
56
|
+
Math.max(endpointMargin, targetRatio + centerBias + directionBias + edgeBias)
|
|
57
|
+
)
|
|
58
|
+
return _buildControlPoint(bounds, safeRatio, axis)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function _buildAdjustmentProbePoints(
|
|
62
|
+
bounds: [number, number, number, number],
|
|
63
|
+
targetValue: number,
|
|
64
|
+
currentValue: number | null,
|
|
65
|
+
min: number,
|
|
66
|
+
max: number,
|
|
67
|
+
axis: 'horizontal' | 'vertical'
|
|
68
|
+
) {
|
|
69
|
+
const targetPoint = _buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis)
|
|
70
|
+
const currentPoint = currentValue !== null
|
|
71
|
+
? _buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
72
|
+
: _buildControlPoint(bounds, 0.5, axis)
|
|
73
|
+
|
|
74
|
+
const [left, top, right, bottom] = bounds
|
|
75
|
+
const width = Math.max(1, right - left)
|
|
76
|
+
const height = Math.max(1, bottom - top)
|
|
77
|
+
const crossAxisBumps = axis === 'horizontal'
|
|
78
|
+
? [Math.max(24, Math.floor(height * 0.75)), Math.max(40, Math.floor(height * 1.5))]
|
|
79
|
+
: [Math.max(24, Math.floor(width * 0.75)), Math.max(40, Math.floor(width * 1.5))]
|
|
80
|
+
|
|
81
|
+
const clampPoint = (point: { x: number, y: number }) => ({
|
|
82
|
+
x: axis === 'horizontal'
|
|
83
|
+
? Math.max(left, Math.min(right, point.x))
|
|
84
|
+
: Math.max(left, Math.min(right + Math.max(width, height), point.x)),
|
|
85
|
+
y: axis === 'vertical'
|
|
86
|
+
? Math.max(top, Math.min(bottom, point.y))
|
|
87
|
+
: Math.max(top, Math.min(bottom + Math.max(height, width), point.y))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const probes = [targetPoint, currentPoint]
|
|
91
|
+
for (const bump of crossAxisBumps) {
|
|
92
|
+
if (axis === 'horizontal') {
|
|
93
|
+
probes.push(
|
|
94
|
+
{ x: targetPoint.x, y: bottom + bump },
|
|
95
|
+
{ x: currentPoint.x, y: bottom + bump }
|
|
96
|
+
)
|
|
97
|
+
} else {
|
|
98
|
+
probes.push(
|
|
99
|
+
{ x: right + bump, y: targetPoint.y },
|
|
100
|
+
{ x: right + bump, y: currentPoint.y }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Array.from(
|
|
106
|
+
new Map(
|
|
107
|
+
probes
|
|
108
|
+
.map(clampPoint)
|
|
109
|
+
.map((point) => [`${point.x}:${point.y}`, point] as const)
|
|
110
|
+
).values()
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function _controlAxis(el: UiElement, bounds: [number, number, number, number]): 'horizontal' | 'vertical' {
|
|
115
|
+
const type = _normalize(el.type ?? el.class ?? '')
|
|
116
|
+
const role = _normalize(el.role ?? '')
|
|
117
|
+
if (/vertical/.test(type) || /vertical/.test(role)) return 'vertical'
|
|
118
|
+
if (/horizontal/.test(type) || /horizontal/.test(role)) return 'horizontal'
|
|
119
|
+
return (bounds[3] - bounds[1]) > (bounds[2] - bounds[0]) ? 'vertical' : 'horizontal'
|
|
120
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { UIElementState } from '../../types.js'
|
|
2
|
+
import type { UiElement } from '../model.js'
|
|
3
|
+
import { _normalize } from '../targeting/element-helpers.js'
|
|
4
|
+
|
|
5
|
+
export function _readNumericControlValue(el: UiElement | null, property: string): number | null {
|
|
6
|
+
if (!el?.state) return null
|
|
7
|
+
const stateValue = el.state[property as keyof UIElementState]
|
|
8
|
+
if (typeof stateValue === 'number' && Number.isFinite(stateValue)) return stateValue
|
|
9
|
+
const parsedStateValue = _parseNumericControlValue(stateValue)
|
|
10
|
+
if (parsedStateValue !== null) return parsedStateValue
|
|
11
|
+
if (property === 'value' || property === 'raw_value') {
|
|
12
|
+
const fallback = el.state.raw_value ?? el.state.value
|
|
13
|
+
if (typeof fallback === 'number' && Number.isFinite(fallback)) return fallback
|
|
14
|
+
return _parseNumericControlValue(fallback)
|
|
15
|
+
}
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function _parseNumericControlValue(value: unknown): number | null {
|
|
20
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
21
|
+
if (typeof value !== 'string') return null
|
|
22
|
+
const trimmed = value.trim()
|
|
23
|
+
if (!trimmed) return null
|
|
24
|
+
|
|
25
|
+
const percent = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s*%$/)
|
|
26
|
+
if (percent) return Number(percent[1])
|
|
27
|
+
|
|
28
|
+
const fraction = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s+of\s+[-+]?\d+(?:\.\d+)?$/i)
|
|
29
|
+
if (fraction) return Number(fraction[1])
|
|
30
|
+
|
|
31
|
+
const plain = Number(trimmed)
|
|
32
|
+
if (Number.isFinite(plain)) return plain
|
|
33
|
+
|
|
34
|
+
const trailing = trimmed.match(/,\s*([-+]?\d+(?:\.\d+)?)$/)
|
|
35
|
+
if (trailing) return Number(trailing[1])
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function _readNumericControlRange(el: UiElement | null): { min: number; max: number } | null {
|
|
41
|
+
const range = el?.state?.value_range
|
|
42
|
+
const min = typeof range?.min === 'number' ? range.min : null
|
|
43
|
+
const max = typeof range?.max === 'number' ? range.max : null
|
|
44
|
+
if (min !== null && max !== null && Number.isFinite(min) && Number.isFinite(max) && max > min) {
|
|
45
|
+
return { min, max }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return rangeFromFraction(el?.state?.value ?? el?.state?.raw_value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rangeFromFraction(value: unknown): { min: number; max: number } | null {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
const fraction = value.trim().match(/^[-+]?\d+(?:\.\d+)?\s+of\s+([-+]?\d+(?:\.\d+)?)$/i)
|
|
54
|
+
if (fraction) {
|
|
55
|
+
const parsedMax = Number(fraction[1])
|
|
56
|
+
if (Number.isFinite(parsedMax) && parsedMax > 0) return { min: 0, max: parsedMax }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function _readControlStep(el: UiElement | null): number | null {
|
|
64
|
+
const candidates = [
|
|
65
|
+
el?.state?.step,
|
|
66
|
+
el?.state?.value_range?.step
|
|
67
|
+
]
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) return candidate
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function _quantizeTargetValue(targetValue: number, min: number, max: number, step: number | null): number {
|
|
75
|
+
if (!step) return targetValue
|
|
76
|
+
const quantized = min + (Math.round((targetValue - min) / step) * step)
|
|
77
|
+
const bounded = Math.max(min, Math.min(max, quantized))
|
|
78
|
+
const precision = _decimalPrecisionForStep(step)
|
|
79
|
+
return Number(bounded.toFixed(precision))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function _decimalPrecisionForStep(step: number): number {
|
|
83
|
+
const normalized = String(step).toLowerCase()
|
|
84
|
+
const [coefficient, exponentPart] = normalized.split('e')
|
|
85
|
+
const coefficientDecimals = coefficient.includes('.')
|
|
86
|
+
? coefficient.split('.')[1]?.length ?? 0
|
|
87
|
+
: 0
|
|
88
|
+
const exponent = exponentPart !== undefined ? Number(exponentPart) : 0
|
|
89
|
+
const precision = exponent < 0
|
|
90
|
+
? coefficientDecimals + Math.abs(exponent)
|
|
91
|
+
: Math.max(0, coefficientDecimals - exponent)
|
|
92
|
+
return Math.max(0, Math.min(12, precision))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function _defaultAdjustTolerance(range: { min: number; max: number } | null, step: number | null): number {
|
|
96
|
+
if (step) return 0
|
|
97
|
+
if (!range) return 0
|
|
98
|
+
return Math.max(0, 0.01 * Math.abs(range.max - range.min))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function _runtimeActionSet(el: UiElement | null): Set<string> {
|
|
102
|
+
const values = [
|
|
103
|
+
...(Array.isArray(el?.actions) ? el!.actions! : []),
|
|
104
|
+
...(Array.isArray(el?.accessibilityActions) ? el!.accessibilityActions! : []),
|
|
105
|
+
...(Array.isArray(el?.runtimeActions) ? el!.runtimeActions! : []),
|
|
106
|
+
...(Array.isArray(el?.platformActions) ? el!.platformActions! : [])
|
|
107
|
+
]
|
|
108
|
+
return new Set(values.map((value) => _normalize(value)).filter(Boolean))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function _hasDirectAdjustmentAction(el: UiElement | null): boolean {
|
|
112
|
+
const actions = _runtimeActionSet(el)
|
|
113
|
+
return [
|
|
114
|
+
'action_set_progress',
|
|
115
|
+
'set_progress',
|
|
116
|
+
'setprogress',
|
|
117
|
+
'semanticsactions.setprogress',
|
|
118
|
+
'set_value',
|
|
119
|
+
'setvalue',
|
|
120
|
+
'direct_set_value'
|
|
121
|
+
].some((action) => actions.has(action))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function _adjustmentDirections(el: UiElement | null): { increment: boolean; decrement: boolean } {
|
|
125
|
+
const actions = _runtimeActionSet(el)
|
|
126
|
+
return {
|
|
127
|
+
increment: ['increment', 'accessibility_increment', 'action_increment'].some((action) => actions.has(action)),
|
|
128
|
+
decrement: ['decrement', 'accessibility_decrement', 'action_decrement'].some((action) => actions.has(action))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function _textFromElement(el: UiElement | null): string {
|
|
133
|
+
return String(el?.text ?? el?.contentDescription ?? el?.contentDesc ?? el?.accessibilityLabel ?? el?.value ?? '').trim()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function _parseLeadingNumber(text: string): number | null {
|
|
137
|
+
const match = text.trim().match(/^([-+]?\d+(?:\.\d+)?)(?:\s*[a-zA-Z%]+)?$/)
|
|
138
|
+
if (!match) return null
|
|
139
|
+
const value = Number(match[1])
|
|
140
|
+
return Number.isFinite(value) ? value : null
|
|
141
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { UiElement, InferredLabeledSliderState } from '../model.js'
|
|
2
|
+
import { _normalize, _normalizeBounds } from '../targeting/element-helpers.js'
|
|
3
|
+
import { _textFromElement, _parseLeadingNumber } from './control-values.js'
|
|
4
|
+
|
|
5
|
+
export function _inferLabeledSliderState(elements: UiElement[], track: UiElement | null): InferredLabeledSliderState | null {
|
|
6
|
+
const trackBounds = _normalizeBounds(track?.bounds)
|
|
7
|
+
if (!track || !trackBounds) return null
|
|
8
|
+
const type = _normalize(track.type ?? track.class ?? '')
|
|
9
|
+
const role = _normalize(track.role ?? '')
|
|
10
|
+
const width = trackBounds[2] - trackBounds[0]
|
|
11
|
+
const height = trackBounds[3] - trackBounds[1]
|
|
12
|
+
const wideTrackLike = width >= 240 && height <= 220
|
|
13
|
+
const knownTrackLike = /slider|seekbar|range/.test(type) || /slider|range/.test(role)
|
|
14
|
+
if (!wideTrackLike && !knownTrackLike) return null
|
|
15
|
+
|
|
16
|
+
const trackTop = trackBounds[1]
|
|
17
|
+
const trackBottom = trackBounds[3]
|
|
18
|
+
const trackCenterX = (trackBounds[0] + trackBounds[2]) / 2
|
|
19
|
+
const labels = elements
|
|
20
|
+
.map((el, index) => {
|
|
21
|
+
const bounds = _normalizeBounds(el.bounds)
|
|
22
|
+
const text = _textFromElement(el)
|
|
23
|
+
const value = _parseLeadingNumber(text)
|
|
24
|
+
return bounds && text && value !== null
|
|
25
|
+
? {
|
|
26
|
+
index,
|
|
27
|
+
el,
|
|
28
|
+
text,
|
|
29
|
+
value,
|
|
30
|
+
bounds,
|
|
31
|
+
centerX: (bounds[0] + bounds[2]) / 2,
|
|
32
|
+
centerY: (bounds[1] + bounds[3]) / 2,
|
|
33
|
+
hasUnit: /[a-zA-Z%]/.test(text)
|
|
34
|
+
}
|
|
35
|
+
: null
|
|
36
|
+
})
|
|
37
|
+
.filter((label): label is NonNullable<typeof label> => !!label)
|
|
38
|
+
|
|
39
|
+
const rangeLabels = labels
|
|
40
|
+
.filter((label) => {
|
|
41
|
+
const verticallyNearTrack = label.centerY >= trackTop - 520 && label.centerY <= trackBottom + 260
|
|
42
|
+
const horizontallyNearTrack = label.centerX >= trackBounds[0] - 120 && label.centerX <= trackBounds[2] + 120
|
|
43
|
+
return verticallyNearTrack && horizontallyNearTrack && label.hasUnit
|
|
44
|
+
})
|
|
45
|
+
.sort((a, b) => a.centerX - b.centerX)
|
|
46
|
+
|
|
47
|
+
if (rangeLabels.length < 2) return null
|
|
48
|
+
const minLabel = rangeLabels[0]
|
|
49
|
+
const maxLabel = rangeLabels[rangeLabels.length - 1]
|
|
50
|
+
if (!minLabel || !maxLabel || maxLabel.value <= minLabel.value) return null
|
|
51
|
+
|
|
52
|
+
const currentCandidates = labels
|
|
53
|
+
.filter((label) => {
|
|
54
|
+
if (label.index === minLabel.index || label.index === maxLabel.index) return false
|
|
55
|
+
if (label.value < minLabel.value || label.value > maxLabel.value) return false
|
|
56
|
+
if (label.centerY < trackTop - 950 || label.centerY > minLabel.centerY - 20) return false
|
|
57
|
+
return label.centerX >= trackCenterX - 80
|
|
58
|
+
})
|
|
59
|
+
.sort((a, b) => {
|
|
60
|
+
const yDistance = Math.abs((trackTop - 420) - a.centerY) - Math.abs((trackTop - 420) - b.centerY)
|
|
61
|
+
if (yDistance !== 0) return yDistance
|
|
62
|
+
return b.centerX - a.centerX
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const currentLabel = currentCandidates[0]
|
|
66
|
+
if (!currentLabel) return null
|
|
67
|
+
|
|
68
|
+
const candidateBelowRangeLabels = minLabel.centerY < trackTop && maxLabel.centerY < trackTop
|
|
69
|
+
const labelDerivedBounds: [number, number, number, number] = [
|
|
70
|
+
Math.min(minLabel.bounds[0], trackBounds[0]),
|
|
71
|
+
Math.max(currentLabel.bounds[3] + 12, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 160),
|
|
72
|
+
Math.max(maxLabel.bounds[2], trackBounds[2]),
|
|
73
|
+
Math.max(currentLabel.bounds[3] + 80, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 32)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
current: currentLabel.value,
|
|
78
|
+
min: minLabel.value,
|
|
79
|
+
max: maxLabel.value,
|
|
80
|
+
value_range: { min: minLabel.value, max: maxLabel.value },
|
|
81
|
+
touch_bounds: candidateBelowRangeLabels ? labelDerivedBounds : trackBounds,
|
|
82
|
+
source: 'nearby_labels'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ExpectScreenResponse } from '../../types.js'
|
|
2
|
+
import type { DeviceTarget } from '../observation.js'
|
|
3
|
+
import { buildObservationTrace } from '../interaction-trace.js'
|
|
4
|
+
export interface ScreenExpectation extends DeviceTarget { fingerprint?: string; screen?: string }
|
|
5
|
+
export interface ScreenAccess {
|
|
6
|
+
fingerprint(target: DeviceTarget): Promise<{ fingerprint: string | null; activity?: string | null } | null>
|
|
7
|
+
current(target: { deviceId?: string }): Promise<{ activity?: string; shortActivity?: string } | null>
|
|
8
|
+
}
|
|
9
|
+
function compareScreen(input: ScreenExpectation, observed: { fingerprint: string | null; screen: string | null }, label: string | null) {
|
|
10
|
+
const { fingerprint, screen } = input
|
|
11
|
+
if (fingerprint) {
|
|
12
|
+
const success = observed.fingerprint === fingerprint
|
|
13
|
+
return { success, basis: 'fingerprint' as const, reason: success ? `observed fingerprint matches expected fingerprint ${fingerprint}` : `expected fingerprint ${fingerprint} but observed ${observed.fingerprint ?? 'null'}` }
|
|
14
|
+
}
|
|
15
|
+
if (screen) {
|
|
16
|
+
const candidates = new Set<string>()
|
|
17
|
+
if (observed.screen) candidates.add(observed.screen)
|
|
18
|
+
if (label) candidates.add(label)
|
|
19
|
+
const success = candidates.has(screen)
|
|
20
|
+
return { success, basis: 'screen' as const, reason: success ? `observed screen matches expected screen ${screen}` : `expected screen ${screen} but observed ${label ?? observed.screen ?? 'null'}` }
|
|
21
|
+
}
|
|
22
|
+
return { success: false, basis: 'none' as const, reason: 'No fingerprint or screen expectation provided' }
|
|
23
|
+
}
|
|
24
|
+
export async function expectScreen(input: ScreenExpectation, deps: ScreenAccess): Promise<ExpectScreenResponse> {
|
|
25
|
+
const observed = await deps.fingerprint({ platform: input.platform, deviceId: input.deviceId })
|
|
26
|
+
const screen = { fingerprint: observed?.fingerprint ?? null, screen: observed?.activity ?? null }
|
|
27
|
+
let label = screen.screen
|
|
28
|
+
if (!input.fingerprint && input.screen && input.platform !== 'ios') {
|
|
29
|
+
try { const current = await deps.current({ deviceId: input.deviceId }); label = current?.shortActivity || current?.activity || label }
|
|
30
|
+
catch { /* Keep the fingerprint-derived activity if current-screen lookup is unavailable. */ }
|
|
31
|
+
}
|
|
32
|
+
const comparison = compareScreen(input, screen, label)
|
|
33
|
+
const expected = { fingerprint: input.fingerprint ?? null, screen: input.screen ?? null }
|
|
34
|
+
const observedScreen = { fingerprint: screen.fingerprint, screen: label }
|
|
35
|
+
return { success: comparison.success, observed_screen: observedScreen, expected_screen: expected,
|
|
36
|
+
confidence: comparison.success ? 1 : 0, comparison: { basis: comparison.basis, matched: comparison.success, reason: comparison.reason },
|
|
37
|
+
trace: buildObservationTrace({ actionType: 'expect_screen', stage: 'verify', success: comparison.success, attempts: 1,
|
|
38
|
+
metadata: { expected_screen: expected, observed_screen: observedScreen, comparison: { basis: comparison.basis, matched: comparison.success } } }) }
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { systemClock, type Clock } from '../../runtime/clock.js'
|
|
2
|
+
import type { ActionTargetResolved, ExpectStateResponse, TraceStep, UIElementState } from '../../types.js'
|
|
3
|
+
import type { DeviceTarget, ObserveUI } from '../observation.js'
|
|
4
|
+
import type { UiSelector } from '../../contracts/selector.js'
|
|
5
|
+
import type { AdjustmentDependencies } from '../adjustment/adjustment-context.js'
|
|
6
|
+
import { createTraceStep, nextActionId } from '../../runtime/action-results.js'
|
|
7
|
+
import type { StateComparison } from './state-comparison.js'
|
|
8
|
+
export interface StateInput extends DeviceTarget {
|
|
9
|
+
selector?: UiSelector; element_id?: string; property: string; expected: ExpectStateResponse['expected_state']['expected']
|
|
10
|
+
stabilization_window_ms?: number; stable_observation_count?: number; snapshot_stale_threshold_ms?: number; poll_interval_ms?: number; exact_budget?: boolean
|
|
11
|
+
}
|
|
12
|
+
export type StateDependencies = Pick<AdjustmentDependencies, 'getHandle' | 'currentHandle' | 'target'> & { observe: ObserveUI; clock?: Clock }
|
|
13
|
+
export function createStateAssertion({ selector, element_id, property, expected, platform, deviceId,
|
|
14
|
+
stabilization_window_ms = 1000, stable_observation_count = 2, snapshot_stale_threshold_ms = 500,
|
|
15
|
+
poll_interval_ms = 150, exact_budget = false }: StateInput, clock: Clock = systemClock) {
|
|
16
|
+
const start = clock.now()
|
|
17
|
+
return { clock, input: { selector, element_id, property, expected, platform, deviceId }, start,
|
|
18
|
+
deadline: start + (exact_budget ? stabilization_window_ms : Math.max(500, stabilization_window_ms)),
|
|
19
|
+
stableTarget: Math.max(1, Math.floor(stable_observation_count || 2)),
|
|
20
|
+
delay: exact_budget ? Math.max(50, Math.min(poll_interval_ms || 150, 1000)) : Math.max(100, Math.min(poll_interval_ms || 150, 200)),
|
|
21
|
+
stale: Math.max(300, Math.min(snapshot_stale_threshold_ms || 500, 800)), attempts: 0, stable: 0, reason: 'element not found',
|
|
22
|
+
failure: 'ELEMENT_NOT_FOUND' as 'ELEMENT_NOT_FOUND' | 'UNKNOWN', element: null as (ActionTargetResolved & { state?: UIElementState | null }) | null,
|
|
23
|
+
value: null as StateComparison['observedValue'], raw: null as StateComparison['rawValue'], id: element_id ?? null,
|
|
24
|
+
trace: [] as TraceStep[], sequence: 0, resolveRecorded: false }
|
|
25
|
+
}
|
|
26
|
+
export type StateAssertion = ReturnType<typeof createStateAssertion>
|
|
27
|
+
export function recordState(s: StateAssertion, stage: TraceStep['stage'], result: TraceStep['result'], metadata: Record<string, unknown>) {
|
|
28
|
+
s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }))
|
|
29
|
+
}
|
|
30
|
+
export function stabilizeState(s: StateAssertion, result: 'success' | 'retry', age: number | null, reason: string) {
|
|
31
|
+
recordState(s, 'stabilize', result, { stabilization_attempts: s.attempts, stable_observation_count: s.stable, snapshot_freshness_ms: age, reason })
|
|
32
|
+
}
|
|
33
|
+
function trace(s: StateAssertion, outcome: 'success' | 'failure') {
|
|
34
|
+
return { action_id: nextActionId('expect_state', s.clock.now()), steps: s.trace, final_outcome: outcome, attempts: s.attempts }
|
|
35
|
+
}
|
|
36
|
+
export function stateSuccess(s: StateAssertion, comparison: StateComparison, age: number | null): ExpectStateResponse {
|
|
37
|
+
return { success: true, selector: s.input.selector, element_id: s.id,
|
|
38
|
+
expected_state: { property: s.input.property, expected: s.input.expected }, element: s.element,
|
|
39
|
+
observed_state: { property: s.input.property, value: comparison.observedValue, ...(comparison.rawValue !== null ? { raw_value: comparison.rawValue } : {}) },
|
|
40
|
+
reason: comparison.reason, stabilization_attempts: s.attempts, stabilization_window_ms: s.clock.now() - s.start,
|
|
41
|
+
stable_observation_count: s.stable, snapshot_freshness_ms: age ?? undefined, trace: trace(s, 'success') }
|
|
42
|
+
}
|
|
43
|
+
export function stateFailure(s: StateAssertion): ExpectStateResponse {
|
|
44
|
+
return { success: false, selector: s.input.selector, element_id: s.id,
|
|
45
|
+
expected_state: { property: s.input.property, expected: s.input.expected }, element: s.element,
|
|
46
|
+
observed_state: { property: s.input.property, value: s.value, ...(s.raw !== null ? { raw_value: s.raw } : {}) },
|
|
47
|
+
reason: s.reason, failure_code: s.failure, retryable: true, trace: trace(s, 'failure') }
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { UiTree } from '../observation.js'
|
|
2
|
+
import type { ExpectStateResponse } from '../../types.js'
|
|
3
|
+
import { _findFirstMatchingElement } from '../targeting/element-helpers.js'
|
|
4
|
+
import { compareReadableState } from './state-comparison.js'
|
|
5
|
+
import { createStateAssertion, recordState, stabilizeState, stateSuccess, stateFailure,
|
|
6
|
+
type StateInput, type StateDependencies, type StateAssertion } from './state-assertion-context.js'
|
|
7
|
+
|
|
8
|
+
function matchState(s: StateAssertion, tree: UiTree, deps: StateDependencies) {
|
|
9
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : []
|
|
10
|
+
const platform = tree?.device?.platform === 'ios' ? 'ios' : s.input.platform || 'android'
|
|
11
|
+
const deviceId = tree?.device?.id || s.input.deviceId
|
|
12
|
+
const age = typeof tree?.captured_at_ms === 'number' ? s.clock.now() - tree.captured_at_ms : null
|
|
13
|
+
let matched = null
|
|
14
|
+
if (s.input.element_id) {
|
|
15
|
+
const handle = deps.getHandle(s.input.element_id)
|
|
16
|
+
if (handle) {
|
|
17
|
+
const current = deps.currentHandle(elements, platform, deviceId, handle)
|
|
18
|
+
if (current) matched = { el: current.el, idx: current.index }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!matched && s.input.selector) matched = _findFirstMatchingElement(elements, s.input.selector)
|
|
22
|
+
return { matched, platform, deviceId, age }
|
|
23
|
+
}
|
|
24
|
+
function retryResolution(s: StateAssertion, matched: boolean, age: number | null) {
|
|
25
|
+
s.reason = matched ? 'stale snapshot' : 'element not found'
|
|
26
|
+
s.failure = matched ? 'UNKNOWN' : 'ELEMENT_NOT_FOUND'; s.stable = 0
|
|
27
|
+
recordState(s, 'resolve', 'retry', { selector: s.input.selector ?? null, element_id: s.id, matched, reason: s.reason, attempt: s.attempts })
|
|
28
|
+
stabilizeState(s, 'retry', age, s.reason)
|
|
29
|
+
}
|
|
30
|
+
async function sample(s: StateAssertion, deps: StateDependencies): Promise<ExpectStateResponse | null> {
|
|
31
|
+
s.attempts++
|
|
32
|
+
const tree = await deps.observe({ platform: s.input.platform, deviceId: s.input.deviceId })
|
|
33
|
+
const { matched, platform, deviceId, age } = matchState(s, tree, deps)
|
|
34
|
+
if (!matched) { retryResolution(s, false, age); return null }
|
|
35
|
+
const element = deps.target(platform, deviceId, matched)
|
|
36
|
+
s.id = element.elementId; s.element = { ...element, state: matched.el.state ?? null }
|
|
37
|
+
if (age !== null && age > s.stale) { retryResolution(s, true, age); return null }
|
|
38
|
+
if (!s.resolveRecorded) {
|
|
39
|
+
recordState(s, 'resolve', 'success', { selector: s.input.selector ?? null, element_id: s.id, matched: true, reason: 'element resolved' })
|
|
40
|
+
s.resolveRecorded = true
|
|
41
|
+
}
|
|
42
|
+
const comparison = compareReadableState(s.input.property, s.input.expected, matched.el.state ?? null)
|
|
43
|
+
if (comparison.success) {
|
|
44
|
+
s.stable++
|
|
45
|
+
stabilizeState(s, 'success', age, comparison.reason)
|
|
46
|
+
recordState(s, 'verify', 'success', { property: s.input.property, expected: s.input.expected, observed_value: comparison.observedValue, reason: comparison.reason })
|
|
47
|
+
if (s.stable >= s.stableTarget) return stateSuccess(s, comparison, age)
|
|
48
|
+
} else {
|
|
49
|
+
s.stable = 0; s.reason = comparison.reason || s.reason; s.failure = 'UNKNOWN'
|
|
50
|
+
stabilizeState(s, 'retry', age, s.reason)
|
|
51
|
+
recordState(s, 'verify', 'retry', { property: s.input.property, expected: s.input.expected, observed_value: comparison.observedValue, reason: s.reason })
|
|
52
|
+
s.value = comparison.observedValue; s.raw = comparison.rawValue
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
export async function expectState(input: StateInput, deps: StateDependencies): Promise<ExpectStateResponse> {
|
|
57
|
+
const state = createStateAssertion(input, deps.clock)
|
|
58
|
+
while (state.clock.now() <= state.deadline) {
|
|
59
|
+
const result = await sample(state, deps)
|
|
60
|
+
if (result) return result
|
|
61
|
+
await state.clock.sleep(state.delay)
|
|
62
|
+
}
|
|
63
|
+
return stateFailure(state)
|
|
64
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { UIElementState, ExpectStateResponse } from '../../types.js';
|
|
2
|
+
type Expected = ExpectStateResponse['expected_state']['expected'];
|
|
3
|
+
type Observed = NonNullable<ExpectStateResponse['observed_state']>['value'];
|
|
4
|
+
export interface StateComparison {
|
|
5
|
+
success: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
rawValue: boolean | number | string | null;
|
|
8
|
+
observedValue: Observed;
|
|
9
|
+
}
|
|
10
|
+
interface ComparisonInput {
|
|
11
|
+
property: string;
|
|
12
|
+
expected: Expected;
|
|
13
|
+
actual: Observed;
|
|
14
|
+
observedState: UIElementState | null;
|
|
15
|
+
}
|
|
16
|
+
const compareBoolean = (value: unknown) => typeof value === 'boolean' ? value : null;
|
|
17
|
+
const compareString = (value: unknown) => typeof value === 'string' ? value : null;
|
|
18
|
+
const compareNumber = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
19
|
+
export function compareReadableState(property: string, expected: Expected, observedState: UIElementState | null): StateComparison {
|
|
20
|
+
const input = { property, expected, observedState, actual: observedState?.[property as keyof UIElementState] ?? null };
|
|
21
|
+
if (['checked', 'focused', 'expanded', 'enabled'].includes(property))
|
|
22
|
+
return compareBooleanState(input);
|
|
23
|
+
if (['value', 'raw_value'].includes(property))
|
|
24
|
+
return compareValueState(input);
|
|
25
|
+
if (property === 'selected')
|
|
26
|
+
return compareSelectedState(input);
|
|
27
|
+
if (property === 'text_value')
|
|
28
|
+
return compareTextState(input);
|
|
29
|
+
return compareOtherState(input);
|
|
30
|
+
}
|
|
31
|
+
function compareBooleanState({ property, expected, actual }: ComparisonInput): StateComparison {
|
|
32
|
+
let success = false;
|
|
33
|
+
let reason = '';
|
|
34
|
+
let rawValue: StateComparison['rawValue'] = null;
|
|
35
|
+
let observedValue: Observed = actual;
|
|
36
|
+
const expectedBool = compareBoolean(expected);
|
|
37
|
+
const actualBool = compareBoolean(actual);
|
|
38
|
+
if (expectedBool === null) {
|
|
39
|
+
reason = `expected ${property} must be boolean`;
|
|
40
|
+
}
|
|
41
|
+
else if (actualBool === null) {
|
|
42
|
+
reason = `${property} state unavailable`;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
rawValue = actualBool;
|
|
46
|
+
success = actualBool === expectedBool;
|
|
47
|
+
reason = success ? `${property} matches expected value` : `expected ${property}=${expectedBool} but observed ${actualBool}`;
|
|
48
|
+
}
|
|
49
|
+
observedValue = actualBool;
|
|
50
|
+
return { success, reason, rawValue, observedValue };
|
|
51
|
+
}
|
|
52
|
+
function compareValueState({ expected, actual }: ComparisonInput): StateComparison {
|
|
53
|
+
let success = false;
|
|
54
|
+
let reason = '';
|
|
55
|
+
let rawValue: StateComparison['rawValue'] = null;
|
|
56
|
+
let observedValue: Observed = actual;
|
|
57
|
+
const expectedNumber = compareNumber(expected);
|
|
58
|
+
const actualNumber = compareNumber(actual);
|
|
59
|
+
if (expectedNumber !== null && actualNumber !== null) {
|
|
60
|
+
success = actualNumber === expectedNumber;
|
|
61
|
+
rawValue = actualNumber;
|
|
62
|
+
observedValue = actualNumber;
|
|
63
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
|
|
64
|
+
return { success, reason, rawValue, observedValue };
|
|
65
|
+
}
|
|
66
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
67
|
+
const actualString = compareString(actual);
|
|
68
|
+
if (expectedString !== null && actualString !== null) {
|
|
69
|
+
success = actualString === expectedString;
|
|
70
|
+
rawValue = actualString;
|
|
71
|
+
observedValue = actualString;
|
|
72
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
reason = 'value state unavailable';
|
|
76
|
+
}
|
|
77
|
+
return { success, reason, rawValue, observedValue };
|
|
78
|
+
}
|
|
79
|
+
function compareSelectedState({ expected, actual, observedState }: ComparisonInput): StateComparison {
|
|
80
|
+
let success = false;
|
|
81
|
+
let reason = '';
|
|
82
|
+
let rawValue: StateComparison['rawValue'] = null;
|
|
83
|
+
let observedValue: Observed = actual;
|
|
84
|
+
const expectedBool = typeof expected === 'boolean' ? expected : null;
|
|
85
|
+
const expectedString = typeof expected === 'string'
|
|
86
|
+
? expected
|
|
87
|
+
: expected && typeof expected === 'object'
|
|
88
|
+
? String((expected as {
|
|
89
|
+
id?: unknown;
|
|
90
|
+
label?: unknown;
|
|
91
|
+
}).id ?? (expected as {
|
|
92
|
+
id?: unknown;
|
|
93
|
+
label?: unknown;
|
|
94
|
+
}).label ?? '')
|
|
95
|
+
: null;
|
|
96
|
+
if (!observedState || observedState.selected === undefined || observedState.selected === null) {
|
|
97
|
+
reason = 'selected state unavailable';
|
|
98
|
+
return { success, reason, rawValue, observedValue };
|
|
99
|
+
}
|
|
100
|
+
if (expectedBool !== null) {
|
|
101
|
+
const actualBool = typeof observedState.selected === 'boolean' ? observedState.selected : null;
|
|
102
|
+
if (actualBool === null) {
|
|
103
|
+
reason = 'selected state is not boolean';
|
|
104
|
+
return { success, reason, rawValue, observedValue };
|
|
105
|
+
}
|
|
106
|
+
rawValue = actualBool;
|
|
107
|
+
observedValue = actualBool;
|
|
108
|
+
success = actualBool === expectedBool;
|
|
109
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedBool} but observed ${actualBool}`;
|
|
110
|
+
return { success, reason, rawValue, observedValue };
|
|
111
|
+
}
|
|
112
|
+
const actualSelected = typeof observedState.selected === 'object' && observedState.selected !== null
|
|
113
|
+
? String((observedState.selected as {
|
|
114
|
+
id?: unknown;
|
|
115
|
+
label?: unknown;
|
|
116
|
+
}).id ?? (observedState.selected as {
|
|
117
|
+
id?: unknown;
|
|
118
|
+
label?: unknown;
|
|
119
|
+
}).label ?? '')
|
|
120
|
+
: String(observedState.selected);
|
|
121
|
+
const actualString = actualSelected.trim();
|
|
122
|
+
if (!expectedString) {
|
|
123
|
+
reason = 'expected selected must be boolean, string, or object with id/label';
|
|
124
|
+
return { success, reason, rawValue, observedValue };
|
|
125
|
+
}
|
|
126
|
+
rawValue = actualString;
|
|
127
|
+
observedValue = actualString;
|
|
128
|
+
success = actualString === expectedString;
|
|
129
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedString} but observed ${actualString}`;
|
|
130
|
+
return { success, reason, rawValue, observedValue };
|
|
131
|
+
}
|
|
132
|
+
function compareTextState({ expected, actual }: ComparisonInput): StateComparison {
|
|
133
|
+
let success = false;
|
|
134
|
+
let reason = '';
|
|
135
|
+
let rawValue: StateComparison['rawValue'] = null;
|
|
136
|
+
let observedValue: Observed = actual;
|
|
137
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
138
|
+
const actualString = compareString(actual);
|
|
139
|
+
if (!expectedString) {
|
|
140
|
+
reason = 'expected text_value must be string';
|
|
141
|
+
}
|
|
142
|
+
else if (!actualString) {
|
|
143
|
+
reason = 'text_value state unavailable';
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
success = actualString === expectedString;
|
|
147
|
+
rawValue = actualString;
|
|
148
|
+
observedValue = actualString;
|
|
149
|
+
reason = success ? 'text_value matches expected value' : `expected text_value=${expectedString} but observed ${actualString}`;
|
|
150
|
+
}
|
|
151
|
+
return { success, reason, rawValue, observedValue };
|
|
152
|
+
}
|
|
153
|
+
function compareOtherState({ property, expected, actual }: ComparisonInput): StateComparison {
|
|
154
|
+
let success = false;
|
|
155
|
+
let reason = '';
|
|
156
|
+
let rawValue: StateComparison['rawValue'] = null;
|
|
157
|
+
let observedValue: Observed = actual;
|
|
158
|
+
if (actual !== null && actual !== undefined) {
|
|
159
|
+
success = actual === expected;
|
|
160
|
+
observedValue = actual;
|
|
161
|
+
rawValue = typeof actual === 'string' || typeof actual === 'number' || typeof actual === 'boolean' ? actual : null;
|
|
162
|
+
reason = success ? `${property} matches expected value` : `expected ${property} to match but observed ${String(actual)}`;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
reason = `unsupported or unavailable state property: ${property}`;
|
|
166
|
+
}
|
|
167
|
+
return { success, reason, rawValue, observedValue };
|
|
168
|
+
}
|