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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package dev.mobiledebugmcp.coroutinefixture
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.widget.Button
|
|
6
|
+
import android.widget.LinearLayout
|
|
7
|
+
import android.widget.TextView
|
|
8
|
+
import java.util.concurrent.Executors
|
|
9
|
+
import kotlinx.coroutines.*
|
|
10
|
+
|
|
11
|
+
class MainActivity : Activity() {
|
|
12
|
+
private val first = Executors.newSingleThreadExecutor { Thread(it, "fixture-first") }.asCoroutineDispatcher()
|
|
13
|
+
private val second = Executors.newSingleThreadExecutor { Thread(it, "fixture-second") }.asCoroutineDispatcher()
|
|
14
|
+
private val scope = CoroutineScope(SupervisorJob() + first)
|
|
15
|
+
private var gate = CompletableDeferred<Unit>()
|
|
16
|
+
private var job: Job? = null
|
|
17
|
+
private lateinit var status: TextView
|
|
18
|
+
|
|
19
|
+
override fun onCreate(saved: Bundle?) {
|
|
20
|
+
super.onCreate(saved)
|
|
21
|
+
val layout = LinearLayout(this)
|
|
22
|
+
layout.orientation = LinearLayout.VERTICAL
|
|
23
|
+
layout.setPadding(0, 160, 0, 0)
|
|
24
|
+
status = TextView(this)
|
|
25
|
+
status.text = "Coroutine ready"
|
|
26
|
+
layout.addView(status)
|
|
27
|
+
fun button(label: String, action: () -> Unit) {
|
|
28
|
+
val button = Button(this)
|
|
29
|
+
button.text = label
|
|
30
|
+
button.setOnClickListener { action() }
|
|
31
|
+
layout.addView(button)
|
|
32
|
+
}
|
|
33
|
+
button("Complete run") { start(false) }
|
|
34
|
+
button("Cancellation run") { start(true) }
|
|
35
|
+
button("Release gate") { gate.complete(Unit) }
|
|
36
|
+
button("Cancel job") { job?.cancel() }
|
|
37
|
+
setContentView(layout)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private fun show(text: String) { runOnUiThread { status.text = text } }
|
|
41
|
+
|
|
42
|
+
private fun start(cancel: Boolean) {
|
|
43
|
+
if (job?.isActive == true) { show("Coroutine busy"); return }
|
|
44
|
+
gate = CompletableDeferred()
|
|
45
|
+
job = scope.launch {
|
|
46
|
+
if (cancel) cancelWork() else completeWork()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private suspend fun completeWork() {
|
|
51
|
+
val firstThread = Thread.currentThread().name
|
|
52
|
+
val known = 42
|
|
53
|
+
check(firstThread == "fixture-first") // BREAK_BEFORE
|
|
54
|
+
// Queued on the same single-thread executor: runs only after gate.await yields.
|
|
55
|
+
first.executor.execute { show("Coroutine waiting") }
|
|
56
|
+
gate.await() // BREAK_SUSPEND
|
|
57
|
+
val after = known + 1 // BREAK_AFTER
|
|
58
|
+
withContext(second) {
|
|
59
|
+
val dispatcherThread = Thread.currentThread().name
|
|
60
|
+
val nested = async { helper(after) } // BREAK_DISPATCH
|
|
61
|
+
check(dispatcherThread == "fixture-second")
|
|
62
|
+
val answer = nested.await()
|
|
63
|
+
show("Coroutine result $answer") // BREAK_RESULT
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private fun helper(value: Int): Int {
|
|
68
|
+
val result = increment(value) // BREAK_HELPER
|
|
69
|
+
return result // BREAK_HELPER_RETURN
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun increment(value: Int): Int {
|
|
73
|
+
val result = value + 1 // BREAK_INCREMENT
|
|
74
|
+
return result // BREAK_INCREMENT_RETURN
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private suspend fun cancelWork() = coroutineScope {
|
|
78
|
+
val child = launch(second) {
|
|
79
|
+
try {
|
|
80
|
+
// The child must yield at gate.await before this queued work can run.
|
|
81
|
+
second.executor.execute { show("Cancellation waiting") }
|
|
82
|
+
gate.await()
|
|
83
|
+
error("Cancelled child must not cross gate")
|
|
84
|
+
} finally {
|
|
85
|
+
show("Cancellation finally") // BREAK_CANCEL
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try { child.join() } finally {
|
|
89
|
+
withContext(NonCancellable) { child.cancelAndJoin() }
|
|
90
|
+
show("Coroutine cancelled")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override fun onDestroy() {
|
|
95
|
+
scope.cancel()
|
|
96
|
+
first.close()
|
|
97
|
+
second.close()
|
|
98
|
+
super.onDestroy()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Isolated Android coroutine debugger fixture
|
|
2
|
+
|
|
3
|
+
Separate disposable package `dev.mobiledebugmcp.coroutinefixture`; the runner
|
|
4
|
+
refuses to replace an existing installation. No server dependency, debug agent,
|
|
5
|
+
automatic download or production app instrumentation is introduced.
|
|
6
|
+
|
|
7
|
+
Requires Node 22+, JDK 17, Android SDK platform/build-tools 36, Kotlin JVM compiler
|
|
8
|
+
and matching stdlib 2.0.21, and kotlinx-coroutines-core-jvm 1.9.0. Provide local
|
|
9
|
+
paths explicitly:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
export JAVA_HOME=/path/to/jdk-17
|
|
13
|
+
export MCP_KOTLINC=/path/to/kotlin/bin/kotlinc
|
|
14
|
+
export MCP_KOTLIN_STDLIB=/path/to/kotlin/lib/kotlin-stdlib.jar
|
|
15
|
+
export MCP_COROUTINES_CORE=/path/to/kotlinx-coroutines-core-jvm-1.9.0.jar
|
|
16
|
+
npm run build
|
|
17
|
+
MCP_COROUTINE_DEVICE=emulator-5554 npx tsx test/device/automated/debugger/coroutine.android.test.ts
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`npm run test:device` discovers the runner and reports **UNRUN** unless
|
|
21
|
+
`MCP_COROUTINE_DEVICE` is set. A configured run with missing prerequisites fails;
|
|
22
|
+
it cannot silently pass. Do not run other fixture journeys concurrently on the
|
|
23
|
+
same serial. A physical device must be selected explicitly and reported separately.
|
|
24
|
+
|
|
25
|
+
The source owns two named single-thread dispatcher executors, an explicit gate,
|
|
26
|
+
nested async work and a cancellation/finally path. Readiness is posted by work
|
|
27
|
+
queued on the same single-thread executor behind the coroutine, so the runner
|
|
28
|
+
releases or cancels only after the gate wait has yielded. UI controls release the
|
|
29
|
+
gate only after debugger resume. Compiler line/variable tables are captured for generated
|
|
30
|
+
continuation classes as well as ordinary methods. The assessment compares existing
|
|
31
|
+
MCP stops, thread stacks and shallow locals; none represents a reconstructed
|
|
32
|
+
logical coroutine stack. A bounded step attempt is recorded as an observation,
|
|
33
|
+
not a promise to follow work across suspension. Source breakpoints are tested
|
|
34
|
+
independently as a workaround.
|
|
35
|
+
|
|
36
|
+
The runner checks pause guards, old frame rejection, event-wait cancellation,
|
|
37
|
+
completion/cancellation UI recovery and owned ADB forward cleanup. Finally it
|
|
38
|
+
removes its package and temporary build/signing output. The overall JSON PASS is
|
|
39
|
+
emitted only after cleanup; cleanup failures emit JSON FAIL and reject the run.
|
|
40
|
+
Forward comparisons filter the selected serial and sort entries, so another
|
|
41
|
+
device changing its forwards does not fail this journey. SIGKILL/host loss cannot
|
|
42
|
+
execute finally; inspect outstanding sessions/packages manually after such loss.
|
|
43
|
+
Local output includes synthetic fixture values, source/APK/server/dependency hashes
|
|
44
|
+
and compiler metadata. Redirect stdout/stderr to an intentional local evidence
|
|
45
|
+
file; no data is uploaded automatically.
|
|
46
|
+
|
|
47
|
+
See the [accepted specification](../../../docs/specs/030-android-coroutine-validation/spec.md)
|
|
48
|
+
and [implementation/verification record](../../../docs/specs/030-android-coroutine-validation/plan.md).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
export const coroutineAppId = 'dev.mobiledebugmcp.coroutinefixture'
|
|
7
|
+
export function buildCoroutineFixture() {
|
|
8
|
+
if (!process.env.MCP_COROUTINES_CORE) throw new Error('MCP_COROUTINES_CORE must identify kotlinx-coroutines-core-jvm.jar')
|
|
9
|
+
if (!process.env.MCP_KOTLIN_STDLIB) throw new Error('MCP_KOTLIN_STDLIB must identify the matching Kotlin standard-library jar')
|
|
10
|
+
const source = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const directory = mkdtempSync(path.join(tmpdir(), 'mcp-coroutine-fixture-'))
|
|
12
|
+
const java = process.env.JAVA_HOME ?? (existsSync('/Applications/Android Studio.app/Contents/jbr/Contents/Home') ? '/Applications/Android Studio.app/Contents/jbr/Contents/Home' : undefined)
|
|
13
|
+
const run = (cmd: string, args: string[]) => execFileSync(cmd, args, { cwd: directory, encoding: 'utf8', stdio: 'pipe', timeout: 120000 })
|
|
14
|
+
const sdk = process.env.ANDROID_HOME ?? path.join(process.env.HOME!, 'Library/Android/sdk')
|
|
15
|
+
const tools = path.join(sdk, 'build-tools/36.0.0')
|
|
16
|
+
const androidJar = path.join(sdk, 'platforms/android-36/android.jar')
|
|
17
|
+
const jdk = (name: string) => java ? path.join(java, 'bin', name) : name
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(path.join(directory, 'classes'))
|
|
20
|
+
writeFileSync(path.join(directory, 'AndroidManifest.xml'), `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${coroutineAppId}" android:versionCode="1" android:versionName="1.0"><uses-sdk android:minSdkVersion="26" android:targetSdkVersion="36"/><application android:debuggable="true" android:label="Debugger fixture" android:theme="@android:style/Theme.Material.Light.NoActionBar"><activity android:name=".MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application></manifest>`)
|
|
21
|
+
run(process.env.MCP_KOTLINC ?? 'kotlinc', ['-no-stdlib', '-no-reflect', '-jvm-target', '1.8', '-classpath', [androidJar, process.env.MCP_KOTLIN_STDLIB!, process.env.MCP_COROUTINES_CORE!].join(path.delimiter), '-d', 'classes', path.join(source, 'MainActivity.kt')])
|
|
22
|
+
run(jdk('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
|
|
23
|
+
run(path.join(tools, 'd8'), ['--min-api', '26', '--lib', androidJar, '--output', directory, 'classes.jar', process.env.MCP_KOTLIN_STDLIB!, process.env.MCP_COROUTINES_CORE!])
|
|
24
|
+
run(path.join(tools, 'aapt2'), ['link', '-I', androidJar, '--manifest', 'AndroidManifest.xml', '-o', 'unsigned.apk'])
|
|
25
|
+
run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
|
|
26
|
+
run(path.join(tools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
|
|
27
|
+
run(jdk('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Readiness Fixture', '-keyalg', 'RSA', '-validity', '365'])
|
|
28
|
+
const artifact = path.join(directory, 'Debugger.apk')
|
|
29
|
+
run(path.join(tools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', artifact, 'aligned.apk'])
|
|
30
|
+
return { artifact, directory }
|
|
31
|
+
} catch (error) {
|
|
32
|
+
rmSync(directory, { recursive: true, force: true })
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -23,8 +23,29 @@ public class MainActivity extends Activity {
|
|
|
23
23
|
try { Class.forName("dev.mobiledebugmcp.debugfixture.NoLocals").getDeclaredMethod("work").invoke(null); }
|
|
24
24
|
catch (Exception e) { status.setText("Metadata failed"); }
|
|
25
25
|
}); layout.addView(noLocals);
|
|
26
|
+
addLevelControl(layout);
|
|
26
27
|
setContentView(layout);
|
|
27
28
|
}
|
|
29
|
+
void addLevelControl(LinearLayout layout) {
|
|
30
|
+
TextView level = new TextView(this);
|
|
31
|
+
level.setGravity(android.view.Gravity.CENTER);
|
|
32
|
+
level.setText("25 %"); layout.addView(level);
|
|
33
|
+
android.widget.SeekBar slider = new android.widget.SeekBar(this);
|
|
34
|
+
slider.setMax(100); slider.setProgress(25);
|
|
35
|
+
slider.setContentDescription("Fixture level");
|
|
36
|
+
slider.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener() {
|
|
37
|
+
public void onProgressChanged(android.widget.SeekBar view, int progress, boolean fromUser) { level.setText(progress + " %"); }
|
|
38
|
+
public void onStartTrackingTouch(android.widget.SeekBar view) {}
|
|
39
|
+
public void onStopTrackingTouch(android.widget.SeekBar view) {}
|
|
40
|
+
});
|
|
41
|
+
layout.addView(slider);
|
|
42
|
+
LinearLayout range = new LinearLayout(this);
|
|
43
|
+
TextView minimum = new TextView(this); minimum.setText("0 %");
|
|
44
|
+
TextView maximum = new TextView(this); maximum.setText("100 %"); maximum.setGravity(android.view.Gravity.RIGHT);
|
|
45
|
+
range.addView(minimum, new LinearLayout.LayoutParams(0, 80, 1));
|
|
46
|
+
range.addView(maximum, new LinearLayout.LayoutParams(0, 80, 1));
|
|
47
|
+
layout.addView(range);
|
|
48
|
+
}
|
|
28
49
|
void calculate(int seed) {
|
|
29
50
|
int known = seed + 35;
|
|
30
51
|
String longText = new String(new char[300]).replace('\0', 'x');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package dev.mobiledebugmcp.debugfixture
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.widget.Button
|
|
6
|
+
import android.widget.LinearLayout
|
|
7
|
+
import android.widget.TextView
|
|
8
|
+
|
|
9
|
+
class MainActivity : Activity() {
|
|
10
|
+
private lateinit var status: TextView
|
|
11
|
+
|
|
12
|
+
override fun onCreate(saved: Bundle?) {
|
|
13
|
+
super.onCreate(saved)
|
|
14
|
+
val layout = LinearLayout(this)
|
|
15
|
+
layout.orientation = LinearLayout.VERTICAL
|
|
16
|
+
layout.setPadding(0, 160, 0, 0)
|
|
17
|
+
status = TextView(this)
|
|
18
|
+
status.text = "Kotlin ready"
|
|
19
|
+
layout.addView(status)
|
|
20
|
+
val button = Button(this)
|
|
21
|
+
button.text = "Kotlin calculate"
|
|
22
|
+
button.setOnClickListener { calculate(7) }
|
|
23
|
+
layout.addView(button)
|
|
24
|
+
setContentView(layout)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private fun calculate(seed: Int) {
|
|
28
|
+
val known = seed + 35 // BREAK_KOTLIN_BEFORE
|
|
29
|
+
val answer = known + 1 // BREAK_KOTLIN_KNOWN
|
|
30
|
+
status.text = "Kotlin result $answer" // BREAK_KOTLIN_RESULT
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Isolated Android debugger fixture
|
|
2
2
|
|
|
3
|
+
[Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
|
|
4
|
+
|
|
3
5
|
Package `dev.mobiledebugmcp.debugfixture`, built with `android:debuggable=true`
|
|
4
6
|
and Java source/line/local metadata. Calculate exposes `known=42`, an increment
|
|
5
7
|
call for into/over/out stepping, and a visible `Result 43` after resume. Late class
|
|
@@ -11,3 +13,53 @@ Build helper uses SDK platform/build-tools 36, the existing direct javac/D8/aapt
|
|
|
11
13
|
pattern, ephemeral signing material and temporary output. It does not modify other
|
|
12
14
|
apps or display settings. The automated journey installs/removes only this fixture
|
|
13
15
|
and checks session-owned ADB forwards. See [debugger tools](../../../docs/tools/debugger.md).
|
|
16
|
+
|
|
17
|
+
## Kotlin locals validation
|
|
18
|
+
|
|
19
|
+
The opt-in Kotlin variant compiles `MainActivity.kt` instead of the Java sources
|
|
20
|
+
into the same disposable package. Do not run the variants concurrently on one
|
|
21
|
+
device. It requires a Kotlin JVM compiler (`MCP_KOTLINC`, or `kotlinc` on PATH),
|
|
22
|
+
the matching standard-library jar (`MCP_KOTLIN_STDLIB`), and a compatible JDK.
|
|
23
|
+
Kotlin 2.0.21 with JDK 17 was used for the recorded validation. The fixture does
|
|
24
|
+
not add Gradle, download a compiler, or change the server's dependencies.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm run build
|
|
28
|
+
export JAVA_HOME=/path/to/jdk-17
|
|
29
|
+
export MCP_KOTLINC=/path/to/kotlin/bin/kotlinc
|
|
30
|
+
export MCP_KOTLIN_STDLIB=/path/to/kotlin/lib/kotlin-stdlib.jar
|
|
31
|
+
MCP_KOTLIN_DEVICE=emulator-5554 npx tsx test/device/automated/debugger/kotlin.android.test.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The compiler retains its default line/local metadata; D8 runs without shrinking
|
|
35
|
+
or obfuscation. The runner checks the class-file local-variable scope tables,
|
|
36
|
+
then reads `seed=7` before `known` is initialized, `known=42` before `answer` is
|
|
37
|
+
initialized, and `answer=43` afterward through MCP. Old references must fail as
|
|
38
|
+
stale. Resume/detach is followed by a fresh hierarchy assertion for
|
|
39
|
+
`Kotlin result 43`; exact owned forwards, package and temporary files are cleaned
|
|
40
|
+
up. Build failures also remove their temporary directory.
|
|
41
|
+
|
|
42
|
+
`npm run test:device` includes this journey only when `MCP_KOTLIN_DEVICE` is set;
|
|
43
|
+
otherwise it reports **UNRUN** explicitly. The existing Java journey separately
|
|
44
|
+
covers absent variable tables. A local outside its compiler scope and missing
|
|
45
|
+
metadata are different conditions; an empty ART table alone proves neither.
|
|
46
|
+
See the [dated validation record](../../../docs/specs/029-kotlin-locals-validation/report.md).
|
|
47
|
+
|
|
48
|
+
## Interaction regression journey
|
|
49
|
+
|
|
50
|
+
The same isolated fixture is also used by the [interaction device check](../../device/automated/interact/interaction.android.test.ts)
|
|
51
|
+
for clickable waits, stored handles, selector taps, visibility and enabled-state
|
|
52
|
+
assertions. Run it after building the server:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
MCP_INTERACTION_DEVICE=emulator-5554 npx tsx test/device/automated/interact/interaction.android.test.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It also runs in `npm run test:device` when `MCP_INTERACTION_DEVICE` or
|
|
59
|
+
`MCP_DEBUGGER_DEVICE` selects a device. Fixture journeys run sequentially and
|
|
60
|
+
remove their own package and temporary build outputs. This check does not attach
|
|
61
|
+
a debugger or modify another app's data.
|
|
62
|
+
|
|
63
|
+
The interaction journey also exercises a labelled native SeekBar. It adjusts
|
|
64
|
+
from 25 toward 75, checks convergence within tolerance, and confirms the displayed
|
|
65
|
+
value with a fresh hierarchy assertion. The same fixture cleanup applies.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
export const debuggerAppId = 'dev.mobiledebugmcp.debugfixture'
|
|
7
|
-
export function buildDebuggerFixture() {
|
|
7
|
+
export function buildDebuggerFixture(kotlin = false) {
|
|
8
|
+
if (kotlin && !process.env.MCP_KOTLIN_STDLIB) throw new Error('MCP_KOTLIN_STDLIB must identify the matching Kotlin standard-library jar')
|
|
8
9
|
const source = path.dirname(fileURLToPath(import.meta.url))
|
|
9
10
|
const directory = mkdtempSync(path.join(tmpdir(), 'mcp-debugger-fixture-'))
|
|
10
11
|
const java = process.env.JAVA_HOME ?? (existsSync('/Applications/Android Studio.app/Contents/jbr/Contents/Home') ? '/Applications/Android Studio.app/Contents/jbr/Contents/Home' : undefined)
|
|
@@ -13,17 +14,26 @@ export function buildDebuggerFixture() {
|
|
|
13
14
|
const tools = path.join(sdk, 'build-tools/36.0.0')
|
|
14
15
|
const androidJar = path.join(sdk, 'platforms/android-36/android.jar')
|
|
15
16
|
const jdk = (name: string) => java ? path.join(java, 'bin', name) : name
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(path.join(directory, 'classes'))
|
|
19
|
+
writeFileSync(path.join(directory, 'AndroidManifest.xml'), `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${debuggerAppId}" android:versionCode="1" android:versionName="1.0"><uses-sdk android:minSdkVersion="26" android:targetSdkVersion="36"/><application android:debuggable="true" android:label="Debugger fixture" android:theme="@android:style/Theme.Material.Light.NoActionBar"><activity android:name=".MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application></manifest>`)
|
|
20
|
+
if (kotlin) {
|
|
21
|
+
run(process.env.MCP_KOTLINC ?? 'kotlinc', ['-no-stdlib', '-no-reflect', '-jvm-target', '1.8', '-classpath', [androidJar, process.env.MCP_KOTLIN_STDLIB!].join(path.delimiter), '-d', 'classes', path.join(source, 'MainActivity.kt')])
|
|
22
|
+
} else {
|
|
23
|
+
run(jdk('javac'), ['-g', '--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(source, 'MainActivity.java')])
|
|
24
|
+
run(jdk('javac'), ['-g:source,lines', '--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(source, 'NoLocals.java')])
|
|
25
|
+
}
|
|
26
|
+
run(jdk('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
|
|
27
|
+
run(path.join(tools, 'd8'), ['--lib', androidJar, '--output', directory, 'classes.jar', ...(kotlin ? [process.env.MCP_KOTLIN_STDLIB!] : [])])
|
|
28
|
+
run(path.join(tools, 'aapt2'), ['link', '-I', androidJar, '--manifest', 'AndroidManifest.xml', '-o', 'unsigned.apk'])
|
|
29
|
+
run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
|
|
30
|
+
run(path.join(tools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
|
|
31
|
+
run(jdk('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Readiness Fixture', '-keyalg', 'RSA', '-validity', '365'])
|
|
32
|
+
const artifact = path.join(directory, 'Debugger.apk')
|
|
33
|
+
run(path.join(tools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', artifact, 'aligned.apk'])
|
|
34
|
+
return { artifact, directory }
|
|
35
|
+
} catch (error) {
|
|
36
|
+
rmSync(directory, { recursive: true, force: true })
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
29
39
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Interaction latency fixture
|
|
2
2
|
|
|
3
|
+
[Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
|
|
4
|
+
|
|
3
5
|
An offline native Android fixture, version 1.0. It requires Android SDK platform 36,
|
|
4
6
|
build-tools 36.0.0 and JDK 21. The harness builds and signs it using those installed
|
|
5
7
|
tools; no Gradle dependency download is needed.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Readiness fixture
|
|
2
2
|
|
|
3
|
+
[Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
|
|
4
|
+
|
|
3
5
|
Offline native Android/iOS simulator apps used only for explicit readiness
|
|
4
6
|
verification. Ready stays visible while Show loader starts a 12-second loader;
|
|
5
7
|
completion hides the loader and changes its status to Done. No permissions,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { deviceForwards, reportAfterCleanup } from '../../device/automated/debugger/coroutine-lifecycle.js'
|
|
3
|
+
|
|
4
|
+
const baseline = deviceForwards('emulator-5554 tcp:2 jdwp:20\nemulator-5554 tcp:1 jdwp:10\nother-device tcp:3 jdwp:30', 'emulator-5554')
|
|
5
|
+
assert.deepEqual(deviceForwards('other-device tcp:4 jdwp:40\nemulator-5554 tcp:1 jdwp:10\nemulator-5554 tcp:2 jdwp:20\n', 'emulator-5554'), baseline)
|
|
6
|
+
assert.notDeepEqual(deviceForwards('emulator-5554 tcp:1 jdwp:10', 'emulator-5554'), baseline)
|
|
7
|
+
assert.deepEqual(deviceForwards('emulator-55540 tcp:1 jdwp:10', 'emulator-5554'), [])
|
|
8
|
+
|
|
9
|
+
const order: string[] = []
|
|
10
|
+
await reportAfterCleanup(async () => { order.push('work'); return 42 }, async () => { order.push('cleanup') }, (status, result) => {
|
|
11
|
+
order.push(status)
|
|
12
|
+
assert.equal(result, 42)
|
|
13
|
+
})
|
|
14
|
+
assert.deepEqual(order, ['work', 'cleanup', 'PASS'])
|
|
15
|
+
|
|
16
|
+
// Reproduce the review bug: cleanup rejects after work succeeds or fails.
|
|
17
|
+
// The runner must reject and emit only FAIL in both cases.
|
|
18
|
+
for (const workFails of [false, true]) {
|
|
19
|
+
const records: string[] = []
|
|
20
|
+
const cleanupError = new Error('cleanup')
|
|
21
|
+
const journeyError = new Error('journey')
|
|
22
|
+
await assert.rejects(reportAfterCleanup(async () => {
|
|
23
|
+
if (workFails) throw journeyError
|
|
24
|
+
return 42
|
|
25
|
+
}, async () => { throw cleanupError }, (status, evidence, error) => {
|
|
26
|
+
records.push(status)
|
|
27
|
+
assert.equal(evidence, undefined)
|
|
28
|
+
if (workFails) {
|
|
29
|
+
assert.ok(error instanceof AggregateError)
|
|
30
|
+
assert.deepEqual(error.errors, [journeyError, cleanupError])
|
|
31
|
+
} else assert.equal(error, cleanupError)
|
|
32
|
+
}), error => workFails ? error instanceof AggregateError : error === cleanupError)
|
|
33
|
+
assert.deepEqual(records, ['FAIL'])
|
|
34
|
+
}
|
|
35
|
+
let cleaned = false
|
|
36
|
+
const workError = new Error('journey')
|
|
37
|
+
await assert.rejects(reportAfterCleanup(async () => { throw workError }, async () => { cleaned = true }, (status, evidence, error) => {
|
|
38
|
+
assert.equal(cleaned, true)
|
|
39
|
+
assert.equal(status, 'FAIL')
|
|
40
|
+
assert.equal(evidence, undefined)
|
|
41
|
+
assert.equal(error, workError)
|
|
42
|
+
}), error => error === workError)
|
|
43
|
+
console.log('PASS coroutine assessment: cleanup precedes PASS; work/cleanup failures report FAIL; forwards scoped and sorted')
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { DebuggerSessions } from '../../../src/debugger/sessions.js'
|
|
3
|
+
import type { DebugNative, DebugTarget } from '../../../src/debugger/android.js'
|
|
4
|
+
import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
5
|
+
import { diagnostic } from '../../../src/debugger/diagnostics.js'
|
|
6
|
+
const target: DebugTarget = { app_id: 'test.app', process_name: 'test.app', pid: 1, uid: 2, start_identity: '3' }
|
|
7
|
+
let removed = 0
|
|
8
|
+
let closed = 0
|
|
9
|
+
let cleanupFails = false
|
|
10
|
+
let mode = ''
|
|
11
|
+
const callbacks = new Map<string, (event: WorkerEvent) => void>()
|
|
12
|
+
const native: DebugNative = {
|
|
13
|
+
candidates: async () => [target], identity: async () => target, forward: async () => 12345,
|
|
14
|
+
removeForward: async () => { if (cleanupFails) throw new Error('private'); removed++ }
|
|
15
|
+
}
|
|
16
|
+
const sessions = new DebuggerSessions(native, async emit => {
|
|
17
|
+
callbacks.set(mode, emit)
|
|
18
|
+
return {
|
|
19
|
+
call: async command => {
|
|
20
|
+
if (command === 'attach' && mode === 'attach') throw new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('attach', 'connection_refused'))
|
|
21
|
+
if (command === 'step') {
|
|
22
|
+
emit({ event: 'worker_failed', diagnostic: diagnostic('ipc', 'invalid_message') })
|
|
23
|
+
throw new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'invalid_message'))
|
|
24
|
+
}
|
|
25
|
+
return {}
|
|
26
|
+
},
|
|
27
|
+
close: async () => { closed++ }
|
|
28
|
+
}
|
|
29
|
+
}, async () => ({ java: 'unused', directory: 'unused' }))
|
|
30
|
+
try {
|
|
31
|
+
mode = 'attach'
|
|
32
|
+
const failed = await sessions.attach('one', 'test.app')
|
|
33
|
+
assert.equal(failed.error.code, 'DEBUGGER_WORKER_FAILED')
|
|
34
|
+
assert.deepEqual(failed.error.diagnostic, diagnostic('attach', 'connection_refused'))
|
|
35
|
+
assert.equal(failed.cleanup.status, 'complete')
|
|
36
|
+
assert.equal(closed, 1); assert.equal(removed, 1)
|
|
37
|
+
mode = 'first'
|
|
38
|
+
const first = await sessions.attach('one', 'test.app')
|
|
39
|
+
mode = 'second'
|
|
40
|
+
const second = await sessions.attach('two', 'test.app')
|
|
41
|
+
callbacks.get('first')!({ event: 'stopped', stop_id: 'stop' })
|
|
42
|
+
await assert.rejects(sessions.command(first.debug_session_id, 'step', { stopId: 'stop' }), /DEBUGGER_WORKER_FAILED/)
|
|
43
|
+
const terminal = await sessions.command(first.debug_session_id, 'state')
|
|
44
|
+
assert.deepEqual(terminal.error.diagnostic, diagnostic('ipc', 'invalid_message'), 'event/rejection race retains primary evidence after cleanup')
|
|
45
|
+
assert.equal(terminal.cleanup.status, 'complete')
|
|
46
|
+
await assert.rejects(sessions.command(first.debug_session_id, 'stack', {}), (e: unknown) => e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED' && e.diagnostic?.reason === 'invalid_message')
|
|
47
|
+
assert.equal((await sessions.command(second.debug_session_id, 'state')).success, true, 'other device is isolated')
|
|
48
|
+
const aborted = new AbortController(); aborted.abort()
|
|
49
|
+
await assert.rejects(sessions.command(second.debug_session_id, 'events', { signal: aborted.signal }), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'cancelled')
|
|
50
|
+
cleanupFails = true
|
|
51
|
+
const incomplete = await sessions.command(second.debug_session_id, 'detach')
|
|
52
|
+
assert.equal(incomplete.error.code, 'DEBUGGER_CLEANUP_FAILED')
|
|
53
|
+
assert.deepEqual(incomplete.error.diagnostic, diagnostic('cleanup', 'operation_failed'))
|
|
54
|
+
cleanupFails = false
|
|
55
|
+
assert.equal((await sessions.command(second.debug_session_id, 'detach')).cleanup.status, 'complete')
|
|
56
|
+
mode = 'shutdown'
|
|
57
|
+
const shutdown = await sessions.attach('three', 'test.app')
|
|
58
|
+
await sessions.shutdown()
|
|
59
|
+
assert.equal(sessions.summary(shutdown.debug_session_id).cleanup.status, 'complete')
|
|
60
|
+
} finally { await sessions.shutdown() }
|
|
61
|
+
const badProbes = new DebuggerSessions({ ...native, candidates: async () => { throw new Error('private-token') } }, undefined,
|
|
62
|
+
async () => { throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('compilation', 'operation_failed')) })
|
|
63
|
+
const probe = await badProbes.capabilities('one', 'test.app')
|
|
64
|
+
assert.equal(probe.success, true); assert.equal(probe.supported, false)
|
|
65
|
+
assert.deepEqual(probe.diagnostics, [diagnostic('compilation', 'operation_failed'), diagnostic('target_resolution', 'unknown')])
|
|
66
|
+
assert.ok(!JSON.stringify(probe).includes('private-token'))
|
|
67
|
+
console.log('PASS diagnostic session retention/races, cleanup retry, isolation, shutdown and ordered capability diagnostics')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { diagnostic, stages, reasons, classify, parseDiagnostic } from '../../../src/debugger/diagnostics.js'
|
|
6
|
+
import { DebuggerError, JdiWorker, withDebuggerBudget, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
7
|
+
import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
|
|
8
|
+
import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
|
|
9
|
+
|
|
10
|
+
const secret = 'PRIVATE_LOCALS_TOKEN_/private/path'
|
|
11
|
+
for (const stage of stages) for (const reason of reasons) {
|
|
12
|
+
const value = diagnostic(stage, reason)
|
|
13
|
+
assert.ok(Buffer.byteLength(JSON.stringify(value)) <= 512)
|
|
14
|
+
assert.ok(value.recovery.length <= 240 && /^[\x00-\x7f]*$/.test(value.recovery))
|
|
15
|
+
assert.deepEqual(Object.keys(value), ['stage', 'reason', 'recovery'])
|
|
16
|
+
}
|
|
17
|
+
assert.deepEqual(parseDiagnostic({ stage: 'attach', reason: 'connection_refused', recovery: secret, raw: secret }), diagnostic('attach', 'connection_refused'))
|
|
18
|
+
assert.equal(classify(new Error(secret), 'attach', 'DEBUGGER_ATTACH_FAILED').diagnostic?.reason, 'unknown')
|
|
19
|
+
assert.equal(classify(new DebuggerError('DEBUGGER_STALE_REFERENCE'), 'command', 'DEBUGGER_WORKER_FAILED').diagnostic, undefined)
|
|
20
|
+
for (const hostile of [secret, { stage: secret, reason: 'unknown' }, { stage: 'attach', reason: secret }]) {
|
|
21
|
+
assert.throws(() => parseWorkerMessage({ id: 1, error: { code: 'DEBUGGER_WORKER_FAILED', diagnostic: hostile } }), /INVALID_WORKER_DIAGNOSTIC/)
|
|
22
|
+
}
|
|
23
|
+
assert.throws(() => parseWorkerMessage({ id: 1, error: { code: secret } }), /INVALID_WORKER_ERROR/)
|
|
24
|
+
const cancelled = new AbortController(); cancelled.abort()
|
|
25
|
+
const response = await handleDebugger('get_debugger_state', { debugSessionId: 'absent' }, cancelled.signal)
|
|
26
|
+
assert.equal(response.isError, true)
|
|
27
|
+
const payload = JSON.parse(response.content[0].text)
|
|
28
|
+
assert.equal(payload.error.code, 'DEBUGGER_CANCELLED')
|
|
29
|
+
assert.deepEqual(payload.error.diagnostic, diagnostic('unknown', 'cancelled'))
|
|
30
|
+
assert.equal((await handleDebugger('get_debugger_state', {})).isError, true)
|
|
31
|
+
|
|
32
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-diagnostics-test-'))
|
|
33
|
+
let index = 0
|
|
34
|
+
async function worker(body: string, onEvent: (event: WorkerEvent) => void = () => {}) {
|
|
35
|
+
const file = path.join(directory, `worker-${index++}`)
|
|
36
|
+
await writeFile(file, `#!${process.execPath}\n${body}\n`, { mode: 0o700 })
|
|
37
|
+
return new JdiWorker(file, directory, onEvent)
|
|
38
|
+
}
|
|
39
|
+
async function fails(body: string, stage: string, reason: string) {
|
|
40
|
+
const w = await worker(body)
|
|
41
|
+
try {
|
|
42
|
+
await assert.rejects(w.call('attach', {}, 3000), (error: unknown) => {
|
|
43
|
+
assert.ok(error instanceof DebuggerError)
|
|
44
|
+
assert.equal(error.code, 'DEBUGGER_WORKER_FAILED')
|
|
45
|
+
assert.deepEqual(error.diagnostic, diagnostic(stage as typeof stages[number], reason as typeof reasons[number]))
|
|
46
|
+
assert.ok(!JSON.stringify(error).includes(secret))
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
} finally { await w.close() }
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const missing = new JdiWorker(path.join(directory, 'missing'), directory, () => {})
|
|
53
|
+
try { await assert.rejects(missing.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'spawn_failed') } finally { await missing.close() }
|
|
54
|
+
await fails(`process.stderr.write(${JSON.stringify(secret)}); process.exit(7)`, 'worker_start', 'process_exited')
|
|
55
|
+
await fails(`process.stdout.write(${JSON.stringify(secret + '\n')}); setInterval(()=>{},1000)`, 'ipc', 'invalid_message')
|
|
56
|
+
await fails(`process.stdout.write('x'.repeat(1048577)+'\\n'); setInterval(()=>{},1000)`, 'ipc', 'message_limit')
|
|
57
|
+
const attach = await worker(`process.stdin.once('data', data => { const {id}=JSON.parse(data); console.log(JSON.stringify({id,error:{code:'DEBUGGER_WORKER_FAILED',diagnostic:{stage:'attach',reason:'connection_refused',raw:${JSON.stringify(secret)}}}})) })`)
|
|
58
|
+
try { await assert.rejects(attach.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'connection_refused') } finally { await attach.close() }
|
|
59
|
+
let ready = () => {}
|
|
60
|
+
const readyPromise = new Promise<void>(resolve => { ready = resolve })
|
|
61
|
+
const brokenPipe = await worker(`require('node:fs').closeSync(0); console.log(JSON.stringify({event:'ready'})); setInterval(()=>{},1000)`, () => ready())
|
|
62
|
+
try {
|
|
63
|
+
await readyPromise
|
|
64
|
+
await assert.rejects(brokenPipe.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'write_failed')
|
|
65
|
+
} finally { await brokenPipe.close() }
|
|
66
|
+
const afterReply = await worker(`process.stdin.once('data',data=>{const {id}=JSON.parse(data); console.log(JSON.stringify({id,result:{}})); setTimeout(()=>process.exit(7),100)})`)
|
|
67
|
+
try {
|
|
68
|
+
await afterReply.call('state')
|
|
69
|
+
await assert.rejects(afterReply.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'ipc' && e.diagnostic.reason === 'process_exited')
|
|
70
|
+
} finally { await afterReply.close() }
|
|
71
|
+
const idle = await worker('process.stdin.resume()')
|
|
72
|
+
try {
|
|
73
|
+
await assert.rejects(idle.call('stack', {}, 50), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'timeout')
|
|
74
|
+
const controller = new AbortController()
|
|
75
|
+
const call = withDebuggerBudget(1000, controller.signal, () => idle.call('attach'))
|
|
76
|
+
controller.abort()
|
|
77
|
+
await assert.rejects(call, (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'attach' && e.diagnostic.reason === 'cancelled')
|
|
78
|
+
} finally { await idle.close() }
|
|
79
|
+
} finally { await rm(directory, { recursive: true, force: true }) }
|
|
80
|
+
console.log('PASS diagnostic bounds/privacy, strict IPC, real worker spawn/exit/malformed/oversize, attach failure, timeout/cancel and MCP wrapping')
|
|
@@ -38,6 +38,10 @@ try {
|
|
|
38
38
|
assert.equal(journey.steps[1].success, false)
|
|
39
39
|
assert.equal(journey.steps[1].result.failure_code, 'DEBUGGER_PAUSED')
|
|
40
40
|
assert.equal(calls, 2, 'no action queued or retried after pause')
|
|
41
|
+
_setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('resolution unavailable') } })
|
|
42
|
+
const failedResolution = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
|
|
43
|
+
assert.equal(failedResolution.error.tool, 'tap')
|
|
44
|
+
assert.match(failedResolution.error.message, /resolution unavailable/, 'resolution errors retain the MCP error envelope')
|
|
41
45
|
console.log('PASS debugger readiness without UI acquisition, device scoping, dispatch guard, mid-journey pause preservation')
|
|
42
46
|
} finally {
|
|
43
47
|
debuggerSessions.paused = originalPause; debuggerSessions.hasActiveSessions = originalActive
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
const exec = promisify(execFile)
|
|
8
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-fake-jdk-'))
|
|
9
|
+
const secret = 'SECRET_COMPILER_OUTPUT'
|
|
10
|
+
const run = async () => {
|
|
11
|
+
const code = `import {prepareWorker} from './src/debugger/worker.ts'; try {await prepareWorker(); process.exitCode=1} catch(e) {console.log(JSON.stringify({code:e.code,diagnostic:e.diagnostic}))}`
|
|
12
|
+
const { stdout, stderr } = await exec(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', code], {
|
|
13
|
+
env: { ...process.env, ANDROID_STUDIO_JDK: directory }, timeout: 10000
|
|
14
|
+
})
|
|
15
|
+
assert.ok(!stdout.includes(secret)); assert.ok(!stderr.includes(secret))
|
|
16
|
+
return JSON.parse(stdout)
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await mkdir(path.join(directory, 'bin'))
|
|
20
|
+
const java = path.join(directory, 'bin', 'java')
|
|
21
|
+
await writeFile(java, `#!${process.execPath}\nconsole.error('openjdk version "17.0.1"');`, { mode: 0o700 })
|
|
22
|
+
await writeFile(path.join(directory, 'bin', 'javac'), `#!${process.execPath}\nconsole.error('${secret}');process.exit(1)`, { mode: 0o700 })
|
|
23
|
+
const compile = await run()
|
|
24
|
+
assert.equal(compile.code, 'DEBUGGER_UNSUPPORTED')
|
|
25
|
+
assert.equal(compile.diagnostic.stage, 'compilation'); assert.equal(compile.diagnostic.reason, 'operation_failed')
|
|
26
|
+
// Discovery succeeds; the actual bounded version subprocess then fails.
|
|
27
|
+
await writeFile(java, `#!${process.execPath}\nconst fs=require('node:fs');const marker=${JSON.stringify(path.join(directory, 'seen'))};if(fs.existsSync(marker)){console.error('${secret}');process.exit(1)}fs.writeFileSync(marker,'');console.error('openjdk version "17.0.1"');`, { mode: 0o700 })
|
|
28
|
+
const prerequisite = await run()
|
|
29
|
+
assert.equal(prerequisite.code, 'DEBUGGER_ATTACH_FAILED')
|
|
30
|
+
assert.equal(prerequisite.diagnostic.stage, 'prerequisite'); assert.equal(prerequisite.diagnostic.reason, 'unavailable')
|
|
31
|
+
} finally { await rm(directory, { recursive: true, force: true }) }
|
|
32
|
+
console.log('PASS isolated fake-JDK compilation/prerequisite classification without compiler output leakage')
|