mobile-debug-mcp 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +23 -2
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +158 -124
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +20 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +79 -0
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +304 -0
- package/dist/debugger/worker.js +176 -0
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3074
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +122 -76
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +13 -2
- package/dist/observe/readiness.js +44 -18
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +294 -0
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/handlers/debugger-handlers.js +48 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/schemas/debugger-schema.js +47 -0
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
- package/dist/server/schemas/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +62 -1134
- package/dist/server-core.js +4 -4
- package/dist/server.js +34 -0
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -438
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +75 -57
- package/dist/utils/operation-budget.js +2 -0
- package/dist/utils/resolve-device.js +6 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +48 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +41 -0
- package/docs/specs/023-android-system-traces/plan.md +115 -0
- package/docs/specs/023-android-system-traces/spec.md +122 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -16
- package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
- package/docs/tools/debugger.md +180 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +64 -25
- package/docs/tools/system-traces.md +134 -0
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +66 -0
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +31 -0
- package/src/debugger/java/DebuggerWorker.java +166 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +327 -0
- package/src/debugger/worker.ts +135 -0
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3491
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +130 -71
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +10 -3
- package/src/observe/readiness.ts +49 -22
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +227 -0
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +43 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
- package/src/server/schemas/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +69 -1140
- package/src/server-core.ts +4 -4
- package/src/server.ts +25 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -523
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +71 -51
- package/src/utils/operation-budget.ts +2 -0
- package/src/utils/resolve-device.ts +11 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +219 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +67 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +65 -0
- package/test/fixtures/debugger-app/build.ts +39 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +50 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/readiness.test.ts +3 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -893
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/tool-handlers.ts +0 -923
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { debuggerResponse } from '../../../../src/server/handlers/debugger-response.js'
|
|
2
|
+
import { debuggerContracts } from '../../../../src/server/schemas/debugger-schema.js'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
5
|
+
import { readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
8
|
+
import { createHash } from 'node:crypto'
|
|
9
|
+
import { buildDebuggerFixture, debuggerAppId } from '../../../fixtures/debugger-app/build.js'
|
|
10
|
+
|
|
11
|
+
// Explicit opt-in: installs/removes only this isolated fixture on the selected device.
|
|
12
|
+
if (!process.env.MCP_DEBUGGER_DEVICE) {
|
|
13
|
+
console.log('UNRUN Android debugger fixture: set MCP_DEBUGGER_DEVICE to an authorized emulator serial')
|
|
14
|
+
} else {
|
|
15
|
+
const deviceId = process.env.MCP_DEBUGGER_DEVICE
|
|
16
|
+
const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
|
|
17
|
+
const client = new Client({ name: 'android-debugger-smoke', version: '1.0.0' })
|
|
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 }[] = []
|
|
21
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
22
|
+
const response = await client.callTool({ name, arguments: { ...args, ...(debuggerContracts[name] && compactMode ? { responseMode: 'compact' } : {}) } }, undefined, { timeout: 45000 })
|
|
23
|
+
const text = (response.content as any[]).find(c => c.type === 'text')!.text!
|
|
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
|
|
36
|
+
}
|
|
37
|
+
const hash = createHash('sha256')
|
|
38
|
+
const fingerprint = async (directory: string): Promise<void> => {
|
|
39
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
40
|
+
const file = directory + '/' + entry.name
|
|
41
|
+
if (entry.isDirectory()) await fingerprint(file)
|
|
42
|
+
else hash.update(file).update(await readFile(file))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await fingerprint('dist'); await fingerprint('src/debugger/java')
|
|
46
|
+
const buildHash = hash.digest('hex')
|
|
47
|
+
const fixture = buildDebuggerFixture()
|
|
48
|
+
const source = await readFile(new URL('../../../fixtures/debugger-app/MainActivity.java', import.meta.url), 'utf8')
|
|
49
|
+
const line = (marker: string) => source.split('\n').findIndex(l => l.includes(marker)) + 1
|
|
50
|
+
let debugSessionId = ''
|
|
51
|
+
let cursor = 0
|
|
52
|
+
const eventsUntilStop = async () => {
|
|
53
|
+
for (let i = 0; i < 10; i++) {
|
|
54
|
+
const result = await call('wait_for_debugger_event', { debugSessionId, afterSequence: cursor, timeoutMs: 1000 })
|
|
55
|
+
cursor = result.next_cursor
|
|
56
|
+
if (result.events.length) console.log('Debug events', JSON.stringify(result.events))
|
|
57
|
+
const event = result.events.find((e: any) => e.event === 'stopped')
|
|
58
|
+
if (event) return event
|
|
59
|
+
}
|
|
60
|
+
throw new Error('No debugger stop observed')
|
|
61
|
+
}
|
|
62
|
+
const forwardsBefore = adb('forward', '--list').trim()
|
|
63
|
+
try {
|
|
64
|
+
await client.connect(transport)
|
|
65
|
+
const listed = await client.listTools()
|
|
66
|
+
assert.ok(listed.tools.some(tool => tool.name === 'attach_debugger'))
|
|
67
|
+
adb('install', '-r', fixture.artifact)
|
|
68
|
+
adb('shell', 'am', 'start', '-W', '-n', `${debuggerAppId}/.MainActivity`)
|
|
69
|
+
const cap = await call('get_debugger_capabilities', { platform: 'android', deviceId, appId: debuggerAppId })
|
|
70
|
+
assert.equal(cap.supported, true, JSON.stringify(cap))
|
|
71
|
+
const timeout = await call('get_debugger_capabilities', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 100 })
|
|
72
|
+
assert.equal(timeout.error.code, 'DEBUGGER_TIMEOUT')
|
|
73
|
+
const unsupported = await call('get_debugger_capabilities', { platform: 'android', deviceId, appId: 'com.missing.debugfixture' })
|
|
74
|
+
assert.equal(unsupported.supported, false)
|
|
75
|
+
assert.ok(unsupported.reasons.includes('PROCESS_NOT_FOUND'))
|
|
76
|
+
const attached = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
|
|
77
|
+
assert.equal(attached.success, true, JSON.stringify(attached)); debugSessionId = attached.debug_session_id
|
|
78
|
+
const bp = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: line('BREAK_CALCULATE') })
|
|
79
|
+
assert.equal(bp.result.status, 'verified', JSON.stringify(bp))
|
|
80
|
+
const pending = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.LateWork`, line: line('BREAK_LATE') })
|
|
81
|
+
assert.equal(pending.result.status, 'pending', JSON.stringify(pending))
|
|
82
|
+
const unavailable = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: 9999 })
|
|
83
|
+
assert.equal(unavailable.result.status, 'rejected')
|
|
84
|
+
// Fixture button placement is obtained from real UI semantics, not fixed coordinates.
|
|
85
|
+
const tree = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
86
|
+
const button = tree.elements.find((e: any) => /^calculate$/i.test(e.text ?? ''))
|
|
87
|
+
assert.ok(button, 'Calculate button visible in real UI tree: ' + JSON.stringify(tree))
|
|
88
|
+
console.log('Fixture target', JSON.stringify({ bounds: button.bounds, device: tree.device, resolution: tree.resolution }))
|
|
89
|
+
const [left, top, right, bottom] = button.bounds
|
|
90
|
+
// Shell input is asynchronous so suspension cannot block the test's next debugger call.
|
|
91
|
+
await writeFile(fixture.directory + '/fixture.png', execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'exec-out', 'screencap', '-p']))
|
|
92
|
+
const tap = spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((left + right) / 2)), String(Math.floor((top + bottom) / 2))], { stdio: 'ignore' })
|
|
93
|
+
tap.on('error', error => { throw error })
|
|
94
|
+
const stop = await eventsUntilStop()
|
|
95
|
+
assert.equal(stop.stops[0].location.line, line('BREAK_CALCULATE'))
|
|
96
|
+
const stack = await call('get_debugger_stack', { debugSessionId, stopId: stop.stop_id, threadId: stop.thread_id })
|
|
97
|
+
assert.equal(stack.result.frames[0].location.method, 'calculate')
|
|
98
|
+
const frameId = stack.result.frames[0].frame_id
|
|
99
|
+
const locals = await call('get_debugger_locals', { debugSessionId, stopId: stop.stop_id, frameId })
|
|
100
|
+
assert.equal(locals.result.locals.find((v: any) => v.name === 'known')?.value, '42', JSON.stringify(locals))
|
|
101
|
+
const longText = locals.result.locals.find((v: any) => v.name === 'longText')
|
|
102
|
+
assert.equal(longText.value.length, 256); assert.equal(longText.truncated, true)
|
|
103
|
+
const object = locals.result.locals.find((v: any) => v.name === 'object')
|
|
104
|
+
assert.equal(object.value.type, 'java.lang.Object'); assert.equal(typeof object.value.object_id, 'string')
|
|
105
|
+
const readiness = await call('get_app_readiness', { platform: 'android', deviceId })
|
|
106
|
+
assert.equal(readiness.readiness.state, 'debugger_paused')
|
|
107
|
+
for (const name of ['tap', 'wait_for_ui', 'terminate_app', 'run_journey']) {
|
|
108
|
+
const blocked = await call(name, { platform: 'android', deviceId, x: 5, y: 5, appId: debuggerAppId })
|
|
109
|
+
assert.equal(blocked.failure_code, 'DEBUGGER_PAUSED', name)
|
|
110
|
+
assert.equal(blocked.dispatch_started, false)
|
|
111
|
+
}
|
|
112
|
+
await call('remove_debugger_breakpoint', { debugSessionId, breakpointId: bp.result.breakpoint_id })
|
|
113
|
+
const step = await call('step_debugger', { debugSessionId, stopId: stop.stop_id, threadId: stop.thread_id, kind: 'into' })
|
|
114
|
+
assert.equal(step.success, true, JSON.stringify(step))
|
|
115
|
+
const stale = await call('get_debugger_locals', { debugSessionId, stopId: stop.stop_id, frameId })
|
|
116
|
+
assert.equal(stale.error.code, 'DEBUGGER_STALE_REFERENCE')
|
|
117
|
+
const into = await eventsUntilStop()
|
|
118
|
+
assert.equal(into.stops[0].location.method, 'increment')
|
|
119
|
+
assert.equal((await call('step_debugger', { debugSessionId, stopId: into.stop_id, threadId: into.thread_id, kind: 'over' })).success, true)
|
|
120
|
+
const over = await eventsUntilStop()
|
|
121
|
+
assert.equal(over.stops[0].location.method, 'increment')
|
|
122
|
+
assert.equal((await call('step_debugger', { debugSessionId, stopId: over.stop_id, threadId: over.thread_id, kind: 'out' })).success, true)
|
|
123
|
+
const out = await eventsUntilStop()
|
|
124
|
+
assert.equal(out.stops[0].location.method, 'calculate')
|
|
125
|
+
assert.equal((await call('resume_debugger', { debugSessionId, stopId: out.stop_id })).success, true)
|
|
126
|
+
let resultVisible = false
|
|
127
|
+
for (let i = 0; i < 10; i++) {
|
|
128
|
+
const observed = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
129
|
+
if (observed.elements.some((e: any) => e.text === 'Result 43')) { resultVisible = true; break }
|
|
130
|
+
}
|
|
131
|
+
assert.ok(resultVisible, 'UI tree independently establishes resumed execution: Result 43')
|
|
132
|
+
const noLocalsSource = await readFile(new URL('../../../fixtures/debugger-app/NoLocals.java', import.meta.url), 'utf8')
|
|
133
|
+
const noLocalsLine = noLocalsSource.split('\n').findIndex(line => line.includes('BREAK_NO_LOCALS')) + 1
|
|
134
|
+
const removedPending = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.NoLocals`, line: noLocalsLine })
|
|
135
|
+
await call('remove_debugger_breakpoint', { debugSessionId, breakpointId: removedPending.result.breakpoint_id })
|
|
136
|
+
await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.NoLocals`, line: noLocalsLine })
|
|
137
|
+
const metadataTree = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
138
|
+
const mb = metadataTree.elements.find((e: any) => /^no locals$/i.test(e.text ?? '')).bounds
|
|
139
|
+
spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((mb[0] + mb[2]) / 2)), String(Math.floor((mb[1] + mb[3]) / 2))], { stdio: 'ignore' })
|
|
140
|
+
const metadataStop = await eventsUntilStop()
|
|
141
|
+
assert.ok(metadataStop.stops.every((stop: any) => stop.breakpoint_id !== removedPending.result.breakpoint_id), 'removed pending breakpoint never rebinds')
|
|
142
|
+
const metadataStack = await call('get_debugger_stack', { debugSessionId, stopId: metadataStop.stop_id, threadId: metadataStop.thread_id })
|
|
143
|
+
const metadataLocals = await call('get_debugger_locals', { debugSessionId, stopId: metadataStop.stop_id, frameId: metadataStack.result.frames[0].frame_id })
|
|
144
|
+
assert.equal(metadataLocals.result.availability, 'unknown')
|
|
145
|
+
assert.equal(metadataLocals.result.reason, 'EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA')
|
|
146
|
+
await call('resume_debugger', { debugSessionId, stopId: metadataStop.stop_id })
|
|
147
|
+
const lateTree = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
148
|
+
const lateButton = lateTree.elements.find((e: any) => /^late class$/i.test(e.text ?? ''))
|
|
149
|
+
assert.ok(lateButton)
|
|
150
|
+
const bounds = lateButton.bounds
|
|
151
|
+
spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((bounds[0] + bounds[2]) / 2)), String(Math.floor((bounds[1] + bounds[3]) / 2))], { stdio: 'ignore' })
|
|
152
|
+
const lateStop = await eventsUntilStop()
|
|
153
|
+
assert.equal(lateStop.stops[0].location.class_name, `${debuggerAppId}.LateWork`)
|
|
154
|
+
assert.equal(lateStop.stops[0].location.line, line('BREAK_LATE'))
|
|
155
|
+
const aborted = new AbortController()
|
|
156
|
+
const cancelled = client.callTool({ name: 'wait_for_debugger_event', arguments: { debugSessionId, afterSequence: cursor, timeoutMs: 1000 } }, undefined, { signal: aborted.signal, timeout: 5000 })
|
|
157
|
+
const cancelTimer = setTimeout(() => aborted.abort(), 100)
|
|
158
|
+
try { await assert.rejects(cancelled) } finally { clearTimeout(cancelTimer) }
|
|
159
|
+
assert.equal((await call('get_debugger_state', { debugSessionId })).state, 'paused')
|
|
160
|
+
if (process.env.MCP_DEBUGGER_EXTENDED === '1') {
|
|
161
|
+
console.log('Checking real 60-second pause expiry while polling state')
|
|
162
|
+
const deadline = Date.now() + 65000
|
|
163
|
+
let state: any
|
|
164
|
+
do {
|
|
165
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
166
|
+
state = await call('get_debugger_state', { debugSessionId })
|
|
167
|
+
} while (state.state === 'paused' && Date.now() < deadline)
|
|
168
|
+
assert.equal(state.state, 'detached', JSON.stringify(state))
|
|
169
|
+
assert.equal(state.reason, 'pause_limit')
|
|
170
|
+
assert.equal(state.cleanup.status, 'complete')
|
|
171
|
+
console.log('PASS real pause expiry despite active client polling')
|
|
172
|
+
}
|
|
173
|
+
const detach = await call('detach_debugger', { debugSessionId })
|
|
174
|
+
assert.equal(detach.cleanup.status, 'complete', JSON.stringify(detach))
|
|
175
|
+
assert.equal((await call('detach_debugger', { debugSessionId })).cleanup.status, 'complete')
|
|
176
|
+
assert.equal(adb('forward', '--list').trim(), forwardsBefore, 'unrelated forwards preserved')
|
|
177
|
+
const reattached = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
|
|
178
|
+
assert.equal(reattached.success, true, JSON.stringify(reattached))
|
|
179
|
+
debugSessionId = reattached.debug_session_id
|
|
180
|
+
adb('shell', 'am', 'force-stop', debuggerAppId)
|
|
181
|
+
let disconnected: any
|
|
182
|
+
for (let i = 0; i < 20; i++) {
|
|
183
|
+
disconnected = await call('get_debugger_state', { debugSessionId })
|
|
184
|
+
if (disconnected.cleanup.status === 'complete') break
|
|
185
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
186
|
+
}
|
|
187
|
+
assert.equal(disconnected.state, 'disconnected', JSON.stringify(disconnected))
|
|
188
|
+
assert.equal(disconnected.cleanup.status, 'complete')
|
|
189
|
+
assert.equal(adb('forward', '--list').trim(), forwardsBefore)
|
|
190
|
+
// Disconnect the MCP client with an actively paused session; shutdown owns resume/dispose.
|
|
191
|
+
adb('shell', 'am', 'start', '-W', '-n', `${debuggerAppId}/.MainActivity`)
|
|
192
|
+
const shutdownSession = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
|
|
193
|
+
assert.equal(shutdownSession.success, true, JSON.stringify(shutdownSession))
|
|
194
|
+
debugSessionId = shutdownSession.debug_session_id; cursor = 0
|
|
195
|
+
await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: line('BREAK_CALCULATE') })
|
|
196
|
+
const shutdownTree = await call('get_ui_tree', { platform: 'android', deviceId })
|
|
197
|
+
const shutdownButton = shutdownTree.elements.find((e: any) => /^calculate$/i.test(e.text ?? ''))
|
|
198
|
+
const sb = shutdownButton.bounds
|
|
199
|
+
spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((sb[0] + sb[2]) / 2)), String(Math.floor((sb[1] + sb[3]) / 2))], { stdio: 'ignore' })
|
|
200
|
+
await eventsUntilStop()
|
|
201
|
+
await client.close(); debugSessionId = ''
|
|
202
|
+
assert.equal(adb('forward', '--list').trim(), forwardsBefore, 'MCP disconnect cleans active owned forward')
|
|
203
|
+
const verifier = new Client({ name: 'debugger-shutdown-verifier', version: '1.0.0' })
|
|
204
|
+
try {
|
|
205
|
+
await verifier.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
|
|
206
|
+
const result = await verifier.callTool({ name: 'get_ui_tree', arguments: { platform: 'android', deviceId } })
|
|
207
|
+
const treeAfterShutdown = JSON.parse((result.content as any[]).find(c => c.type === 'text').text)
|
|
208
|
+
assert.ok(treeAfterShutdown.elements.some((e: any) => e.text === 'Result 43'), 'fresh UI proves paused fixture resumed after server shutdown')
|
|
209
|
+
} finally { await verifier.close() }
|
|
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) }))
|
|
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')
|
|
213
|
+
} finally {
|
|
214
|
+
if (debugSessionId) await call('detach_debugger', { debugSessionId }).catch(() => {})
|
|
215
|
+
await client.close()
|
|
216
|
+
adb('uninstall', debuggerAppId)
|
|
217
|
+
await rm(fixture.directory, { recursive: true, force: true })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { rm } from 'node:fs/promises'
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
5
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
6
|
+
import { buildDebuggerFixture, debuggerAppId } from '../../../fixtures/debugger-app/build.js'
|
|
7
|
+
|
|
8
|
+
// Reuse the isolated fixture; the device runner executes fixture journeys sequentially.
|
|
9
|
+
const deviceId = process.env.MCP_INTERACTION_DEVICE ?? process.env.MCP_DEBUGGER_DEVICE
|
|
10
|
+
if (!deviceId) {
|
|
11
|
+
console.log('UNRUN Android interaction fixture: select an emulator with MCP_INTERACTION_DEVICE')
|
|
12
|
+
} else {
|
|
13
|
+
const fixture = buildDebuggerFixture()
|
|
14
|
+
const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 30000 })
|
|
15
|
+
const client = new Client({ name: 'interaction-service-smoke', version: '1.0.0' })
|
|
16
|
+
const target = { platform: 'android', deviceId }
|
|
17
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
18
|
+
const response = await client.callTool({ name, arguments: args }, undefined, { timeout: 120000 })
|
|
19
|
+
const text = response.content.find(item => item.type === 'text')
|
|
20
|
+
assert(text?.type === 'text')
|
|
21
|
+
return JSON.parse(text.text)
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
await client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
|
|
25
|
+
adb('install', '-r', fixture.artifact)
|
|
26
|
+
const started = await call('start_app', { ...target, appId: debuggerAppId, verificationMode: 'none', responseMode: 'compact' })
|
|
27
|
+
assert.equal(started.success, true)
|
|
28
|
+
const waited = await call('wait_for_ui', { ...target, selector: { text: 'Calculate' }, condition: 'clickable', timeout_ms: 20000 })
|
|
29
|
+
assert.equal(waited.status, 'success', JSON.stringify(waited))
|
|
30
|
+
assert.ok(waited.element?.elementId)
|
|
31
|
+
const tapped = await call('tap_element', { ...target, elementId: waited.element.elementId, verificationMode: 'none', actionTimeoutMs: 30000 })
|
|
32
|
+
assert.equal(tapped.success, true, JSON.stringify(tapped))
|
|
33
|
+
const visible = await call('expect_element_visible', { ...target, selector: { text: 'Result 43' }, timeout_ms: 20000 })
|
|
34
|
+
assert.equal(visible.success, true, JSON.stringify(visible))
|
|
35
|
+
const journey = await call('run_journey', { ...target, steps: [
|
|
36
|
+
{ id: 'calculate-again', type: 'tap', selector: { text: 'Calculate' }, verificationMode: 'none' },
|
|
37
|
+
{ id: 'enabled-result', type: 'assert', timeoutMs: 30000, assertion: { kind: 'state_equals', selector: { text: 'Result 43' }, property: 'enabled', expected: true } },
|
|
38
|
+
{ id: 'visible-result', type: 'assert', timeoutMs: 20000, assertion: { kind: 'element_visible', selector: { text: 'Result 43' } } }
|
|
39
|
+
], defaults: { actionTimeoutMs: 30000 } })
|
|
40
|
+
assert.equal(journey.success, true, JSON.stringify(journey))
|
|
41
|
+
const adjusted = await call('adjust_control', { ...target, selector: { accessibility_id: 'Fixture level' },
|
|
42
|
+
property: 'value', targetValue: 75, tolerance: 5, maxAttempts: 5 })
|
|
43
|
+
assert.equal(adjusted.success, true, JSON.stringify(adjusted))
|
|
44
|
+
assert.equal(adjusted.converged, true, JSON.stringify(adjusted))
|
|
45
|
+
assert(Math.abs(adjusted.actual_state.value - 75) <= 5, JSON.stringify(adjusted))
|
|
46
|
+
const level = await call('expect_element_visible', { ...target, selector: { text: `${adjusted.actual_state.value} %` }, timeout_ms: 20000 })
|
|
47
|
+
assert.equal(level.success, true, JSON.stringify(level))
|
|
48
|
+
console.log(JSON.stringify({ status: 'PASS', server: client.getServerVersion(), device: deviceId,
|
|
49
|
+
api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version,
|
|
50
|
+
journey: 'clickable wait, resolved-handle tap, visible assertion, selector journey and stable enabled-state assertion and labelled SeekBar convergence',
|
|
51
|
+
evidence: 'Result 43, enabled=true and adjusted slider label established by fresh hierarchy assertions', cleanup: 'Owned fixture removed in finally; no other app data or device settings changed' }))
|
|
52
|
+
} finally {
|
|
53
|
+
await client.close()
|
|
54
|
+
try { adb('uninstall', debuggerAppId) } finally { await rm(fixture.directory, { recursive: true, force: true }) }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { execCmd } from '../../../../src/utils/exec.js'
|
|
2
|
+
import { getAdbCmd } from '../../../../src/utils/android/utils.js'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
7
|
+
import { promises as fs } from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { inspectPerfetto } from '../../../helpers/perfetto.js'
|
|
10
|
+
|
|
11
|
+
async function run() {
|
|
12
|
+
if (process.env.SKIP_DEVICE_TESTS === '1') { console.log('System trace smoke: SKIPPED (SKIP_DEVICE_TESTS=1)'); return }
|
|
13
|
+
const client = new Client({ name: 'system-trace-smoke', version: '1.0.0' })
|
|
14
|
+
const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
|
|
15
|
+
const parse = (r: any) => JSON.parse(r.content[0].text)
|
|
16
|
+
const call = async (name: string, args: Record<string, unknown>) => parse(await client.callTool({ name, arguments: args }, undefined, { timeout: 120000 }))
|
|
17
|
+
const owned: string[] = [], artifacts: string[] = []
|
|
18
|
+
try {
|
|
19
|
+
await client.connect(transport)
|
|
20
|
+
const devices = await call('list_devices', {})
|
|
21
|
+
const device = devices.devices.find((d: any) => d.platform === 'android' && d.simulator)
|
|
22
|
+
assert.ok(device, 'A running Android emulator is required')
|
|
23
|
+
const target = { platform: 'android', deviceId: device.id }
|
|
24
|
+
const cap = await call('get_system_trace_capabilities', target)
|
|
25
|
+
assert.equal(cap.supported, true, JSON.stringify(cap))
|
|
26
|
+
const fixtureLaunch = await call('start_app', { ...target, appId: 'com.android.settings', verificationMode: 'none', responseMode: 'compact' })
|
|
27
|
+
assert.equal(fixtureLaunch.success, true)
|
|
28
|
+
const baseline = await call('get_ui_tree', { ...target, responseMode: 'debug' })
|
|
29
|
+
const settingsNode = baseline.elements?.find((e: any) => e.visible && e.resourceId?.startsWith('com.android.settings:id/'))
|
|
30
|
+
assert.ok(settingsNode, 'Fresh baseline must contain a visible Settings-owned UI resource')
|
|
31
|
+
const settingsSelector = { resource_id: settingsNode.resourceId }
|
|
32
|
+
const start = await call('start_system_trace', { ...target, durationMs: 30000 })
|
|
33
|
+
owned.push(start.trace_id)
|
|
34
|
+
assert.equal(start.state, 'recording', JSON.stringify(start))
|
|
35
|
+
const journey = await call('run_journey', { ...target, systemTraceId: start.trace_id,
|
|
36
|
+
steps: [
|
|
37
|
+
{ id: 'settings', type: 'start_app', appId: 'com.android.settings', verificationMode: 'none' },
|
|
38
|
+
{ id: 'settings-visible', type: 'assert', timeoutMs: 10000, assertion: { kind: 'element_visible', selector: settingsSelector } }
|
|
39
|
+
] })
|
|
40
|
+
assert.equal(journey.success, true, JSON.stringify(journey))
|
|
41
|
+
assert.equal(journey.steps[1].status, 'passed', 'The baseline Settings-owned UI element must remain semantically visible')
|
|
42
|
+
assert.equal(journey.evidence.trace_context.trace_id, start.trace_id)
|
|
43
|
+
assert.equal(journey.steps[0].result.evidence.trace_context.trace_id, start.trace_id)
|
|
44
|
+
assert.equal(journey.system_trace.coverage, 'covered')
|
|
45
|
+
const stop = await call('stop_system_trace', { traceId: start.trace_id })
|
|
46
|
+
if (stop.artifact) artifacts.push(stop.artifact.path)
|
|
47
|
+
assert.equal(stop.state, 'completed', JSON.stringify(stop)); assert.equal(stop.cleanup.status, 'complete')
|
|
48
|
+
assert.deepEqual(stop.correlation_ids, [journey.evidence.correlation_id])
|
|
49
|
+
const content = inspectPerfetto(await fs.readFile(stop.artifact.path), 30000)
|
|
50
|
+
assert.equal((await call('stop_system_trace', { traceId: start.trace_id })).artifact.path, stop.artifact.path)
|
|
51
|
+
const rejected = await call('run_journey', { ...target, systemTraceId: start.trace_id,
|
|
52
|
+
steps: [{ id: 'must-not-launch', type: 'start_app', appId: 'com.invalid.trace.smoke' }] })
|
|
53
|
+
assert.equal(rejected.error.code, 'TRACE_NOT_FOUND')
|
|
54
|
+
const cancel = await call('start_system_trace', { ...target, durationMs: 30000 })
|
|
55
|
+
owned.push(cancel.trace_id)
|
|
56
|
+
const cancelled = await call('cancel_system_trace', { traceId: cancel.trace_id })
|
|
57
|
+
assert.equal(cancelled.state, 'cancelled', JSON.stringify(cancelled))
|
|
58
|
+
assert.equal((await call('get_system_trace_status', { traceId: cancel.trace_id })).artifact, null)
|
|
59
|
+
const short = await call('start_system_trace', { ...target, durationMs: 1001 })
|
|
60
|
+
owned.push(short.trace_id)
|
|
61
|
+
assert.equal(short.state, 'recording', JSON.stringify(short))
|
|
62
|
+
let ended = short
|
|
63
|
+
for (let i = 0; i < 30 && ended.state === 'recording'; i++) {
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
65
|
+
ended = await call('get_system_trace_status', { traceId: short.trace_id })
|
|
66
|
+
}
|
|
67
|
+
if (ended.artifact) artifacts.push(ended.artifact.path)
|
|
68
|
+
assert.equal(ended.state, 'completed', JSON.stringify(ended)); assert.equal(ended.completion_reason, 'duration_limit')
|
|
69
|
+
const durationContent = inspectPerfetto(await fs.readFile(ended.artifact.path), 1001)
|
|
70
|
+
const shutdownClient = new Client({ name: 'system-trace-shutdown-smoke', version: '1.0.0' })
|
|
71
|
+
const shutdownTransport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
|
|
72
|
+
let shutdownTrace: any
|
|
73
|
+
let shutdownDiagnostics = ''
|
|
74
|
+
try {
|
|
75
|
+
await shutdownClient.connect(shutdownTransport)
|
|
76
|
+
shutdownTransport.stderr?.on('data', data => { shutdownDiagnostics += String(data) })
|
|
77
|
+
shutdownTrace = parse(await shutdownClient.callTool({ name: 'start_system_trace', arguments: { ...target, durationMs: 30000 } }, undefined, { timeout: 120000 }))
|
|
78
|
+
assert.equal(shutdownTrace.state, 'recording')
|
|
79
|
+
} finally { await shutdownClient.close() }
|
|
80
|
+
const remote = shutdownTrace.cleanup.remaining_resources.find((r: string) => r.startsWith('/data/misc/perfetto-traces/mcp-'))
|
|
81
|
+
assert.ok(remote)
|
|
82
|
+
const absent = await execCmd(getAdbCmd(), ['-s', device.id, 'shell', 'test', '!', '-e', remote], { timeout: 10000 })
|
|
83
|
+
assert.equal(absent.exitCode, 0, `Shutdown must stop and remove its owned device capture: ${shutdownTrace.trace_id}; ${shutdownDiagnostics}; ${absent.stderr}`)
|
|
84
|
+
const buildHash = createHash('sha256')
|
|
85
|
+
for (const file of ['dist/server.js', 'dist/observe/system-trace.js', 'dist/observe/system-trace-android.js', 'dist/server/handlers/tool-handlers.js', 'dist/observe/evidence.js']) buildHash.update(await fs.readFile(file))
|
|
86
|
+
const report = { status: 'PASS', build_sha256: buildHash.digest('hex'), runtime: process.version, server: client.getServerVersion(), device: device.id, osVersion: device.osVersion,
|
|
87
|
+
starting_state: 'System Settings fixture', journey: 'start Settings, assert baseline Settings-owned resource visible in UI tree; explicit trace association',
|
|
88
|
+
trace_id: start.trace_id, correlation_id: journey.evidence.correlation_id, phases: {
|
|
89
|
+
capture_started_at_ms: stop.capture_started_at_ms, capture_completed_at_ms: stop.capture_completed_at_ms },
|
|
90
|
+
artifact_bytes: stop.artifact.size_bytes, content, durationContent, cancel: 'PASS', expired_journey: 'PASS', active_shutdown: 'PASS',
|
|
91
|
+
cleanup: 'Device capture files removed; test host artifacts removed in finally. No settings or app data changed. Settings remains foreground.' }
|
|
92
|
+
console.log(JSON.stringify(report))
|
|
93
|
+
if (process.env.SYSTEM_TRACE_EVIDENCE_PATH) await fs.writeFile(process.env.SYSTEM_TRACE_EVIDENCE_PATH, JSON.stringify(report, null, 2) + '\n')
|
|
94
|
+
} finally {
|
|
95
|
+
const cleanupErrors: string[] = []
|
|
96
|
+
for (const traceId of owned) {
|
|
97
|
+
try { const r = await call('cancel_system_trace', { traceId }); if (r.artifact && !artifacts.includes(r.artifact.path)) artifacts.push(r.artifact.path); if (r.cleanup?.status !== 'complete') cleanupErrors.push(traceId) } catch { cleanupErrors.push(traceId) }
|
|
98
|
+
}
|
|
99
|
+
await client.close()
|
|
100
|
+
for (const file of artifacts) await fs.rm(path.dirname(file), { recursive: true, force: true })
|
|
101
|
+
assert.deepEqual(cleanupErrors, [], 'Owned trace cleanup failed')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -11,7 +11,7 @@ async function run() {
|
|
|
11
11
|
const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
|
|
12
12
|
const target = { platform: 'android', deviceId: 'emulator-5554' }
|
|
13
13
|
const report: any = { verified_on: new Date().toISOString(), device: target, package: 'com.ideamechanics.modul8.dev',
|
|
14
|
-
source: 'rebuilt local MCP stdio server', build_sha256: createHash('sha256').update(readFileSync('dist/server/tool-handlers.js')).digest('hex'),
|
|
14
|
+
source: 'rebuilt local MCP stdio server', build_sha256: createHash('sha256').update(readFileSync('dist/server/handlers/tool-handlers.js')).digest('hex'),
|
|
15
15
|
steps: [], status: 'UNRUN', cleanup: 'UNRUN' }
|
|
16
16
|
const call = async (name: string, args: Record<string, unknown>) => {
|
|
17
17
|
const response: any = await client.callTool({ name, arguments: { ...target, ...args } }, undefined, { timeout: 120000 })
|