mobile-debug-mcp 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +23 -2
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +158 -124
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +20 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +79 -0
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +304 -0
- package/dist/debugger/worker.js +176 -0
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3074
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +122 -76
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +13 -2
- package/dist/observe/readiness.js +44 -18
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +294 -0
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/handlers/debugger-handlers.js +48 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/schemas/debugger-schema.js +47 -0
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
- package/dist/server/schemas/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +62 -1134
- package/dist/server-core.js +4 -4
- package/dist/server.js +34 -0
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -438
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +75 -57
- package/dist/utils/operation-budget.js +2 -0
- package/dist/utils/resolve-device.js +6 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +48 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +41 -0
- package/docs/specs/023-android-system-traces/plan.md +115 -0
- package/docs/specs/023-android-system-traces/spec.md +122 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -16
- package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
- package/docs/tools/debugger.md +180 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +64 -25
- package/docs/tools/system-traces.md +134 -0
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +66 -0
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +31 -0
- package/src/debugger/java/DebuggerWorker.java +166 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +327 -0
- package/src/debugger/worker.ts +135 -0
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3491
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +130 -71
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +10 -3
- package/src/observe/readiness.ts +49 -22
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +227 -0
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +43 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
- package/src/server/schemas/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +69 -1140
- package/src/server-core.ts +4 -4
- package/src/server.ts +25 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -523
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +71 -51
- package/src/utils/operation-budget.ts +2 -0
- package/src/utils/resolve-device.ts +11 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +219 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +67 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +65 -0
- package/test/fixtures/debugger-app/build.ts +39 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +50 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/readiness.test.ts +3 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -893
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/tool-handlers.ts +0 -923
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { diagnostic, stages, reasons, classify, parseDiagnostic } from '../../../src/debugger/diagnostics.js'
|
|
6
|
+
import { DebuggerError, JdiWorker, withDebuggerBudget, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
7
|
+
import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
|
|
8
|
+
import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
|
|
9
|
+
|
|
10
|
+
const secret = 'PRIVATE_LOCALS_TOKEN_/private/path'
|
|
11
|
+
for (const stage of stages) for (const reason of reasons) {
|
|
12
|
+
const value = diagnostic(stage, reason)
|
|
13
|
+
assert.ok(Buffer.byteLength(JSON.stringify(value)) <= 512)
|
|
14
|
+
assert.ok(value.recovery.length <= 240 && /^[\x00-\x7f]*$/.test(value.recovery))
|
|
15
|
+
assert.deepEqual(Object.keys(value), ['stage', 'reason', 'recovery'])
|
|
16
|
+
}
|
|
17
|
+
assert.deepEqual(parseDiagnostic({ stage: 'attach', reason: 'connection_refused', recovery: secret, raw: secret }), diagnostic('attach', 'connection_refused'))
|
|
18
|
+
assert.equal(classify(new Error(secret), 'attach', 'DEBUGGER_ATTACH_FAILED').diagnostic?.reason, 'unknown')
|
|
19
|
+
assert.equal(classify(new DebuggerError('DEBUGGER_STALE_REFERENCE'), 'command', 'DEBUGGER_WORKER_FAILED').diagnostic, undefined)
|
|
20
|
+
for (const hostile of [secret, { stage: secret, reason: 'unknown' }, { stage: 'attach', reason: secret }]) {
|
|
21
|
+
assert.throws(() => parseWorkerMessage({ id: 1, error: { code: 'DEBUGGER_WORKER_FAILED', diagnostic: hostile } }), /INVALID_WORKER_DIAGNOSTIC/)
|
|
22
|
+
}
|
|
23
|
+
assert.throws(() => parseWorkerMessage({ id: 1, error: { code: secret } }), /INVALID_WORKER_ERROR/)
|
|
24
|
+
const cancelled = new AbortController(); cancelled.abort()
|
|
25
|
+
const response = await handleDebugger('get_debugger_state', { debugSessionId: 'absent' }, cancelled.signal)
|
|
26
|
+
assert.equal(response.isError, true)
|
|
27
|
+
const payload = JSON.parse(response.content[0].text)
|
|
28
|
+
assert.equal(payload.error.code, 'DEBUGGER_CANCELLED')
|
|
29
|
+
assert.deepEqual(payload.error.diagnostic, diagnostic('unknown', 'cancelled'))
|
|
30
|
+
assert.equal((await handleDebugger('get_debugger_state', {})).isError, true)
|
|
31
|
+
|
|
32
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-diagnostics-test-'))
|
|
33
|
+
let index = 0
|
|
34
|
+
async function worker(body: string, onEvent: (event: WorkerEvent) => void = () => {}) {
|
|
35
|
+
const file = path.join(directory, `worker-${index++}`)
|
|
36
|
+
await writeFile(file, `#!${process.execPath}\n${body}\n`, { mode: 0o700 })
|
|
37
|
+
return new JdiWorker(file, directory, onEvent)
|
|
38
|
+
}
|
|
39
|
+
async function fails(body: string, stage: string, reason: string) {
|
|
40
|
+
const w = await worker(body)
|
|
41
|
+
try {
|
|
42
|
+
await assert.rejects(w.call('attach', {}, 3000), (error: unknown) => {
|
|
43
|
+
assert.ok(error instanceof DebuggerError)
|
|
44
|
+
assert.equal(error.code, 'DEBUGGER_WORKER_FAILED')
|
|
45
|
+
assert.deepEqual(error.diagnostic, diagnostic(stage as typeof stages[number], reason as typeof reasons[number]))
|
|
46
|
+
assert.ok(!JSON.stringify(error).includes(secret))
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
} finally { await w.close() }
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const missing = new JdiWorker(path.join(directory, 'missing'), directory, () => {})
|
|
53
|
+
try { await assert.rejects(missing.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'spawn_failed') } finally { await missing.close() }
|
|
54
|
+
await fails(`process.stderr.write(${JSON.stringify(secret)}); process.exit(7)`, 'worker_start', 'process_exited')
|
|
55
|
+
await fails(`process.stdout.write(${JSON.stringify(secret + '\n')}); setInterval(()=>{},1000)`, 'ipc', 'invalid_message')
|
|
56
|
+
await fails(`process.stdout.write('x'.repeat(1048577)+'\\n'); setInterval(()=>{},1000)`, 'ipc', 'message_limit')
|
|
57
|
+
const attach = await worker(`process.stdin.once('data', data => { const {id}=JSON.parse(data); console.log(JSON.stringify({id,error:{code:'DEBUGGER_WORKER_FAILED',diagnostic:{stage:'attach',reason:'connection_refused',raw:${JSON.stringify(secret)}}}})) })`)
|
|
58
|
+
try { await assert.rejects(attach.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'connection_refused') } finally { await attach.close() }
|
|
59
|
+
let ready = () => {}
|
|
60
|
+
const readyPromise = new Promise<void>(resolve => { ready = resolve })
|
|
61
|
+
const brokenPipe = await worker(`require('node:fs').closeSync(0); console.log(JSON.stringify({event:'ready'})); setInterval(()=>{},1000)`, () => ready())
|
|
62
|
+
try {
|
|
63
|
+
await readyPromise
|
|
64
|
+
await assert.rejects(brokenPipe.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'write_failed')
|
|
65
|
+
} finally { await brokenPipe.close() }
|
|
66
|
+
const afterReply = await worker(`process.stdin.once('data',data=>{const {id}=JSON.parse(data); console.log(JSON.stringify({id,result:{}})); setTimeout(()=>process.exit(7),100)})`)
|
|
67
|
+
try {
|
|
68
|
+
await afterReply.call('state')
|
|
69
|
+
await assert.rejects(afterReply.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'ipc' && e.diagnostic.reason === 'process_exited')
|
|
70
|
+
} finally { await afterReply.close() }
|
|
71
|
+
const idle = await worker('process.stdin.resume()')
|
|
72
|
+
try {
|
|
73
|
+
await assert.rejects(idle.call('stack', {}, 50), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'timeout')
|
|
74
|
+
const controller = new AbortController()
|
|
75
|
+
const call = withDebuggerBudget(1000, controller.signal, () => idle.call('attach'))
|
|
76
|
+
controller.abort()
|
|
77
|
+
await assert.rejects(call, (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'attach' && e.diagnostic.reason === 'cancelled')
|
|
78
|
+
} finally { await idle.close() }
|
|
79
|
+
} finally { await rm(directory, { recursive: true, force: true }) }
|
|
80
|
+
console.log('PASS diagnostic bounds/privacy, strict IPC, real worker spawn/exit/malformed/oversize, attach failure, timeout/cancel and MCP wrapping')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { debuggerSessions } from '../../../src/debugger/sessions.js'
|
|
3
|
+
import { withDebuggerGuard, checkDebuggerGuard } from '../../../src/debugger/guard.js'
|
|
4
|
+
import { handleToolCall } from '../../../src/server-core.js'
|
|
5
|
+
import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
|
|
6
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
7
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
8
|
+
import { getAppReadiness } from '../../../src/observe/readiness-service.js'
|
|
9
|
+
const originalPause = debuggerSessions.paused
|
|
10
|
+
const originalActive = debuggerSessions.hasActiveSessions
|
|
11
|
+
const originalTree = ToolsObserve.getUITreeHandler
|
|
12
|
+
const originalWait = ToolsInteract.waitForUIHandler
|
|
13
|
+
let paused = true
|
|
14
|
+
const pause: any = { debug_session_id: 'session', target: { app_id: 'test.app', pid: 10 }, state: 'paused', captured_at_ms: 1000 }
|
|
15
|
+
debuggerSessions.paused = device => paused && device === 'fixture' ? pause : null
|
|
16
|
+
debuggerSessions.hasActiveSessions = () => true
|
|
17
|
+
_setDeviceListersForTests({ listAndroidDevices: async () => [{ id: 'fixture', platform: 'android' } as any] })
|
|
18
|
+
const payload = (response: any) => JSON.parse(response.content.find((c: any) => c.type === 'text').text)
|
|
19
|
+
try {
|
|
20
|
+
ToolsObserve.getUITreeHandler = async () => { throw new Error('Paused readiness must not acquire a UI tree') }
|
|
21
|
+
const readiness = await getAppReadiness({ platform: 'android', deviceId: 'fixture' })
|
|
22
|
+
assert.equal(readiness.readiness.state, 'debugger_paused')
|
|
23
|
+
const wait = await getAppReadiness({ platform: 'android', deviceId: 'fixture' }, true)
|
|
24
|
+
assert.equal(wait.failure_code, 'DEBUGGER_PAUSED')
|
|
25
|
+
const blocked = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
|
|
26
|
+
assert.equal(blocked.delivery_status, 'not_delivered')
|
|
27
|
+
await withDebuggerGuard('android', 'other', async () => checkDebuggerGuard(true))
|
|
28
|
+
await assert.rejects(withDebuggerGuard('android', 'fixture', async () => checkDebuggerGuard()), /DEBUGGER_PAUSED/)
|
|
29
|
+
paused = false
|
|
30
|
+
let calls = 0
|
|
31
|
+
ToolsInteract.waitForUIHandler = async () => { calls++; if (calls === 2) paused = true; return { status: 'success' } as any }
|
|
32
|
+
const journey = payload(await handleToolCall('run_journey', { platform: 'android', deviceId: 'fixture', responseMode: 'compact', steps: [
|
|
33
|
+
{ id: 'first', type: 'wait', selector: { text: 'First' } },
|
|
34
|
+
{ id: 'second', type: 'wait', selector: { text: 'Second' } },
|
|
35
|
+
{ id: 'third', type: 'wait', selector: { text: 'Third' } }
|
|
36
|
+
] }))
|
|
37
|
+
assert.equal(journey.steps[0].status, 'passed', 'completed steps survive a later pause')
|
|
38
|
+
assert.equal(journey.steps[1].success, false)
|
|
39
|
+
assert.equal(journey.steps[1].result.failure_code, 'DEBUGGER_PAUSED')
|
|
40
|
+
assert.equal(calls, 2, 'no action queued or retried after pause')
|
|
41
|
+
_setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('resolution unavailable') } })
|
|
42
|
+
const failedResolution = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
|
|
43
|
+
assert.equal(failedResolution.error.tool, 'tap')
|
|
44
|
+
assert.match(failedResolution.error.message, /resolution unavailable/, 'resolution errors retain the MCP error envelope')
|
|
45
|
+
console.log('PASS debugger readiness without UI acquisition, device scoping, dispatch guard, mid-journey pause preservation')
|
|
46
|
+
} finally {
|
|
47
|
+
debuggerSessions.paused = originalPause; debuggerSessions.hasActiveSessions = originalActive
|
|
48
|
+
ToolsObserve.getUITreeHandler = originalTree; ToolsInteract.waitForUIHandler = originalWait
|
|
49
|
+
_resetDeviceListersForTests()
|
|
50
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { AndroidDebugNative } from '../../../src/debugger/android.js'
|
|
3
|
+
import { DebuggerError, debuggerRemaining, withDebuggerBudget } from '../../../src/debugger/worker.js'
|
|
4
|
+
class Native extends AndroidDebugNative {
|
|
5
|
+
calls: string[][] = []
|
|
6
|
+
forwards = 'device tcp:1234 jdwp:10\nother tcp:3456 jdwp:20'
|
|
7
|
+
async adb(_device: string, args: string[]) {
|
|
8
|
+
this.calls.push(args)
|
|
9
|
+
if (args[0] === 'forward' && args[1] === '--list') return this.forwards
|
|
10
|
+
if (args.at(-1)?.endsWith('/stat')) return '10 (complex process name) S ' + Array(18).fill('0').join(' ') + ' 999 0'
|
|
11
|
+
if (args.at(-1)?.endsWith('/status')) return 'Name:\ttest\nUid:\t42\t42\t42\t42'
|
|
12
|
+
if (args.at(-1)?.endsWith('/cmdline')) return 'test.app\0'
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const native = new Native()
|
|
17
|
+
await native.removeForward('device', 1234, 10)
|
|
18
|
+
assert.deepEqual(native.calls.at(-1), ['forward', '--remove', 'tcp:1234'])
|
|
19
|
+
await assert.rejects(native.removeForward('device', 3456, 10), /DEBUGGER_CLEANUP_FAILED/)
|
|
20
|
+
const before = native.calls.length
|
|
21
|
+
await native.removeForward('device', 9999, 10)
|
|
22
|
+
assert.equal(native.calls.length, before + 1, 'absent owned forward requires no removal')
|
|
23
|
+
const target = { app_id: 'test.app', pid: 10, process_name: 'test.app', start_identity: '', uid: 42 }
|
|
24
|
+
assert.equal((await native.identity('device', target)).start_identity, '999')
|
|
25
|
+
await assert.rejects(native.identity('device', { ...target, start_identity: 'old' }), /PROCESS_CHANGED/)
|
|
26
|
+
await assert.rejects(native.identity('device', { ...target, uid: 44 }), /PROCESS_CHANGED/)
|
|
27
|
+
await assert.rejects(native.candidates('device', 'test.app;evil'), /INVALID_ARGUMENT/)
|
|
28
|
+
const aborted = new AbortController(); aborted.abort()
|
|
29
|
+
await assert.rejects(withDebuggerBudget(100, aborted.signal, async () => debuggerRemaining(10)), /DEBUGGER_CANCELLED/)
|
|
30
|
+
await assert.rejects(withDebuggerBudget(-1, undefined, async () => debuggerRemaining(10)), /DEBUGGER_TIMEOUT/)
|
|
31
|
+
assert.equal(await withDebuggerBudget(100, undefined, async () => debuggerRemaining(10)), 10)
|
|
32
|
+
assert.equal(new DebuggerError('DEBUGGER_TIMEOUT').code, 'DEBUGGER_TIMEOUT')
|
|
33
|
+
console.log('PASS debugger native identity, PID reuse, forward ownership, deadlines and cancellation')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
const exec = promisify(execFile)
|
|
8
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-fake-jdk-'))
|
|
9
|
+
const secret = 'SECRET_COMPILER_OUTPUT'
|
|
10
|
+
const run = async () => {
|
|
11
|
+
const code = `import {prepareWorker} from './src/debugger/worker.ts'; try {await prepareWorker(); process.exitCode=1} catch(e) {console.log(JSON.stringify({code:e.code,diagnostic:e.diagnostic}))}`
|
|
12
|
+
const { stdout, stderr } = await exec(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', code], {
|
|
13
|
+
env: { ...process.env, ANDROID_STUDIO_JDK: directory }, timeout: 10000
|
|
14
|
+
})
|
|
15
|
+
assert.ok(!stdout.includes(secret)); assert.ok(!stderr.includes(secret))
|
|
16
|
+
return JSON.parse(stdout)
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await mkdir(path.join(directory, 'bin'))
|
|
20
|
+
const java = path.join(directory, 'bin', 'java')
|
|
21
|
+
await writeFile(java, `#!${process.execPath}\nconsole.error('openjdk version "17.0.1"');`, { mode: 0o700 })
|
|
22
|
+
await writeFile(path.join(directory, 'bin', 'javac'), `#!${process.execPath}\nconsole.error('${secret}');process.exit(1)`, { mode: 0o700 })
|
|
23
|
+
const compile = await run()
|
|
24
|
+
assert.equal(compile.code, 'DEBUGGER_UNSUPPORTED')
|
|
25
|
+
assert.equal(compile.diagnostic.stage, 'compilation'); assert.equal(compile.diagnostic.reason, 'operation_failed')
|
|
26
|
+
// Discovery succeeds; the actual bounded version subprocess then fails.
|
|
27
|
+
await writeFile(java, `#!${process.execPath}\nconst fs=require('node:fs');const marker=${JSON.stringify(path.join(directory, 'seen'))};if(fs.existsSync(marker)){console.error('${secret}');process.exit(1)}fs.writeFileSync(marker,'');console.error('openjdk version "17.0.1"');`, { mode: 0o700 })
|
|
28
|
+
const prerequisite = await run()
|
|
29
|
+
assert.equal(prerequisite.code, 'DEBUGGER_ATTACH_FAILED')
|
|
30
|
+
assert.equal(prerequisite.diagnostic.stage, 'prerequisite'); assert.equal(prerequisite.diagnostic.reason, 'unavailable')
|
|
31
|
+
} finally { await rm(directory, { recursive: true, force: true }) }
|
|
32
|
+
console.log('PASS isolated fake-JDK compilation/prerequisite classification without compiler output leakage')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
|
|
3
|
+
|
|
4
|
+
const event = { event: 'stopped', stop_id: 'stop-1', thread_id: 'thread-1', stops: [] }
|
|
5
|
+
assert.deepEqual(parseWorkerMessage(event), { kind: 'event', value: event })
|
|
6
|
+
assert.deepEqual(parseWorkerMessage({ id: 1, result: { locals: [] } }), { kind: 'reply', id: 1, result: { locals: [] } })
|
|
7
|
+
assert.deepEqual(parseWorkerMessage({ id: 2, error: { code: 'DEBUGGER_STALE_REFERENCE' } }), {
|
|
8
|
+
kind: 'reply', id: 2, result: {}, error: { code: 'DEBUGGER_STALE_REFERENCE' }
|
|
9
|
+
})
|
|
10
|
+
for (const invalid of [null, [], { event: 42 }, { event: 'stopped', stop_id: 42 },
|
|
11
|
+
{ id: '1', result: {} }, { id: -1, result: {} }, { id: 1.2, result: {} },
|
|
12
|
+
{ id: 1, result: 'private-debuggee-value' }, { id: 1, error: { code: 42 } }]) {
|
|
13
|
+
assert.throws(() => parseWorkerMessage(invalid), /^Error: INVALID_WORKER_/, 'malformed worker input is rejected without exposing its contents')
|
|
14
|
+
}
|
|
15
|
+
console.log('PASS validated worker events/replies and content-free protocol errors')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { DebuggerSessions, debuggerExpiry } from '../../../src/debugger/sessions.js'
|
|
3
|
+
import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
4
|
+
import type { DebugNative, DebugTarget } from '../../../src/debugger/android.js'
|
|
5
|
+
import { debuggerContracts, debuggerTools } from '../../../src/server/schemas/debugger-schema.js'
|
|
6
|
+
assert.equal(debuggerExpiry(0, 0, 0, 59999), null)
|
|
7
|
+
assert.equal(debuggerExpiry(0, 60000, 0, 60000), 'pause_limit', 'polling cannot extend pause expiry')
|
|
8
|
+
assert.equal(debuggerExpiry(0, 0, undefined, 300000), 'idle_limit')
|
|
9
|
+
assert.equal(debuggerExpiry(0, 1800000, undefined, 1800000), 'session_limit', 'activity cannot extend lifetime')
|
|
10
|
+
const target: DebugTarget = { app_id: 'test.app', pid: 123, uid: 456, process_name: 'test.app', start_identity: '100' }
|
|
11
|
+
let candidates = [target]
|
|
12
|
+
let removed = 0
|
|
13
|
+
let emit: (event: WorkerEvent) => void = () => {}
|
|
14
|
+
let close = 0
|
|
15
|
+
let failCleanup = false
|
|
16
|
+
let failCommand = ''
|
|
17
|
+
const native: DebugNative = {
|
|
18
|
+
candidates: async () => candidates, identity: async () => target, forward: async () => 12345,
|
|
19
|
+
removeForward: async () => { if (failCleanup) throw new Error('offline'); removed++ }
|
|
20
|
+
}
|
|
21
|
+
const sessions = new DebuggerSessions(native, async event => {
|
|
22
|
+
emit = event
|
|
23
|
+
return { call: async (command: string) => { if (command === failCommand) throw new DebuggerError('DEBUGGER_TIMEOUT'); return { state: 'running', breakpoints: [] } }, close: async () => { close++ } }
|
|
24
|
+
}, async () => ({ java: 'java', directory: '/unused' }))
|
|
25
|
+
try {
|
|
26
|
+
candidates = [target, { ...target, pid: 124 }]
|
|
27
|
+
const ambiguous = await sessions.attach('emulator', 'test.app')
|
|
28
|
+
assert.equal(ambiguous.error.code, 'PROCESS_AMBIGUOUS')
|
|
29
|
+
assert.equal(removed, 0)
|
|
30
|
+
candidates = [target]
|
|
31
|
+
const first = await sessions.attach('emulator', 'test.app')
|
|
32
|
+
const id = first.debug_session_id
|
|
33
|
+
assert.equal(first.state, 'running')
|
|
34
|
+
await assert.rejects(sessions.attach('emulator', 'test.app'), /DEBUGGER_BUSY/)
|
|
35
|
+
emit({ event: 'stopped', stop_id: 'stop-1', thread_id: 't1', stops: [] })
|
|
36
|
+
assert.equal(sessions.paused('emulator')?.stop.stop_id, 'stop-1')
|
|
37
|
+
assert.equal(sessions.paused('other'), null)
|
|
38
|
+
assert.throws(() => sessions.assertRunning('emulator'), /DEBUGGER_PAUSED/)
|
|
39
|
+
const pendingWait = sessions.command(id, 'events', { afterSequence: 1 }, 500)
|
|
40
|
+
await sessions.command(id, 'resume', { stopId: 'stop-1' })
|
|
41
|
+
assert.equal(sessions.paused('emulator'), null)
|
|
42
|
+
await assert.rejects(sessions.command(id, 'locals', { stopId: 'stop-1', frameId: 'old' }), /DEBUGGER_STALE_REFERENCE/)
|
|
43
|
+
emit({ event: 'stopped', stop_id: 'stop-2', thread_id: 't2' })
|
|
44
|
+
assert.equal((await pendingWait).events[0].stop_id, 'stop-2', 'event wait cannot block resume')
|
|
45
|
+
for (let i = 0; i < 260; i++) emit({ event: 'breakpoint', i })
|
|
46
|
+
const events = await sessions.command(id, 'events', { afterSequence: 0 }, 100)
|
|
47
|
+
assert.equal(events.overflow, true); assert.equal(events.events.length, 100)
|
|
48
|
+
await assert.rejects(sessions.command(id, 'events', { afterSequence: 9999 }, 100), /INVALID_ARGUMENT/)
|
|
49
|
+
const controller = new AbortController(); controller.abort()
|
|
50
|
+
await assert.rejects(sessions.command(id, 'events', { afterSequence: 262, signal: controller.signal }, 100), /DEBUGGER_CANCELLED/)
|
|
51
|
+
failCommand = 'locals'
|
|
52
|
+
await assert.rejects(sessions.command(id, 'locals', { stopId: 'stop-2' }, 100), /DEBUGGER_TIMEOUT/)
|
|
53
|
+
assert.equal(sessions.paused('emulator')?.state, 'paused', 'inspection timeout preserves known suspension')
|
|
54
|
+
failCommand = 'step'
|
|
55
|
+
await assert.rejects(sessions.command(id, 'step', { stopId: 'stop-2', threadId: 't2' }, 100), /DEBUGGER_TIMEOUT/)
|
|
56
|
+
assert.equal((await sessions.command(id, 'state')).cleanup.status, 'complete')
|
|
57
|
+
assert.equal(close, 1); assert.equal(removed, 1)
|
|
58
|
+
failCommand = ''
|
|
59
|
+
const second = await sessions.attach('emulator', 'test.app')
|
|
60
|
+
failCleanup = true
|
|
61
|
+
const partial = await sessions.command(second.debug_session_id, 'detach')
|
|
62
|
+
assert.equal(partial.success, false); assert.equal(partial.cleanup.status, 'failed')
|
|
63
|
+
await assert.rejects(sessions.attach('emulator', 'test.app'), /DEBUGGER_BUSY/)
|
|
64
|
+
failCleanup = false
|
|
65
|
+
const retry = await sessions.command(second.debug_session_id, 'detach')
|
|
66
|
+
assert.equal(retry.cleanup.status, 'complete'); assert.equal(close, 2)
|
|
67
|
+
assert.equal((await sessions.command(second.debug_session_id, 'detach')).cleanup.status, 'complete')
|
|
68
|
+
const third = await sessions.attach('emulator', 'test.app')
|
|
69
|
+
emit({ event: 'worker_failed' })
|
|
70
|
+
const crashed = await sessions.command(third.debug_session_id, 'state')
|
|
71
|
+
assert.equal(crashed.state, 'failed')
|
|
72
|
+
assert.equal(crashed.cleanup.status, 'complete')
|
|
73
|
+
assert.equal(crashed.error.code, 'DEBUGGER_WORKER_FAILED')
|
|
74
|
+
assert.equal(sessions.paused('emulator'), null)
|
|
75
|
+
for (const tool of debuggerTools) {
|
|
76
|
+
const contract = debuggerContracts[tool.name]
|
|
77
|
+
assert.equal(tool.inputSchema.additionalProperties, false)
|
|
78
|
+
assert.equal(contract.schema.safeParse({ ...Object.fromEntries(tool.inputSchema.required.map(k => [k, 'x'])), unexpected: true }).success, false)
|
|
79
|
+
}
|
|
80
|
+
assert.equal(debuggerContracts.attach_debugger.schema.safeParse({ platform: 'android', appId: 'test.app' }).success, false)
|
|
81
|
+
assert.equal(debuggerContracts.set_debugger_breakpoint.schema.safeParse({ debugSessionId: 's', className: '$(bad)', line: 2 }).success, false)
|
|
82
|
+
console.log('PASS debugger session ownership, stop epochs, event concurrency/overflow, timeout recovery, cleanup retries, strict contracts')
|
|
83
|
+
} finally { await sessions.shutdown() }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { adjustControl } from '../../../src/interact/adjustment/adjustment.js'
|
|
3
|
+
import type { AdjustmentDependencies } from '../../../src/interact/adjustment/adjustment-context.js'
|
|
4
|
+
import type { ActionTargetResolved, DeviceInfo } from '../../../src/types.js'
|
|
5
|
+
import type { UiElement } from '../../../src/interact/model.js'
|
|
6
|
+
|
|
7
|
+
const device: DeviceInfo = { platform: 'android', id: 'fixture', model: 'fixture', osVersion: 'fixture', simulator: true }
|
|
8
|
+
let value = 10
|
|
9
|
+
const calls: string[] = []
|
|
10
|
+
const element = (): UiElement => ({ visible: true, enabled: true, role: 'slider', stable_id: 'duration', bounds: [0, 0, 300, 40],
|
|
11
|
+
state: { value, value_range: { min: 0, max: 100 } }, actions: ['set_value'] })
|
|
12
|
+
const handle = { elementId: 'handle', platform: 'android' as const, deviceId: device.id, bounds: [0, 0, 300, 40] as [number, number, number, number], index: 0, stable_id: 'duration' }
|
|
13
|
+
const target = (): ActionTargetResolved => ({ elementId: 'handle', text: null, resource_id: null, accessibility_id: null, class: 'Slider', bounds: handle.bounds, index: 0 })
|
|
14
|
+
const deps: AdjustmentDependencies = {
|
|
15
|
+
observe: async () => ({ device, elements: [element()] }), fingerprint: async () => 'fingerprint',
|
|
16
|
+
getHandle: () => handle, currentHandle: () => ({ el: element(), index: 0 }), target,
|
|
17
|
+
wait: async () => { throw new Error('handle targeting must not use selector waits') },
|
|
18
|
+
find: async () => { throw new Error('handle targeting must not use semantic search') },
|
|
19
|
+
expectState: async input => {
|
|
20
|
+
calls.push('verify')
|
|
21
|
+
return { success: true, element_id: 'handle', expected_state: { property: input.property, expected: input.expected },
|
|
22
|
+
observed_state: { property: input.property, value }, trace: { action_id: 'assertion', steps: [], final_outcome: 'success', attempts: 1 } }
|
|
23
|
+
},
|
|
24
|
+
service: async () => ({ resolved: { id: device.id }, interact: { setAdjustableValue: async () => { calls.push('semantic'); return { success: false } } } }),
|
|
25
|
+
tap: async () => { calls.push('coordinate'); value = 20; return { success: true, device } },
|
|
26
|
+
swipe: async () => { throw new Error('a converged coordinate adjustment must not dispatch a gesture') }
|
|
27
|
+
}
|
|
28
|
+
const already = await adjustControl({ element_id: 'handle', targetValue: 10 }, deps)
|
|
29
|
+
assert.equal(already.success, true)
|
|
30
|
+
assert.equal(already.attempts, 0)
|
|
31
|
+
assert.deepEqual(calls, [])
|
|
32
|
+
const adjusted = await adjustControl({ element_id: 'handle', targetValue: 20 }, deps)
|
|
33
|
+
assert.equal(adjusted.success, true)
|
|
34
|
+
assert.equal(adjusted.attempts, 2)
|
|
35
|
+
assert.equal(adjusted.adjustment_mode, 'coordinate')
|
|
36
|
+
assert.deepEqual(calls, ['semantic', 'coordinate', 'verify'], 'failed direct capability falls back in the established order')
|
|
37
|
+
calls.length = 0
|
|
38
|
+
const stale = await adjustControl({ element_id: 'missing', targetValue: 20 }, { ...deps, getHandle: () => undefined })
|
|
39
|
+
assert.equal(stale.success, false)
|
|
40
|
+
assert.equal(stale.failure_code, 'STALE_REFERENCE')
|
|
41
|
+
assert.deepEqual(calls, [], 'missing handles cannot dispatch native input')
|
|
42
|
+
console.log('PASS injected adjustment service: read-only convergence, strategy order, attempt accounting and stale-handle rejection')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import type { ExpectStateResponse } from '../../../src/types.js'
|
|
3
|
+
import { verifyAdjustment, type AdjustmentVerificationInput } from '../../../src/interact/adjustment/adjustment-verification.js'
|
|
4
|
+
|
|
5
|
+
const input: AdjustmentVerificationInput = {
|
|
6
|
+
labeled: false, currentEl: { state: { value: 10 } },
|
|
7
|
+
actualState: { property: 'value', value: 10 }, property: 'value', target: 20, tolerance: 0.5,
|
|
8
|
+
expectation: { element_id: 'control', property: 'value', expected: 20, platform: 'android', deviceId: 'fixture' }
|
|
9
|
+
}
|
|
10
|
+
let observations = 0
|
|
11
|
+
let assertions = 0
|
|
12
|
+
const verified = await verifyAdjustment(input, {
|
|
13
|
+
observe: async () => { throw new Error('ordinary verification must use the assertion boundary') },
|
|
14
|
+
expectState: async args => {
|
|
15
|
+
assertions++
|
|
16
|
+
assert.equal(args.element_id, 'control')
|
|
17
|
+
return { success: false, observed_state: { value: 20.25 }, reason: 'exact comparison failed' } as ExpectStateResponse
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
assert.equal(assertions, 1)
|
|
21
|
+
assert.equal(verified.withinTolerance, true, 'numeric tolerance is independent of exact assertion success')
|
|
22
|
+
assert.equal(verified.observedState?.value, 20.25)
|
|
23
|
+
const stale = await verifyAdjustment({ ...input, labeled: true, actualState: { property: 'value', value: 20 } }, {
|
|
24
|
+
observe: async () => { observations++; return { elements: [] } },
|
|
25
|
+
expectState: async () => { throw new Error('label verification must acquire a new tree') }
|
|
26
|
+
})
|
|
27
|
+
assert.equal(observations, 1)
|
|
28
|
+
assert.equal(stale.withinTolerance, false, 'fallback state must not establish fresh convergence')
|
|
29
|
+
assert.equal(stale.verification.success, false)
|
|
30
|
+
assert.equal(stale.observedState?.value, 20, 'last readable state remains available for diagnostics')
|
|
31
|
+
await assert.rejects(verifyAdjustment(input, {
|
|
32
|
+
observe: async () => null,
|
|
33
|
+
expectState: async () => { throw new Error('ACTION_TIMEOUT') }
|
|
34
|
+
}), /ACTION_TIMEOUT/, 'timeouts propagate without manufacturing an observation')
|
|
35
|
+
console.log('PASS adjustment verification dependencies, tolerance, freshness and timeout preservation')
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import type { Clock } from '../../../src/runtime/clock.js'
|
|
3
|
+
import type { UiTree } from '../../../src/interact/observation.js'
|
|
4
|
+
import { waitForUI } from '../../../src/interact/waits/ui-wait.js'
|
|
5
|
+
import { waitForScreenChange } from '../../../src/interact/waits/screen-wait.js'
|
|
6
|
+
import { findElement } from '../../../src/interact/targeting/find-element.js'
|
|
7
|
+
import { expectState } from '../../../src/interact/assertions/state-assertion.js'
|
|
8
|
+
import { _resolvedTargetFromElement } from '../../../src/interact/targeting/element-helpers.js'
|
|
9
|
+
|
|
10
|
+
function fakeClock() {
|
|
11
|
+
let now = 1000
|
|
12
|
+
const delays: number[] = []
|
|
13
|
+
const clock: Clock = { now: () => now, sleep: async ms => { delays.push(ms); now += ms } }
|
|
14
|
+
return { clock, delays }
|
|
15
|
+
}
|
|
16
|
+
const resolve = (_platform: string, _device: string | undefined, element: NonNullable<UiTree['elements']>[number], index: number) => _resolvedTargetFromElement('test-id', element, index)
|
|
17
|
+
const present = (clock: Clock): UiTree => ({ captured_at_ms: clock.now(), elements: [{ text: 'Button', state: { enabled: true } }] })
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
const { clock, delays } = fakeClock()
|
|
21
|
+
const result = await waitForUI({ selector: { text: 'Button' }, timeout_ms: 500, poll_interval_ms: 50 }, {
|
|
22
|
+
clock, observe: async () => present(clock), resolve
|
|
23
|
+
})
|
|
24
|
+
assert.equal(result?.status, 'success')
|
|
25
|
+
assert.deepEqual(result?.metrics, { latency_ms: 50, poll_count: 2, attempts: 1 })
|
|
26
|
+
assert.deepEqual(delays, [50])
|
|
27
|
+
}
|
|
28
|
+
{
|
|
29
|
+
const { clock, delays } = fakeClock()
|
|
30
|
+
const result = await waitForUI({ selector: { text: 'missing' }, timeout_ms: 100, poll_interval_ms: 50,
|
|
31
|
+
retry: { max_attempts: 2, backoff_ms: 20 } }, { clock, observe: async () => ({ elements: [] }), resolve })
|
|
32
|
+
assert.equal(result?.status, 'timeout')
|
|
33
|
+
assert.deepEqual(result?.metrics, { latency_ms: 320, poll_count: 6, attempts: 2 })
|
|
34
|
+
assert.deepEqual(delays, [50, 50, 50, 20, 50, 50, 50])
|
|
35
|
+
}
|
|
36
|
+
{
|
|
37
|
+
const { clock, delays } = fakeClock()
|
|
38
|
+
let samples = 0
|
|
39
|
+
const result = await waitForScreenChange({ previousFingerprint: 'old', timeoutMs: 500, pollIntervalMs: 50 }, {
|
|
40
|
+
clock, fingerprint: async () => ({ fingerprint: samples++ === 0 ? 'old' : 'new' })
|
|
41
|
+
})
|
|
42
|
+
assert.equal(result.success, true)
|
|
43
|
+
assert.equal(result.elapsedMs, 100)
|
|
44
|
+
assert.deepEqual(delays, [50, 50])
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
const { clock, delays } = fakeClock()
|
|
48
|
+
const result = await findElement({ query: 'missing', timeoutMs: 200 }, async () => ({ elements: [] }), clock)
|
|
49
|
+
assert.equal(result.found, false)
|
|
50
|
+
assert.deepEqual(delays, [100, 100, 100])
|
|
51
|
+
}
|
|
52
|
+
{
|
|
53
|
+
const { clock, delays } = fakeClock()
|
|
54
|
+
const result = await expectState({ selector: { text: 'Button' }, property: 'enabled', expected: true }, {
|
|
55
|
+
clock, observe: async () => present(clock), getHandle: () => undefined,
|
|
56
|
+
currentHandle: () => null, target: (platform, device, match) => resolve(platform, device, match.el, match.idx)
|
|
57
|
+
})
|
|
58
|
+
assert.equal(result.success, true)
|
|
59
|
+
assert.equal(result.stabilization_window_ms, 150)
|
|
60
|
+
assert.deepEqual(delays, [150])
|
|
61
|
+
assert(result.trace.steps.every(step => step.timestamp >= 1000 && step.timestamp <= 1150))
|
|
62
|
+
}
|
|
63
|
+
console.log('PASS injected clocks: wait stability, retry/backoff deadlines, screen confirmation, search expiry and state trace timestamps without global timer replacement')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { _matchesSelector, _resolveActionableAncestor, _normalizeBounds } from '../../../src/interact/targeting/element-helpers.js'
|
|
3
|
+
import { _readNumericControlRange, _quantizeTargetValue } from '../../../src/interact/adjustment/control-values.js'
|
|
4
|
+
|
|
5
|
+
assert(_matchesSelector({ text: 'Save', resourceId: 'app:id/save' }, { text: 'save', resource_id: 'app:id/save' }))
|
|
6
|
+
assert(!_matchesSelector({ text: 'Save', resourceId: 'app:id/other' }, { text: 'save', resource_id: 'app:id/save' }), 'all supplied selector fields constrain selection')
|
|
7
|
+
assert(_matchesSelector({ contentDesc: 'Save changes' }, { accessibility_id: 'changes', contains: true }))
|
|
8
|
+
assert(!_matchesSelector({ text: 'Save' }, undefined))
|
|
9
|
+
assert.deepEqual(_normalizeBounds(['1', 2, 3, 4]), [1, 2, 3, 4])
|
|
10
|
+
assert.equal(_normalizeBounds([1, 'invalid', 3, 4]), null)
|
|
11
|
+
const elements = [
|
|
12
|
+
{ id: 'parent', clickable: true, bounds: [0, 0, 100, 100] },
|
|
13
|
+
{ parentId: 'parent', text: 'Save', bounds: [20, 20, 40, 40] }
|
|
14
|
+
]
|
|
15
|
+
assert.deepEqual(_resolveActionableAncestor(elements, { el: elements[1], idx: 1 }), { el: elements[0], idx: 0 })
|
|
16
|
+
assert.deepEqual(_readNumericControlRange({ state: { value: '3 of 10' } }), { min: 0, max: 10 })
|
|
17
|
+
assert.equal(_readNumericControlRange({ state: { value_range: { min: 10, max: 0 } } }), null)
|
|
18
|
+
assert.equal(_quantizeTargetValue(0.26, 0, 1, 0.1), 0.3)
|
|
19
|
+
console.log('PASS injected-free selector, actionable ancestor, numeric range and snapping helpers')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { waitForUICore } from '../../../src/interact/waits/legacy-wait.js'
|
|
3
|
+
import type { LegacyWaitDependencies } from '../../../src/interact/waits/legacy-wait-state.js'
|
|
4
|
+
|
|
5
|
+
const calls: string[] = []
|
|
6
|
+
const deps: LegacyWaitDependencies = {
|
|
7
|
+
fingerprint: async () => { calls.push('fingerprint'); return { fingerprint: 'initial' } },
|
|
8
|
+
logs: async () => { calls.push('logs'); return { logs: ['before'] } },
|
|
9
|
+
stream: async () => { calls.push('stream'); return { entries: [{ message: 'operation done' }] } },
|
|
10
|
+
find: async () => { throw new Error('log waits must not search UI elements') },
|
|
11
|
+
snapshot: async () => { throw new Error('successful waits must not collect failure snapshots') }
|
|
12
|
+
}
|
|
13
|
+
const result = await waitForUICore({ type: 'log', query: 'done', timeoutMs: 1000 }, deps)
|
|
14
|
+
assert.equal(result.success, true)
|
|
15
|
+
assert.equal(result.observed_state, true)
|
|
16
|
+
assert.deepEqual(calls, ['fingerprint', 'logs', 'stream'])
|
|
17
|
+
console.log('PASS injected legacy wait: baseline collection, stream-first matching and no failure snapshot')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { compareReadableState } from '../../../src/interact/assertions/state-comparison.js'
|
|
3
|
+
|
|
4
|
+
assert.equal(compareReadableState('checked', true, { checked: true }).success, true)
|
|
5
|
+
const invalidBoolean = compareReadableState('checked', 'true', { checked: true })
|
|
6
|
+
assert.equal(invalidBoolean.success, false)
|
|
7
|
+
assert.equal(invalidBoolean.reason, 'expected checked must be boolean')
|
|
8
|
+
assert.equal(invalidBoolean.observedValue, true)
|
|
9
|
+
assert.equal(invalidBoolean.rawValue, null)
|
|
10
|
+
assert.equal(compareReadableState('value', 20, { value: '20' }).success, false, 'numeric assertions do not coerce readable strings')
|
|
11
|
+
assert.equal(compareReadableState('value', '20', { value: '20' }).success, true)
|
|
12
|
+
assert.equal(compareReadableState('selected', { id: 'dark' }, { selected: { id: 'dark', label: 'Dark' } }).success, true)
|
|
13
|
+
assert.equal(compareReadableState('selected', false, { selected: false }).success, true)
|
|
14
|
+
assert.equal(compareReadableState('selected', true, { selected: 'true' }).reason, 'selected state is not boolean')
|
|
15
|
+
assert.equal(compareReadableState('text_value', '', { text_value: '' }).reason, 'expected text_value must be string')
|
|
16
|
+
assert.equal(compareReadableState('value', 20, null).reason, 'value state unavailable')
|
|
17
|
+
const range = { min: 0, max: 20 }
|
|
18
|
+
assert.equal(compareReadableState('value_range', range, { value_range: range }).success, true, 'fallback comparison preserves reference equality')
|
|
19
|
+
assert.equal(compareReadableState('value_range', { min: 0, max: 20 }, { value_range: range }).success, false)
|
|
20
|
+
console.log('PASS readable-state comparison compatibility without UI or global mocks')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { ResolvedElements } from '../../../src/interact/targeting/resolved-elements.js'
|
|
3
|
+
import { findScopedElement, resolveUiChangeScope } from '../../../src/interact/targeting/target-scope.js'
|
|
4
|
+
import type { ResolvedUiElementContext } from '../../../src/interact/model.js'
|
|
5
|
+
|
|
6
|
+
const store = new ResolvedElements(2)
|
|
7
|
+
const context = (id: string): ResolvedUiElementContext => ({
|
|
8
|
+
elementId: id, platform: 'android', deviceId: 'fixture', bounds: null, index: 0, stable_id: id
|
|
9
|
+
})
|
|
10
|
+
store.remember('one', context('one'))
|
|
11
|
+
store.remember('two', context('two'))
|
|
12
|
+
store.remember('one', context('one'))
|
|
13
|
+
store.remember('three', context('three'))
|
|
14
|
+
assert.equal(store.get('two'), undefined, 'oldest handle is evicted; refresh updates insertion order')
|
|
15
|
+
assert(store.get('one'))
|
|
16
|
+
const tree = { elements: [{ text: 'Root', stable_id: 'one', children: [1] }, { text: 'Child', parentId: 0 }] }
|
|
17
|
+
const match = findScopedElement(tree, 'one', store)
|
|
18
|
+
assert.equal(match?.index, 0)
|
|
19
|
+
assert.equal(match?.reason, 'stable_id_match')
|
|
20
|
+
let calls = 0
|
|
21
|
+
const subtree = resolveUiChangeScope(tree, 'subtree', 'custom-handle', (_tree, id) => {
|
|
22
|
+
calls++
|
|
23
|
+
assert.equal(id, 'custom-handle')
|
|
24
|
+
return { index: 0, stableId: 'one', reason: 'injected_lookup' }
|
|
25
|
+
})
|
|
26
|
+
assert.equal(calls, 1)
|
|
27
|
+
assert.equal(subtree.elements.length, 2)
|
|
28
|
+
assert.equal(subtree.resolution.reason, 'injected_lookup')
|
|
29
|
+
assert.equal(resolveUiChangeScope(tree, 'subtree', '', () => { throw new Error('must not resolve empty targets') }).error?.code, 'INVALID_SCOPE')
|
|
30
|
+
assert.equal(resolveUiChangeScope(tree, 'subtree', 'missing', () => null).error?.code, 'ELEMENT_NOT_FOUND')
|
|
31
|
+
const other = new ResolvedElements(2)
|
|
32
|
+
assert.equal(other.get('one'), undefined, 'resolution stores do not share process-global state')
|
|
33
|
+
store.clear()
|
|
34
|
+
assert.equal(store.get('one'), undefined)
|
|
35
|
+
console.log('PASS bounded handle retention and injected subtree resolution without static replacements')
|
|
@@ -32,8 +32,10 @@ for (const extra of [{ error: 'offline' }, { elements: [] }, { captured_at_ms: 9
|
|
|
32
32
|
for (let index = 0; index < 20; index++) tracker.observe(sample(), 10000, 8000 + index)
|
|
33
33
|
assert.equal(tracker.summary.evidence.length, 8)
|
|
34
34
|
assert.equal(tracker.summary.capabilities.unresponsive, false)
|
|
35
|
-
assert.equal(tracker.summary.capabilities.debugger_paused,
|
|
35
|
+
assert.equal(tracker.summary.capabilities.debugger_paused, true)
|
|
36
36
|
assert.ok(tracker.deadline().limitations.includes('DEADLINE_EXCEEDED'))
|
|
37
37
|
assert.equal(new ReadinessTracker('android', 'fixture').observe(sample(), 10000, 9000).state, 'unknown', 'no cross-request history')
|
|
38
38
|
assert.ok(!JSON.stringify(tracker.summary).includes('Ready'), 'evidence contains codes rather than private labels')
|
|
39
|
+
assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused, false)
|
|
40
|
+
assert.ok(tracker.summary.limitations.includes('MANAGED_DEBUGGER_ONLY'))
|
|
39
41
|
console.log('Readiness classifier: PASS')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import sharp from 'sharp'
|
|
3
|
+
import { encodeScreenshot } from '../../../src/observe/screenshot-encoding.js'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
const alpha = await sharp({ create: { width: 2, height: 2, channels: 4, background: { r: 255, g: 0, b: 0, alpha: 0.5 } } }).png().toBuffer()
|
|
7
|
+
const opaque = await sharp({ create: { width: 2, height: 2, channels: 3, background: 'red' } }).png().toBuffer()
|
|
8
|
+
for (const platform of ['android', 'ios'] as const) {
|
|
9
|
+
const transparent = await encodeScreenshot(alpha, platform)
|
|
10
|
+
assert.equal(transparent.screenshot_mime, 'image/webp')
|
|
11
|
+
assert.equal((await sharp(Buffer.from(transparent.screenshot, 'base64')).metadata()).hasAlpha, true)
|
|
12
|
+
assert.equal(transparent.screenshot_fallback_mime, platform === 'ios' ? 'image/png' : 'image/jpeg')
|
|
13
|
+
if (platform === 'ios') assert.equal(transparent.screenshot_fallback, alpha.toString('base64'))
|
|
14
|
+
const solid = await encodeScreenshot(opaque, platform)
|
|
15
|
+
assert.equal(solid.screenshot_mime, 'image/webp')
|
|
16
|
+
assert.equal(solid.screenshot_fallback_mime, 'image/jpeg')
|
|
17
|
+
assert.equal((await sharp(Buffer.from(solid.screenshot_fallback!, 'base64')).metadata()).format, 'jpeg')
|
|
18
|
+
}
|
|
19
|
+
// An unreadable capture must survive conversion failures unchanged.
|
|
20
|
+
const invalid = Buffer.from('unreadable screenshot')
|
|
21
|
+
const originalError = console.error
|
|
22
|
+
try {
|
|
23
|
+
console.error = () => {}
|
|
24
|
+
for (const platform of ['android', 'ios'] as const) {
|
|
25
|
+
assert.deepEqual(await encodeScreenshot(invalid, platform), { screenshot: invalid.toString('base64'), screenshot_mime: 'image/png' })
|
|
26
|
+
}
|
|
27
|
+
} finally { console.error = originalError }
|
|
28
|
+
console.log('Screenshot alpha, fallback format and failed conversion tests passed')
|
|
29
|
+
}
|
|
30
|
+
run().catch(error => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { snapshotLog, streamLog } from '../../../src/observe/snapshot-logs.js'
|
|
3
|
+
|
|
4
|
+
const iso = '2026-10-30T12:00:00Z'
|
|
5
|
+
assert.deepEqual(snapshotLog({ timestamp: 123, message: 'warning', levelName: 'warn' }), { timestamp: 123, message: 'warning', level: 'WARN' })
|
|
6
|
+
assert.equal(streamLog({ timestamp: 123, message: 'warning' }).timestamp, null)
|
|
7
|
+
assert.equal(snapshotLog({ timestamp: 123, _iso: iso, msg: 'hello' }).timestamp, 123)
|
|
8
|
+
assert.equal(streamLog({ timestamp: 123, _iso: iso, msg: 'hello' }).timestamp, Date.parse(iso))
|
|
9
|
+
assert.equal(snapshotLog('FATAL EXCEPTION: crash').level, 'ERROR')
|
|
10
|
+
assert.equal(streamLog('WARN: slow').level, 'WARN')
|
|
11
|
+
assert.equal(snapshotLog({ timestamp: 'invalid', msg: 'hello' }).timestamp, null)
|
|
12
|
+
assert.deepEqual(snapshotLog(null), { timestamp: null, level: 'INFO', message: '' })
|
|
13
|
+
assert.equal(snapshotLog({ context: 'value' }).message, '{"context":"value"}')
|
|
14
|
+
console.log('Snapshot and stream timestamp precedence and log normalization tests passed')
|