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/src/utils/ui/index.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { remainingBudget } from '../operation-budget.js'
|
|
|
2
2
|
import crypto from 'crypto'
|
|
3
3
|
import { GetUITreeResponse, GetCurrentScreenResponse, UIElement, SwipeResponse } from '../../types.js'
|
|
4
4
|
|
|
5
|
-
const ANDROID_STRUCTURAL_TYPES = ['Window','Application','View','ViewGroup','LinearLayout','FrameLayout','RelativeLayout','ScrollView','RecyclerView','TextView','ImageView']
|
|
6
|
-
const IOS_STRUCTURAL_TYPES = ['Window','Application','View','ViewController','UITableView','UICollectionView','UILabel','UIImageView','UIView','UIWindow','UIStackView','UITextView','UITableViewCell']
|
|
5
|
+
const ANDROID_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewGroup', 'LinearLayout', 'FrameLayout', 'RelativeLayout', 'ScrollView', 'RecyclerView', 'TextView', 'ImageView']
|
|
6
|
+
const IOS_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewController', 'UITableView', 'UICollectionView', 'UILabel', 'UIImageView', 'UIView', 'UIWindow', 'UIStackView', 'UITextView', 'UITableViewCell']
|
|
7
7
|
|
|
8
8
|
function isDynamicText(t?: string): boolean {
|
|
9
9
|
if (!t) return false
|
|
@@ -23,15 +23,15 @@ function normalizeElement(e: UIElement) {
|
|
|
23
23
|
resourceId: (e.resourceId || '').toString(),
|
|
24
24
|
text: typeof e.text === 'string' ? (isDynamicText(e.text) ? '' : e.text.trim().toLowerCase()) : '',
|
|
25
25
|
contentDesc: (e.contentDescription || '').toString(),
|
|
26
|
-
bounds: Array.isArray(e.bounds) ? e.bounds.slice(0,4).map((n:
|
|
26
|
+
bounds: Array.isArray(e.bounds) ? e.bounds.slice(0, 4).map((n: number) => Number(n) || 0) : [0, 0, 0, 0]
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function computeScreenFingerprint(tree: GetUITreeResponse, current: GetCurrentScreenResponse | null, platform: 'android' | 'ios', limit: number = 50): { fingerprint: string | null; activity?: string; error?: string } {
|
|
31
31
|
try {
|
|
32
|
-
if (!tree ||
|
|
32
|
+
if (!tree || tree.error) return { fingerprint: null, error: tree.error }
|
|
33
33
|
|
|
34
|
-
const activity = current && (current.activity ||
|
|
34
|
+
const activity = current && (current.activity || current.shortActivity) ? (current.activity || current.shortActivity) : ''
|
|
35
35
|
|
|
36
36
|
const candidates: UIElement[] = (tree.elements || []).filter(e => {
|
|
37
37
|
if (!e) return false
|
|
@@ -48,7 +48,7 @@ export function computeScreenFingerprint(tree: GetUITreeResponse, current: GetCu
|
|
|
48
48
|
|
|
49
49
|
const filteredNormalized = normalized.filter(e => (e.text && e.text.length > 0) || (e.resourceId && e.resourceId.length > 0) || (e.contentDesc && e.contentDesc.length > 0))
|
|
50
50
|
|
|
51
|
-
filteredNormalized.sort((a,b) => {
|
|
51
|
+
filteredNormalized.sort((a, b) => {
|
|
52
52
|
const ay = (a.bounds && a.bounds[1]) || 0
|
|
53
53
|
const by = (b.bounds && b.bounds[1]) || 0
|
|
54
54
|
if (ay !== by) return ay - by
|
|
@@ -61,7 +61,7 @@ export function computeScreenFingerprint(tree: GetUITreeResponse, current: GetCu
|
|
|
61
61
|
|
|
62
62
|
const payload = {
|
|
63
63
|
activity: platform === 'android' ? (activity || '') : '',
|
|
64
|
-
resolution:
|
|
64
|
+
resolution: tree.resolution || { width: 0, height: 0 },
|
|
65
65
|
elements: limited.map(e => ({ type: e.type, resourceId: e.resourceId, text: e.text, contentDesc: e.contentDesc }))
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -87,29 +87,10 @@ export async function scrollToElementShared(opts: {
|
|
|
87
87
|
}): Promise<{ success: boolean; reason?: string; element?: Partial<UIElement>; scrollsPerformed: number }> {
|
|
88
88
|
const { selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId, fetchTree, swipe, stabilizationDelayMs = 350 } = opts
|
|
89
89
|
|
|
90
|
-
const matchElement = (el?: UIElement) => {
|
|
91
|
-
if (!el) return false
|
|
92
|
-
if (selector.text !== undefined && selector.text !== el.text) return false
|
|
93
|
-
if (selector.resourceId !== undefined && selector.resourceId !== el.resourceId) return false
|
|
94
|
-
if (selector.contentDesc !== undefined && selector.contentDesc !== el.contentDescription) return false
|
|
95
|
-
if (selector.className !== undefined && selector.className !== el.type) return false
|
|
96
|
-
return true
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const isVisible = (el?: UIElement, resolution?: GetUITreeResponse['resolution']) => {
|
|
100
|
-
if (!el) return false
|
|
101
|
-
if (el.visible === false) return false
|
|
102
|
-
if (!el.bounds || !resolution || !resolution.width || !resolution.height) return (el.visible === undefined ? true : !!el.visible)
|
|
103
|
-
const [left, top, right, bottom] = el.bounds
|
|
104
|
-
const withinY = bottom > 0 && top < resolution.height
|
|
105
|
-
const withinX = right > 0 && left < resolution.width
|
|
106
|
-
return withinX && withinY
|
|
107
|
-
}
|
|
108
|
-
|
|
109
90
|
const findVisibleMatch = (elements?: UIElement[], resolution?: GetUITreeResponse['resolution']) => {
|
|
110
91
|
if (!Array.isArray(elements)) return null
|
|
111
92
|
for (const e of elements) {
|
|
112
|
-
if (matchElement(e) && isVisible(e, resolution)) return e
|
|
93
|
+
if (matchElement(selector, e) && isVisible(e, resolution)) return e
|
|
113
94
|
}
|
|
114
95
|
return null
|
|
115
96
|
}
|
|
@@ -158,7 +139,7 @@ export async function scrollToElementShared(opts: {
|
|
|
158
139
|
try {
|
|
159
140
|
await swipe(x1, y1, x2, y2, duration, deviceId)
|
|
160
141
|
} catch (e) {
|
|
161
|
-
try { console.warn(`scrollToElement swipe failed: ${e instanceof Error ? e.message : String(e)}`) } catch {}
|
|
142
|
+
try { console.warn(`scrollToElement swipe failed: ${e instanceof Error ? e.message : String(e)}`) } catch { }
|
|
162
143
|
}
|
|
163
144
|
|
|
164
145
|
scrollsPerformed++
|
|
@@ -181,3 +162,22 @@ export async function scrollToElementShared(opts: {
|
|
|
181
162
|
|
|
182
163
|
return { success: false, reason: 'Element not found after scrolling', scrollsPerformed: scrollsPerformed }
|
|
183
164
|
}
|
|
165
|
+
|
|
166
|
+
function matchElement(selector: ScrollSelector, el?: UIElement) {
|
|
167
|
+
if (!el) return false
|
|
168
|
+
if (selector.text !== undefined && selector.text !== el.text) return false
|
|
169
|
+
if (selector.resourceId !== undefined && selector.resourceId !== el.resourceId) return false
|
|
170
|
+
if (selector.contentDesc !== undefined && selector.contentDesc !== el.contentDescription) return false
|
|
171
|
+
if (selector.className !== undefined && selector.className !== el.type) return false
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isVisible(el?: UIElement, resolution?: GetUITreeResponse['resolution']) {
|
|
176
|
+
if (!el) return false
|
|
177
|
+
if (el.visible === false) return false
|
|
178
|
+
if (!el.bounds || !resolution || !resolution.width || !resolution.height) return (el.visible === undefined ? true : !!el.visible)
|
|
179
|
+
const [left, top, right, bottom] = el.bounds
|
|
180
|
+
const withinY = bottom > 0 && top < resolution.height
|
|
181
|
+
const withinX = right > 0 && left < resolution.width
|
|
182
|
+
return withinX && withinY
|
|
183
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** ADB lists every device even with -s. Compare only this serial, independent of order. */
|
|
2
|
+
export function deviceForwards(table: string, deviceId: string): string[] {
|
|
3
|
+
return table.split('\n').map(row => row.trim().split(/\s+/))
|
|
4
|
+
.filter(columns => columns[0] === deviceId)
|
|
5
|
+
.map(columns => columns.join(' ')).sort()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Cleanup is part of the assessment; a finally failure must never follow a PASS. */
|
|
9
|
+
export async function reportAfterCleanup<T>(
|
|
10
|
+
work: () => Promise<T>,
|
|
11
|
+
cleanup: () => Promise<void>,
|
|
12
|
+
report: (status: 'PASS' | 'FAIL', evidence?: T, error?: unknown) => void,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
let evidence: T | undefined
|
|
15
|
+
let failed = false
|
|
16
|
+
let failure: unknown
|
|
17
|
+
try { evidence = await work() } catch (error) { failed = true; failure = error }
|
|
18
|
+
try { await cleanup() } catch (error) {
|
|
19
|
+
failure = failed ? new AggregateError([failure, error], `${String(failure)}; cleanup: ${String(error)}`) : error
|
|
20
|
+
failed = true
|
|
21
|
+
}
|
|
22
|
+
if (failed) {
|
|
23
|
+
report('FAIL', undefined, failure)
|
|
24
|
+
throw failure
|
|
25
|
+
}
|
|
26
|
+
report('PASS', evidence)
|
|
27
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
7
|
+
import { coroutineAppId } from '../../../fixtures/coroutine-app/build.js'
|
|
8
|
+
|
|
9
|
+
type Location = { line: number; method: string; source: string; class_name?: string }
|
|
10
|
+
export type Stop = { stop_id: string; thread_id: string; stops: { location: Location }[] }
|
|
11
|
+
type Frame = { frame_id: string; location: Location }
|
|
12
|
+
type Reply = {
|
|
13
|
+
success: boolean; debug_session_id: string; supported: boolean; next_cursor: number; overflow: boolean
|
|
14
|
+
events: (Stop & { event: string })[]; error: { code: string }; failure_code: string; dispatch_started: boolean
|
|
15
|
+
result: { breakpoint_id: string; status: string; frames: Frame[]; locals: { name: string; value: unknown }[]; availability: string }
|
|
16
|
+
elements: { text?: string; bounds: number[] }[]
|
|
17
|
+
cleanup: { status: string; remaining_resources: unknown[] }
|
|
18
|
+
}
|
|
19
|
+
export class CoroutineJourney {
|
|
20
|
+
readonly client = new Client({ name: 'coroutine-validation', version: '1.0.0' })
|
|
21
|
+
readonly interruption = new AbortController()
|
|
22
|
+
session = ''
|
|
23
|
+
cursor = 0
|
|
24
|
+
observations: unknown[] = []
|
|
25
|
+
constructor(readonly device: string, readonly source: string) {}
|
|
26
|
+
adb(...args: string[]) {
|
|
27
|
+
return execFileSync(process.env.ADB_PATH ?? 'adb', ['-s', this.device, ...args], { encoding: 'utf8', timeout: 15000 })
|
|
28
|
+
}
|
|
29
|
+
async connect() {
|
|
30
|
+
await this.client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
|
|
31
|
+
}
|
|
32
|
+
async call(name: string, args: Record<string, unknown> = {}) {
|
|
33
|
+
console.log('Coroutine MCP', name)
|
|
34
|
+
const reply = await this.client.callTool({ name, arguments: { ...args, ...(this.session ? { debugSessionId: this.session } : {}) } }, undefined, { timeout: 45000, ...(name === 'detach_debugger' ? {} : { signal: this.interruption.signal }) })
|
|
35
|
+
const content = reply.content.find(item => item.type === 'text')
|
|
36
|
+
assert.ok(content?.type === 'text')
|
|
37
|
+
return JSON.parse(content.text) as Reply
|
|
38
|
+
}
|
|
39
|
+
line(marker: string) {
|
|
40
|
+
const index = this.source.split('\n').findIndex(row => row.includes('// ' + marker))
|
|
41
|
+
assert.ok(index >= 0, marker)
|
|
42
|
+
return index + 1
|
|
43
|
+
}
|
|
44
|
+
async attach() {
|
|
45
|
+
this.cursor = 0
|
|
46
|
+
const capabilities = await this.call('get_debugger_capabilities', { platform: 'android', deviceId: this.device, appId: coroutineAppId })
|
|
47
|
+
assert.equal(capabilities.supported, true, JSON.stringify(capabilities))
|
|
48
|
+
const result = await this.call('attach_debugger', { platform: 'android', deviceId: this.device, appId: coroutineAppId, timeoutMs: 30000 })
|
|
49
|
+
this.session = result.debug_session_id ?? ''
|
|
50
|
+
assert.equal(result.success, true, JSON.stringify(result))
|
|
51
|
+
}
|
|
52
|
+
async detach() {
|
|
53
|
+
if (!this.session) return
|
|
54
|
+
const result = await this.call('detach_debugger')
|
|
55
|
+
assert.equal(result.cleanup.status, 'complete', JSON.stringify(result))
|
|
56
|
+
assert.deepEqual(result.cleanup.remaining_resources, [])
|
|
57
|
+
this.session = ''
|
|
58
|
+
}
|
|
59
|
+
async breakpoint(marker: string, suffix = '') {
|
|
60
|
+
const result = await this.call('set_debugger_breakpoint', { className: coroutineAppId + '.MainActivity' + suffix, line: this.line(marker), responseMode: 'compact' })
|
|
61
|
+
assert.ok(['verified', 'pending'].includes(result.result.status), JSON.stringify(result))
|
|
62
|
+
return result.result.breakpoint_id
|
|
63
|
+
}
|
|
64
|
+
async remove(breakpointId: string) {
|
|
65
|
+
assert.equal((await this.call('remove_debugger_breakpoint', { breakpointId })).success, true)
|
|
66
|
+
}
|
|
67
|
+
async stop(marker?: string, attempts = 10): Promise<Stop | undefined> {
|
|
68
|
+
for (let index = 0; index < attempts; index++) {
|
|
69
|
+
const result = await this.call('wait_for_debugger_event', { afterSequence: this.cursor, timeoutMs: 1000, responseMode: 'compact' })
|
|
70
|
+
assert.equal(result.success, true, JSON.stringify(result))
|
|
71
|
+
assert.equal(result.overflow, false)
|
|
72
|
+
this.cursor = result.next_cursor
|
|
73
|
+
const stop = result.events.find(event => event.event === 'stopped')
|
|
74
|
+
if (!stop) continue
|
|
75
|
+
if (marker) {
|
|
76
|
+
assert.equal(stop.stops[0].location.line, this.line(marker), JSON.stringify(stop))
|
|
77
|
+
assert.equal(stop.stops[0].location.source, 'MainActivity.kt')
|
|
78
|
+
}
|
|
79
|
+
this.observations.push({ marker, stop })
|
|
80
|
+
return stop
|
|
81
|
+
}
|
|
82
|
+
if (marker) throw new Error('Expected source stop: ' + marker)
|
|
83
|
+
}
|
|
84
|
+
async inspect(stop: Stop, label: string) {
|
|
85
|
+
const stack = await this.call('get_debugger_stack', { stopId: stop.stop_id, threadId: stop.thread_id, responseMode: 'compact' })
|
|
86
|
+
assert.equal(stack.success, true, JSON.stringify(stack))
|
|
87
|
+
const frame = stack.result.frames[0]
|
|
88
|
+
const locals = await this.call('get_debugger_locals', { stopId: stop.stop_id, frameId: frame.frame_id, responseMode: 'compact' })
|
|
89
|
+
assert.equal(locals.success, true, JSON.stringify(locals))
|
|
90
|
+
this.observations.push({ label, frames: stack.result.frames, locals: locals.result })
|
|
91
|
+
return { frameId: frame.frame_id, locals: locals.result.locals }
|
|
92
|
+
}
|
|
93
|
+
async resume(stop: Stop) {
|
|
94
|
+
assert.equal((await this.call('resume_debugger', { stopId: stop.stop_id })).success, true)
|
|
95
|
+
}
|
|
96
|
+
async step(stop: Stop, kind: string) {
|
|
97
|
+
const result = await this.call('step_debugger', { stopId: stop.stop_id, threadId: stop.thread_id, kind })
|
|
98
|
+
assert.equal(result.success, true, JSON.stringify(result))
|
|
99
|
+
}
|
|
100
|
+
async tree() { return this.call('get_ui_tree', { platform: 'android', deviceId: this.device }) }
|
|
101
|
+
async visible(text: string) {
|
|
102
|
+
for (let index = 0; index < 10; index++) {
|
|
103
|
+
if ((await this.tree()).elements.some(element => element.text === text)) return
|
|
104
|
+
}
|
|
105
|
+
throw new Error('Fresh UI did not show ' + text + ': ' + JSON.stringify(await this.tree()))
|
|
106
|
+
}
|
|
107
|
+
async tap(text: string) {
|
|
108
|
+
const button = (await this.tree()).elements.find(element => element.text?.toLowerCase() === text.toLowerCase())
|
|
109
|
+
assert.ok(button, 'UI button ' + text)
|
|
110
|
+
const [left, top, right, bottom] = button.bounds
|
|
111
|
+
// Input stays asynchronous: its shell process may wait while the VM is paused.
|
|
112
|
+
const child = spawn(process.env.ADB_PATH ?? 'adb', ['-s', this.device, 'shell', 'input', 'tap', String(Math.floor((left + right) / 2)), String(Math.floor((top + bottom) / 2))], { stdio: 'ignore' })
|
|
113
|
+
const timer = setTimeout(() => child.kill(), 15000)
|
|
114
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
115
|
+
child.once('error', reject)
|
|
116
|
+
child.once('exit', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error('Fixture tap failed: ' + code)) })
|
|
117
|
+
})
|
|
118
|
+
void done.catch(() => {})
|
|
119
|
+
return { done }
|
|
120
|
+
}
|
|
121
|
+
async cancelledWait() {
|
|
122
|
+
const controller = new AbortController()
|
|
123
|
+
const waiting = this.client.callTool({ name: 'wait_for_debugger_event', arguments: { debugSessionId: this.session, afterSequence: this.cursor, timeoutMs: 1000 } }, undefined, { signal: controller.signal, timeout: 5000 })
|
|
124
|
+
const timer = setTimeout(() => controller.abort(), 100)
|
|
125
|
+
try { await assert.rejects(waiting) } finally { clearTimeout(timer) }
|
|
126
|
+
assert.equal((await this.call('get_debugger_state')).success, true)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function fingerprint(directory: string): Promise<string> {
|
|
130
|
+
const hash = createHash('sha256')
|
|
131
|
+
async function visit(folder: string): Promise<void> {
|
|
132
|
+
for (const entry of (await readdir(folder, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
133
|
+
const file = folder + '/' + entry.name
|
|
134
|
+
if (entry.isDirectory()) await visit(file)
|
|
135
|
+
else hash.update(file).update(await readFile(file))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await visit(directory)
|
|
139
|
+
return hash.digest('hex')
|
|
140
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { readFile, readdir, rm } from 'node:fs/promises'
|
|
5
|
+
import { buildCoroutineFixture, coroutineAppId } from '../../../fixtures/coroutine-app/build.js'
|
|
6
|
+
import { CoroutineJourney, fingerprint, type Stop } from './coroutine-support.js'
|
|
7
|
+
import { deviceForwards, reportAfterCleanup } from './coroutine-lifecycle.js'
|
|
8
|
+
|
|
9
|
+
const device = process.env.MCP_COROUTINE_DEVICE
|
|
10
|
+
const source = await readFile(new URL('../../../fixtures/coroutine-app/MainActivity.kt', import.meta.url), 'utf8')
|
|
11
|
+
const hash = (value: string | Uint8Array) => createHash('sha256').update(value).digest('hex')
|
|
12
|
+
|
|
13
|
+
async function requiredStop(journey: CoroutineJourney, marker?: string) {
|
|
14
|
+
const stop = await journey.stop(marker)
|
|
15
|
+
assert.ok(stop, 'Expected debugger stop')
|
|
16
|
+
return stop
|
|
17
|
+
}
|
|
18
|
+
async function guards(journey: CoroutineJourney, stop: Stop, frameId: string) {
|
|
19
|
+
const blocked = await journey.call('tap', { platform: 'android', deviceId: journey.device, x: 5, y: 5 })
|
|
20
|
+
assert.equal(blocked.failure_code, 'DEBUGGER_PAUSED')
|
|
21
|
+
assert.equal(blocked.dispatch_started, false)
|
|
22
|
+
await journey.resume(stop)
|
|
23
|
+
const stale = await journey.call('get_debugger_locals', { stopId: stop.stop_id, frameId })
|
|
24
|
+
assert.equal(stale.error.code, 'DEBUGGER_STALE_REFERENCE')
|
|
25
|
+
}
|
|
26
|
+
async function completion(journey: CoroutineJourney) {
|
|
27
|
+
await journey.attach()
|
|
28
|
+
const beforeBp = await journey.breakpoint('BREAK_BEFORE')
|
|
29
|
+
const afterBp = await journey.breakpoint('BREAK_AFTER')
|
|
30
|
+
const dispatchBp = await journey.breakpoint('BREAK_DISPATCH', '$completeWork$3')
|
|
31
|
+
const helperBp = await journey.breakpoint('BREAK_HELPER')
|
|
32
|
+
const resultBp = await journey.breakpoint('BREAK_RESULT', '$completeWork$3')
|
|
33
|
+
const tap = await journey.tap('Complete run')
|
|
34
|
+
const before = await requiredStop(journey, 'BREAK_BEFORE')
|
|
35
|
+
const values = await journey.inspect(before, 'before suspension')
|
|
36
|
+
assert.equal(values.locals.find(value => value.name === 'known')?.value, '42')
|
|
37
|
+
assert.equal(values.locals.find(value => value.name === 'firstThread')?.value, 'fixture-first')
|
|
38
|
+
await journey.remove(beforeBp)
|
|
39
|
+
await guards(journey, before, values.frameId)
|
|
40
|
+
await tap.done
|
|
41
|
+
await journey.visible('Coroutine waiting')
|
|
42
|
+
await journey.cancelledWait()
|
|
43
|
+
const release = await journey.tap('Release gate')
|
|
44
|
+
const after = await requiredStop(journey, 'BREAK_AFTER')
|
|
45
|
+
const afterValues = await journey.inspect(after, 'after suspension')
|
|
46
|
+
// A spill may not be represented as a shallow local; retain that observation.
|
|
47
|
+
const known = afterValues.locals.find(value => value.name === 'known')
|
|
48
|
+
if (known) assert.equal(known.value, '42')
|
|
49
|
+
await journey.remove(afterBp)
|
|
50
|
+
await journey.resume(after)
|
|
51
|
+
const dispatched = await requiredStop(journey, 'BREAK_DISPATCH')
|
|
52
|
+
const dispatcherValues = await journey.inspect(dispatched, 'second dispatcher')
|
|
53
|
+
assert.equal(dispatcherValues.locals.find(value => value.name === 'dispatcherThread')?.value, 'fixture-second')
|
|
54
|
+
await journey.remove(dispatchBp)
|
|
55
|
+
await journey.resume(dispatched)
|
|
56
|
+
const helper = await requiredStop(journey, 'BREAK_HELPER')
|
|
57
|
+
await journey.inspect(helper, 'nested child helper')
|
|
58
|
+
await journey.remove(helperBp)
|
|
59
|
+
await journey.step(helper, 'into')
|
|
60
|
+
const into = await requiredStop(journey, 'BREAK_INCREMENT')
|
|
61
|
+
await journey.step(into, 'over')
|
|
62
|
+
const over = await requiredStop(journey, 'BREAK_INCREMENT_RETURN')
|
|
63
|
+
await journey.step(over, 'out')
|
|
64
|
+
const out = await requiredStop(journey)
|
|
65
|
+
assert.equal(out.stops[0].location.method, 'helper')
|
|
66
|
+
await journey.resume(out)
|
|
67
|
+
const result = await requiredStop(journey, 'BREAK_RESULT')
|
|
68
|
+
const resultValues = await journey.inspect(result, 'nested result')
|
|
69
|
+
assert.equal(resultValues.locals.find(value => value.name === 'answer')?.value, '44')
|
|
70
|
+
await journey.remove(resultBp)
|
|
71
|
+
await journey.resume(result)
|
|
72
|
+
await release.done
|
|
73
|
+
await journey.detach()
|
|
74
|
+
await journey.visible('Coroutine result 44')
|
|
75
|
+
journey.observations.push({ journey: 'completion, breakpoint workaround, synchronous steps, guards, cancelled wait', status: 'PASS' })
|
|
76
|
+
}
|
|
77
|
+
async function boundaryStep(journey: CoroutineJourney) {
|
|
78
|
+
await journey.attach()
|
|
79
|
+
const bp = await journey.breakpoint('BREAK_SUSPEND')
|
|
80
|
+
const tap = await journey.tap('Complete run')
|
|
81
|
+
const stop = await requiredStop(journey, 'BREAK_SUSPEND')
|
|
82
|
+
await journey.remove(bp)
|
|
83
|
+
await journey.step(stop, 'over')
|
|
84
|
+
const next = await journey.stop(undefined, 3)
|
|
85
|
+
if (next) await journey.inspect(next, 'suspension step observed stop')
|
|
86
|
+
journey.observations.push({ journey: 'suspension boundary step', status: 'PASS', observation: next ?? 'No stop within three one-second polls; no coroutine-following claim' })
|
|
87
|
+
await journey.detach()
|
|
88
|
+
await tap.done
|
|
89
|
+
await journey.visible('Coroutine waiting')
|
|
90
|
+
const release = await journey.tap('Release gate')
|
|
91
|
+
await release.done
|
|
92
|
+
await journey.visible('Coroutine result 44')
|
|
93
|
+
}
|
|
94
|
+
async function cancellation(journey: CoroutineJourney) {
|
|
95
|
+
await journey.attach()
|
|
96
|
+
const bp = await journey.breakpoint('BREAK_CANCEL', '$cancelWork$2$child$1')
|
|
97
|
+
const start = await journey.tap('Cancellation run')
|
|
98
|
+
await start.done
|
|
99
|
+
await journey.visible('Cancellation waiting')
|
|
100
|
+
const cancel = await journey.tap('Cancel job')
|
|
101
|
+
const stop = await requiredStop(journey, 'BREAK_CANCEL')
|
|
102
|
+
await journey.inspect(stop, 'cancelled child finally')
|
|
103
|
+
await journey.remove(bp)
|
|
104
|
+
await journey.resume(stop)
|
|
105
|
+
await cancel.done
|
|
106
|
+
await journey.detach()
|
|
107
|
+
await journey.visible('Coroutine cancelled')
|
|
108
|
+
journey.observations.push({ journey: 'job cancellation, child finally, cancelAndJoin and recovery', status: 'PASS' })
|
|
109
|
+
}
|
|
110
|
+
async function evidence(directory: string, artifact: string, journey: CoroutineJourney) {
|
|
111
|
+
const javap = process.env.JAVA_HOME ? process.env.JAVA_HOME + '/bin/javap' : 'javap'
|
|
112
|
+
const classes = (await readdir(directory + '/classes/dev/mobiledebugmcp/coroutinefixture')).filter(file => file.endsWith('.class')).sort()
|
|
113
|
+
const metadata = execFileSync(javap, ['-p', '-c', '-l', '-classpath', directory + '/classes', ...classes.map(file => coroutineAppId + '.' + file.slice(0, -6))], { encoding: 'utf8', timeout: 15000 })
|
|
114
|
+
for (const name of ['known', 'after', 'answer']) assert.ok(metadata.includes(name), 'Compiler variable metadata: ' + name)
|
|
115
|
+
for (const marker of ['BREAK_BEFORE', 'BREAK_AFTER', 'BREAK_DISPATCH', 'BREAK_CANCEL']) assert.ok(metadata.includes('line ' + journey.line(marker) + ':'), marker)
|
|
116
|
+
const version = spawnSync(process.env.MCP_KOTLINC ?? 'kotlinc', ['-version'], { encoding: 'utf8', timeout: 15000 })
|
|
117
|
+
assert.equal(version.status, 0, 'Kotlin compiler version probe')
|
|
118
|
+
const compiler = (version.stdout + version.stderr).trim()
|
|
119
|
+
const dependencies = []
|
|
120
|
+
for (const name of ['MCP_KOTLIN_STDLIB', 'MCP_COROUTINES_CORE']) dependencies.push({ name, file: process.env[name], sha256: hash(await readFile(process.env[name]!)) })
|
|
121
|
+
return { source_sha256: hash(source), apk_sha256: hash(await readFile(artifact)), dist_sha256: await fingerprint('dist'), worker_sha256: await fingerprint('src/debugger/java'), compiler, dependencies, metadata_sha256: hash(metadata), compiler_metadata: metadata }
|
|
122
|
+
}
|
|
123
|
+
async function run(deviceId: string) {
|
|
124
|
+
const journey = new CoroutineJourney(deviceId, source)
|
|
125
|
+
assert.equal(journey.adb('shell', 'pm', 'list', 'packages', coroutineAppId).trim(), '', 'Refusing to replace pre-existing fixture package')
|
|
126
|
+
let fixture: ReturnType<typeof buildCoroutineFixture> | undefined
|
|
127
|
+
let installed = false
|
|
128
|
+
const interrupt = () => journey.interruption.abort()
|
|
129
|
+
process.once('SIGINT', interrupt)
|
|
130
|
+
process.once('SIGTERM', interrupt)
|
|
131
|
+
const forwards = () => deviceForwards(journey.adb('forward', '--list'), deviceId)
|
|
132
|
+
const baselineForwards = forwards()
|
|
133
|
+
await reportAfterCleanup(async () => {
|
|
134
|
+
try { fixture = buildCoroutineFixture() } catch (error) {
|
|
135
|
+
console.error('BLOCKED coroutine fixture build: check documented local compiler, jars and SDK prerequisites')
|
|
136
|
+
throw error
|
|
137
|
+
}
|
|
138
|
+
const fingerprints = await evidence(fixture.directory, fixture.artifact, journey)
|
|
139
|
+
await journey.connect()
|
|
140
|
+
installed = true // A failed install may have left a partial owned package.
|
|
141
|
+
journey.adb('install', fixture.artifact)
|
|
142
|
+
journey.adb('shell', 'am', 'start', '-W', '-n', coroutineAppId + '/.MainActivity')
|
|
143
|
+
await journey.visible('Coroutine ready')
|
|
144
|
+
await completion(journey)
|
|
145
|
+
await boundaryStep(journey)
|
|
146
|
+
journey.adb('shell', 'am', 'force-stop', coroutineAppId)
|
|
147
|
+
journey.adb('shell', 'am', 'start', '-W', '-n', coroutineAppId + '/.MainActivity')
|
|
148
|
+
await journey.visible('Coroutine ready')
|
|
149
|
+
await cancellation(journey)
|
|
150
|
+
await journey.detach()
|
|
151
|
+
assert.deepEqual(forwards(), baselineForwards)
|
|
152
|
+
return { api: journey.adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, ...fingerprints }
|
|
153
|
+
}, async () => {
|
|
154
|
+
process.removeListener('SIGINT', interrupt)
|
|
155
|
+
process.removeListener('SIGTERM', interrupt)
|
|
156
|
+
try { await journey.detach() } finally {
|
|
157
|
+
try { await journey.client.close() } finally {
|
|
158
|
+
try { if (installed) journey.adb('uninstall', coroutineAppId) } finally { if (fixture) await rm(fixture.directory, { recursive: true, force: true }) }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
assert.deepEqual(forwards(), baselineForwards, 'Owned forward cleanup')
|
|
162
|
+
}, (status, fingerprints, error) => {
|
|
163
|
+
const record = JSON.stringify({ status, device: deviceId, ...fingerprints, observations: journey.observations, ...(error ? { error: error instanceof Error ? error.message : String(error) } : {}) })
|
|
164
|
+
if (status === 'PASS') console.log(record)
|
|
165
|
+
else console.error(record)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
if (!device) console.log('UNRUN coroutine debugger fixture: set MCP_COROUTINE_DEVICE, MCP_KOTLINC, MCP_KOTLIN_STDLIB and MCP_COROUTINES_CORE')
|
|
169
|
+
else await run(device)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync, spawn, spawnSync } from 'node:child_process'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { readFile, readdir, rm } from 'node:fs/promises'
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
7
|
+
import { buildDebuggerFixture, debuggerAppId } from '../../../fixtures/debugger-app/build.js'
|
|
8
|
+
|
|
9
|
+
const deviceId = process.env.MCP_KOTLIN_DEVICE
|
|
10
|
+
if (!deviceId) {
|
|
11
|
+
console.log('UNRUN Kotlin debugger fixture: set MCP_KOTLIN_DEVICE, MCP_KOTLINC and MCP_KOTLIN_STDLIB')
|
|
12
|
+
} else {
|
|
13
|
+
const fixture = buildDebuggerFixture(true)
|
|
14
|
+
const adbPath = process.env.ADB_PATH ?? 'adb'
|
|
15
|
+
const adb = (...args: string[]) => execFileSync(adbPath, ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
|
|
16
|
+
const client = new Client({ name: 'kotlin-locals-validation', version: '1.0.0' })
|
|
17
|
+
let debugSessionId = ''
|
|
18
|
+
let cursor = 0
|
|
19
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
20
|
+
const reply = await client.callTool({ name, arguments: args }, undefined, { timeout: 45000 })
|
|
21
|
+
const text = reply.content.find(item => item.type === 'text')
|
|
22
|
+
assert.ok(text?.type === 'text')
|
|
23
|
+
return JSON.parse(text.text)
|
|
24
|
+
}
|
|
25
|
+
const source = await readFile(new URL('../../../fixtures/debugger-app/MainActivity.kt', import.meta.url))
|
|
26
|
+
const line = (marker: string) => source.toString().split('\n').findIndex(row => row.includes(marker)) + 1
|
|
27
|
+
const hash = createHash('sha256')
|
|
28
|
+
const fingerprint = async (directory: string): Promise<void> => {
|
|
29
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
30
|
+
const file = directory + '/' + entry.name
|
|
31
|
+
if (entry.isDirectory()) await fingerprint(file)
|
|
32
|
+
else hash.update(file).update(await readFile(file))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const waitStop = async (expectedLine: number) => {
|
|
36
|
+
for (let i = 0; i < 10; i++) {
|
|
37
|
+
const response = await call('wait_for_debugger_event', { debugSessionId, afterSequence: cursor, timeoutMs: 1000, responseMode: 'compact' })
|
|
38
|
+
assert.equal(response.overflow, false)
|
|
39
|
+
cursor = response.next_cursor
|
|
40
|
+
const event = response.events.find((item: any) => item.event === 'stopped')
|
|
41
|
+
if (event) {
|
|
42
|
+
assert.equal(event.stops[0].location.line, expectedLine)
|
|
43
|
+
assert.equal(event.stops[0].location.method, 'calculate')
|
|
44
|
+
assert.equal(event.stops[0].location.source, 'MainActivity.kt')
|
|
45
|
+
return event
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Expected Kotlin source stop not observed')
|
|
49
|
+
}
|
|
50
|
+
const inspect = async (stop: any) => {
|
|
51
|
+
const stack = await call('get_debugger_stack', { debugSessionId, stopId: stop.stop_id, threadId: stop.thread_id, responseMode: 'compact' })
|
|
52
|
+
const frameId = stack.result.frames[0].frame_id
|
|
53
|
+
const locals = await call('get_debugger_locals', { debugSessionId, stopId: stop.stop_id, frameId, responseMode: 'compact' })
|
|
54
|
+
assert.equal(locals.success, true)
|
|
55
|
+
return { locals: locals.result.locals as { name: string; value: string }[], frameId, availability: locals.result.availability }
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const forwardsBefore = adb('forward', '--list').trim()
|
|
59
|
+
const javap = process.env.JAVA_HOME ? process.env.JAVA_HOME + '/bin/javap' : 'javap'
|
|
60
|
+
const bytecode = execFileSync(javap, ['-p', '-c', '-l', '-classpath', fixture.directory + '/classes', `${debuggerAppId}.MainActivity`], { encoding: 'utf8' })
|
|
61
|
+
const scopes = Object.fromEntries(['seed', 'known', 'answer'].map(name => {
|
|
62
|
+
const match = bytecode.match(new RegExp('\\s+(\\d+)\\s+(\\d+)\\s+\\d+\\s+' + name + '\\s+I'))
|
|
63
|
+
assert.ok(match, 'Compiler emits a variable scope for ' + name)
|
|
64
|
+
return [name, { start: Number(match[1]), length: Number(match[2]) }]
|
|
65
|
+
}))
|
|
66
|
+
assert.equal(scopes.seed.start, 0)
|
|
67
|
+
assert.ok(scopes.known.start > 0 && scopes.answer.start > scopes.known.start)
|
|
68
|
+
await fingerprint('dist'); await fingerprint('src/debugger/java')
|
|
69
|
+
const version = spawnSync(process.env.MCP_KOTLINC ?? 'kotlinc', ['-version'], { encoding: 'utf8' })
|
|
70
|
+
assert.equal(version.status, 0)
|
|
71
|
+
const compiler = (version.stdout + version.stderr).match(/kotlinc-jvm [^\r\n]+/)?.[0]
|
|
72
|
+
assert.ok(compiler, 'Kotlin compiler identifies its version')
|
|
73
|
+
await client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
|
|
74
|
+
adb('install', '-r', fixture.artifact)
|
|
75
|
+
adb('shell', 'am', 'start', '-W', '-n', `${debuggerAppId}/.MainActivity`)
|
|
76
|
+
const ready = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
77
|
+
const button = ready.elements.find((element: any) => /^kotlin calculate$/i.test(element.text ?? ''))
|
|
78
|
+
assert.ok(button, 'Fresh UI identifies the isolated Kotlin calculation: ' + JSON.stringify(ready))
|
|
79
|
+
const attached = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
|
|
80
|
+
debugSessionId = attached.debug_session_id ?? ''
|
|
81
|
+
assert.equal(attached.success, true, JSON.stringify(attached))
|
|
82
|
+
const breakpoints = []
|
|
83
|
+
for (const marker of ['BREAK_KOTLIN_BEFORE', 'BREAK_KOTLIN_KNOWN', 'BREAK_KOTLIN_RESULT']) {
|
|
84
|
+
const breakpoint = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: line(marker), responseMode: 'compact' })
|
|
85
|
+
assert.equal(breakpoint.result.status, 'verified', JSON.stringify(breakpoint))
|
|
86
|
+
breakpoints.push(breakpoint.result.breakpoint_id)
|
|
87
|
+
}
|
|
88
|
+
const bounds = button.bounds
|
|
89
|
+
const tap = spawn(adbPath, ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((bounds[0] + bounds[2]) / 2)), String(Math.floor((bounds[1] + bounds[3]) / 2))], { stdio: 'ignore' })
|
|
90
|
+
const tapDone = new Promise<void>((resolve, reject) => { tap.on('error', reject); tap.on('exit', code => code === 0 ? resolve() : reject(new Error('Fixture tap failed'))) })
|
|
91
|
+
void tapDone.catch(() => {})
|
|
92
|
+
const before = await waitStop(line('BREAK_KOTLIN_BEFORE'))
|
|
93
|
+
const beforeValues = await inspect(before)
|
|
94
|
+
assert.equal(beforeValues.locals.find(value => value.name === 'seed')?.value, '7')
|
|
95
|
+
assert.equal(beforeValues.locals.find(value => value.name === 'known'), undefined, 'Local is not initialized/in scope at its assignment')
|
|
96
|
+
await call('resume_debugger', { debugSessionId, stopId: before.stop_id, responseMode: 'compact' })
|
|
97
|
+
const known = await waitStop(line('BREAK_KOTLIN_KNOWN'))
|
|
98
|
+
const knownValues = await inspect(known)
|
|
99
|
+
assert.equal(knownValues.locals.find(value => value.name === 'known')?.value, '42')
|
|
100
|
+
assert.equal(knownValues.locals.find(value => value.name === 'answer'), undefined)
|
|
101
|
+
const stale = await call('get_debugger_locals', { debugSessionId, stopId: before.stop_id, frameId: beforeValues.frameId, responseMode: 'compact' })
|
|
102
|
+
assert.equal(stale.error.code, 'DEBUGGER_STALE_REFERENCE')
|
|
103
|
+
await call('resume_debugger', { debugSessionId, stopId: known.stop_id, responseMode: 'compact' })
|
|
104
|
+
const result = await waitStop(line('BREAK_KOTLIN_RESULT'))
|
|
105
|
+
const resultValues = await inspect(result)
|
|
106
|
+
assert.equal(resultValues.locals.find(value => value.name === 'known')?.value, '42')
|
|
107
|
+
assert.equal(resultValues.locals.find(value => value.name === 'answer')?.value, '43')
|
|
108
|
+
for (const breakpointId of breakpoints) await call('remove_debugger_breakpoint', { debugSessionId, breakpointId, responseMode: 'compact' })
|
|
109
|
+
await call('resume_debugger', { debugSessionId, stopId: result.stop_id, responseMode: 'compact' })
|
|
110
|
+
await tapDone
|
|
111
|
+
const detached = await call('detach_debugger', { debugSessionId, responseMode: 'compact' })
|
|
112
|
+
assert.equal(detached.cleanup.status, 'complete')
|
|
113
|
+
assert.deepEqual(detached.cleanup.remaining_resources, [])
|
|
114
|
+
assert.equal(adb('forward', '--list').trim(), forwardsBefore)
|
|
115
|
+
const recovered = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
116
|
+
assert.ok(recovered.elements.some((element: any) => element.text === 'Kotlin result 43'), 'Fresh hierarchy proves execution recovery')
|
|
117
|
+
console.log(JSON.stringify({ status: 'PASS', server_sha256: hash.digest('hex'), source_sha256: createHash('sha256').update(source).digest('hex'), apk_sha256: createHash('sha256').update(await readFile(fixture.artifact)).digest('hex'), compiler, bytecode_scopes: scopes, device: deviceId, api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, lines: [line('BREAK_KOTLIN_BEFORE'), line('BREAK_KOTLIN_KNOWN'), line('BREAK_KOTLIN_RESULT')], observations: ['seed=7; known absent before assignment', 'known=42; answer absent before assignment', 'known=42; answer=43', 'old frame rejected as stale', 'fresh UI Kotlin result 43 after detach'], cleanup: 'owned forwards removed; fixture package and temporary files removed in finally' }))
|
|
118
|
+
} finally {
|
|
119
|
+
if (debugSessionId) {
|
|
120
|
+
const cleanup = await call('detach_debugger', { debugSessionId }).catch(() => null)
|
|
121
|
+
if (cleanup?.cleanup.status !== 'complete') console.error('UNRESOLVED Kotlin debugger cleanup', JSON.stringify(cleanup?.cleanup ?? null))
|
|
122
|
+
}
|
|
123
|
+
try { await client.close() } finally {
|
|
124
|
+
try { adb('uninstall', debuggerAppId) } finally { await rm(fixture.directory, { recursive: true, force: true }) }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { debuggerResponse } from '../../../../src/server/handlers/debugger-response.js'
|
|
2
|
+
import { debuggerContracts } from '../../../../src/server/schemas/debugger-schema.js'
|
|
1
3
|
import assert from 'node:assert/strict'
|
|
2
4
|
import { execFileSync, spawn } from 'node:child_process'
|
|
3
5
|
import { readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
@@ -14,10 +16,23 @@ if (!process.env.MCP_DEBUGGER_DEVICE) {
|
|
|
14
16
|
const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
|
|
15
17
|
const client = new Client({ name: 'android-debugger-smoke', version: '1.0.0' })
|
|
16
18
|
const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
|
|
19
|
+
const compactMode = process.env.MCP_DEBUGGER_RESPONSE_MODE === 'compact'
|
|
20
|
+
const measurements: { tool: string; debug_bytes: number; compact_bytes: number }[] = []
|
|
17
21
|
const call = async (name: string, args: Record<string, unknown>) => {
|
|
18
|
-
const response = await client.callTool({ name, arguments: args }, undefined, { timeout: 45000 })
|
|
22
|
+
const response = await client.callTool({ name, arguments: { ...args, ...(debuggerContracts[name] && compactMode ? { responseMode: 'compact' } : {}) } }, undefined, { timeout: 45000 })
|
|
19
23
|
const text = (response.content as any[]).find(c => c.type === 'text')!.text!
|
|
20
|
-
|
|
24
|
+
const payload = JSON.parse(text)
|
|
25
|
+
if (debuggerContracts[name]) {
|
|
26
|
+
const command = debuggerContracts[name].command
|
|
27
|
+
if (compactMode && !['attach', 'state', 'capabilities'].includes(command) && payload.debug_session_id != null) {
|
|
28
|
+
for (const field of ['device', 'target', 'capabilities']) assert.ok(!Object.hasOwn(payload, field))
|
|
29
|
+
}
|
|
30
|
+
if (!compactMode) {
|
|
31
|
+
const compact = debuggerResponse(command, 'compact', payload)
|
|
32
|
+
measurements.push({ tool: name, debug_bytes: Buffer.byteLength(JSON.stringify(response)), compact_bytes: Buffer.byteLength(JSON.stringify({ ...response, content: [{ type: 'text', text: JSON.stringify(compact, null, 2) }] })) })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return payload
|
|
21
36
|
}
|
|
22
37
|
const hash = createHash('sha256')
|
|
23
38
|
const fingerprint = async (directory: string): Promise<void> => {
|
|
@@ -27,7 +42,7 @@ if (!process.env.MCP_DEBUGGER_DEVICE) {
|
|
|
27
42
|
else hash.update(file).update(await readFile(file))
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
|
-
await fingerprint('dist'); await fingerprint('
|
|
45
|
+
await fingerprint('dist'); await fingerprint('src/debugger/java')
|
|
31
46
|
const buildHash = hash.digest('hex')
|
|
32
47
|
const fixture = buildDebuggerFixture()
|
|
33
48
|
const source = await readFile(new URL('../../../fixtures/debugger-app/MainActivity.java', import.meta.url), 'utf8')
|
|
@@ -193,6 +208,7 @@ if (!process.env.MCP_DEBUGGER_DEVICE) {
|
|
|
193
208
|
assert.ok(treeAfterShutdown.elements.some((e: any) => e.text === 'Result 43'), 'fresh UI proves paused fixture resumed after server shutdown')
|
|
194
209
|
} finally { await verifier.close() }
|
|
195
210
|
console.log(JSON.stringify({ status: 'PASS', build_sha256: buildHash, device: deviceId, api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, transport: 'MCP stdio', extended: process.env.MCP_DEBUGGER_EXTENDED === '1' }))
|
|
211
|
+
console.log('Debugger response measurement', JSON.stringify({ mode: compactMode ? 'compact' : 'debug', calls: measurements, debug_bytes: measurements.reduce((sum, row) => sum + row.debug_bytes, 0), compact_bytes: measurements.reduce((sum, row) => sum + row.compact_bytes, 0) }))
|
|
196
212
|
console.log('PASS Android debugger: explicit attach, source stop, stack/known locals, pause guards, into/over/out, stale frame, resume UI, detach/forward ownership')
|
|
197
213
|
} finally {
|
|
198
214
|
if (debugSessionId) await call('detach_debugger', { debugSessionId }).catch(() => {})
|