mobile-debug-mcp 0.34.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 +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +156 -129
- 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 +16 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
- 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 +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +104 -62
- package/dist/debugger/worker.js +53 -42
- 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 -3090
- 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 +114 -75
- 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 +4 -2
- package/dist/observe/readiness.js +29 -10
- 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.js +22 -13
- 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/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
- 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/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- 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/ios/utils.js +45 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +44 -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/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- 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 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +76 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +9 -1
- 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 +2 -1
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +6 -1
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +314 -155
- package/src/debugger/worker.ts +51 -38
- 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 -3492
- 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 +122 -70
- 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 +5 -3
- package/src/observe/readiness.ts +39 -18
- 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.ts +24 -15
- 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/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +1 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- 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/ios/utils.ts +57 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -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 +19 -3
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- 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 +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -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 +4 -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 +1 -1
- 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/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -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/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -1000
- 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/debugger-handlers.ts +0 -29
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/dist/server/{system-trace-schema.js → schemas/system-trace-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
package/dist/debugger/android.js
CHANGED
|
@@ -41,7 +41,8 @@ export class AndroidDebugNative {
|
|
|
41
41
|
try {
|
|
42
42
|
jdwpOutput = (await exec(resolveAdbCmd(), ['-s', device, 'jdwp'], { timeout: debuggerRemaining(500), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout;
|
|
43
43
|
}
|
|
44
|
-
catch (
|
|
44
|
+
catch (error) {
|
|
45
|
+
const e = error;
|
|
45
46
|
if (debuggerSignal()?.aborted)
|
|
46
47
|
throw new DebuggerError('DEBUGGER_CANCELLED');
|
|
47
48
|
debuggerRemaining(1);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Only catalog values cross the debugger error boundary. Never inspect exception text. */
|
|
2
|
+
export const stages = ['prerequisite', 'compilation', 'worker_start', 'ipc', 'target_resolution', 'forwarding', 'attach', 'command', 'transport', 'cleanup', 'unknown'];
|
|
3
|
+
export const reasons = ['unavailable', 'unsupported', 'spawn_failed', 'process_exited', 'invalid_message', 'message_limit', 'write_failed', 'connection_refused', 'transport_lost', 'timeout', 'cancelled', 'operation_failed', 'unknown'];
|
|
4
|
+
const recovery = {
|
|
5
|
+
prerequisite: 'Check JDK 17+ availability, including java, javac and jdk.jdi.',
|
|
6
|
+
compilation: 'Check the JDK compiler and worker source/cache availability.',
|
|
7
|
+
worker_start: 'Check Java worker launch availability. Inspect session state and cleanup before attaching again.',
|
|
8
|
+
ipc: 'Inspect session state and cleanup. Detach before attempting a fresh attach.',
|
|
9
|
+
target_resolution: 'Check the explicit device and running debuggable app/process.',
|
|
10
|
+
forwarding: 'Check the explicit device and debugger forwarding availability.',
|
|
11
|
+
attach: 'Check the running debuggable target and whether another debugger is attached. Inspect cleanup before a fresh attach.',
|
|
12
|
+
command: 'Inspect debugger session state and cleanup before another state-changing command.',
|
|
13
|
+
transport: 'Inspect session state and cleanup. Detach before attempting a fresh attach; execution state may be unknown.',
|
|
14
|
+
cleanup: 'Retry detach_debugger and inspect cleanup.remaining_resources. Do not kill the app or ADB server.',
|
|
15
|
+
unknown: 'Inspect debugger session state and cleanup; the failure cause is unknown.'
|
|
16
|
+
};
|
|
17
|
+
export function diagnostic(stage, reason) {
|
|
18
|
+
const guidance = reason === 'timeout' || reason === 'cancelled'
|
|
19
|
+
? 'Execution may be uncertain. Inspect session state and cleanup before another state-changing command.' : recovery[stage];
|
|
20
|
+
return { stage, reason, recovery: guidance };
|
|
21
|
+
}
|
|
22
|
+
export function parseDiagnostic(value) {
|
|
23
|
+
if (!value || typeof value !== 'object')
|
|
24
|
+
throw new Error('INVALID_WORKER_DIAGNOSTIC');
|
|
25
|
+
const { stage, reason } = value;
|
|
26
|
+
if (!stages.includes(stage) || !reasons.includes(reason))
|
|
27
|
+
throw new Error('INVALID_WORKER_DIAGNOSTIC');
|
|
28
|
+
return diagnostic(stage, reason);
|
|
29
|
+
}
|
|
30
|
+
export class DebuggerError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
diagnostic;
|
|
33
|
+
constructor(code, diagnostic) {
|
|
34
|
+
super(code);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.diagnostic = diagnostic;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const validationCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_PAUSED', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE']);
|
|
40
|
+
export function classify(error, stage, fallback, reason = 'unknown') {
|
|
41
|
+
const existing = error instanceof DebuggerError ? error : new DebuggerError(fallback);
|
|
42
|
+
if (existing.diagnostic || validationCodes.has(existing.code))
|
|
43
|
+
return existing;
|
|
44
|
+
const knownReason = existing.code === 'DEBUGGER_TIMEOUT' ? 'timeout' : existing.code === 'DEBUGGER_CANCELLED' ? 'cancelled' : reason;
|
|
45
|
+
return new DebuggerError(existing.code, diagnostic(stage, knownReason));
|
|
46
|
+
}
|
|
47
|
+
export async function atStage(stage, fallback, operation) {
|
|
48
|
+
try {
|
|
49
|
+
return await operation();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw classify(error, stage, fallback);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { parseDiagnostic } from './diagnostics.js';
|
|
2
|
+
const workerCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE', 'DEBUGGER_DISCONNECTED', 'DEBUGGER_WORKER_FAILED']);
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
export function parseWorkerMessage(value) {
|
|
7
|
+
if (!record(value))
|
|
8
|
+
throw new Error('INVALID_WORKER_MESSAGE');
|
|
9
|
+
if (value.event) {
|
|
10
|
+
if (typeof value.event !== 'string')
|
|
11
|
+
throw new Error('INVALID_WORKER_EVENT');
|
|
12
|
+
for (const field of ['reason', 'stop_id']) {
|
|
13
|
+
if (value[field] !== undefined && typeof value[field] !== 'string')
|
|
14
|
+
throw new Error('INVALID_WORKER_EVENT');
|
|
15
|
+
}
|
|
16
|
+
const event = { ...value };
|
|
17
|
+
if (value.diagnostic !== undefined)
|
|
18
|
+
event.diagnostic = parseDiagnostic(value.diagnostic);
|
|
19
|
+
return { kind: 'event', value: event };
|
|
20
|
+
}
|
|
21
|
+
if (typeof value.id !== 'number' || !Number.isSafeInteger(value.id) || value.id < 1)
|
|
22
|
+
throw new Error('INVALID_WORKER_REPLY');
|
|
23
|
+
if (value.error) {
|
|
24
|
+
if (!record(value.error) || (value.error.code !== undefined && typeof value.error.code !== 'string'))
|
|
25
|
+
throw new Error('INVALID_WORKER_ERROR');
|
|
26
|
+
if (value.error.code !== undefined && !workerCodes.has(value.error.code))
|
|
27
|
+
throw new Error('INVALID_WORKER_ERROR');
|
|
28
|
+
return { kind: 'reply', id: value.id, result: {}, error: { ...(value.error.code === undefined ? {} : { code: value.error.code }), ...(value.error.diagnostic === undefined ? {} : { diagnostic: parseDiagnostic(value.error.diagnostic) }) } };
|
|
29
|
+
}
|
|
30
|
+
if (!record(value.result))
|
|
31
|
+
throw new Error('INVALID_WORKER_RESULT');
|
|
32
|
+
return { kind: 'reply', id: value.id, result: value.result };
|
|
33
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { atStage, classify, diagnostic } from './diagnostics.js';
|
|
1
2
|
import { registerDebuggerPauseProvider } from './guard.js';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { AndroidDebugNative } from './android.js';
|
|
@@ -28,21 +29,29 @@ export class DebuggerSessions {
|
|
|
28
29
|
for (const s of remaining.slice(0, Math.max(0, remaining.length - 100)))
|
|
29
30
|
this.sessions.delete(s.id);
|
|
30
31
|
}
|
|
31
|
-
get(id) {
|
|
32
|
-
|
|
32
|
+
get(id) {
|
|
33
|
+
this.prune();
|
|
34
|
+
const s = this.sessions.get(id);
|
|
35
|
+
if (!s)
|
|
36
|
+
throw new DebuggerError('DEBUGGER_NOT_FOUND');
|
|
37
|
+
s.activity = Date.now();
|
|
38
|
+
return s;
|
|
39
|
+
}
|
|
33
40
|
serial(s, fn) { const out = s.queue.then(fn); s.queue = out.catch(() => { }); return out; }
|
|
34
41
|
result(s, payload = {}) {
|
|
35
42
|
const success = payload.success ?? (!['failed', 'disconnected'].includes(s.state) && s.cleanup.status !== 'failed');
|
|
36
43
|
const code = s.cleanup.status === 'failed' ? 'DEBUGGER_CLEANUP_FAILED' : s.state === 'disconnected' ? 'DEBUGGER_DISCONNECTED' : s.reason?.match(/^[A-Z_]+$/) ? s.reason : 'DEBUGGER_WORKER_FAILED';
|
|
37
|
-
return structuredClone({ success, ...(!success ? { error: { code, message: code, retryable: false } } : {}), debug_session_id: s.id,
|
|
44
|
+
return structuredClone({ success, ...(!success ? { error: { code, message: code, retryable: false, diagnostic: s.cleanup.status === 'failed' ? diagnostic('cleanup', 'operation_failed') : s.diagnostic ?? diagnostic('unknown', 'unknown') } } : {}), debug_session_id: s.id,
|
|
38
45
|
device: { platform: 'android', id: s.device }, target: s.target, state: s.state, captured_at_ms: Date.now(),
|
|
39
46
|
capabilities: debuggerCapabilities, limitations, cleanup: s.cleanup, reason: s.reason, stop: s.stop, ...payload });
|
|
40
47
|
}
|
|
41
48
|
summary(id) { return this.result(this.get(id)); }
|
|
42
49
|
hasActiveSessions() { return [...this.sessions.values()].some(s => s.cleanup.status !== 'complete'); }
|
|
43
50
|
paused(device) { const s = [...this.sessions.values()].find(s => s.device === device && s.state === 'paused'); return s ? this.result(s) : null; }
|
|
44
|
-
assertRunning(device) {
|
|
45
|
-
|
|
51
|
+
assertRunning(device) {
|
|
52
|
+
if (this.paused(device))
|
|
53
|
+
throw new DebuggerError('DEBUGGER_PAUSED');
|
|
54
|
+
}
|
|
46
55
|
event(s, event) {
|
|
47
56
|
if (s.cleanup.status === 'complete')
|
|
48
57
|
return;
|
|
@@ -52,6 +61,8 @@ export class DebuggerSessions {
|
|
|
52
61
|
s.pausedAt ??= Date.now();
|
|
53
62
|
}
|
|
54
63
|
if (event.event === 'terminated' || event.event === 'worker_failed') {
|
|
64
|
+
if (!s.cleaning)
|
|
65
|
+
s.diagnostic ??= event.diagnostic ?? diagnostic(event.event === 'worker_failed' ? 'ipc' : 'transport', event.reason === 'transport_lost' ? 'transport_lost' : 'unknown');
|
|
55
66
|
if (!s.cleaning && s.state !== 'failed')
|
|
56
67
|
s.reason = event.reason ?? 'worker_failed';
|
|
57
68
|
if (!s.cleaning) {
|
|
@@ -69,30 +80,35 @@ export class DebuggerSessions {
|
|
|
69
80
|
const prerequisites = { jdk: false, device: false, process: false };
|
|
70
81
|
let candidates = [];
|
|
71
82
|
const reasons = [];
|
|
83
|
+
const diagnostics = [];
|
|
72
84
|
try {
|
|
73
|
-
await this.prerequisite();
|
|
85
|
+
await atStage('prerequisite', 'DEBUGGER_UNSUPPORTED', () => this.prerequisite());
|
|
74
86
|
prerequisites.jdk = true;
|
|
75
87
|
}
|
|
76
88
|
catch (e) {
|
|
77
89
|
if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
|
|
78
90
|
throw e;
|
|
79
91
|
reasons.push('DEBUGGER_UNSUPPORTED');
|
|
92
|
+
diagnostics.push(classify(e, 'prerequisite', 'DEBUGGER_UNSUPPORTED').diagnostic ?? diagnostic('prerequisite', 'unknown'));
|
|
80
93
|
}
|
|
81
94
|
try {
|
|
82
|
-
candidates = await this.native.candidates(device, app);
|
|
95
|
+
candidates = await atStage('target_resolution', 'DEVICE_UNAVAILABLE', () => this.native.candidates(device, app));
|
|
83
96
|
prerequisites.device = true;
|
|
84
97
|
prerequisites.process = candidates.some(t => pid === undefined || pid === t.pid);
|
|
85
|
-
if (!prerequisites.process)
|
|
98
|
+
if (!prerequisites.process) {
|
|
86
99
|
reasons.push('PROCESS_NOT_FOUND');
|
|
100
|
+
diagnostics.push(diagnostic('target_resolution', 'unavailable'));
|
|
101
|
+
}
|
|
87
102
|
}
|
|
88
103
|
catch (e) {
|
|
89
104
|
if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
|
|
90
105
|
throw e;
|
|
91
106
|
reasons.push(e instanceof DebuggerError ? e.code : 'DEVICE_UNAVAILABLE');
|
|
107
|
+
diagnostics.push(classify(e, 'target_resolution', 'DEVICE_UNAVAILABLE').diagnostic ?? diagnostic('target_resolution', 'unknown'));
|
|
92
108
|
}
|
|
93
109
|
return { success: true, debug_session_id: null, device: { platform: 'android', id: device }, target: null, state: null,
|
|
94
110
|
captured_at_ms: Date.now(), supported: Object.values(prerequisites).every(Boolean), prerequisites, candidates,
|
|
95
|
-
capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } };
|
|
111
|
+
...(diagnostics.length ? { diagnostics } : {}), capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } };
|
|
96
112
|
}
|
|
97
113
|
async attach(device, app, pid, timeout = 10000) {
|
|
98
114
|
this.prune();
|
|
@@ -103,27 +119,43 @@ export class DebuggerSessions {
|
|
|
103
119
|
queue: Promise.resolve(), events: [], sequence: 0, cleanup: { status: 'pending', remaining_resources: [] }, created: Date.now(), activity: Date.now() };
|
|
104
120
|
this.sessions.set(s.id, s);
|
|
105
121
|
return this.serial(s, async () => {
|
|
122
|
+
let stage = 'prerequisite';
|
|
106
123
|
const deadline = Date.now() + timeout;
|
|
107
|
-
const remaining = () => {
|
|
108
|
-
|
|
124
|
+
const remaining = () => {
|
|
125
|
+
try {
|
|
126
|
+
debuggerRemaining(timeout);
|
|
127
|
+
const ms = deadline - Date.now();
|
|
128
|
+
if (ms <= 0)
|
|
129
|
+
throw new DebuggerError('DEBUGGER_TIMEOUT');
|
|
130
|
+
return ms;
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
throw classify(e, stage, 'DEBUGGER_ATTACH_FAILED');
|
|
134
|
+
}
|
|
135
|
+
};
|
|
109
136
|
try {
|
|
110
|
-
await this.prerequisite();
|
|
137
|
+
await atStage('prerequisite', 'DEBUGGER_ATTACH_FAILED', () => this.prerequisite());
|
|
111
138
|
remaining();
|
|
112
|
-
|
|
139
|
+
stage = 'target_resolution';
|
|
140
|
+
const candidates = (await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.candidates(device, app))).filter(t => pid === undefined || t.pid === pid);
|
|
113
141
|
remaining();
|
|
114
142
|
if (!candidates.length)
|
|
115
|
-
throw new DebuggerError('PROCESS_NOT_FOUND');
|
|
143
|
+
throw new DebuggerError('PROCESS_NOT_FOUND', diagnostic('target_resolution', 'unavailable'));
|
|
116
144
|
if (candidates.length > 1)
|
|
117
145
|
throw new DebuggerError('PROCESS_AMBIGUOUS');
|
|
118
146
|
s.target = candidates[0];
|
|
119
|
-
|
|
147
|
+
stage = 'forwarding';
|
|
148
|
+
s.port = await atStage('forwarding', 'DEBUGGER_ATTACH_FAILED', () => this.native.forward(device, s.target.pid));
|
|
120
149
|
remaining();
|
|
121
150
|
s.cleanup.remaining_resources.push(`forward:tcp:${s.port}`);
|
|
122
|
-
|
|
151
|
+
stage = 'worker_start';
|
|
152
|
+
s.worker = await atStage('worker_start', 'DEBUGGER_ATTACH_FAILED', () => this.workerFactory(event => this.event(s, event)));
|
|
123
153
|
remaining();
|
|
124
154
|
s.cleanup.remaining_resources.push('jdi_worker');
|
|
125
|
-
|
|
126
|
-
await
|
|
155
|
+
stage = 'attach';
|
|
156
|
+
await atStage('attach', 'DEBUGGER_ATTACH_FAILED', () => s.worker.call('attach', { port: s.port, timeoutMs: remaining() }, remaining()));
|
|
157
|
+
stage = 'target_resolution';
|
|
158
|
+
await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.identity(device, s.target));
|
|
127
159
|
remaining();
|
|
128
160
|
if (s.state === 'attaching')
|
|
129
161
|
s.state = 'running';
|
|
@@ -140,10 +172,11 @@ export class DebuggerSessions {
|
|
|
140
172
|
s.timer.unref();
|
|
141
173
|
}
|
|
142
174
|
catch (e) {
|
|
175
|
+
s.diagnostic ??= classify(e, stage, 'DEBUGGER_ATTACH_FAILED').diagnostic;
|
|
143
176
|
s.state = 'failed';
|
|
144
177
|
s.reason = e instanceof DebuggerError ? e.code : 'DEBUGGER_ATTACH_FAILED';
|
|
145
178
|
await this.cleanup(s, true);
|
|
146
|
-
return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false } });
|
|
179
|
+
return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false, diagnostic: s.diagnostic } });
|
|
147
180
|
}
|
|
148
181
|
return this.result(s);
|
|
149
182
|
});
|
|
@@ -203,58 +236,67 @@ export class DebuggerSessions {
|
|
|
203
236
|
if (after > s.sequence)
|
|
204
237
|
throw new DebuggerError('INVALID_ARGUMENT');
|
|
205
238
|
if (args.signal?.aborted)
|
|
206
|
-
throw new DebuggerError('DEBUGGER_CANCELLED');
|
|
239
|
+
throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
|
|
207
240
|
if (s.worker && ['running', 'paused'].includes(s.state))
|
|
208
|
-
await s.worker.call('heartbeat', {}, Math.min(timeout, 1000));
|
|
241
|
+
await atStage('command', 'DEBUGGER_WORKER_FAILED', () => s.worker.call('heartbeat', {}, Math.min(timeout, 1000)));
|
|
209
242
|
while (Date.now() < deadline && !s.events.some(e => e.sequence > after) && s.cleanup.status === 'pending') {
|
|
210
243
|
if (args.signal?.aborted)
|
|
211
|
-
throw new DebuggerError('DEBUGGER_CANCELLED');
|
|
244
|
+
throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
|
|
212
245
|
await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
213
246
|
}
|
|
214
247
|
const events = s.events.filter(e => e.sequence > after).slice(0, 100);
|
|
215
248
|
return this.result(s, { events, next_cursor: events.at(-1)?.sequence ?? s.sequence, overflow: after < (s.events[0]?.sequence ?? s.sequence + 1) - 1, timed_out: events.length === 0 });
|
|
216
249
|
}
|
|
217
|
-
return this.serial(s,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
250
|
+
return this.serial(s, () => this.executeCommand(s, command, args, timeout));
|
|
251
|
+
}
|
|
252
|
+
assertCommandTarget(s, command, args) {
|
|
253
|
+
if (!s.worker || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state))
|
|
254
|
+
throw new DebuggerError('DEBUGGER_DISCONNECTED', s.diagnostic ?? diagnostic('command', 'unavailable'));
|
|
255
|
+
if (['stack', 'locals', 'step', 'resume'].includes(command)) {
|
|
256
|
+
if (args.stopId !== s.stop?.stop_id)
|
|
257
|
+
throw new DebuggerError('DEBUGGER_STALE_REFERENCE');
|
|
258
|
+
if (s.state !== 'paused')
|
|
259
|
+
throw new DebuggerError('DEBUGGER_NOT_PAUSED');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async executeCommand(s, command, args, timeout) {
|
|
263
|
+
if (command === 'detach') {
|
|
264
|
+
if (s.cleanup.status !== 'complete')
|
|
265
|
+
await this.cleanup(s, true);
|
|
266
|
+
return this.result(s, { success: s.cleanup.status === 'complete', execution_state: 'unknown' });
|
|
267
|
+
}
|
|
268
|
+
if (command === 'state' && (s.cleanup.status === 'complete' || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state)))
|
|
269
|
+
return this.result(s);
|
|
270
|
+
this.assertCommandTarget(s, command, args);
|
|
271
|
+
if (command === 'step' || command === 'resume') {
|
|
272
|
+
s.state = 'running';
|
|
273
|
+
s.stop = null;
|
|
274
|
+
s.pausedAt = undefined;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const data = await s.worker.call(command, args, timeout);
|
|
278
|
+
return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data });
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
return this.handleCommandError(s, command, classify(e, 'command', 'DEBUGGER_WORKER_FAILED'));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async handleCommandError(s, command, e) {
|
|
285
|
+
if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
|
|
286
|
+
s.diagnostic ??= e.diagnostic ?? diagnostic('transport', 'transport_lost');
|
|
287
|
+
s.state = 'disconnected';
|
|
288
|
+
s.reason = 'transport_lost';
|
|
289
|
+
await this.cleanup(s, false);
|
|
290
|
+
if (command === 'state')
|
|
224
291
|
return this.result(s);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (command === 'step' || command === 'resume') {
|
|
234
|
-
s.state = 'running';
|
|
235
|
-
s.stop = null;
|
|
236
|
-
s.pausedAt = undefined;
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const data = await s.worker.call(command, args, timeout);
|
|
240
|
-
return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data });
|
|
241
|
-
}
|
|
242
|
-
catch (e) {
|
|
243
|
-
if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
|
|
244
|
-
s.state = 'disconnected';
|
|
245
|
-
s.reason = 'transport_lost';
|
|
246
|
-
await this.cleanup(s, false);
|
|
247
|
-
if (command === 'state')
|
|
248
|
-
return this.result(s);
|
|
249
|
-
}
|
|
250
|
-
if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
|
|
251
|
-
s.state = 'failed';
|
|
252
|
-
s.reason = e.code;
|
|
253
|
-
await this.cleanup(s, true);
|
|
254
|
-
}
|
|
255
|
-
throw e;
|
|
256
|
-
}
|
|
257
|
-
});
|
|
292
|
+
}
|
|
293
|
+
if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
|
|
294
|
+
s.diagnostic ??= e.diagnostic;
|
|
295
|
+
s.state = 'failed';
|
|
296
|
+
s.reason = e.code;
|
|
297
|
+
await this.cleanup(s, true);
|
|
298
|
+
}
|
|
299
|
+
throw e;
|
|
258
300
|
}
|
|
259
301
|
async shutdown() { this.closing = true; await Promise.allSettled([...this.sessions.values()].filter(s => s.cleanup.status !== 'complete').map(s => this.serial(s, () => this.cleanup(s, true)))); return [...this.sessions.values()].filter(s => s.cleanup.status !== 'complete').map(s => this.result(s)); }
|
|
260
302
|
}
|
package/dist/debugger/worker.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { DebuggerError, classify, diagnostic, atStage } from './diagnostics.js';
|
|
2
|
+
export { DebuggerError } from './diagnostics.js';
|
|
3
|
+
import { parseWorkerMessage } from './protocol.js';
|
|
1
4
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
5
|
import { execFile, spawn } from 'node:child_process';
|
|
3
6
|
import { promisify } from 'node:util';
|
|
@@ -27,71 +30,60 @@ export function nativeError(error, fallback) {
|
|
|
27
30
|
return error;
|
|
28
31
|
if (debuggerSignal()?.aborted)
|
|
29
32
|
return new DebuggerError('DEBUGGER_CANCELLED');
|
|
30
|
-
if (error
|
|
33
|
+
if (typeof error === 'object' && error !== null && 'killed' in error && error.killed)
|
|
31
34
|
return new DebuggerError('DEBUGGER_TIMEOUT');
|
|
32
35
|
return new DebuggerError(fallback);
|
|
33
36
|
}
|
|
34
|
-
export
|
|
35
|
-
code;
|
|
36
|
-
constructor(code) {
|
|
37
|
-
super(code);
|
|
38
|
-
this.code = code;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
export const workerSource = fileURLToPath(new URL('../../worker/DebuggerWorker.java', import.meta.url));
|
|
37
|
+
export const workerSource = fileURLToPath(new URL('../../src/debugger/java/DebuggerWorker.java', import.meta.url));
|
|
42
38
|
let compiled;
|
|
43
39
|
export async function prepareWorker() {
|
|
40
|
+
let stage = 'prerequisite';
|
|
44
41
|
compiled ??= (async () => {
|
|
45
|
-
const home = await detectJavaHome({ quiet: true });
|
|
42
|
+
const home = await detectJavaHome({ quiet: true }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', 'unavailable'); });
|
|
46
43
|
const java = home ? path.join(home, 'bin', 'java') : 'java';
|
|
47
44
|
const javac = home ? path.join(home, 'bin', 'javac') : 'javac';
|
|
48
|
-
const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() });
|
|
45
|
+
const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', debuggerSignal()?.aborted ? 'cancelled' : typeof e === 'object' && e !== null && 'killed' in e && e.killed ? 'timeout' : 'unavailable'); });
|
|
49
46
|
const major = Number(/version "(\d+)/.exec(version.stderr)?.[1]);
|
|
50
47
|
if (!(major >= 17))
|
|
51
|
-
throw new DebuggerError('DEBUGGER_UNSUPPORTED');
|
|
52
|
-
|
|
48
|
+
throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('prerequisite', 'unsupported'));
|
|
49
|
+
stage = 'compilation';
|
|
50
|
+
const source = await readFile(workerSource).catch(e => { throw classify(e, 'compilation', 'DEBUGGER_ATTACH_FAILED', 'unavailable'); });
|
|
53
51
|
const key = createHash('sha256').update(source).update(version.stderr).digest('hex').slice(0, 16);
|
|
54
|
-
const directory = await mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`));
|
|
55
|
-
await chmod(directory, 0o700);
|
|
52
|
+
const directory = await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`)));
|
|
56
53
|
try {
|
|
54
|
+
await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => chmod(directory, 0o700));
|
|
57
55
|
await exec(javac, ['--release', '17', '--add-modules', 'jdk.jdi', '-d', directory, workerSource], { timeout: debuggerRemaining(15000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 });
|
|
58
56
|
}
|
|
59
57
|
catch (e) {
|
|
60
58
|
await rm(directory, { recursive: true, force: true });
|
|
61
|
-
throw nativeError(e, 'DEBUGGER_UNSUPPORTED');
|
|
59
|
+
throw classify(nativeError(e, 'DEBUGGER_UNSUPPORTED'), 'compilation', 'DEBUGGER_UNSUPPORTED', 'operation_failed');
|
|
62
60
|
}
|
|
63
61
|
return { java, directory };
|
|
64
|
-
})().catch(error => { compiled = undefined; throw error; });
|
|
62
|
+
})().catch(error => { compiled = undefined; throw classify(error, stage, 'DEBUGGER_ATTACH_FAILED'); });
|
|
65
63
|
return compiled;
|
|
66
64
|
}
|
|
65
|
+
function commandStage(command) { return command === 'attach' ? 'attach' : command === 'detach' ? 'cleanup' : 'command'; }
|
|
67
66
|
export class JdiWorker {
|
|
67
|
+
onEvent;
|
|
68
68
|
child;
|
|
69
69
|
pending = new Map();
|
|
70
70
|
sequence = 0;
|
|
71
71
|
dead = false;
|
|
72
|
+
failure;
|
|
73
|
+
replied = false;
|
|
72
74
|
exited;
|
|
73
75
|
constructor(java, directory, onEvent) {
|
|
76
|
+
this.onEvent = onEvent;
|
|
74
77
|
this.child = spawn(java, ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
75
78
|
let buffer = '';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
this.dead = true;
|
|
80
|
-
for (const request of this.pending.values()) {
|
|
81
|
-
clearTimeout(request.timer);
|
|
82
|
-
request.reject(new DebuggerError('DEBUGGER_WORKER_FAILED'));
|
|
83
|
-
}
|
|
84
|
-
this.pending.clear();
|
|
85
|
-
onEvent({ event: 'worker_failed' });
|
|
86
|
-
};
|
|
87
|
-
this.exited = new Promise(resolve => { this.child.once('exit', () => { fail(); resolve(); }); this.child.once('error', () => { fail(); resolve(); }); });
|
|
88
|
-
this.child.stdin.on('error', fail);
|
|
79
|
+
this.exited = new Promise(resolve => { this.child.once('exit', () => { this.fail('process_exited'); resolve(); }); this.child.once('error', () => { this.fail('spawn_failed'); resolve(); }); });
|
|
80
|
+
this.child.stdin.on('error', () => this.fail('write_failed'));
|
|
89
81
|
this.child.stderr.resume(); // Never copy worker data or VM diagnostics into MCP stdout/logs.
|
|
90
82
|
this.child.stdout.setEncoding('utf8');
|
|
91
83
|
this.child.stdout.on('data', (chunk) => {
|
|
92
84
|
buffer += chunk;
|
|
93
85
|
if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) {
|
|
94
|
-
fail();
|
|
86
|
+
this.fail('message_limit');
|
|
95
87
|
this.child.kill('SIGKILL');
|
|
96
88
|
return;
|
|
97
89
|
}
|
|
@@ -102,37 +94,51 @@ export class JdiWorker {
|
|
|
102
94
|
try {
|
|
103
95
|
if (Buffer.byteLength(line) > 1024 * 1024)
|
|
104
96
|
throw new Error('IPC_LIMIT');
|
|
105
|
-
const msg = JSON.parse(line);
|
|
106
|
-
if (msg.event)
|
|
107
|
-
onEvent(msg);
|
|
97
|
+
const msg = parseWorkerMessage(JSON.parse(line));
|
|
98
|
+
if (msg.kind === 'event')
|
|
99
|
+
onEvent(msg.value);
|
|
108
100
|
else {
|
|
101
|
+
this.replied = true;
|
|
109
102
|
const request = this.pending.get(msg.id);
|
|
110
103
|
if (!request)
|
|
111
104
|
continue;
|
|
112
105
|
clearTimeout(request.timer);
|
|
113
106
|
this.pending.delete(msg.id);
|
|
114
107
|
if (msg.error)
|
|
115
|
-
request.reject(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED'));
|
|
108
|
+
request.reject(classify(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED', msg.error.diagnostic), commandStage(request.command), 'DEBUGGER_WORKER_FAILED'));
|
|
116
109
|
else
|
|
117
110
|
request.resolve(msg.result);
|
|
118
111
|
}
|
|
119
112
|
}
|
|
120
113
|
catch {
|
|
121
|
-
fail();
|
|
114
|
+
this.fail(Buffer.byteLength(line) > 1024 * 1024 ? 'message_limit' : 'invalid_message');
|
|
122
115
|
this.child.kill('SIGKILL');
|
|
123
116
|
return;
|
|
124
117
|
}
|
|
125
118
|
}
|
|
126
119
|
});
|
|
127
120
|
}
|
|
121
|
+
fail(reason) {
|
|
122
|
+
if (this.dead)
|
|
123
|
+
return;
|
|
124
|
+
this.dead = true;
|
|
125
|
+
const stage = reason === 'spawn_failed' || (reason === 'process_exited' && !this.replied) ? 'worker_start' : 'ipc';
|
|
126
|
+
this.failure = new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic(stage, reason));
|
|
127
|
+
for (const request of this.pending.values()) {
|
|
128
|
+
clearTimeout(request.timer);
|
|
129
|
+
request.reject(this.failure);
|
|
130
|
+
}
|
|
131
|
+
this.pending.clear();
|
|
132
|
+
this.onEvent({ event: 'worker_failed', diagnostic: this.failure.diagnostic });
|
|
133
|
+
}
|
|
128
134
|
call(command, args = {}, timeout = 10000) {
|
|
129
135
|
if (this.dead)
|
|
130
|
-
return Promise.reject(new DebuggerError('DEBUGGER_WORKER_FAILED'));
|
|
136
|
+
return Promise.reject(this.failure ?? new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'unknown')));
|
|
131
137
|
try {
|
|
132
138
|
timeout = Math.min(timeout, debuggerRemaining(timeout));
|
|
133
139
|
}
|
|
134
140
|
catch (e) {
|
|
135
|
-
return Promise.reject(e);
|
|
141
|
+
return Promise.reject(classify(e, commandStage(command), 'DEBUGGER_WORKER_FAILED'));
|
|
136
142
|
}
|
|
137
143
|
const id = ++this.sequence;
|
|
138
144
|
const message = JSON.stringify({ ...args, command, id }) + '\n';
|
|
@@ -140,12 +146,17 @@ export class JdiWorker {
|
|
|
140
146
|
return Promise.reject(new DebuggerError('INVALID_ARGUMENT'));
|
|
141
147
|
return new Promise((resolve, reject) => {
|
|
142
148
|
const signal = debuggerSignal();
|
|
143
|
-
const abort = () => { this.pending.delete(id); clearTimeout(timer); reject(new DebuggerError('DEBUGGER_CANCELLED')); };
|
|
149
|
+
const abort = () => { this.pending.delete(id); clearTimeout(timer); done(); reject(new DebuggerError('DEBUGGER_CANCELLED', diagnostic(commandStage(command), 'cancelled'))); };
|
|
144
150
|
const done = () => signal?.removeEventListener('abort', abort);
|
|
145
|
-
const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT')); }, timeout);
|
|
146
|
-
this.pending.set(id, { resolve: value => { done(); resolve(value); }, reject: error => { done(); reject(error); }, timer });
|
|
151
|
+
const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT', diagnostic(commandStage(command), 'timeout'))); }, timeout);
|
|
152
|
+
this.pending.set(id, { command, resolve: value => { done(); resolve(value); }, reject: error => { done(); reject(error); }, timer });
|
|
147
153
|
signal?.addEventListener('abort', abort, { once: true });
|
|
148
|
-
|
|
154
|
+
try {
|
|
155
|
+
this.child.stdin.write(message);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
this.fail('write_failed');
|
|
159
|
+
}
|
|
149
160
|
});
|
|
150
161
|
}
|
|
151
162
|
async close() {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { systemClock } from '../../runtime/clock.js';
|
|
2
|
+
import { createTraceStep, buildActionExecutionResult } from '../../runtime/action-results.js';
|
|
3
|
+
import { completeAdjustment } from './adjustment-result.js';
|
|
4
|
+
export async function createAdjustment(input, deps) {
|
|
5
|
+
const platform = input.platform || 'android';
|
|
6
|
+
const tolerance = typeof input.tolerance === 'number' && Number.isFinite(input.tolerance) ? Math.max(0, input.tolerance) : null;
|
|
7
|
+
const limit = input.maxAttempts == null ? 5 : Math.max(1, Math.floor(Number(input.maxAttempts) || 1));
|
|
8
|
+
const stored = input.element_id ? deps.getHandle(input.element_id) ?? null : null;
|
|
9
|
+
const before = await deps.fingerprint(platform, input.deviceId);
|
|
10
|
+
return { clock: deps.clock ?? systemClock, input: { ...input, property: input.property === undefined ? 'value' : input.property }, platform, deviceId: input.deviceId, stored,
|
|
11
|
+
selector: input.selector ?? (input.element_id ? { elementId: input.element_id } : null), tolerance, limit, before,
|
|
12
|
+
fallback: null, trace: [], sequence: 0, observed: null, mode: 'gesture', target: null,
|
|
13
|
+
attempts: 0, cached: null, coordinatePending: false };
|
|
14
|
+
}
|
|
15
|
+
export function recordAdjustment(s, stage, result, metadata) {
|
|
16
|
+
s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }));
|
|
17
|
+
}
|
|
18
|
+
export function failAdjustment(s, code, reason, retryable = false, target = s.target, device = s.device, observed = s.observed, attempts = s.attempts, mode = s.mode, after = null) {
|
|
19
|
+
if (!s.trace.some(step => step.stage === 'resolve'))
|
|
20
|
+
recordAdjustment(s, 'resolve', 'failure', { reason, failure_code: code });
|
|
21
|
+
if (!s.trace.some(step => step.stage === 'recover'))
|
|
22
|
+
recordAdjustment(s, 'recover', retryable ? 'retry' : 'failure', {
|
|
23
|
+
reason, failure_code: code, retry_allowed: retryable, recovery_attempts: attempts, retry_depth: attempts
|
|
24
|
+
});
|
|
25
|
+
const tolerance = s.tolerance ?? 0;
|
|
26
|
+
const base = buildActionExecutionResult({ actionType: 'adjust_control', sourceModule: 'interact', device,
|
|
27
|
+
selector: s.selector, resolved: target, success: false, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
|
|
28
|
+
failure: { failureCode: code, retryable }, traceSteps: s.trace,
|
|
29
|
+
details: { target_value: s.input.targetValue, tolerance, property: s.input.property, attempts,
|
|
30
|
+
adjustment_mode: mode, actual_state: observed, converged: false, within_tolerance: false, reason } });
|
|
31
|
+
return { ...base, target_state: { property: s.input.property, target_value: s.input.targetValue, tolerance },
|
|
32
|
+
actual_state: observed, within_tolerance: false, converged: false, attempts, adjustment_mode: mode };
|
|
33
|
+
}
|
|
34
|
+
export async function succeedAdjustment(s, p, deps, reason = 'control converged to target value', mode = s.mode, device = p.actionDevice ?? s.device) {
|
|
35
|
+
const after = await deps.fingerprint(s.platform, s.deviceId);
|
|
36
|
+
return completeAdjustment({ actionType: 'adjust_control', sourceModule: 'interact', device,
|
|
37
|
+
selector: s.selector, resolved: s.target, success: true, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
|
|
38
|
+
traceSteps: s.trace, details: { target_value: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
|
|
39
|
+
property: s.input.property, attempts: s.attempts, adjustment_mode: mode, actual_state: p.observed,
|
|
40
|
+
converged: true, within_tolerance: true, reason } });
|
|
41
|
+
}
|