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
|
@@ -1,893 +0,0 @@
|
|
|
1
|
-
import { withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js';
|
|
2
|
-
import { measure, withTiming } from '../utils/timing.js';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { runWithBudget } from '../utils/operation-budget.js';
|
|
5
|
-
import { journeySchema, tapElementControlsSchema } from './journey-schema.js';
|
|
6
|
-
import { readinessSchema, readinessWaitSchema } from './readiness-schema.js';
|
|
7
|
-
import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js';
|
|
8
|
-
import { validateExpectedState } from './condition-schema.js';
|
|
9
|
-
import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js';
|
|
10
|
-
import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js';
|
|
11
|
-
import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js';
|
|
12
|
-
import { ToolsInteract } from '../interact/index.js';
|
|
13
|
-
import { ToolsObserve } from '../observe/index.js';
|
|
14
|
-
import { classifyActionOutcome } from '../interact/classify.js';
|
|
15
|
-
import { ToolsNetwork } from '../network/index.js';
|
|
16
|
-
import { getSystemStatus } from '../system/index.js';
|
|
17
|
-
import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js';
|
|
18
|
-
import { buildActionExecutionResult, captureActionFingerprint, getArrayArg, getBooleanArg, getNumberArg, getObjectArg, getStringArg, inferGenericFailure, inferScrollFailure, requireBooleanArg, requireNumberArg, requireObjectArg, requireStringArg, wrapResponse, wrapToolError } from './common.js';
|
|
19
|
-
function interactionControls(args) {
|
|
20
|
-
const verificationMode = getStringArg(args, 'verificationMode') ?? 'full';
|
|
21
|
-
const responseMode = getStringArg(args, 'responseMode') ?? 'debug';
|
|
22
|
-
if (!['none', 'light', 'full'].includes(verificationMode) || !['compact', 'debug'].includes(responseMode))
|
|
23
|
-
throw new Error('INVALID_ARGUMENT');
|
|
24
|
-
for (const key of ['verificationMode', 'responseMode'])
|
|
25
|
-
if (args[key] !== undefined && typeof args[key] !== 'string')
|
|
26
|
-
throw new Error('INVALID_ARGUMENT');
|
|
27
|
-
for (const key of ['actionTimeoutMs', 'verificationTimeoutMs'])
|
|
28
|
-
if (args[key] !== undefined && (typeof args[key] !== 'number' || !Number.isInteger(args[key])))
|
|
29
|
-
throw new Error('INVALID_ARGUMENT');
|
|
30
|
-
const actionTimeoutMs = getNumberArg(args, 'actionTimeoutMs');
|
|
31
|
-
const verificationTimeoutMs = getNumberArg(args, 'verificationTimeoutMs');
|
|
32
|
-
return { verificationMode, responseMode,
|
|
33
|
-
actionTimeoutMs: actionTimeoutMs === undefined ? undefined : Math.max(100, Math.min(30000, actionTimeoutMs)),
|
|
34
|
-
verificationTimeoutMs: verificationTimeoutMs === undefined ? 3000 : Math.max(100, Math.min(60000, verificationTimeoutMs)) };
|
|
35
|
-
}
|
|
36
|
-
function compactAction(result, verificationMode, started = true, stateDelta, totalMs = 0) {
|
|
37
|
-
return {
|
|
38
|
-
action_id: result.action_id,
|
|
39
|
-
action_type: result.action_type,
|
|
40
|
-
success: !!result.success,
|
|
41
|
-
lifecycle_state: result.success ? (verificationMode === 'full' ? result.lifecycle_state : 'pending_verification') : 'failed',
|
|
42
|
-
...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
43
|
-
...(typeof result.retryable === 'boolean' ? { retryable: result.retryable } : {}),
|
|
44
|
-
delivery_status: result.success ? 'delivered' : (started ? 'unknown' : 'not_delivered'),
|
|
45
|
-
dispatch_started: started,
|
|
46
|
-
verification: { mode: verificationMode, status: result.success ? (verificationMode === 'none' ? 'not_requested' : verificationMode === 'light' ? 'observed' : (result.lifecycle_state === 'verified' ? 'verified' : 'observed')) : 'failed' },
|
|
47
|
-
...(stateDelta ? { state_delta: stateDelta } : {}),
|
|
48
|
-
timing: { total_ms: totalMs }
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function correlatedHandler(name, handler, action = false, journey = false) {
|
|
52
|
-
return async (args) => {
|
|
53
|
-
const ref = validateActionRef(args.actionRef);
|
|
54
|
-
const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
|
|
55
|
-
try {
|
|
56
|
-
return await handler(args);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
return wrapToolError(name, error);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
const item = result.content.find(item => item.type === 'text');
|
|
63
|
-
if (item?.text) {
|
|
64
|
-
const payload = JSON.parse(item.text);
|
|
65
|
-
payload.evidence = evidence;
|
|
66
|
-
item.text = JSON.stringify(payload);
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function actionHandler(type, execute) {
|
|
72
|
-
return correlatedHandler(type, async (args) => {
|
|
73
|
-
const timing = {};
|
|
74
|
-
return withTiming(timing, async () => {
|
|
75
|
-
const started = performance.now();
|
|
76
|
-
const controls = interactionControls(args);
|
|
77
|
-
const expected = validateExpectedState(type, args);
|
|
78
|
-
const condition = expected ? createConditionalAction(args.platform, args.deviceId, expected) : undefined;
|
|
79
|
-
const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000);
|
|
80
|
-
let dispatchStarted = false;
|
|
81
|
-
let result;
|
|
82
|
-
const dispatchWithBudget = (timeoutMs) => runWithBudget(timeoutMs, async (budget) => {
|
|
83
|
-
try {
|
|
84
|
-
if (!condition)
|
|
85
|
-
return payloadOf(await execute(args));
|
|
86
|
-
return await withConditionalAction(condition, async () => {
|
|
87
|
-
const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId });
|
|
88
|
-
return withResolvedDevice(device, async () => payloadOf(await execute(args)));
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
finally {
|
|
92
|
-
dispatchStarted = budget.dispatchStarted;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
try {
|
|
96
|
-
// Omitted controls retain the existing adapter budgets.
|
|
97
|
-
result = args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined
|
|
98
|
-
? await dispatchWithBudget(Infinity) : await dispatchWithBudget(budgetMs);
|
|
99
|
-
if (result.success)
|
|
100
|
-
dispatchStarted = true;
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
|
|
104
|
-
return wrapToolError(type, error);
|
|
105
|
-
result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) };
|
|
106
|
-
}
|
|
107
|
-
const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status);
|
|
108
|
-
if (blocked) {
|
|
109
|
-
dispatchStarted = false;
|
|
110
|
-
result = buildActionExecutionResult({
|
|
111
|
-
actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
|
|
112
|
-
selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
|
|
113
|
-
success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
|
|
114
|
-
failure: { failureCode: conditionFailureCode(condition), retryable: false },
|
|
115
|
-
traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? ''))
|
|
119
|
-
result.failure_code = 'ACTION_TIMEOUT';
|
|
120
|
-
if (result.failure_code === 'ACTION_TIMEOUT')
|
|
121
|
-
result.retryable = !dispatchStarted;
|
|
122
|
-
if (!result.success && typeof result.retryable !== 'boolean')
|
|
123
|
-
result.retryable = !dispatchStarted;
|
|
124
|
-
result.action_id ??= currentEvidenceActionId() ?? randomUUID();
|
|
125
|
-
result.action_type ??= type;
|
|
126
|
-
recordObservationEvent({
|
|
127
|
-
step_id: args._stepId,
|
|
128
|
-
action_type: type,
|
|
129
|
-
dispatch_ms: timing.dispatch_ms,
|
|
130
|
-
verification_mode: controls.verificationMode,
|
|
131
|
-
dispatch_started: dispatchStarted
|
|
132
|
-
});
|
|
133
|
-
let delta;
|
|
134
|
-
let observationAvailable = false;
|
|
135
|
-
if (result.success && controls.verificationMode === 'light') {
|
|
136
|
-
const verificationStarted = performance.now();
|
|
137
|
-
const tree = await withObservationContext({ stepId: args._stepId, purpose: 'verification' }, () => measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () => ToolsObserve.getUITreeHandler({ platform: args.platform ?? result.device?.platform, deviceId: args.deviceId ?? result.device?.id })))).catch(() => null);
|
|
138
|
-
markEvidenceVerification();
|
|
139
|
-
observationAvailable = !!tree && !tree.error;
|
|
140
|
-
recordObservationEvent({
|
|
141
|
-
verification_budget_ms: controls.verificationTimeoutMs,
|
|
142
|
-
verification_elapsed_ms: performance.now() - verificationStarted,
|
|
143
|
-
verification_outcome: observationAvailable ? 'observed' : 'unavailable',
|
|
144
|
-
verification_budget_expired: !observationAvailable && performance.now() - verificationStarted >= controls.verificationTimeoutMs
|
|
145
|
-
});
|
|
146
|
-
if (observationAvailable && tree.device)
|
|
147
|
-
delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined;
|
|
148
|
-
}
|
|
149
|
-
if (!blocked && controls.verificationMode === 'full' && dispatchStarted)
|
|
150
|
-
markEvidenceVerification();
|
|
151
|
-
const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started);
|
|
152
|
-
if (blocked)
|
|
153
|
-
compact.verification.status = 'not_requested';
|
|
154
|
-
if (result.success && controls.verificationMode === 'light' && !observationAvailable)
|
|
155
|
-
compact.verification.status = 'unavailable';
|
|
156
|
-
const evidence = {
|
|
157
|
-
...compact,
|
|
158
|
-
...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
|
|
159
|
-
? { ...condition.result, observed: condition.result.observed ? {
|
|
160
|
-
snapshot_session_id: condition.result.observed.snapshot_session_id,
|
|
161
|
-
snapshot_revision: condition.result.observed.snapshot_revision,
|
|
162
|
-
captured_at_ms: condition.result.observed.captured_at_ms
|
|
163
|
-
} : null } : condition.result } : {}),
|
|
164
|
-
...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
|
|
165
|
-
...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
|
|
166
|
-
};
|
|
167
|
-
return wrapResponse(controls.responseMode === 'compact' ? evidence : {
|
|
168
|
-
...result, ...evidence, timing: { ...timing, total_ms: performance.now() - started },
|
|
169
|
-
effective_controls: { ...controls, actionTimeoutMs: args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined ? null : budgetMs, timeout_bound: !(args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined) }
|
|
170
|
-
}, controls.responseMode === 'debug');
|
|
171
|
-
});
|
|
172
|
-
}, true);
|
|
173
|
-
}
|
|
174
|
-
const handleStartApp = actionHandler('start_app', executeStartApp);
|
|
175
|
-
const handleRestartApp = actionHandler('restart_app', executeRestartApp);
|
|
176
|
-
const handleTap = actionHandler('tap', executeTap);
|
|
177
|
-
const handleTapElement = actionHandler('tap_element', executeTapElement);
|
|
178
|
-
const handleSwipe = actionHandler('swipe', executeSwipe);
|
|
179
|
-
const handleScrollToElement = actionHandler('scroll_to_element', executeScrollToElement);
|
|
180
|
-
const handleTypeText = actionHandler('type_text', executeTypeText);
|
|
181
|
-
const handlePressBack = actionHandler('press_back', executePressBack);
|
|
182
|
-
async function executeStartApp(args) {
|
|
183
|
-
const platform = requireStringArg(args, 'platform');
|
|
184
|
-
const appId = requireStringArg(args, 'appId');
|
|
185
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
186
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
187
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
188
|
-
notifyActionStart();
|
|
189
|
-
const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')));
|
|
190
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
191
|
-
const result = buildActionExecutionResult({
|
|
192
|
-
actionType: 'start_app',
|
|
193
|
-
sourceModule: 'server',
|
|
194
|
-
device: res.device,
|
|
195
|
-
selector: { appId },
|
|
196
|
-
success: !!res.appStarted,
|
|
197
|
-
uiFingerprintBefore,
|
|
198
|
-
uiFingerprintAfter,
|
|
199
|
-
failure: res.appStarted ? undefined : inferGenericFailure(res.error),
|
|
200
|
-
details: {
|
|
201
|
-
launch_time_ms: res.launchTimeMs,
|
|
202
|
-
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
203
|
-
...(res.device ? { device_id: res.device.id } : {}),
|
|
204
|
-
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
205
|
-
...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
|
|
206
|
-
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
return wrapResponse(result);
|
|
210
|
-
}
|
|
211
|
-
async function handleTerminateApp(args) {
|
|
212
|
-
const platform = requireStringArg(args, 'platform');
|
|
213
|
-
const appId = requireStringArg(args, 'appId');
|
|
214
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
215
|
-
const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId));
|
|
216
|
-
const response = { device: res.device, appTerminated: res.appTerminated };
|
|
217
|
-
return wrapResponse(response);
|
|
218
|
-
}
|
|
219
|
-
async function executeRestartApp(args) {
|
|
220
|
-
const platform = requireStringArg(args, 'platform');
|
|
221
|
-
const appId = requireStringArg(args, 'appId');
|
|
222
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
223
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
224
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
225
|
-
notifyActionStart();
|
|
226
|
-
const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined;
|
|
227
|
-
const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)));
|
|
228
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
229
|
-
const result = buildActionExecutionResult({
|
|
230
|
-
actionType: 'restart_app',
|
|
231
|
-
sourceModule: 'server',
|
|
232
|
-
device: res.device,
|
|
233
|
-
selector: { appId },
|
|
234
|
-
success: !!res.appRestarted,
|
|
235
|
-
uiFingerprintBefore,
|
|
236
|
-
uiFingerprintAfter,
|
|
237
|
-
failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
|
|
238
|
-
details: {
|
|
239
|
-
launch_time_ms: res.launchTimeMs,
|
|
240
|
-
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
241
|
-
...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
|
|
242
|
-
...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
|
|
243
|
-
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
244
|
-
...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
|
|
245
|
-
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
return wrapResponse(result);
|
|
249
|
-
}
|
|
250
|
-
async function handleResetAppData(args) {
|
|
251
|
-
const platform = requireStringArg(args, 'platform');
|
|
252
|
-
const appId = requireStringArg(args, 'appId');
|
|
253
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
254
|
-
const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId));
|
|
255
|
-
const response = { device: res.device, dataCleared: res.dataCleared };
|
|
256
|
-
return wrapResponse(response);
|
|
257
|
-
}
|
|
258
|
-
async function handleInstallApp(args) {
|
|
259
|
-
const platform = requireStringArg(args, 'platform');
|
|
260
|
-
const projectType = requireStringArg(args, 'projectType');
|
|
261
|
-
const appPath = requireStringArg(args, 'appPath');
|
|
262
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
263
|
-
const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType });
|
|
264
|
-
const response = {
|
|
265
|
-
device: res.device,
|
|
266
|
-
installed: res.installed,
|
|
267
|
-
output: res.output,
|
|
268
|
-
error: res.error
|
|
269
|
-
};
|
|
270
|
-
return wrapResponse(response);
|
|
271
|
-
}
|
|
272
|
-
async function handleBuildApp(args) {
|
|
273
|
-
const platform = requireStringArg(args, 'platform');
|
|
274
|
-
const projectType = requireStringArg(args, 'projectType');
|
|
275
|
-
const projectPath = requireStringArg(args, 'projectPath');
|
|
276
|
-
const variant = getStringArg(args, 'variant');
|
|
277
|
-
const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType });
|
|
278
|
-
return wrapResponse(res);
|
|
279
|
-
}
|
|
280
|
-
async function handleBuildAndInstall(args) {
|
|
281
|
-
const platform = requireStringArg(args, 'platform');
|
|
282
|
-
const projectType = requireStringArg(args, 'projectType');
|
|
283
|
-
const projectPath = requireStringArg(args, 'projectPath');
|
|
284
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
285
|
-
const timeout = getNumberArg(args, 'timeout');
|
|
286
|
-
const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType });
|
|
287
|
-
return {
|
|
288
|
-
content: [
|
|
289
|
-
{ type: 'text', text: res.ndjson },
|
|
290
|
-
{ type: 'text', text: JSON.stringify(res.result, null, 2) }
|
|
291
|
-
]
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
async function handleGetLogs(args) {
|
|
295
|
-
const platform = requireStringArg(args, 'platform');
|
|
296
|
-
const appId = getStringArg(args, 'appId');
|
|
297
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
298
|
-
const pid = getNumberArg(args, 'pid');
|
|
299
|
-
const tag = getStringArg(args, 'tag');
|
|
300
|
-
const level = getStringArg(args, 'level');
|
|
301
|
-
const contains = getStringArg(args, 'contains');
|
|
302
|
-
const since_seconds = getNumberArg(args, 'since_seconds');
|
|
303
|
-
const limit = getNumberArg(args, 'limit');
|
|
304
|
-
const lines = getNumberArg(args, 'lines');
|
|
305
|
-
const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines });
|
|
306
|
-
const filtered = !!(pid || tag || level || contains || since_seconds || appId);
|
|
307
|
-
return {
|
|
308
|
-
content: [
|
|
309
|
-
{ type: 'text', text: JSON.stringify({ device: res.device, result: { count: res.logCount, filtered, crashLines: (res.crashLines || []), source: res.source, meta: res.meta || {} } }, null, 2) },
|
|
310
|
-
{ type: 'text', text: JSON.stringify({ logs: res.logs }, null, 2) }
|
|
311
|
-
]
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
async function handleListDevices(args) {
|
|
315
|
-
const platform = getStringArg(args, 'platform');
|
|
316
|
-
const appId = getStringArg(args, 'appId');
|
|
317
|
-
const res = await ToolsManage.listDevicesHandler({ platform, appId });
|
|
318
|
-
return wrapResponse(res);
|
|
319
|
-
}
|
|
320
|
-
async function handleGetSystemStatus() {
|
|
321
|
-
const result = await getSystemStatus();
|
|
322
|
-
return wrapResponse(result);
|
|
323
|
-
}
|
|
324
|
-
async function handleCaptureScreenshot(args) {
|
|
325
|
-
const platform = requireStringArg(args, 'platform');
|
|
326
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
327
|
-
const started = performance.now();
|
|
328
|
-
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
329
|
-
const screenshotMs = performance.now() - started;
|
|
330
|
-
const mime = res.screenshot_mime || 'image/png';
|
|
331
|
-
const content = [
|
|
332
|
-
{ type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution, mimeType: mime }, timing: { total_ms: screenshotMs, screenshot_ms: screenshotMs } }, null, 2) },
|
|
333
|
-
{ type: 'image', data: res.screenshot, mimeType: mime }
|
|
334
|
-
];
|
|
335
|
-
if (res.screenshot_fallback) {
|
|
336
|
-
content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: res.screenshot_fallback_mime || 'image/jpeg' }) });
|
|
337
|
-
content.push({ type: 'image', data: res.screenshot_fallback, mimeType: res.screenshot_fallback_mime || 'image/jpeg' });
|
|
338
|
-
}
|
|
339
|
-
return { content };
|
|
340
|
-
}
|
|
341
|
-
async function handleCaptureDebugSnapshot(args) {
|
|
342
|
-
const reason = getStringArg(args, 'reason');
|
|
343
|
-
const includeLogs = getBooleanArg(args, 'includeLogs');
|
|
344
|
-
const logLines = getNumberArg(args, 'logLines');
|
|
345
|
-
const platform = getStringArg(args, 'platform');
|
|
346
|
-
const appId = getStringArg(args, 'appId');
|
|
347
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
348
|
-
const sessionId = getStringArg(args, 'sessionId');
|
|
349
|
-
const includeScreenshot = getBooleanArg(args, 'includeScreenshot');
|
|
350
|
-
const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, includeScreenshot, logLines, platform, appId, deviceId, sessionId });
|
|
351
|
-
return wrapResponse(res);
|
|
352
|
-
}
|
|
353
|
-
async function handleGetUITree(args) {
|
|
354
|
-
const platform = requireStringArg(args, 'platform');
|
|
355
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
356
|
-
const responseMode = getStringArg(args, 'responseMode') ?? 'debug';
|
|
357
|
-
const sinceSnapshotRevision = getNumberArg(args, 'sinceSnapshotRevision');
|
|
358
|
-
const metricsStepId = process.env.MOBILE_DEBUG_MCP_UI_TREE_METRICS === '1' ? getStringArg(args, '_metricsStepId') : undefined;
|
|
359
|
-
const res = await withObservationContext({ stepId: metricsStepId, purpose: metricsStepId?.includes('post-response') ? 'assertion' : 'debug' }, () => ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision }));
|
|
360
|
-
return wrapResponse(res);
|
|
361
|
-
}
|
|
362
|
-
async function handleGetCurrentScreen(args) {
|
|
363
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
364
|
-
const res = await ToolsObserve.getCurrentScreenHandler({ deviceId });
|
|
365
|
-
return wrapResponse(res);
|
|
366
|
-
}
|
|
367
|
-
async function handleGetScreenFingerprint(args) {
|
|
368
|
-
const platform = getStringArg(args, 'platform');
|
|
369
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
370
|
-
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
371
|
-
return wrapResponse(res);
|
|
372
|
-
}
|
|
373
|
-
async function handleWaitForScreenChange(args) {
|
|
374
|
-
const platform = getStringArg(args, 'platform');
|
|
375
|
-
const previousFingerprint = requireStringArg(args, 'previousFingerprint');
|
|
376
|
-
const timeoutMs = getNumberArg(args, 'timeoutMs');
|
|
377
|
-
const pollIntervalMs = getNumberArg(args, 'pollIntervalMs');
|
|
378
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
379
|
-
const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId });
|
|
380
|
-
return wrapResponse(res);
|
|
381
|
-
}
|
|
382
|
-
async function handleExpectScreen(args) {
|
|
383
|
-
const platform = getStringArg(args, 'platform');
|
|
384
|
-
const fingerprint = getStringArg(args, 'fingerprint');
|
|
385
|
-
const screen = getStringArg(args, 'screen');
|
|
386
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
387
|
-
const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId });
|
|
388
|
-
return wrapResponse(res);
|
|
389
|
-
}
|
|
390
|
-
async function handleExpectElementVisible(args) {
|
|
391
|
-
const selector = requireObjectArg(args, 'selector');
|
|
392
|
-
const element_id = getStringArg(args, 'element_id');
|
|
393
|
-
const timeout_ms = getNumberArg(args, 'timeout_ms');
|
|
394
|
-
const poll_interval_ms = getNumberArg(args, 'poll_interval_ms');
|
|
395
|
-
const platform = getStringArg(args, 'platform');
|
|
396
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
397
|
-
const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId });
|
|
398
|
-
return wrapResponse(res);
|
|
399
|
-
}
|
|
400
|
-
async function handleExpectState(args) {
|
|
401
|
-
const selector = getObjectArg(args, 'selector');
|
|
402
|
-
const element_id = getStringArg(args, 'element_id');
|
|
403
|
-
const property = requireStringArg(args, 'property');
|
|
404
|
-
const platform = getStringArg(args, 'platform');
|
|
405
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
406
|
-
if (!selector && !element_id) {
|
|
407
|
-
throw new Error('Missing selector or element_id argument');
|
|
408
|
-
}
|
|
409
|
-
if (!Object.prototype.hasOwnProperty.call(args, 'expected')) {
|
|
410
|
-
throw new Error('Missing expected argument');
|
|
411
|
-
}
|
|
412
|
-
const expected = args.expected;
|
|
413
|
-
const res = await ToolsInteract.expectStateHandler({ selector: selector ?? undefined, element_id: element_id ?? undefined, property, expected, platform, deviceId });
|
|
414
|
-
return wrapResponse(res);
|
|
415
|
-
}
|
|
416
|
-
async function handleAdjustControl(args) {
|
|
417
|
-
const selector = getObjectArg(args, 'selector');
|
|
418
|
-
const element_id = getStringArg(args, 'element_id');
|
|
419
|
-
const property = getStringArg(args, 'property') ?? 'value';
|
|
420
|
-
const targetValue = requireNumberArg(args, 'targetValue');
|
|
421
|
-
const tolerance = getNumberArg(args, 'tolerance');
|
|
422
|
-
const maxAttempts = getNumberArg(args, 'maxAttempts');
|
|
423
|
-
const platform = getStringArg(args, 'platform');
|
|
424
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
425
|
-
if ((!selector && !element_id) || (selector && element_id)) {
|
|
426
|
-
throw new Error('Exactly one of selector or element_id argument is required');
|
|
427
|
-
}
|
|
428
|
-
const res = await ToolsInteract.adjustControlHandler({
|
|
429
|
-
selector: selector ?? undefined,
|
|
430
|
-
element_id: element_id ?? undefined,
|
|
431
|
-
property,
|
|
432
|
-
targetValue,
|
|
433
|
-
tolerance,
|
|
434
|
-
maxAttempts,
|
|
435
|
-
platform,
|
|
436
|
-
deviceId
|
|
437
|
-
});
|
|
438
|
-
markEvidenceVerification();
|
|
439
|
-
return wrapResponse(res);
|
|
440
|
-
}
|
|
441
|
-
async function handleWaitForUI(args) {
|
|
442
|
-
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
|
|
443
|
-
throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
|
|
444
|
-
const selector = getObjectArg(args, 'selector');
|
|
445
|
-
const condition = getStringArg(args, 'condition') ?? 'exists';
|
|
446
|
-
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
447
|
-
const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300;
|
|
448
|
-
const match = getObjectArg(args, 'match');
|
|
449
|
-
const retry = getObjectArg(args, 'retry');
|
|
450
|
-
const platform = getStringArg(args, 'platform');
|
|
451
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
452
|
-
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId });
|
|
453
|
-
const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms, device => execute(device.platform, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute();
|
|
454
|
-
return wrapResponse(res);
|
|
455
|
-
}
|
|
456
|
-
async function handleWaitForUIChange(args) {
|
|
457
|
-
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
|
|
458
|
-
throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
|
|
459
|
-
const platform = getStringArg(args, 'platform');
|
|
460
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
461
|
-
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
462
|
-
const stability_window_ms = getNumberArg(args, 'stability_window_ms') ?? 300;
|
|
463
|
-
const expected_change = getStringArg(args, 'expected_change');
|
|
464
|
-
const scope = getStringArg(args, 'scope');
|
|
465
|
-
const target = getStringArg(args, 'target');
|
|
466
|
-
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target });
|
|
467
|
-
const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms, device => execute(device.platform, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute();
|
|
468
|
-
return wrapResponse(res);
|
|
469
|
-
}
|
|
470
|
-
async function handleFindElement(args) {
|
|
471
|
-
const query = requireStringArg(args, 'query');
|
|
472
|
-
const exact = getBooleanArg(args, 'exact') ?? false;
|
|
473
|
-
const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000;
|
|
474
|
-
const platform = getStringArg(args, 'platform');
|
|
475
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
476
|
-
const targetStarted = performance.now();
|
|
477
|
-
const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }));
|
|
478
|
-
recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' });
|
|
479
|
-
return wrapResponse(res);
|
|
480
|
-
}
|
|
481
|
-
async function executeTap(args) {
|
|
482
|
-
const platform = getStringArg(args, 'platform');
|
|
483
|
-
const x = requireNumberArg(args, 'x');
|
|
484
|
-
const y = requireNumberArg(args, 'y');
|
|
485
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
486
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
487
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
488
|
-
notifyActionStart();
|
|
489
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }));
|
|
490
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
491
|
-
const result = buildActionExecutionResult({
|
|
492
|
-
actionType: 'tap',
|
|
493
|
-
sourceModule: 'server',
|
|
494
|
-
selector: { x, y },
|
|
495
|
-
success: !!res.success,
|
|
496
|
-
uiFingerprintBefore,
|
|
497
|
-
uiFingerprintAfter,
|
|
498
|
-
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
499
|
-
});
|
|
500
|
-
return wrapResponse(result);
|
|
501
|
-
}
|
|
502
|
-
async function executeTapElement(args) {
|
|
503
|
-
const controls = tapElementControlsSchema.safeParse({ elementId: args.elementId, selector: args.selector, waitFor: args.waitFor });
|
|
504
|
-
if (!controls.success)
|
|
505
|
-
throw new Error(`INVALID_ARGUMENT: ${controls.error.message}`);
|
|
506
|
-
const elementId = controls.data.elementId;
|
|
507
|
-
const selector = controls.data.selector;
|
|
508
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
509
|
-
let resolvedId = elementId;
|
|
510
|
-
let freshTree;
|
|
511
|
-
if (selector) {
|
|
512
|
-
const waitFor = getObjectArg(args, 'waitFor');
|
|
513
|
-
const targetStarted = performance.now();
|
|
514
|
-
const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000;
|
|
515
|
-
const resolution = await withObservationContext({ stepId: args._stepId, purpose: 'target' }, () => measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }))));
|
|
516
|
-
recordObservationEvent({ step_id: args._stepId, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status });
|
|
517
|
-
if (resolution.status !== 'success' || !resolution.element?.elementId)
|
|
518
|
-
return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true });
|
|
519
|
-
resolvedId = resolution.element.elementId;
|
|
520
|
-
freshTree = resolution._tree;
|
|
521
|
-
}
|
|
522
|
-
notifyActionStart();
|
|
523
|
-
if (freshTree)
|
|
524
|
-
recordObservationConsumer(freshTree, 'target', args._stepId);
|
|
525
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }));
|
|
526
|
-
return wrapResponse(res);
|
|
527
|
-
}
|
|
528
|
-
function payloadOf(response) {
|
|
529
|
-
const text = response.content.find((item) => item.type === 'text')?.text;
|
|
530
|
-
return text ? JSON.parse(text) : null;
|
|
531
|
-
}
|
|
532
|
-
async function expectElementAbsent({ selector, timeoutMs, pollIntervalMs, platform, deviceId }) {
|
|
533
|
-
const deadline = performance.now() + timeoutMs;
|
|
534
|
-
let consecutive = 0;
|
|
535
|
-
let observed;
|
|
536
|
-
while (performance.now() < deadline) {
|
|
537
|
-
const result = await ToolsInteract.waitForUIHandler({ selector, condition: 'not_exists', timeout_ms: 0, singleObservation: true, singleAttempt: true, platform, deviceId });
|
|
538
|
-
observed = result.observed;
|
|
539
|
-
consecutive = result.status === 'success' ? consecutive + 1 : 0;
|
|
540
|
-
if (consecutive === 2 && performance.now() <= deadline)
|
|
541
|
-
return { success: true, observed };
|
|
542
|
-
const delay = Math.max(100, pollIntervalMs);
|
|
543
|
-
if (performance.now() + delay >= deadline)
|
|
544
|
-
break;
|
|
545
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
546
|
-
}
|
|
547
|
-
return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed };
|
|
548
|
-
}
|
|
549
|
-
async function expectFingerprint({ fingerprint, timeoutMs, platform, deviceId }) {
|
|
550
|
-
const deadline = performance.now() + timeoutMs;
|
|
551
|
-
let consecutive = 0;
|
|
552
|
-
let observed = null;
|
|
553
|
-
while (performance.now() < deadline) {
|
|
554
|
-
const result = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
555
|
-
observed = result?.fingerprint ?? null;
|
|
556
|
-
consecutive = observed === fingerprint ? consecutive + 1 : 0;
|
|
557
|
-
if (consecutive === 2 && performance.now() <= deadline)
|
|
558
|
-
return { success: true, observed_fingerprint: observed };
|
|
559
|
-
if (performance.now() + 100 >= deadline)
|
|
560
|
-
break;
|
|
561
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
562
|
-
}
|
|
563
|
-
return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed_fingerprint: observed };
|
|
564
|
-
}
|
|
565
|
-
async function handleRunJourney(args) {
|
|
566
|
-
const parsed = journeySchema.safeParse(args);
|
|
567
|
-
if (!parsed.success)
|
|
568
|
-
throw new Error(`INVALID_ARGUMENT: ${parsed.error.message}`);
|
|
569
|
-
const { platform, responseMode, defaults, captureOnFailure } = parsed.data;
|
|
570
|
-
const steps = parsed.data.steps;
|
|
571
|
-
const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId });
|
|
572
|
-
const deviceId = device.id;
|
|
573
|
-
return withResolvedDevice(device, async () => {
|
|
574
|
-
const started = performance.now();
|
|
575
|
-
const results = [];
|
|
576
|
-
let stoppedAt = null;
|
|
577
|
-
let pendingLightStep = null;
|
|
578
|
-
for (let index = 0; index < steps.length; index++) {
|
|
579
|
-
const step = steps[index];
|
|
580
|
-
const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full';
|
|
581
|
-
const next = steps[index + 1];
|
|
582
|
-
const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
|
|
583
|
-
next.assertion?.kind === 'element_visible';
|
|
584
|
-
const stepStart = performance.now();
|
|
585
|
-
let result;
|
|
586
|
-
if (stoppedAt) {
|
|
587
|
-
results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } });
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
try {
|
|
591
|
-
if (step.type === 'tap') {
|
|
592
|
-
result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
|
|
593
|
-
}
|
|
594
|
-
else if (step.type === 'wait') {
|
|
595
|
-
result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId }));
|
|
596
|
-
result = { success: result.status === 'success', ...result };
|
|
597
|
-
}
|
|
598
|
-
else if (step.type === 'assert') {
|
|
599
|
-
result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, async () => {
|
|
600
|
-
const assertion = step.assertion;
|
|
601
|
-
if (assertion?.kind === 'element_visible')
|
|
602
|
-
result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
|
|
603
|
-
else if (assertion?.kind === 'element_absent')
|
|
604
|
-
result = await expectElementAbsent({ selector: assertion.selector, timeoutMs: step.timeoutMs ?? 5000, pollIntervalMs: step.pollIntervalMs ?? 100, platform, deviceId });
|
|
605
|
-
else if (assertion?.kind === 'screen_fingerprint')
|
|
606
|
-
result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: step.timeoutMs ?? 5000, platform, deviceId });
|
|
607
|
-
else if (assertion?.kind === 'state_equals')
|
|
608
|
-
result = await ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, exact_budget: true });
|
|
609
|
-
else
|
|
610
|
-
result = { success: false, failure_code: 'INVALID_ASSERTION' };
|
|
611
|
-
return result;
|
|
612
|
-
}));
|
|
613
|
-
}
|
|
614
|
-
else if (step.type === 'start_app') {
|
|
615
|
-
result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
catch (error) {
|
|
622
|
-
result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) };
|
|
623
|
-
}
|
|
624
|
-
if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? ''))
|
|
625
|
-
result.failure_code = 'ASSERTION_TIMEOUT';
|
|
626
|
-
const success = !!result?.success;
|
|
627
|
-
if (lightCoveredByAssertion) {
|
|
628
|
-
result.verification = { mode: 'light', status: success ? 'unavailable' :
|
|
629
|
-
(['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') };
|
|
630
|
-
if (success) {
|
|
631
|
-
result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true };
|
|
632
|
-
pendingLightStep = results.length;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (step.type === 'assert' && pendingLightStep !== null) {
|
|
636
|
-
const pending = results[pendingLightStep].result;
|
|
637
|
-
if (typeof result?.observed?.matched_count === 'number') {
|
|
638
|
-
pending.verification.status = 'observed';
|
|
639
|
-
if (pending.evidence)
|
|
640
|
-
completeJourneyVerification(pending.evidence.action_id);
|
|
641
|
-
delete pending.verification_diagnostic;
|
|
642
|
-
const delta = ToolsObserve.getLatestStateDelta(platform, deviceId);
|
|
643
|
-
if (delta) {
|
|
644
|
-
pending.state_delta = delta;
|
|
645
|
-
pending.state_delta_available = true;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
pendingLightStep = null;
|
|
649
|
-
}
|
|
650
|
-
if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type))
|
|
651
|
-
result = {
|
|
652
|
-
success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
653
|
-
...(result.error?.code ? { failure_code: result.error.code } : {}),
|
|
654
|
-
...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
|
|
655
|
-
...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
|
|
656
|
-
...(result.actual_state ? { observed: result.actual_state } : {})
|
|
657
|
-
};
|
|
658
|
-
results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } });
|
|
659
|
-
if (!success)
|
|
660
|
-
stoppedAt = step.id;
|
|
661
|
-
}
|
|
662
|
-
const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
|
|
663
|
-
? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined;
|
|
664
|
-
const componentTiming = {};
|
|
665
|
-
for (const step of results)
|
|
666
|
-
for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
|
|
667
|
-
if (key !== 'total_ms' && typeof value === 'number')
|
|
668
|
-
componentTiming[key] = (componentTiming[key] ?? 0) + value;
|
|
669
|
-
}
|
|
670
|
-
refreshJourneyEvidence(results);
|
|
671
|
-
return wrapResponse({ success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug');
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
async function executeSwipe(args) {
|
|
675
|
-
const platform = getStringArg(args, 'platform') ?? 'android';
|
|
676
|
-
const x1 = requireNumberArg(args, 'x1');
|
|
677
|
-
const y1 = requireNumberArg(args, 'y1');
|
|
678
|
-
const x2 = requireNumberArg(args, 'x2');
|
|
679
|
-
const y2 = requireNumberArg(args, 'y2');
|
|
680
|
-
const duration = requireNumberArg(args, 'duration');
|
|
681
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
682
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
683
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
684
|
-
notifyActionStart();
|
|
685
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }));
|
|
686
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
687
|
-
const result = buildActionExecutionResult({
|
|
688
|
-
actionType: 'swipe',
|
|
689
|
-
sourceModule: 'server',
|
|
690
|
-
selector: { x1, y1, x2, y2, duration },
|
|
691
|
-
success: !!res.success,
|
|
692
|
-
uiFingerprintBefore,
|
|
693
|
-
uiFingerprintAfter,
|
|
694
|
-
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
695
|
-
});
|
|
696
|
-
return wrapResponse(result);
|
|
697
|
-
}
|
|
698
|
-
async function executeScrollToElement(args) {
|
|
699
|
-
const platform = requireStringArg(args, 'platform');
|
|
700
|
-
const selector = requireObjectArg(args, 'selector');
|
|
701
|
-
const direction = getStringArg(args, 'direction');
|
|
702
|
-
const maxScrolls = getNumberArg(args, 'maxScrolls');
|
|
703
|
-
const scrollAmount = getNumberArg(args, 'scrollAmount');
|
|
704
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
705
|
-
const { verificationMode } = interactionControls(args);
|
|
706
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
707
|
-
notifyActionStart();
|
|
708
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }));
|
|
709
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
710
|
-
const result = buildActionExecutionResult({
|
|
711
|
-
actionType: 'scroll_to_element',
|
|
712
|
-
sourceModule: 'server',
|
|
713
|
-
selector: selector ?? null,
|
|
714
|
-
resolved: res?.success && res?.element ? {
|
|
715
|
-
elementId: null,
|
|
716
|
-
text: res.element.text ?? null,
|
|
717
|
-
resource_id: res.element.resourceId ?? null,
|
|
718
|
-
accessibility_id: res.element.contentDesc ?? null,
|
|
719
|
-
class: res.element.className ?? null,
|
|
720
|
-
bounds: res.element.bounds ?? null,
|
|
721
|
-
index: null
|
|
722
|
-
} : null,
|
|
723
|
-
success: !!res.success,
|
|
724
|
-
uiFingerprintBefore,
|
|
725
|
-
uiFingerprintAfter,
|
|
726
|
-
failure: res.success ? undefined : inferScrollFailure(res.reason)
|
|
727
|
-
});
|
|
728
|
-
return wrapResponse(result);
|
|
729
|
-
}
|
|
730
|
-
async function executeTypeText(args) {
|
|
731
|
-
const text = requireStringArg(args, 'text');
|
|
732
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
733
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
734
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
735
|
-
notifyActionStart();
|
|
736
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }));
|
|
737
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
738
|
-
const result = buildActionExecutionResult({
|
|
739
|
-
actionType: 'type_text',
|
|
740
|
-
sourceModule: 'server',
|
|
741
|
-
selector: { text },
|
|
742
|
-
success: !!res.success,
|
|
743
|
-
uiFingerprintBefore,
|
|
744
|
-
uiFingerprintAfter,
|
|
745
|
-
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
746
|
-
});
|
|
747
|
-
return wrapResponse(result);
|
|
748
|
-
}
|
|
749
|
-
async function executePressBack(args) {
|
|
750
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
751
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
752
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
753
|
-
notifyActionStart();
|
|
754
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }));
|
|
755
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
756
|
-
const result = buildActionExecutionResult({
|
|
757
|
-
actionType: 'press_back',
|
|
758
|
-
sourceModule: 'server',
|
|
759
|
-
selector: { key: 'back' },
|
|
760
|
-
success: !!res.success,
|
|
761
|
-
uiFingerprintBefore,
|
|
762
|
-
uiFingerprintAfter,
|
|
763
|
-
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
764
|
-
});
|
|
765
|
-
return wrapResponse(result);
|
|
766
|
-
}
|
|
767
|
-
async function handleStartLogStream(args) {
|
|
768
|
-
const platform = getStringArg(args, 'platform') ?? 'android';
|
|
769
|
-
const packageName = requireStringArg(args, 'packageName');
|
|
770
|
-
const level = getStringArg(args, 'level') ?? 'error';
|
|
771
|
-
const sessionId = getStringArg(args, 'sessionId');
|
|
772
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
773
|
-
const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
|
|
774
|
-
return wrapResponse(res);
|
|
775
|
-
}
|
|
776
|
-
async function handleReadLogStream(args) {
|
|
777
|
-
const platform = getStringArg(args, 'platform');
|
|
778
|
-
const sessionId = getStringArg(args, 'sessionId');
|
|
779
|
-
const limit = getNumberArg(args, 'limit');
|
|
780
|
-
const since = getStringArg(args, 'since');
|
|
781
|
-
const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since });
|
|
782
|
-
return wrapResponse(res);
|
|
783
|
-
}
|
|
784
|
-
async function handleStopLogStream(args) {
|
|
785
|
-
const platform = getStringArg(args, 'platform');
|
|
786
|
-
const sessionId = getStringArg(args, 'sessionId');
|
|
787
|
-
const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId });
|
|
788
|
-
return wrapResponse(res);
|
|
789
|
-
}
|
|
790
|
-
function handleClassifyActionOutcome(args) {
|
|
791
|
-
const uiChanged = requireBooleanArg(args, 'uiChanged');
|
|
792
|
-
const expectedElementVisible = getBooleanArg(args, 'expectedElementVisible');
|
|
793
|
-
const actionType = getStringArg(args, 'actionType');
|
|
794
|
-
const networkRequests = getArrayArg(args, 'networkRequests');
|
|
795
|
-
const hasLogErrors = getBooleanArg(args, 'hasLogErrors');
|
|
796
|
-
const result = classifyActionOutcome({
|
|
797
|
-
uiChanged,
|
|
798
|
-
expectedElementVisible: expectedElementVisible ?? null,
|
|
799
|
-
actionType: actionType ?? null,
|
|
800
|
-
networkRequests: networkRequests ?? null,
|
|
801
|
-
hasLogErrors: hasLogErrors ?? null
|
|
802
|
-
});
|
|
803
|
-
return Promise.resolve(wrapResponse(result));
|
|
804
|
-
}
|
|
805
|
-
async function handleGetNetworkActivity(args) {
|
|
806
|
-
const platform = requireStringArg(args, 'platform');
|
|
807
|
-
const deviceId = getStringArg(args, 'deviceId');
|
|
808
|
-
const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId });
|
|
809
|
-
return wrapResponse(result);
|
|
810
|
-
}
|
|
811
|
-
export const toolHandlers = {
|
|
812
|
-
get_app_readiness: async (args) => {
|
|
813
|
-
const parsed = readinessSchema.safeParse(args);
|
|
814
|
-
if (!parsed.success)
|
|
815
|
-
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
816
|
-
return wrapResponse(await getAppReadiness(parsed.data));
|
|
817
|
-
},
|
|
818
|
-
wait_for_app_ready: async (args) => {
|
|
819
|
-
const parsed = readinessWaitSchema.safeParse(args);
|
|
820
|
-
if (!parsed.success)
|
|
821
|
-
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
822
|
-
return wrapResponse(await getAppReadiness(parsed.data, true));
|
|
823
|
-
},
|
|
824
|
-
start_app: handleStartApp,
|
|
825
|
-
terminate_app: handleTerminateApp,
|
|
826
|
-
restart_app: handleRestartApp,
|
|
827
|
-
reset_app_data: handleResetAppData,
|
|
828
|
-
install_app: handleInstallApp,
|
|
829
|
-
build_app: handleBuildApp,
|
|
830
|
-
build_and_install: handleBuildAndInstall,
|
|
831
|
-
get_logs: handleGetLogs,
|
|
832
|
-
list_devices: handleListDevices,
|
|
833
|
-
get_system_status: handleGetSystemStatus,
|
|
834
|
-
capture_screenshot: handleCaptureScreenshot,
|
|
835
|
-
capture_debug_snapshot: handleCaptureDebugSnapshot,
|
|
836
|
-
get_ui_tree: handleGetUITree,
|
|
837
|
-
get_current_screen: handleGetCurrentScreen,
|
|
838
|
-
get_screen_fingerprint: handleGetScreenFingerprint,
|
|
839
|
-
wait_for_screen_change: handleWaitForScreenChange,
|
|
840
|
-
wait_for_ui_change: handleWaitForUIChange,
|
|
841
|
-
expect_screen: handleExpectScreen,
|
|
842
|
-
expect_element_visible: handleExpectElementVisible,
|
|
843
|
-
expect_state: handleExpectState,
|
|
844
|
-
adjust_control: handleAdjustControl,
|
|
845
|
-
wait_for_ui: handleWaitForUI,
|
|
846
|
-
find_element: handleFindElement,
|
|
847
|
-
tap: handleTap,
|
|
848
|
-
tap_element: handleTapElement,
|
|
849
|
-
run_journey: handleRunJourney,
|
|
850
|
-
swipe: handleSwipe,
|
|
851
|
-
scroll_to_element: handleScrollToElement,
|
|
852
|
-
type_text: handleTypeText,
|
|
853
|
-
press_back: handlePressBack,
|
|
854
|
-
start_log_stream: handleStartLogStream,
|
|
855
|
-
read_log_stream: handleReadLogStream,
|
|
856
|
-
stop_log_stream: handleStopLogStream,
|
|
857
|
-
classify_action_outcome: handleClassifyActionOutcome,
|
|
858
|
-
get_network_activity: handleGetNetworkActivity
|
|
859
|
-
};
|
|
860
|
-
for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
|
|
861
|
-
toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey');
|
|
862
|
-
}
|
|
863
|
-
export async function handleToolCall(name, args = {}) {
|
|
864
|
-
const handler = toolHandlers[name];
|
|
865
|
-
if (!handler)
|
|
866
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
867
|
-
const started = performance.now();
|
|
868
|
-
const measured = {};
|
|
869
|
-
const addTiming = (response) => {
|
|
870
|
-
if (!['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'wait_for_ui', 'get_ui_tree', 'capture_screenshot', 'capture_debug_snapshot'].includes(name))
|
|
871
|
-
return response;
|
|
872
|
-
const item = response.content.find(item => item.type === 'text');
|
|
873
|
-
if (item?.text) {
|
|
874
|
-
try {
|
|
875
|
-
const value = JSON.parse(item.text);
|
|
876
|
-
value.timing = { ...measured, ...value.timing, total_ms: performance.now() - started };
|
|
877
|
-
item.text = JSON.stringify(value);
|
|
878
|
-
}
|
|
879
|
-
catch { /* Non-JSON text responses retain their original content. */ }
|
|
880
|
-
}
|
|
881
|
-
return response;
|
|
882
|
-
};
|
|
883
|
-
return withTiming(measured, async () => {
|
|
884
|
-
try {
|
|
885
|
-
validateExpectedState(name, args);
|
|
886
|
-
return addTiming(await handler(args));
|
|
887
|
-
}
|
|
888
|
-
catch (error) {
|
|
889
|
-
console.error(`Error executing tool ${name}:`, error);
|
|
890
|
-
return addTiming(wrapToolError(name, error));
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
|
-
}
|