mobile-debug-mcp 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +156 -129
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +16 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +104 -62
- package/dist/debugger/worker.js +53 -42
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3090
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +114 -75
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +4 -2
- package/dist/observe/readiness.js +29 -10
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace.js +22 -13
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +44 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +76 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +9 -1
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +2 -1
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +6 -1
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +314 -155
- package/src/debugger/worker.ts +51 -38
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3492
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +122 -70
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +5 -3
- package/src/observe/readiness.ts +39 -18
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace.ts +24 -15
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +1 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +19 -3
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +4 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +1 -1
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -1000
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/debugger-handlers.ts +0 -29
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
package/dist/observe/android.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { captureEvidence } from './evidence.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { deriveSnapshotMetadata } from "./snapshot-metadata.js";
|
|
11
|
-
import { remainingBudget } from '../utils/operation-budget.js';
|
|
12
|
-
import { createObservationMeasurement } from './performance-metrics.js';
|
|
13
|
-
const activeLogStreams = new Map();
|
|
2
|
+
import { getAndroidDeviceMetadata } from "../utils/android/utils.js";
|
|
3
|
+
import { activeLogStreams } from './android/context.js';
|
|
4
|
+
import { collectGetUITree } from './android/ui-tree.js';
|
|
5
|
+
import { collectGetLogs } from './android/logs.js';
|
|
6
|
+
import { collectCaptureScreen } from './android/screenshot.js';
|
|
7
|
+
import { collectGetScreenFingerprint } from './android/fingerprint.js';
|
|
8
|
+
import { startLogStream, stopLogStream, collectReadLogStream } from './android/log-stream.js';
|
|
9
|
+
import { collectGetCurrentScreen } from './android/current-screen.js';
|
|
14
10
|
export class AndroidObserve {
|
|
15
11
|
getLogStreamDevice(sessionId = 'default') { return activeLogStreams.get(sessionId)?.device ?? null; }
|
|
16
12
|
async getDeviceMetadata(appId, deviceId) {
|
|
@@ -19,527 +15,27 @@ export class AndroidObserve {
|
|
|
19
15
|
async getUITree(deviceId) {
|
|
20
16
|
return captureEvidence('ui_tree', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId));
|
|
21
17
|
}
|
|
22
|
-
async collectGetUITree(deviceId) {
|
|
23
|
-
const measurement = createObservationMeasurement('android');
|
|
24
|
-
let metadata;
|
|
25
|
-
try {
|
|
26
|
-
metadata = await measurement.timed('metadata_ms', () => getAndroidDeviceMetadata("", deviceId));
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
measurement.setOutcome('unavailable');
|
|
30
|
-
measurement.finish();
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
34
|
-
try {
|
|
35
|
-
// Get screen resolution first
|
|
36
|
-
const resolution = await measurement.timed('resolution_ms', () => getScreenResolution(deviceId));
|
|
37
|
-
if (resolution.width === 0 && resolution.height === 0) {
|
|
38
|
-
throw new Error("Failed to get screen resolution. Is the device connected and authorized?");
|
|
39
|
-
}
|
|
40
|
-
// Retry Logic
|
|
41
|
-
let xmlContent = '';
|
|
42
|
-
let attempts = 0;
|
|
43
|
-
const maxAttempts = 3;
|
|
44
|
-
while (attempts < maxAttempts) {
|
|
45
|
-
remainingBudget();
|
|
46
|
-
attempts++;
|
|
47
|
-
try {
|
|
48
|
-
// The first read starts immediately so it remains fresh for in-place UI changes.
|
|
49
|
-
// Delay only a retry after a failed or invalid acquisition.
|
|
50
|
-
if (attempts > 1) {
|
|
51
|
-
await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
|
|
52
|
-
}
|
|
53
|
-
remainingBudget();
|
|
54
|
-
xmlContent = await measurement.physicalRead(async () => {
|
|
55
|
-
await execAdb(['shell', 'uiautomator', 'dump', '/sdcard/ui.xml'], deviceId);
|
|
56
|
-
const content = await execAdb(['shell', 'cat', '/sdcard/ui.xml'], deviceId);
|
|
57
|
-
if (!content || !content.trim() || content.includes('ERROR:'))
|
|
58
|
-
throw new Error('Empty or invalid UI dump');
|
|
59
|
-
return content;
|
|
60
|
-
});
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
if (e instanceof Error && e.message === 'ACTION_TIMEOUT')
|
|
65
|
-
throw e;
|
|
66
|
-
console.error(`Attempt ${attempts} failed: ${e}`);
|
|
67
|
-
}
|
|
68
|
-
if (attempts === maxAttempts) {
|
|
69
|
-
throw new Error(`Failed to retrieve valid UI dump after ${maxAttempts} attempts.`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const parser = new XMLParser({
|
|
73
|
-
ignoreAttributes: false,
|
|
74
|
-
attributeNamePrefix: "@_"
|
|
75
|
-
});
|
|
76
|
-
const result = await measurement.timed('parse_ms', async () => parser.parse(xmlContent));
|
|
77
|
-
const elements = [];
|
|
78
|
-
// The root is usually hierarchy -> node
|
|
79
|
-
if (result.hierarchy && result.hierarchy.node) {
|
|
80
|
-
// If the root is an array (unlikely for root, but good to be safe) or single object
|
|
81
|
-
if (Array.isArray(result.hierarchy.node)) {
|
|
82
|
-
result.hierarchy.node.forEach((n) => traverseNode(n, elements));
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
traverseNode(result.hierarchy.node, elements);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const snapshotMetadata = deriveSnapshotMetadata(`android:${deviceInfo.id}`, {
|
|
89
|
-
screen: "",
|
|
90
|
-
resolution,
|
|
91
|
-
elements
|
|
92
|
-
}, 'ui_tree');
|
|
93
|
-
measurement.setOutcome('observed');
|
|
94
|
-
const response = measurement.bindObservation({
|
|
95
|
-
device: deviceInfo,
|
|
96
|
-
screen: "",
|
|
97
|
-
resolution,
|
|
98
|
-
elements,
|
|
99
|
-
...snapshotMetadata
|
|
100
|
-
});
|
|
101
|
-
measurement.finish();
|
|
102
|
-
return response;
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
const errorMessage = `Failed to get UI tree. ADB Path: '${getAdbCmd()}'. Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
106
|
-
console.error(errorMessage);
|
|
107
|
-
const snapshotMetadata = deriveSnapshotMetadata(`android:${deviceInfo.id}`, null, 'ui_tree');
|
|
108
|
-
measurement.setOutcome('unavailable');
|
|
109
|
-
measurement.finish();
|
|
110
|
-
return {
|
|
111
|
-
device: deviceInfo,
|
|
112
|
-
screen: "",
|
|
113
|
-
resolution: { width: 0, height: 0 },
|
|
114
|
-
elements: [],
|
|
115
|
-
...snapshotMetadata,
|
|
116
|
-
error: errorMessage
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
18
|
+
async collectGetUITree(deviceId) { return collectGetUITree(deviceId); }
|
|
120
19
|
async getLogs(filters = {}) {
|
|
121
20
|
return captureEvidence('logs', { platform: 'android', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters));
|
|
122
21
|
}
|
|
123
|
-
async collectGetLogs(filters = {}) {
|
|
124
|
-
const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters;
|
|
125
|
-
const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
|
|
126
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
127
|
-
try {
|
|
128
|
-
const effectiveLimit = typeof limit === 'number' && limit > 0 ? limit : 50;
|
|
129
|
-
// If appId provided, try to get pid to filter at source
|
|
130
|
-
let pidArg = null;
|
|
131
|
-
if (appId) {
|
|
132
|
-
try {
|
|
133
|
-
const pidOut = await execAdb(['shell', 'pidof', appId], deviceId).catch(() => '');
|
|
134
|
-
const pidTrim = (pidOut || '').trim();
|
|
135
|
-
if (pidTrim)
|
|
136
|
-
pidArg = pidTrim.split('\n')[0];
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
// Log a warning so failures to detect PID are visible during debugging
|
|
140
|
-
try {
|
|
141
|
-
console.warn(`getLogs: pid detection failed for appId=${appId}:`, err instanceof Error ? err.message : String(err));
|
|
142
|
-
}
|
|
143
|
-
catch { }
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const args = ['logcat', '-d', '-v', 'threadtime'];
|
|
147
|
-
// Apply pid filter via --pid if available
|
|
148
|
-
if (pidArg)
|
|
149
|
-
args.push(`--pid=${pidArg}`);
|
|
150
|
-
else if (pid)
|
|
151
|
-
args.push(`--pid=${pid}`);
|
|
152
|
-
// Apply tag/level filter if provided
|
|
153
|
-
if (tag && level) {
|
|
154
|
-
// Map verbose/debug/info/warn/error to single-letter levels
|
|
155
|
-
const levelMap = { 'VERBOSE': 'V', 'DEBUG': 'D', 'INFO': 'I', 'WARN': 'W', 'ERROR': 'E' };
|
|
156
|
-
const L = (levelMap[(level || '').toUpperCase()] || 'V');
|
|
157
|
-
args.push(`${tag}:${L}`);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
// Default: show all levels
|
|
161
|
-
args.push('*:V');
|
|
162
|
-
}
|
|
163
|
-
// Use -t to limit lines (apply early)
|
|
164
|
-
args.push('-t', effectiveLimit.toString());
|
|
165
|
-
const stdout = await execAdb(args, deviceId);
|
|
166
|
-
const allLines = stdout ? stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
167
|
-
// Parse lines into structured entries
|
|
168
|
-
const parsed = allLines.map(l => {
|
|
169
|
-
const entry = parseLogLine(l);
|
|
170
|
-
// normalize level
|
|
171
|
-
const levelChar = (entry.level || '').toUpperCase();
|
|
172
|
-
const levelMapChar = { 'V': 'VERBOSE', 'D': 'DEBUG', 'I': 'INFO', 'W': 'WARN', 'E': 'ERROR' };
|
|
173
|
-
const normLevel = levelMapChar[levelChar] || (entry.level && String(entry.level).toUpperCase()) || 'INFO';
|
|
174
|
-
const iso = entry._iso || (() => {
|
|
175
|
-
const d = new Date(entry.timestamp || '');
|
|
176
|
-
if (!isNaN(d.getTime()))
|
|
177
|
-
return d.toISOString();
|
|
178
|
-
return null;
|
|
179
|
-
})();
|
|
180
|
-
let pidNum = null;
|
|
181
|
-
if (entry.pid)
|
|
182
|
-
pidNum = Number(entry.pid);
|
|
183
|
-
else if (pidArg)
|
|
184
|
-
pidNum = Number(pidArg);
|
|
185
|
-
const pidVal = (pidNum !== null && !isNaN(pidNum)) ? pidNum : null;
|
|
186
|
-
return { timestamp: iso, level: normLevel, tag: entry.tag || '', pid: pidVal, message: entry.message || '' };
|
|
187
|
-
});
|
|
188
|
-
// Apply client-side filters: contains, since_seconds
|
|
189
|
-
let filtered = parsed;
|
|
190
|
-
if (contains)
|
|
191
|
-
filtered = filtered.filter(e => e.message && e.message.includes(contains));
|
|
192
|
-
if (since_seconds) {
|
|
193
|
-
const sinceMs = Date.now() - (since_seconds * 1000);
|
|
194
|
-
filtered = filtered.filter(e => {
|
|
195
|
-
if (!e.timestamp)
|
|
196
|
-
return false;
|
|
197
|
-
const t = new Date(e.timestamp).getTime();
|
|
198
|
-
return t >= sinceMs;
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
// If appId provided and no pidArg, try to filter by appId substring in message/tag
|
|
202
|
-
if (appId && !pidArg) {
|
|
203
|
-
const matched = filtered.filter(e => (e.message && e.message.includes(appId)) || (e.tag && e.tag.includes(appId)));
|
|
204
|
-
if (matched.length > 0)
|
|
205
|
-
filtered = matched;
|
|
206
|
-
}
|
|
207
|
-
// Ensure ordering oldest -> newest (by timestamp when available)
|
|
208
|
-
filtered.sort((a, b) => {
|
|
209
|
-
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
210
|
-
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
211
|
-
return ta - tb;
|
|
212
|
-
});
|
|
213
|
-
const limited = filtered.slice(-Math.max(0, effectiveLimit));
|
|
214
|
-
const source = pidArg ? 'pid' : (appId ? 'package' : 'broad');
|
|
215
|
-
const meta = { pidArg, appIdProvided: !!appId, filters: { tag, level, contains, since_seconds, limit: effectiveLimit }, pidExplicit: !!pid };
|
|
216
|
-
return { device: deviceInfo, logs: limited, logCount: limited.length, source, meta };
|
|
217
|
-
}
|
|
218
|
-
catch (e) {
|
|
219
|
-
console.error("Error fetching logs:", e);
|
|
220
|
-
return { device: deviceInfo, logs: [], logCount: 0, source: 'broad', meta: { error: e instanceof Error ? e.message : String(e) } };
|
|
221
|
-
}
|
|
222
|
-
}
|
|
22
|
+
async collectGetLogs(filters = {}) { return collectGetLogs(filters); }
|
|
223
23
|
async captureScreen(deviceId) {
|
|
224
24
|
return captureEvidence('screenshot', { platform: 'android', id: deviceId ?? '' }, () => this.collectCaptureScreen(deviceId));
|
|
225
25
|
}
|
|
226
|
-
async collectCaptureScreen(deviceId) {
|
|
227
|
-
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
228
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
229
|
-
return new Promise((resolve, reject) => {
|
|
230
|
-
const args = deviceId ? ['-s', deviceId, 'exec-out', 'screencap', '-p'] : ['exec-out', 'screencap', '-p'];
|
|
231
|
-
const child = spawn(getAdbCmd(), args);
|
|
232
|
-
const chunks = [];
|
|
233
|
-
let stderr = '';
|
|
234
|
-
child.stdout.on('data', (chunk) => {
|
|
235
|
-
chunks.push(Buffer.from(chunk));
|
|
236
|
-
});
|
|
237
|
-
child.stderr.on('data', (data) => {
|
|
238
|
-
stderr += data.toString();
|
|
239
|
-
});
|
|
240
|
-
const timeout = setTimeout(() => {
|
|
241
|
-
child.kill();
|
|
242
|
-
reject(new Error(`ADB screencap timed out after 10s`));
|
|
243
|
-
}, 10000);
|
|
244
|
-
child.on('close', async (code) => {
|
|
245
|
-
clearTimeout(timeout);
|
|
246
|
-
if (code !== 0) {
|
|
247
|
-
reject(new Error(stderr.trim() || `Screencap failed with code ${code}`));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const screenshotBuffer = Buffer.concat(chunks);
|
|
251
|
-
const screenshotBase64 = screenshotBuffer.toString('base64');
|
|
252
|
-
const parsed = parsePngSize(screenshotBuffer);
|
|
253
|
-
if (parsed.width > 0 && parsed.height > 0) {
|
|
254
|
-
// Attempt to convert to WebP (preferred) and provide JPEG fallback (awaited to avoid race)
|
|
255
|
-
try {
|
|
256
|
-
const sharpModule = await import('sharp');
|
|
257
|
-
const sharp = sharpModule && sharpModule.default ? sharpModule.default : sharpModule;
|
|
258
|
-
const buf = screenshotBuffer;
|
|
259
|
-
const img = sharp(buf);
|
|
260
|
-
const meta = await img.metadata().catch((err) => { console.error('sharp.metadata failed (Android):', err); return {}; });
|
|
261
|
-
const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
|
|
262
|
-
let webpBuf = null;
|
|
263
|
-
let jpegBuf = null;
|
|
264
|
-
try {
|
|
265
|
-
webpBuf = await img.webp({ quality: 80 }).toBuffer();
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
console.error('WebP conversion failed (Android):', err instanceof Error ? err.message : String(err));
|
|
269
|
-
webpBuf = null;
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
console.error('JPEG conversion failed (Android):', err instanceof Error ? err.message : String(err));
|
|
276
|
-
jpegBuf = null;
|
|
277
|
-
}
|
|
278
|
-
if (hasAlpha) {
|
|
279
|
-
if (webpBuf) {
|
|
280
|
-
const webpB64 = webpBuf.toString('base64');
|
|
281
|
-
const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null;
|
|
282
|
-
resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const pngB64 = buf.toString('base64');
|
|
286
|
-
resolve({ device: deviceInfo, screenshot: pngB64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } });
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (webpBuf) {
|
|
290
|
-
const webpB64 = webpBuf.toString('base64');
|
|
291
|
-
const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null;
|
|
292
|
-
resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } });
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (jpegBuf) {
|
|
296
|
-
resolve({ device: deviceInfo, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: parsed.width, height: parsed.height } });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
// No conversions succeeded; return original PNG
|
|
300
|
-
resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } });
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
console.error('Screenshot conversion pipeline failed (Android):', err instanceof Error ? err.message : String(err));
|
|
305
|
-
// Conversion failed - fall back to original PNG with parsed resolution
|
|
306
|
-
resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
// Fallback to querying wm size if parsing failed
|
|
312
|
-
execAdb(['shell', 'wm', 'size'], deviceId)
|
|
313
|
-
.then(sizeStdout => {
|
|
314
|
-
let width = 0;
|
|
315
|
-
let height = 0;
|
|
316
|
-
const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/);
|
|
317
|
-
if (match) {
|
|
318
|
-
width = parseInt(match[1], 10);
|
|
319
|
-
height = parseInt(match[2], 10);
|
|
320
|
-
}
|
|
321
|
-
resolve({
|
|
322
|
-
device: deviceInfo,
|
|
323
|
-
screenshot: screenshotBase64,
|
|
324
|
-
screenshot_mime: 'image/png',
|
|
325
|
-
resolution: { width, height }
|
|
326
|
-
});
|
|
327
|
-
})
|
|
328
|
-
.catch(() => {
|
|
329
|
-
resolve({
|
|
330
|
-
device: deviceInfo,
|
|
331
|
-
screenshot: screenshotBase64,
|
|
332
|
-
screenshot_mime: 'image/png',
|
|
333
|
-
resolution: { width: 0, height: 0 }
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
child.on('error', (err) => {
|
|
339
|
-
clearTimeout(timeout);
|
|
340
|
-
reject(err);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
}
|
|
26
|
+
async collectCaptureScreen(deviceId) { return collectCaptureScreen(deviceId); }
|
|
344
27
|
async getCurrentScreen(deviceId) {
|
|
345
28
|
return captureEvidence('activity', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetCurrentScreen(deviceId));
|
|
346
29
|
}
|
|
347
|
-
async collectGetCurrentScreen(deviceId) {
|
|
348
|
-
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
349
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
350
|
-
try {
|
|
351
|
-
const output = await execAdb(['shell', 'dumpsys', 'activity', 'activities'], deviceId, { timeout: 10000 });
|
|
352
|
-
const lines = output.split('\n');
|
|
353
|
-
let resumedLine = lines.find(line => /^\s*mResumedActivity:/.test(line));
|
|
354
|
-
if (!resumedLine) {
|
|
355
|
-
resumedLine = lines.find(line => /^\s*ResumedActivity:/.test(line));
|
|
356
|
-
}
|
|
357
|
-
if (!resumedLine) {
|
|
358
|
-
return {
|
|
359
|
-
device: deviceInfo,
|
|
360
|
-
package: "",
|
|
361
|
-
activity: "",
|
|
362
|
-
shortActivity: "",
|
|
363
|
-
error: "Could not find 'mResumedActivity' in dumpsys output"
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
const match = resumedLine.match(/ActivityRecord\{[^ ]*(?:\s+[^ ]+)*\s+([^\/ ]+)\/([^ \{}]+)[^}]*\}/);
|
|
367
|
-
if (match) {
|
|
368
|
-
const packageName = match[1];
|
|
369
|
-
let activityName = match[2];
|
|
370
|
-
if (activityName.startsWith('.')) {
|
|
371
|
-
activityName = packageName + activityName;
|
|
372
|
-
}
|
|
373
|
-
const shortActivity = activityName.split('.').pop() || activityName;
|
|
374
|
-
return {
|
|
375
|
-
device: deviceInfo,
|
|
376
|
-
package: packageName,
|
|
377
|
-
activity: activityName,
|
|
378
|
-
shortActivity: shortActivity
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
return {
|
|
383
|
-
device: deviceInfo,
|
|
384
|
-
package: "",
|
|
385
|
-
activity: "",
|
|
386
|
-
shortActivity: "",
|
|
387
|
-
error: `Found resumed activity line but failed to parse: '${resumedLine.trim()}'`
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
catch (e) {
|
|
392
|
-
return {
|
|
393
|
-
device: deviceInfo,
|
|
394
|
-
package: "",
|
|
395
|
-
activity: "",
|
|
396
|
-
shortActivity: "",
|
|
397
|
-
error: e instanceof Error ? e.message : String(e)
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
}
|
|
30
|
+
async collectGetCurrentScreen(deviceId) { return collectGetCurrentScreen(deviceId); }
|
|
401
31
|
async getScreenFingerprint(deviceId) {
|
|
402
32
|
return captureEvidence('fingerprint', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId));
|
|
403
33
|
}
|
|
404
|
-
async collectGetScreenFingerprint(deviceId) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (!tree || tree.error)
|
|
408
|
-
return { fingerprint: null, error: tree.error };
|
|
409
|
-
const current = await this.getCurrentScreen(deviceId).catch(() => null);
|
|
410
|
-
return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms };
|
|
411
|
-
}
|
|
412
|
-
catch (e) {
|
|
413
|
-
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async startLogStream(packageName, level = 'error', deviceId, sessionId = 'default') {
|
|
417
|
-
try {
|
|
418
|
-
const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '');
|
|
419
|
-
const pid = (pidOutput || '').trim();
|
|
420
|
-
if (!pid)
|
|
421
|
-
return { success: false, error: 'app_not_running' };
|
|
422
|
-
const levelMap = { error: '*:E', warn: '*:W', info: '*:I', debug: '*:D' };
|
|
423
|
-
const filter = levelMap[level] || levelMap['error'];
|
|
424
|
-
if (activeLogStreams.has(sessionId)) {
|
|
425
|
-
try {
|
|
426
|
-
activeLogStreams.get(sessionId).proc.kill();
|
|
427
|
-
}
|
|
428
|
-
catch { }
|
|
429
|
-
activeLogStreams.delete(sessionId);
|
|
430
|
-
}
|
|
431
|
-
const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter];
|
|
432
|
-
const proc = spawn(getAdbCmd(), args);
|
|
433
|
-
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
434
|
-
const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
|
|
435
|
-
const stream = createWriteStream(file, { flags: 'a' });
|
|
436
|
-
proc.stdout.on('data', (chunk) => {
|
|
437
|
-
const text = chunk.toString();
|
|
438
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
439
|
-
for (const l of lines) {
|
|
440
|
-
const entry = parseLogLine(l);
|
|
441
|
-
stream.write(JSON.stringify(entry) + '\n');
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
proc.stderr.on('data', (chunk) => {
|
|
445
|
-
const text = chunk.toString();
|
|
446
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
447
|
-
for (const l of lines) {
|
|
448
|
-
const entry = { timestamp: '', level: 'E', tag: 'adb', message: l };
|
|
449
|
-
stream.write(JSON.stringify(entry) + '\n');
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
proc.on('close', () => {
|
|
453
|
-
stream.end();
|
|
454
|
-
if (activeLogStreams.get(sessionId)?.proc === proc)
|
|
455
|
-
activeLogStreams.delete(sessionId);
|
|
456
|
-
});
|
|
457
|
-
activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } });
|
|
458
|
-
return { success: true, stream_started: true };
|
|
459
|
-
}
|
|
460
|
-
catch (e) {
|
|
461
|
-
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
async stopLogStream(sessionId = 'default') {
|
|
465
|
-
const entry = activeLogStreams.get(sessionId);
|
|
466
|
-
if (!entry)
|
|
467
|
-
return { success: true };
|
|
468
|
-
try {
|
|
469
|
-
entry.proc.kill();
|
|
470
|
-
}
|
|
471
|
-
catch { }
|
|
472
|
-
activeLogStreams.delete(sessionId);
|
|
473
|
-
return { success: true };
|
|
474
|
-
}
|
|
34
|
+
async collectGetScreenFingerprint(deviceId) { return collectGetScreenFingerprint(this, deviceId); }
|
|
35
|
+
async startLogStream(packageName, level = 'error', deviceId, sessionId = 'default') { return startLogStream(packageName, level, deviceId, sessionId); }
|
|
36
|
+
async stopLogStream(sessionId = 'default') { return stopLogStream(sessionId); }
|
|
475
37
|
async readLogStream(sessionId = 'default', limit = 100, since) {
|
|
476
38
|
return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since));
|
|
477
39
|
}
|
|
478
|
-
async collectReadLogStream(sessionId = 'default', limit = 100, since) {
|
|
479
|
-
const entry = activeLogStreams.get(sessionId);
|
|
480
|
-
let file;
|
|
481
|
-
if (entry && entry.file)
|
|
482
|
-
file = entry.file;
|
|
483
|
-
else {
|
|
484
|
-
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
485
|
-
const candidate = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
|
|
486
|
-
file = candidate;
|
|
487
|
-
}
|
|
488
|
-
try {
|
|
489
|
-
const data = await fsPromises.readFile(file, 'utf8').catch(() => '');
|
|
490
|
-
if (!data)
|
|
491
|
-
return { entries: [], crash_summary: { crash_detected: false } };
|
|
492
|
-
const lines = data.split(/\r?\n/).filter(Boolean);
|
|
493
|
-
const parsed = lines.map(l => {
|
|
494
|
-
try {
|
|
495
|
-
const obj = JSON.parse(l);
|
|
496
|
-
if (typeof obj._iso === 'undefined') {
|
|
497
|
-
let iso = null;
|
|
498
|
-
if (obj.timestamp) {
|
|
499
|
-
const d = new Date(obj.timestamp);
|
|
500
|
-
if (!isNaN(d.getTime()))
|
|
501
|
-
iso = d.toISOString();
|
|
502
|
-
}
|
|
503
|
-
obj._iso = iso;
|
|
504
|
-
}
|
|
505
|
-
if (typeof obj.crash === 'undefined') {
|
|
506
|
-
const msg = (obj.message || '').toString();
|
|
507
|
-
const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/);
|
|
508
|
-
if (/FATAL EXCEPTION/i.test(msg) || exMatch) {
|
|
509
|
-
obj.crash = true;
|
|
510
|
-
if (exMatch)
|
|
511
|
-
obj.exception = exMatch[1];
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
obj.crash = false;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return obj;
|
|
518
|
-
}
|
|
519
|
-
catch {
|
|
520
|
-
return { message: l, _iso: null, crash: false };
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
let filtered = parsed;
|
|
524
|
-
if (since) {
|
|
525
|
-
let sinceMs = null;
|
|
526
|
-
if (/^\d+$/.test(since))
|
|
527
|
-
sinceMs = Number(since);
|
|
528
|
-
else {
|
|
529
|
-
const sDate = new Date(since);
|
|
530
|
-
if (!isNaN(sDate.getTime()))
|
|
531
|
-
sinceMs = sDate.getTime();
|
|
532
|
-
}
|
|
533
|
-
if (sinceMs !== null)
|
|
534
|
-
filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
|
|
535
|
-
}
|
|
536
|
-
const entries = filtered.slice(-Math.max(0, limit));
|
|
537
|
-
const crashEntry = entries.find(e => e.crash);
|
|
538
|
-
const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
|
|
539
|
-
return { entries, crash_summary };
|
|
540
|
-
}
|
|
541
|
-
catch {
|
|
542
|
-
return { entries: [], crash_summary: { crash_detected: false } };
|
|
543
|
-
}
|
|
544
|
-
}
|
|
40
|
+
async collectReadLogStream(sessionId = 'default', limit = 100, since) { return collectReadLogStream(sessionId, limit, since); }
|
|
545
41
|
}
|