mobile-debug-mcp 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +156 -129
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +16 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +104 -62
- package/dist/debugger/worker.js +53 -42
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3090
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +114 -75
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +4 -2
- package/dist/observe/readiness.js +29 -10
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace.js +22 -13
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +44 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +76 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +9 -1
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +2 -1
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +6 -1
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +314 -155
- package/src/debugger/worker.ts +51 -38
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3492
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +122 -70
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +5 -3
- package/src/observe/readiness.ts +39 -18
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace.ts +24 -15
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +1 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +19 -3
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +4 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +1 -1
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -1000
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/debugger-handlers.ts +0 -29
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Fields consumed from persisted NDJSON log events; other fields pass through. */
|
|
2
|
+
export interface StreamLogEntry {
|
|
3
|
+
timestamp?: string | number | null
|
|
4
|
+
_iso?: string | null
|
|
5
|
+
level?: string
|
|
6
|
+
message?: string
|
|
7
|
+
exception?: string
|
|
8
|
+
crash?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LogFilters { appId?: string; deviceId?: string; pid?: number; tag?: string; level?: string; contains?: string; since_seconds?: number; limit?: number }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { execAdb, parseLogLine } from '
|
|
2
|
-
import { execCommand } from '
|
|
1
|
+
import { execAdb, parseLogLine } from '../../utils/android/utils.js'
|
|
2
|
+
import { execCommand } from '../../utils/ios/utils.js'
|
|
3
3
|
|
|
4
4
|
export type NetworkErrorCode =
|
|
5
5
|
| 'timeout'
|
|
@@ -42,7 +42,7 @@ export async function getAppReadiness(args: Target & { timeoutMs?: number; stabi
|
|
|
42
42
|
const tree = await ToolsObserve.getUITreeHandler({ platform: device!.platform as 'android' | 'ios', deviceId: device!.id })
|
|
43
43
|
remainingBudget()
|
|
44
44
|
tracker.observe(tree, captureStarted)
|
|
45
|
-
if (isDeviceUnavailable(
|
|
45
|
+
if (isDeviceUnavailable(tree?.error)) { failureCode = 'DEVICE_UNAVAILABLE'; return }
|
|
46
46
|
} catch (error) {
|
|
47
47
|
if (/ACTION_TIMEOUT/.test(String(error))) throw error
|
|
48
48
|
tracker.unavailable()
|
|
@@ -64,8 +64,10 @@ export async function getAppReadiness(args: Target & { timeoutMs?: number; stabi
|
|
|
64
64
|
if (debugPause) return { success: !wait, device, readiness: debugPause, elapsed_ms: performance.now() - started, ...(wait ? { failure_code: 'DEBUGGER_PAUSED' } : {}) }
|
|
65
65
|
const state = tracker.summary.state
|
|
66
66
|
const success = !failureCode && (wait ? state === 'settled' : state !== 'unknown')
|
|
67
|
-
return {
|
|
68
|
-
|
|
67
|
+
return {
|
|
68
|
+
success, device, readiness: tracker.summary, elapsed_ms: performance.now() - started,
|
|
69
|
+
...(!success ? { failure_code: failureCode ?? (wait ? 'READINESS_TIMEOUT' : 'READINESS_UNAVAILABLE') } : {})
|
|
70
|
+
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export async function withReadyWait<T extends object | undefined, F extends object>(target: Target, timeoutMs: number, operation: (device: DeviceInfo) => Promise<T>, failure: (code: string, elapsedMs: number, samples: number) => F) {
|
package/src/observe/readiness.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GetUITreeResponse } from '../types.js'
|
|
1
2
|
import { pausedReadiness } from '../debugger/guard.js'
|
|
2
3
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
4
|
import type { ReadinessSummary } from '../types.js'
|
|
@@ -17,19 +18,21 @@ export class ReadinessTracker {
|
|
|
17
18
|
summary: ReadinessSummary
|
|
18
19
|
|
|
19
20
|
constructor(private platform: string, private deviceId: string, private stabilityMs = 500) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
this.summary = {
|
|
22
|
+
state: 'unknown', scope: 'device_ui', basis: 'insufficient', captured_at_ms: null,
|
|
23
|
+
sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
|
|
24
|
+
evidence: [], limitations: [...baselineLimitations, this.platform === 'android' ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', 'INSUFFICIENT_SAMPLES'], capabilities: { ...capabilities, debugger_paused: this.platform === 'android' }
|
|
25
|
+
}
|
|
25
26
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
unavailable(code = 'OBSERVATION_FAILED'): ReadinessSummary {
|
|
29
30
|
this.signature = null
|
|
30
|
-
this.summary = {
|
|
31
|
+
this.summary = {
|
|
32
|
+
...this.summary, state: 'unknown', basis: 'insufficient', stable_for_ms: 0,
|
|
31
33
|
sample_count: this.summary.sample_count + 1, limitations: [...baselineLimitations, this.platform === 'android' ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', code],
|
|
32
|
-
evidence: [...this.summary.evidence, { code, source: 'ui_tree', captured_at_ms: null, snapshot_revision: null }].slice(-8)
|
|
34
|
+
evidence: [...this.summary.evidence, { code, source: 'ui_tree', captured_at_ms: null, snapshot_revision: null }].slice(-8)
|
|
35
|
+
}
|
|
33
36
|
return this.summary
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -38,27 +41,24 @@ export class ReadinessTracker {
|
|
|
38
41
|
return this.summary
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
observe(
|
|
44
|
+
observe(sample: unknown, acquisitionStartedAtMs: number, now = performance.now()): ReadinessSummary {
|
|
42
45
|
const paused = this.platform === 'android' ? pausedReadiness(this.deviceId) : null
|
|
43
46
|
if (paused) { this.summary = paused; return this.summary }
|
|
47
|
+
const tree = sample as GetUITreeResponse | null | undefined
|
|
44
48
|
if (tree?.error) return this.unavailable('OBSERVATION_FAILED')
|
|
45
|
-
if (!tree
|
|
46
|
-
tree.snapshot_session_id !== getSnapshotSessionId() || !Number.isSafeInteger(tree.snapshot_revision) || tree.snapshot_revision <= 0 ||
|
|
47
|
-
!Number.isFinite(tree.captured_at_ms) || tree.captured_at_ms < acquisitionStartedAtMs || tree.captured_at_ms <= 0 ||
|
|
48
|
-
!Array.isArray(tree.elements) || !tree.elements.length || tree.elements.some((element: unknown) => !element || typeof element !== 'object') ||
|
|
49
|
-
!Number.isFinite(tree.resolution?.width) || !Number.isFinite(tree.resolution?.height) || tree.resolution.width <= 0 || tree.resolution.height <= 0) return this.unavailable('OBSERVATION_INVALID')
|
|
49
|
+
if (!validReadinessSample(tree, this.platform, this.deviceId, acquisitionStartedAtMs)) return this.unavailable('OBSERVATION_INVALID')
|
|
50
50
|
const signature = computeSnapshotSignature(tree)
|
|
51
51
|
if (!signature) return this.unavailable('OBSERVATION_INVALID')
|
|
52
|
-
const nativeLoading = tree.elements.some((element
|
|
53
|
-
const textLoading = tree.elements.some((element
|
|
52
|
+
const nativeLoading = tree.elements.some((element) => element.visible === true && ['android.widget.ProgressBar', 'ActivityIndicator', 'ProgressIndicator'].includes(element.type))
|
|
53
|
+
const textLoading = tree.elements.some((element) => element.visible === true && [element.text, element.contentDescription].some(label =>
|
|
54
54
|
typeof label === 'string' && /^(loading|please\s+wait)[.\s…]*$/i.test(label.trim())))
|
|
55
55
|
const previous = this.signature
|
|
56
56
|
const unchanged = previous === signature
|
|
57
57
|
if (!unchanged || nativeLoading || textLoading) this.stableSince = now
|
|
58
58
|
this.signature = signature
|
|
59
59
|
const stableFor = unchanged && !nativeLoading && !textLoading ? Math.max(0, now - this.stableSince) : 0
|
|
60
|
-
const state = nativeLoading || textLoading
|
|
61
|
-
const code = nativeLoading
|
|
60
|
+
const state = readinessState(nativeLoading || textLoading, previous, unchanged, stableFor, this.stabilityMs)
|
|
61
|
+
const code = readinessCode(nativeLoading, textLoading, state)
|
|
62
62
|
this.summary = {
|
|
63
63
|
state, scope: 'device_ui', basis: textLoading && !nativeLoading ? 'heuristic' : state === 'unknown' ? 'insufficient' : 'observed',
|
|
64
64
|
captured_at_ms: tree.captured_at_ms, snapshot_session_id: tree.snapshot_session_id, snapshot_revision: tree.snapshot_revision,
|
|
@@ -70,6 +70,20 @@ export class ReadinessTracker {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function validReadinessSample(tree: GetUITreeResponse | null | undefined, platform: string, deviceId: string, acquisitionStartedAtMs: number): tree is GetUITreeResponse & { snapshot_session_id: string } {
|
|
74
|
+
return !!tree && validIdentity(tree, platform, deviceId) && validFreshness(tree, acquisitionStartedAtMs) && validGeometry(tree)
|
|
75
|
+
}
|
|
76
|
+
function validIdentity(tree: GetUITreeResponse, platform: string, deviceId: string) {
|
|
77
|
+
return tree.device?.platform === platform && tree.device?.id === deviceId &&
|
|
78
|
+
tree.snapshot_session_id === getSnapshotSessionId() && Number.isSafeInteger(tree.snapshot_revision) && tree.snapshot_revision > 0
|
|
79
|
+
}
|
|
80
|
+
function validFreshness(tree: GetUITreeResponse, startedAtMs: number) {
|
|
81
|
+
return Number.isFinite(tree.captured_at_ms) && !(tree.captured_at_ms < startedAtMs) && tree.captured_at_ms > 0
|
|
82
|
+
}
|
|
83
|
+
function validGeometry(tree: GetUITreeResponse) {
|
|
84
|
+
return Array.isArray(tree.elements) && tree.elements.length > 0 && !tree.elements.some((element: unknown) => !element || typeof element !== 'object') &&
|
|
85
|
+
Number.isFinite(tree.resolution?.width) && Number.isFinite(tree.resolution?.height) && tree.resolution.width > 0 && tree.resolution.height > 0
|
|
86
|
+
}
|
|
73
87
|
const waits = new AsyncLocalStorage<ReadinessTracker>()
|
|
74
88
|
export function currentReadinessWait() { return waits.getStore() }
|
|
75
89
|
export function withReadinessTracker<T>(tracker: ReadinessTracker, operation: () => Promise<T>) { return waits.run(tracker, operation) }
|
|
@@ -79,6 +93,13 @@ export function observeReadinessWait(tree: unknown, startedAtMs: number) {
|
|
|
79
93
|
if (!tracker) return
|
|
80
94
|
remainingBudget()
|
|
81
95
|
const summary = tracker.observe(tree, startedAtMs)
|
|
82
|
-
if (isDeviceUnavailable((tree as
|
|
96
|
+
if (isDeviceUnavailable((tree as { error?: unknown } | null)?.error)) throw new Error('READINESS_DEVICE_UNAVAILABLE')
|
|
83
97
|
if (summary.limitations.some(code => ['OBSERVATION_INVALID', 'OBSERVATION_FAILED'].includes(code))) throw new Error('READINESS_SAMPLE_INVALID')
|
|
84
98
|
}
|
|
99
|
+
|
|
100
|
+
function readinessState(loading: boolean, previous: string | null, unchanged: boolean, stableFor: number, stabilityMs: number): ReadinessSummary['state'] {
|
|
101
|
+
return loading ? 'loading' : !previous ? 'unknown' : !unchanged ? 'transitioning' : stableFor >= stabilityMs ? 'settled' : 'unknown'
|
|
102
|
+
}
|
|
103
|
+
function readinessCode(nativeLoading: boolean, textLoading: boolean, state: ReadinessSummary['state']) {
|
|
104
|
+
return nativeLoading ? 'NATIVE_LOADING_INDICATOR' : textLoading ? 'LOADING_LABEL' : state === 'transitioning' ? 'SNAPSHOT_CHANGED' : state === 'settled' ? 'SNAPSHOT_STABLE' : 'INSUFFICIENT_SAMPLES'
|
|
105
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Metadata } from 'sharp'
|
|
2
|
+
interface EncodedScreenshot { screenshot: string; screenshot_mime: string; screenshot_fallback?: string | null; screenshot_fallback_mime?: string }
|
|
3
|
+
async function convert(buffer: Buffer, platform: 'android' | 'ios') {
|
|
4
|
+
const { default: sharp } = await import('sharp')
|
|
5
|
+
const image = sharp(buffer)
|
|
6
|
+
const meta = await image.metadata().catch((error: unknown) => {
|
|
7
|
+
console.error(platform === 'android' ? 'sharp.metadata failed (Android):' : 'sharp.metadata failed:', error)
|
|
8
|
+
return {} as Metadata
|
|
9
|
+
})
|
|
10
|
+
const hasAlpha = !!meta.hasAlpha || !!(meta.channels && meta.channels > 3)
|
|
11
|
+
let webp: Buffer | null = null, jpeg: Buffer | null = null
|
|
12
|
+
try { webp = await image.webp({ quality: 80 }).toBuffer() }
|
|
13
|
+
catch (error) { console.error(`WebP conversion failed (${platform === 'android' ? 'Android' : 'iOS'}):`, error instanceof Error ? error.message : String(error)) }
|
|
14
|
+
try { jpeg = await image.jpeg({ quality: 80 }).toBuffer() }
|
|
15
|
+
catch (error) { console.error(`JPEG conversion failed (${platform === 'android' ? 'Android' : 'iOS'}):`, error instanceof Error ? error.message : String(error)) }
|
|
16
|
+
return { hasAlpha, webp, jpeg }
|
|
17
|
+
}
|
|
18
|
+
export async function encodeScreenshot(buffer: Buffer, platform: 'android' | 'ios'): Promise<EncodedScreenshot> {
|
|
19
|
+
const png = buffer.toString('base64')
|
|
20
|
+
try {
|
|
21
|
+
const { hasAlpha, webp, jpeg } = await convert(buffer, platform)
|
|
22
|
+
if (webp) {
|
|
23
|
+
if (hasAlpha && platform === 'ios') return { screenshot: webp.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: png, screenshot_fallback_mime: 'image/png' }
|
|
24
|
+
const fallback = jpeg ? jpeg.toString('base64') : platform === 'android' ? null : undefined
|
|
25
|
+
return { screenshot: webp.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: fallback, screenshot_fallback_mime: jpeg ? 'image/jpeg' : undefined }
|
|
26
|
+
}
|
|
27
|
+
if (!hasAlpha && jpeg) return { screenshot: jpeg.toString('base64'), screenshot_mime: 'image/jpeg' }
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`Screenshot conversion pipeline failed (${platform === 'android' ? 'Android' : 'iOS'}):`, error instanceof Error ? error.message : String(error))
|
|
30
|
+
}
|
|
31
|
+
return { screenshot: png, screenshot_mime: 'image/png' }
|
|
32
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CaptureDebugSnapshotRawResponse } from '../types.js'
|
|
2
|
+
import type { ToolsObserve } from './index.js'
|
|
3
|
+
import { omitEvidence } from './evidence.js'
|
|
4
|
+
import { measure } from '../utils/timing.js'
|
|
5
|
+
import { deriveSnapshotMetadata } from './snapshot-metadata.js'
|
|
6
|
+
import { deriveSnapshotSemantic } from './snapshot-semantic.js'
|
|
7
|
+
import { snapshotLog, streamLog, type SnapshotLogEntry } from './snapshot-logs.js'
|
|
8
|
+
export interface SnapshotInput { reason?: string; includeLogs?: boolean; includeScreenshot?: boolean; logLines?: number; platform?: 'android' | 'ios'; appId?: string; deviceId?: string; sessionId?: string }
|
|
9
|
+
type Dependencies = Pick<typeof ToolsObserve, 'captureScreenshotHandler' | 'getCurrentScreenHandler' | 'getScreenFingerprintHandler' | 'getUITreeHandler' | 'readLogStreamHandler' | 'getLogsHandler'>
|
|
10
|
+
async function settle<T extends Record<string, Promise<unknown>>>(tasks: T) {
|
|
11
|
+
const results = await Promise.allSettled(Object.values(tasks))
|
|
12
|
+
return Object.fromEntries(Object.keys(tasks).map((key, index) => [key, results[index]])) as { [K in keyof T]: PromiseSettledResult<Awaited<T[K]>> }
|
|
13
|
+
}
|
|
14
|
+
function apply<T>(result: PromiseSettledResult<T>, success: (value: T) => void, failure: (message: string) => void) {
|
|
15
|
+
if (result.status === 'fulfilled') success(result.value)
|
|
16
|
+
else failure(result.reason instanceof Error ? result.reason.message : String(result.reason))
|
|
17
|
+
}
|
|
18
|
+
async function collect(raw: CaptureDebugSnapshotRawResponse, input: SnapshotInput, deps: Dependencies) {
|
|
19
|
+
const { platform, deviceId, includeScreenshot = true, includeLogs = true, sessionId, logLines = 200 } = input
|
|
20
|
+
const results = await settle({
|
|
21
|
+
screenshot: includeScreenshot ? measure('screenshot_ms', () => deps.captureScreenshotHandler({ platform, deviceId })) : Promise.resolve(null),
|
|
22
|
+
currentScreen: !platform || platform === 'android' ? deps.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
|
|
23
|
+
fingerprint: deps.getScreenFingerprintHandler({ platform, deviceId }),
|
|
24
|
+
uiTree: deps.getUITreeHandler({ platform, deviceId }),
|
|
25
|
+
readLogStream: includeLogs ? deps.readLogStreamHandler({ platform, deviceId, sessionId: sessionId || 'default', limit: logLines }) : Promise.resolve({ entries: [] })
|
|
26
|
+
})
|
|
27
|
+
let entries: unknown[] = []
|
|
28
|
+
apply(results.screenshot, value => { raw.screenshot = value && value.screenshot ? value.screenshot : null }, error => { raw.screenshot_error = error })
|
|
29
|
+
apply(results.currentScreen, value => { raw.activity = value && (value.activity || value.shortActivity) ? value.activity || value.shortActivity : raw.activity || '' }, error => { raw.activity_error = error })
|
|
30
|
+
apply(results.fingerprint, value => {
|
|
31
|
+
if (value && value.fingerprint) raw.fingerprint = value.fingerprint
|
|
32
|
+
if (value && value.activity) raw.activity = raw.activity || value.activity
|
|
33
|
+
if (value && value.error) raw.fingerprint_error = value.error
|
|
34
|
+
}, error => { raw.fingerprint = null; raw.fingerprint_error = error })
|
|
35
|
+
apply(results.uiTree, value => { raw.ui_tree = value; if (value && value.error) raw.ui_tree_error = value.error }, error => { raw.ui_tree = null; raw.ui_tree_error = error })
|
|
36
|
+
apply(results.readLogStream, value => { entries = Array.isArray(value?.entries) ? value.entries : [] }, error => { raw.logs_error = error })
|
|
37
|
+
return entries
|
|
38
|
+
}
|
|
39
|
+
async function collectLogs(entries: unknown[], input: SnapshotInput, deps: Dependencies): Promise<SnapshotLogEntry[]> {
|
|
40
|
+
if (entries.length) return entries.map(streamLog)
|
|
41
|
+
const { platform, appId, deviceId, logLines = 200 } = input
|
|
42
|
+
const response = await deps.getLogsHandler({ platform, appId, deviceId, lines: logLines })
|
|
43
|
+
return (response && response.logs ? response.logs : []).slice(-Math.max(0, logLines)).map(snapshotLog)
|
|
44
|
+
}
|
|
45
|
+
function metadata(raw: CaptureDebugSnapshotRawResponse, input: SnapshotInput) {
|
|
46
|
+
const key = raw.ui_tree?.device ? `${raw.ui_tree.device.platform}:${raw.ui_tree.device.id}` : `${input.platform || 'unknown'}:${input.deviceId || 'default'}`
|
|
47
|
+
const value = deriveSnapshotMetadata(key, raw.ui_tree, 'snapshot', raw.ui_tree?.snapshot_revision ? null : raw.fingerprint || raw.activity || null)
|
|
48
|
+
raw.snapshot_revision = raw.ui_tree?.snapshot_revision ?? value.snapshot_revision
|
|
49
|
+
raw.captured_at_ms = raw.ui_tree?.captured_at_ms ?? value.captured_at_ms
|
|
50
|
+
raw.snapshot_delta = raw.ui_tree?.snapshot_delta ?? value.snapshot_delta ?? null
|
|
51
|
+
raw.loading_state = raw.ui_tree?.loading_state ?? value.loading_state
|
|
52
|
+
}
|
|
53
|
+
export async function collectDebugSnapshot(input: SnapshotInput = {}, deps: Dependencies) {
|
|
54
|
+
if (input.includeScreenshot === false) omitEvidence('screenshot')
|
|
55
|
+
if (input.includeLogs === false) omitEvidence('logs')
|
|
56
|
+
const timestamp = Date.now()
|
|
57
|
+
const raw: CaptureDebugSnapshotRawResponse = {
|
|
58
|
+
timestamp, snapshot_revision: 0, captured_at_ms: timestamp,
|
|
59
|
+
reason: input.reason || '', activity: null, fingerprint: null, screenshot: null, ui_tree: null, logs: []
|
|
60
|
+
}
|
|
61
|
+
const entries = await collect(raw, input, deps)
|
|
62
|
+
if (input.includeLogs !== false) {
|
|
63
|
+
try { raw.logs = await collectLogs(entries, input, deps) }
|
|
64
|
+
catch (error) { raw.logs = []; raw.logs_error = error instanceof Error ? error.message : String(error) }
|
|
65
|
+
}
|
|
66
|
+
metadata(raw, input)
|
|
67
|
+
const semantic = deriveSnapshotSemantic(raw)
|
|
68
|
+
return semantic ? { raw, semantic } : { raw }
|
|
69
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SnapshotLogEntry { timestamp: number | null; level: string; message: string }
|
|
2
|
+
interface LogFields { message?: string; msg?: string; level?: string; levelName?: string; _level?: string; timestamp?: string | number; _iso?: string }
|
|
3
|
+
function levelOf(message: string, raw: string | undefined, stream: boolean) {
|
|
4
|
+
return (raw && String(raw).toUpperCase()) || (stream
|
|
5
|
+
? /\bERROR\b/i.test(message) ? 'ERROR' : /\bWARN\b/i.test(message) ? 'WARN' : 'INFO'
|
|
6
|
+
: /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(message) ? 'ERROR' : /\b(WARN| W )\b/i.test(message) ? 'WARN' : 'INFO')
|
|
7
|
+
}
|
|
8
|
+
export function snapshotLog(item: unknown): SnapshotLogEntry {
|
|
9
|
+
if (!item) return { timestamp: null, level: 'INFO', message: '' }
|
|
10
|
+
if (typeof item === 'string') return { timestamp: null, level: levelOf(item, undefined, false), message: item }
|
|
11
|
+
const fields = item as LogFields
|
|
12
|
+
const message = fields.message || fields.msg || JSON.stringify(item)
|
|
13
|
+
const raw = fields.level || fields.levelName || fields._level || ''
|
|
14
|
+
const time = fields.timestamp || fields._iso || null
|
|
15
|
+
const timestamp = time && typeof time === 'string' ? (isNaN(new Date(time).getTime()) ? null : new Date(time).getTime()) : typeof time === 'number' ? time : null
|
|
16
|
+
return { timestamp, level: levelOf(message, raw, true), message }
|
|
17
|
+
}
|
|
18
|
+
export function streamLog(item: unknown): SnapshotLogEntry {
|
|
19
|
+
const fields = item as LogFields | null
|
|
20
|
+
const message = fields && (fields.message || fields.msg) ? fields.message || fields.msg! : typeof item === 'string' ? item : JSON.stringify(item)
|
|
21
|
+
const raw = fields && (fields.level || fields.levelName || fields._level) ? fields.level || fields.levelName || fields._level : ''
|
|
22
|
+
const iso = fields && (fields._iso || fields.timestamp)
|
|
23
|
+
let timestamp: number | null = null
|
|
24
|
+
if (iso && typeof iso === 'string') { const date = new Date(iso); if (!isNaN(date.getTime())) timestamp = date.getTime() }
|
|
25
|
+
return { timestamp, level: levelOf(message, raw, true), message }
|
|
26
|
+
}
|
|
@@ -38,7 +38,10 @@ function normalizeBounds(bounds: unknown): [number, number, number, number] | nu
|
|
|
38
38
|
return normalized as [number, number, number, number]
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
type SignatureElement = Partial<Pick<UIElement, 'text' | 'contentDescription' | 'resourceId' | 'stable_id' | 'role' | 'test_tag' | 'selector' | 'clickable' | 'enabled' | 'visible' | 'state'>> & { bounds?: number[] | null; type?: string | null }
|
|
42
|
+
export interface SnapshotSignatureInput { elements?: SignatureElement[]; screen?: unknown; resolution?: unknown; error?: unknown }
|
|
43
|
+
|
|
44
|
+
function stableElementSignature(element: SignatureElement) {
|
|
42
45
|
return {
|
|
43
46
|
text: normalize(element.text),
|
|
44
47
|
contentDescription: normalize(element.contentDescription),
|
|
@@ -115,7 +118,7 @@ function summarizeSnapshotDelta(previous: SnapshotState | undefined, currentElem
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
export function computeSnapshotSignature(tree:
|
|
121
|
+
export function computeSnapshotSignature(tree: SnapshotSignatureInput | null | undefined): string | null {
|
|
119
122
|
if (!tree || tree.error) return null
|
|
120
123
|
|
|
121
124
|
const payload = {
|
|
@@ -162,34 +165,10 @@ export function deriveSnapshotMetadata(
|
|
|
162
165
|
const existing = snapshotStateByDevice.get(deviceKey)
|
|
163
166
|
const previous = existing && Date.now() - existing.updatedAt <= 10 * 60 * 1000 ? existing : undefined
|
|
164
167
|
if (!previous && existing) snapshotStateByDevice.delete(deviceKey)
|
|
165
|
-
const hasValidTree =
|
|
166
|
-
const records = hasValidTree ? buildElementRecords(tree) : null
|
|
167
|
-
const currentElementSignatures = records?.signatures ?? previous?.elementSignatures ?? new Map<string, string>()
|
|
168
|
-
const currentElements = records?.elementsByIdentity ?? previous?.elements ?? new Map<string, UIElement>()
|
|
168
|
+
const { hasValidTree, currentElementSignatures, currentElements } = snapshotRecords(tree, previous)
|
|
169
169
|
|
|
170
170
|
const revision = previous && (signature === null || previous.signature === signature) ? previous.revision : nextRevision++
|
|
171
|
-
const history = previous
|
|
172
|
-
const historyTimes = previous?.historyTimes ?? new Map<number, number>()
|
|
173
|
-
const oversized = previous?.oversized ?? new Map<number, number>()
|
|
174
|
-
if (hasValidTree) {
|
|
175
|
-
history.set(revision, currentElements)
|
|
176
|
-
historyTimes.set(revision, Date.now())
|
|
177
|
-
if ((tree.elements?.length ?? 0) > 500) oversized.set(revision, (tree.elements?.length ?? 0) - 500)
|
|
178
|
-
}
|
|
179
|
-
for (const [id, timestamp] of historyTimes) {
|
|
180
|
-
if (Date.now() - timestamp > 10 * 60 * 1000) {
|
|
181
|
-
history.delete(id)
|
|
182
|
-
historyTimes.delete(id)
|
|
183
|
-
oversized.delete(id)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
while (history.size > 8) {
|
|
187
|
-
const oldest = history.keys().next().value
|
|
188
|
-
if (oldest === undefined) break
|
|
189
|
-
history.delete(oldest)
|
|
190
|
-
historyTimes.delete(oldest)
|
|
191
|
-
oversized.delete(oldest)
|
|
192
|
-
}
|
|
171
|
+
const { history, historyTimes, oversized } = retainHistory(previous, revision, currentElements, tree, hasValidTree)
|
|
193
172
|
|
|
194
173
|
snapshotStateByDevice.set(deviceKey, {
|
|
195
174
|
revision,
|
|
@@ -226,19 +205,20 @@ export function getStateDelta(deviceKey: string, baseRevision: number, currentRe
|
|
|
226
205
|
}
|
|
227
206
|
let index = 0
|
|
228
207
|
for (const [identity, element] of base) {
|
|
229
|
-
if (!current.has(identity)) removed.push(
|
|
208
|
+
if (!current.has(identity)) removed.push(removedElement(element, index))
|
|
230
209
|
index++
|
|
231
210
|
}
|
|
232
211
|
const all = added.length + changed.length + removed.length
|
|
233
212
|
const limit = 200
|
|
213
|
+
const truncated = all > limit || state.oversized.has(baseRevision) || state.oversized.has(currentRevision)
|
|
234
214
|
return {
|
|
235
215
|
base_snapshot_revision: baseRevision,
|
|
236
216
|
snapshot_revision: currentRevision,
|
|
237
217
|
added: added.slice(0, limit),
|
|
238
218
|
changed: changed.slice(0, Math.max(0, limit - added.length)),
|
|
239
219
|
removed: removed.slice(0, Math.max(0, limit - added.length - changed.length)),
|
|
240
|
-
truncated
|
|
241
|
-
...((
|
|
220
|
+
truncated,
|
|
221
|
+
...((truncated)
|
|
242
222
|
? { omitted_changes: Math.max(all - limit, state.oversized.get(baseRevision) ?? 0, state.oversized.get(currentRevision) ?? 0) } : {})
|
|
243
223
|
}
|
|
244
224
|
}
|
|
@@ -262,3 +242,43 @@ export function resetSnapshotMetadataForTests() {
|
|
|
262
242
|
snapshotStateByDevice.clear()
|
|
263
243
|
nextRevision = 1
|
|
264
244
|
}
|
|
245
|
+
|
|
246
|
+
function retainHistory(previous: SnapshotState | undefined, revision: number, currentElements: Map<string, UIElement>, tree: Parameters<typeof deriveSnapshotMetadata>[1], hasValidTree: boolean) {
|
|
247
|
+
const history = previous?.history ?? new Map<number, Map<string, UIElement>>()
|
|
248
|
+
const historyTimes = previous?.historyTimes ?? new Map<number, number>()
|
|
249
|
+
const oversized = previous?.oversized ?? new Map<number, number>()
|
|
250
|
+
if (hasValidTree) {
|
|
251
|
+
history.set(revision, currentElements)
|
|
252
|
+
historyTimes.set(revision, Date.now())
|
|
253
|
+
if ((tree!.elements?.length ?? 0) > 500) oversized.set(revision, (tree!.elements?.length ?? 0) - 500)
|
|
254
|
+
}
|
|
255
|
+
for (const [id, timestamp] of historyTimes) {
|
|
256
|
+
if (Date.now() - timestamp > 10 * 60 * 1000) {
|
|
257
|
+
history.delete(id)
|
|
258
|
+
historyTimes.delete(id)
|
|
259
|
+
oversized.delete(id)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
while (history.size > 8) {
|
|
263
|
+
const oldest = history.keys().next().value
|
|
264
|
+
if (oldest === undefined) break
|
|
265
|
+
history.delete(oldest)
|
|
266
|
+
historyTimes.delete(oldest)
|
|
267
|
+
oversized.delete(oldest)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { history, historyTimes, oversized }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function snapshotRecords(tree: Parameters<typeof deriveSnapshotMetadata>[1], previous: SnapshotState | undefined) {
|
|
274
|
+
const hasValidTree = !!tree && !tree.error
|
|
275
|
+
const records = hasValidTree ? buildElementRecords(tree) : null
|
|
276
|
+
const currentElementSignatures = records?.signatures ?? previous?.elementSignatures ?? new Map<string, string>()
|
|
277
|
+
const currentElements = records?.elementsByIdentity ?? previous?.elements ?? new Map<string, UIElement>()
|
|
278
|
+
|
|
279
|
+
return { hasValidTree, currentElementSignatures, currentElements }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function removedElement(element: UIElement, index: number) {
|
|
283
|
+
return { stable_id: element.stable_id ?? undefined, resourceId: element.resourceId ?? undefined, contentDescription: element.contentDescription ?? undefined, text: element.text ?? undefined, index }
|
|
284
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CaptureDebugSnapshotRawResponse,
|
|
3
|
+
SnapshotSemanticResponse
|
|
4
|
+
} from '../types.js'
|
|
5
|
+
export interface SnapshotTreeElementLike {
|
|
6
|
+
text?: string | null
|
|
7
|
+
contentDescription?: string | null
|
|
8
|
+
contentDesc?: string | null
|
|
9
|
+
accessibilityLabel?: string | null
|
|
10
|
+
resourceId?: string | null
|
|
11
|
+
id?: string | null
|
|
12
|
+
type?: string | null
|
|
13
|
+
class?: string | null
|
|
14
|
+
clickable?: boolean
|
|
15
|
+
enabled?: boolean
|
|
16
|
+
visible?: boolean
|
|
17
|
+
state?: unknown
|
|
18
|
+
stable_id?: string | null
|
|
19
|
+
role?: string | null
|
|
20
|
+
test_tag?: string | null
|
|
21
|
+
selector?: unknown
|
|
22
|
+
semantic?: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SnapshotTreeLike {
|
|
26
|
+
screen?: string | null
|
|
27
|
+
elements?: SnapshotTreeElementLike[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeHint(value: unknown): string {
|
|
31
|
+
if (value === null || value === undefined) return ''
|
|
32
|
+
return String(value).trim().replace(/\s+/g, ' ').toLowerCase()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function titleCase(value: string): string {
|
|
36
|
+
return value
|
|
37
|
+
.replace(/[_-]+/g, ' ')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim()
|
|
40
|
+
.replace(/\b\w/g, (match) => match.toUpperCase())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function shortActivityName(activity: string | null | undefined): string | null {
|
|
44
|
+
if (!activity) return null
|
|
45
|
+
const trimmed = String(activity).trim()
|
|
46
|
+
if (!trimmed) return null
|
|
47
|
+
const lastSegment = trimmed.split('.').pop() || trimmed
|
|
48
|
+
const withoutSuffix = lastSegment.replace(/Activity$/, '')
|
|
49
|
+
return withoutSuffix ? titleCase(withoutSuffix) : titleCase(lastSegment)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function elementText(element: SnapshotTreeElementLike | undefined) {
|
|
53
|
+
return normalizeHint(element?.text ?? element?.contentDescription ?? element?.contentDesc ?? element?.accessibilityLabel ?? element?.resourceId ?? element?.id ?? '')
|
|
54
|
+
}
|
|
55
|
+
export function collectSnapshotTexts(tree: SnapshotTreeLike | null | undefined) {
|
|
56
|
+
const elements = Array.isArray(tree?.elements) ? tree!.elements! : []
|
|
57
|
+
const texts: string[] = []
|
|
58
|
+
const actionables: string[] = []
|
|
59
|
+
|
|
60
|
+
for (const element of elements) {
|
|
61
|
+
const text = elementText(element)
|
|
62
|
+
if (text) texts.push(text)
|
|
63
|
+
if (element?.clickable && element?.enabled !== false && text) {
|
|
64
|
+
actionables.push(text)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
texts: Array.from(new Set(texts)),
|
|
70
|
+
actionables: Array.from(new Set(actionables))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function inferSnapshotScreen(raw: CaptureDebugSnapshotRawResponse): string | null {
|
|
75
|
+
const tree = raw.ui_tree as SnapshotTreeLike | null | undefined
|
|
76
|
+
const treeScreen = normalizeHint(tree?.screen)
|
|
77
|
+
if (treeScreen) return titleCase(treeScreen)
|
|
78
|
+
|
|
79
|
+
const activity = shortActivityName(raw.activity)
|
|
80
|
+
if (activity) return activity
|
|
81
|
+
|
|
82
|
+
const { texts } = collectSnapshotTexts(tree)
|
|
83
|
+
if (texts.length > 0) return titleCase(texts[0])
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function deriveSnapshotSemantic(raw: CaptureDebugSnapshotRawResponse): SnapshotSemanticResponse | null {
|
|
89
|
+
const tree = raw.ui_tree as SnapshotTreeLike | null | undefined
|
|
90
|
+
const { texts, actionables } = collectSnapshotTexts(tree)
|
|
91
|
+
const screenFromTree = normalizeHint(tree?.screen)
|
|
92
|
+
const activityHint = normalizeHint(raw.activity)
|
|
93
|
+
const screen = inferSnapshotScreen(raw)
|
|
94
|
+
|
|
95
|
+
if (!screen && !activityHint && texts.length === 0 && !raw.logs.length) return null
|
|
96
|
+
|
|
97
|
+
const hasErrorLogs = raw.logs.some((entry) => /error|fatal exception|exception|failed/i.test(entry.message))
|
|
98
|
+
const hasLoadingSignals = texts.some((text) => /loading|please wait|spinner|progress/i.test(text))
|
|
99
|
+
const hasPrimaryText = texts.some((text) => /sign in|log in|login|home|checkout|settings|menu|profile|search/i.test(text))
|
|
100
|
+
const hasScreenshot = typeof raw.screenshot === 'string' && raw.screenshot.length > 0
|
|
101
|
+
const hasUiTree = !!tree && Array.isArray(tree.elements)
|
|
102
|
+
|
|
103
|
+
const signals: Record<string, string | number | boolean> = {
|
|
104
|
+
has_activity: !!activityHint,
|
|
105
|
+
has_ui_tree: hasUiTree,
|
|
106
|
+
has_screenshot: hasScreenshot,
|
|
107
|
+
has_visible_text: texts.length > 0,
|
|
108
|
+
has_clickable_elements: actionables.length > 0,
|
|
109
|
+
has_error_logs: hasErrorLogs,
|
|
110
|
+
has_loading_signals: hasLoadingSignals,
|
|
111
|
+
has_primary_text: hasPrimaryText
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const warnings = snapshotWarnings(screenFromTree, activityHint, hasUiTree, hasErrorLogs)
|
|
115
|
+
const confidence = snapshotConfidence(hasUiTree, screen, activityHint, actionables.length, texts.length, hasScreenshot, hasErrorLogs, hasLoadingSignals)
|
|
116
|
+
|
|
117
|
+
if (!screen && confidence < 0.3) return null
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
screen,
|
|
121
|
+
signals,
|
|
122
|
+
actions_available: actionables.length > 0 ? actionables.slice(0, 10) : null,
|
|
123
|
+
confidence,
|
|
124
|
+
warnings
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function snapshotWarnings(screenFromTree: string, activityHint: string, hasUiTree: boolean, hasErrorLogs: boolean) {
|
|
129
|
+
const warnings: string[] = []
|
|
130
|
+
if (screenFromTree && activityHint && screenFromTree !== activityHint) {
|
|
131
|
+
warnings.push('ui_tree.screen and activity hints differ')
|
|
132
|
+
}
|
|
133
|
+
if (!hasUiTree) warnings.push('ui tree unavailable')
|
|
134
|
+
if (!activityHint) warnings.push('activity unavailable')
|
|
135
|
+
if (hasErrorLogs) warnings.push('error signals present in logs')
|
|
136
|
+
|
|
137
|
+
return warnings
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function snapshotConfidence(hasUiTree: boolean, screen: string | null, activityHint: string, actionableCount: number, textCount: number, hasScreenshot: boolean, hasErrorLogs: boolean, hasLoadingSignals: boolean) {
|
|
141
|
+
const evidenceScore =
|
|
142
|
+
(hasUiTree ? 0.35 : 0) +
|
|
143
|
+
(screen ? 0.2 : 0) +
|
|
144
|
+
(activityHint ? 0.15 : 0) +
|
|
145
|
+
(actionableCount > 0 ? 0.15 : 0) +
|
|
146
|
+
(textCount > 0 ? 0.1 : 0) +
|
|
147
|
+
(hasScreenshot ? 0.05 : 0) +
|
|
148
|
+
(hasErrorLogs ? -0.15 : 0) +
|
|
149
|
+
(hasLoadingSignals ? -0.05 : 0)
|
|
150
|
+
|
|
151
|
+
const confidence = Math.max(0, Math.min(1, Number(evidenceScore.toFixed(2))))
|
|
152
|
+
return confidence
|
|
153
|
+
}
|
|
@@ -37,7 +37,7 @@ const clock: TraceClock = {
|
|
|
37
37
|
export class SystemTraceSessions {
|
|
38
38
|
private sessions = new Map<string, Session>()
|
|
39
39
|
private closing = false
|
|
40
|
-
constructor(private native: TraceNative = new AndroidTraceNative(), private time: TraceClock = clock) {}
|
|
40
|
+
constructor(private native: TraceNative = new AndroidTraceNative(), private time: TraceClock = clock) { }
|
|
41
41
|
private prune() {
|
|
42
42
|
const terminal = [...this.sessions.values()].filter(s => s.terminalAt !== undefined && s.result.cleanup.status === 'complete')
|
|
43
43
|
for (const s of terminal) if (this.time.mono() - s.terminalAt! >= 600000) this.sessions.delete(s.result.trace_id)
|
|
@@ -49,7 +49,7 @@ export class SystemTraceSessions {
|
|
|
49
49
|
private snapshot(s: Session): TraceResult { return structuredClone(s.result) }
|
|
50
50
|
private serial<T>(s: Session, fn: () => Promise<T>): Promise<T> {
|
|
51
51
|
const result = s.queue.then(fn)
|
|
52
|
-
s.queue = result.catch(() => {})
|
|
52
|
+
s.queue = result.catch(() => { })
|
|
53
53
|
return result
|
|
54
54
|
}
|
|
55
55
|
async capabilities(device: string) { return this.native.capabilities(device) }
|
|
@@ -59,11 +59,15 @@ export class SystemTraceSessions {
|
|
|
59
59
|
const reserved = [...this.sessions.values()].filter(s => s.result.cleanup.status !== 'complete')
|
|
60
60
|
if (this.closing || reserved.length >= 4 || reserved.some(s => s.result.device.id === device)) throw new TraceError('TRACE_BUSY')
|
|
61
61
|
const id = randomUUID()
|
|
62
|
-
const result: TraceResult = {
|
|
62
|
+
const result: TraceResult = {
|
|
63
|
+
trace_id: id, session_id: evidenceSessionId(), device: { platform: 'android', id: device }, backend: 'perfetto',
|
|
63
64
|
state: 'starting', reason: null, completion_reason: null, capture_started_at_ms: null, capture_completed_at_ms: null,
|
|
64
|
-
artifact: null, cleanup: { status: 'pending', remaining_resources: [] }, correlation_ids: [], enabled_sources: [], unavailable_categories: []
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
artifact: null, cleanup: { status: 'pending', remaining_resources: [] }, correlation_ids: [], enabled_sources: [], unavailable_categories: []
|
|
66
|
+
}
|
|
67
|
+
const s: Session = {
|
|
68
|
+
result, remote: `/data/misc/perfetto-traces/mcp-${id}.perfetto-trace`, owner: null, stopped: true,
|
|
69
|
+
discard: false, deadline: Infinity, queue: Promise.resolve(), linked: new Set(), partial: new Set()
|
|
70
|
+
}
|
|
67
71
|
this.sessions.set(id, s)
|
|
68
72
|
return this.serial(s, async () => {
|
|
69
73
|
let cap: TraceCapabilities
|
|
@@ -81,7 +85,7 @@ export class SystemTraceSessions {
|
|
|
81
85
|
await this.native.start(device, s.remote, duration, cap.enabled_sources)
|
|
82
86
|
s.owner = await this.native.find(device, s.remote)
|
|
83
87
|
s.result.state = 'recording'
|
|
84
|
-
s.timer = this.time.schedule(() => { void this.control(id, 'duration').catch(() => {}) }, Math.max(0, s.deadline - this.time.mono()))
|
|
88
|
+
s.timer = this.time.schedule(() => { void this.control(id, 'duration').catch(() => { }) }, Math.max(0, s.deadline - this.time.mono()))
|
|
85
89
|
} catch (error) {
|
|
86
90
|
s.discard = true
|
|
87
91
|
this.fail(s, this.code(error, 'TRACE_START_FAILED'))
|
|
@@ -173,15 +177,18 @@ export class SystemTraceSessions {
|
|
|
173
177
|
}
|
|
174
178
|
try { await this.remove(s) }
|
|
175
179
|
catch { this.fail(s, 'TRACE_STOP_FAILED'); s.result.cleanup.status = 'failed'; return this.snapshot(s) }
|
|
176
|
-
|
|
177
|
-
else {
|
|
178
|
-
s.result.state = s.discard ? 'cancelled' : 'completed'
|
|
179
|
-
s.result.reason = s.discard ? 'TRACE_CANCELLED' : null
|
|
180
|
-
s.result.completion_reason = s.discard ? 'cancelled' : operation === 'duration' || this.time.mono() >= s.deadline ? 'duration_limit' : 'requested'
|
|
181
|
-
}
|
|
180
|
+
this.complete(s, hadStartFailure, operation)
|
|
182
181
|
return this.snapshot(s)
|
|
183
182
|
})
|
|
184
183
|
}
|
|
184
|
+
private complete(s: Session, hadStartFailure: boolean, operation: string) {
|
|
185
|
+
if (hadStartFailure) s.result.state = 'failed'
|
|
186
|
+
else {
|
|
187
|
+
s.result.state = s.discard ? 'cancelled' : 'completed'
|
|
188
|
+
s.result.reason = s.discard ? 'TRACE_CANCELLED' : null
|
|
189
|
+
s.result.completion_reason = s.discard ? 'cancelled' : operation === 'duration' || this.time.mono() >= s.deadline ? 'duration_limit' : 'requested'
|
|
190
|
+
}
|
|
191
|
+
}
|
|
185
192
|
async link(id: string, device: { platform: string; id: string }, correlation: string) {
|
|
186
193
|
const s = this.get(id)
|
|
187
194
|
if (device.platform !== 'android' || device.id !== s.result.device.id) throw new TraceError('TRACE_NOT_FOUND')
|
|
@@ -201,8 +208,10 @@ export class SystemTraceSessions {
|
|
|
201
208
|
try { recording = (await this.control(id, 'status')).state === 'recording' }
|
|
202
209
|
catch { /* Unavailable state cannot establish complete coverage. */ }
|
|
203
210
|
s.linked.delete(correlation)
|
|
204
|
-
return {
|
|
205
|
-
|
|
211
|
+
return {
|
|
212
|
+
trace_id: id, coverage: !recording || s.partial.has(correlation) || this.time.mono() >= s.deadline ? 'partial' : 'covered',
|
|
213
|
+
clock_alignment: 'unknown'
|
|
214
|
+
}
|
|
206
215
|
})()
|
|
207
216
|
})
|
|
208
217
|
}
|