mobile-debug-mcp 0.34.0 → 0.36.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/.cursor-plugin/marketplace.json +1 -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 +218 -129
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- 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 +50 -11
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
- 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/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +133 -74
- package/dist/debugger/worker.js +59 -46
- 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 +77 -3092
- 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 +44 -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-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +244 -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 +5 -3
- package/dist/observe/readiness.js +33 -14
- 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-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +26 -21
- 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 +67 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +77 -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 +170 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/schemas/system-trace-schema.js +22 -0
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/server.js +4 -4
- 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 +48 -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 +67 -1
- package/docs/README.md +38 -0
- package/docs/agent-plugin-installation-verification.md +39 -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 +120 -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/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +138 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +120 -4
- 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 +89 -14
- package/docs/tools/system.md +12 -5
- package/package.json +12 -8
- package/scripts/agent-plugin.mjs +55 -5
- 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 +3 -2
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +10 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +326 -155
- package/src/debugger/worker.ts +57 -41
- 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 +89 -3494
- 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 +67 -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-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +204 -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 +6 -4
- package/src/observe/readiness.ts +42 -21
- 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-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +28 -19
- 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 +58 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +86 -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 +170 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +46 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/schemas/system-trace-schema.ts +23 -0
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +4 -4
- 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 +60 -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/debugger/runtime.ios.test.ts +221 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -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 +3696 -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/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/ios-trace-shutdown.ts +29 -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/handle-guards.test.ts +88 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -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/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
- package/test/unit/observe/readiness.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/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -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 +12 -3
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/debugger-handlers.js +0 -38
- package/dist/server/system-trace-schema.js +0 -17
- 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/debugger-schema.ts +0 -37
- package/src/server/system-trace-schema.ts +0 -17
- 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/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,67 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { DebuggerSessions } from '../../../src/debugger/sessions.js'
|
|
3
|
+
import type { DebugNative, DebugTarget } from '../../../src/debugger/android.js'
|
|
4
|
+
import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
5
|
+
import { diagnostic } from '../../../src/debugger/diagnostics.js'
|
|
6
|
+
const target: DebugTarget = { app_id: 'test.app', process_name: 'test.app', pid: 1, uid: 2, start_identity: '3' }
|
|
7
|
+
let removed = 0
|
|
8
|
+
let closed = 0
|
|
9
|
+
let cleanupFails = false
|
|
10
|
+
let mode = ''
|
|
11
|
+
const callbacks = new Map<string, (event: WorkerEvent) => void>()
|
|
12
|
+
const native: DebugNative = {
|
|
13
|
+
candidates: async () => [target], identity: async () => target, forward: async () => 12345,
|
|
14
|
+
removeForward: async () => { if (cleanupFails) throw new Error('private'); removed++ }
|
|
15
|
+
}
|
|
16
|
+
const sessions = new DebuggerSessions(native, async emit => {
|
|
17
|
+
callbacks.set(mode, emit)
|
|
18
|
+
return {
|
|
19
|
+
call: async command => {
|
|
20
|
+
if (command === 'attach' && mode === 'attach') throw new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('attach', 'connection_refused'))
|
|
21
|
+
if (command === 'step') {
|
|
22
|
+
emit({ event: 'worker_failed', diagnostic: diagnostic('ipc', 'invalid_message') })
|
|
23
|
+
throw new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'invalid_message'))
|
|
24
|
+
}
|
|
25
|
+
return {}
|
|
26
|
+
},
|
|
27
|
+
close: async () => { closed++ }
|
|
28
|
+
}
|
|
29
|
+
}, async () => ({ java: 'unused', directory: 'unused' }))
|
|
30
|
+
try {
|
|
31
|
+
mode = 'attach'
|
|
32
|
+
const failed = await sessions.attach('one', 'test.app')
|
|
33
|
+
assert.equal(failed.error.code, 'DEBUGGER_WORKER_FAILED')
|
|
34
|
+
assert.deepEqual(failed.error.diagnostic, diagnostic('attach', 'connection_refused'))
|
|
35
|
+
assert.equal(failed.cleanup.status, 'complete')
|
|
36
|
+
assert.equal(closed, 1); assert.equal(removed, 1)
|
|
37
|
+
mode = 'first'
|
|
38
|
+
const first = await sessions.attach('one', 'test.app')
|
|
39
|
+
mode = 'second'
|
|
40
|
+
const second = await sessions.attach('two', 'test.app')
|
|
41
|
+
callbacks.get('first')!({ event: 'stopped', stop_id: 'stop' })
|
|
42
|
+
await assert.rejects(sessions.command(first.debug_session_id, 'step', { stopId: 'stop' }), /DEBUGGER_WORKER_FAILED/)
|
|
43
|
+
const terminal = await sessions.command(first.debug_session_id, 'state')
|
|
44
|
+
assert.deepEqual(terminal.error.diagnostic, diagnostic('ipc', 'invalid_message'), 'event/rejection race retains primary evidence after cleanup')
|
|
45
|
+
assert.equal(terminal.cleanup.status, 'complete')
|
|
46
|
+
await assert.rejects(sessions.command(first.debug_session_id, 'stack', {}), (e: unknown) => e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED' && e.diagnostic?.reason === 'invalid_message')
|
|
47
|
+
assert.equal((await sessions.command(second.debug_session_id, 'state')).success, true, 'other device is isolated')
|
|
48
|
+
const aborted = new AbortController(); aborted.abort()
|
|
49
|
+
await assert.rejects(sessions.command(second.debug_session_id, 'events', { signal: aborted.signal }), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'cancelled')
|
|
50
|
+
cleanupFails = true
|
|
51
|
+
const incomplete = await sessions.command(second.debug_session_id, 'detach')
|
|
52
|
+
assert.equal(incomplete.error.code, 'DEBUGGER_CLEANUP_FAILED')
|
|
53
|
+
assert.deepEqual(incomplete.error.diagnostic, diagnostic('cleanup', 'operation_failed'))
|
|
54
|
+
cleanupFails = false
|
|
55
|
+
assert.equal((await sessions.command(second.debug_session_id, 'detach')).cleanup.status, 'complete')
|
|
56
|
+
mode = 'shutdown'
|
|
57
|
+
const shutdown = await sessions.attach('three', 'test.app')
|
|
58
|
+
await sessions.shutdown()
|
|
59
|
+
assert.equal(sessions.summary(shutdown.debug_session_id).cleanup.status, 'complete')
|
|
60
|
+
} finally { await sessions.shutdown() }
|
|
61
|
+
const badProbes = new DebuggerSessions({ ...native, candidates: async () => { throw new Error('private-token') } }, undefined,
|
|
62
|
+
async () => { throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('compilation', 'operation_failed')) })
|
|
63
|
+
const probe = await badProbes.capabilities('one', 'test.app')
|
|
64
|
+
assert.equal(probe.success, true); assert.equal(probe.supported, false)
|
|
65
|
+
assert.deepEqual(probe.diagnostics, [diagnostic('compilation', 'operation_failed'), diagnostic('target_resolution', 'unknown')])
|
|
66
|
+
assert.ok(!JSON.stringify(probe).includes('private-token'))
|
|
67
|
+
console.log('PASS diagnostic session retention/races, cleanup retry, isolation, shutdown and ordered capability diagnostics')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { diagnostic, stages, reasons, classify, parseDiagnostic } from '../../../src/debugger/diagnostics.js'
|
|
6
|
+
import { DebuggerError, JdiWorker, withDebuggerBudget, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
7
|
+
import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
|
|
8
|
+
import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
|
|
9
|
+
|
|
10
|
+
const secret = 'PRIVATE_LOCALS_TOKEN_/private/path'
|
|
11
|
+
for (const stage of stages) for (const reason of reasons) {
|
|
12
|
+
const value = diagnostic(stage, reason)
|
|
13
|
+
assert.ok(Buffer.byteLength(JSON.stringify(value)) <= 512)
|
|
14
|
+
assert.ok(value.recovery.length <= 240 && /^[\x00-\x7f]*$/.test(value.recovery))
|
|
15
|
+
assert.deepEqual(Object.keys(value), ['stage', 'reason', 'recovery'])
|
|
16
|
+
}
|
|
17
|
+
assert.deepEqual(parseDiagnostic({ stage: 'attach', reason: 'connection_refused', recovery: secret, raw: secret }), diagnostic('attach', 'connection_refused'))
|
|
18
|
+
assert.equal(classify(new Error(secret), 'attach', 'DEBUGGER_ATTACH_FAILED').diagnostic?.reason, 'unknown')
|
|
19
|
+
assert.equal(classify(new DebuggerError('DEBUGGER_STALE_REFERENCE'), 'command', 'DEBUGGER_WORKER_FAILED').diagnostic, undefined)
|
|
20
|
+
for (const hostile of [secret, { stage: secret, reason: 'unknown' }, { stage: 'attach', reason: secret }]) {
|
|
21
|
+
assert.throws(() => parseWorkerMessage({ id: 1, error: { code: 'DEBUGGER_WORKER_FAILED', diagnostic: hostile } }), /INVALID_WORKER_DIAGNOSTIC/)
|
|
22
|
+
}
|
|
23
|
+
assert.throws(() => parseWorkerMessage({ id: 1, error: { code: secret } }), /INVALID_WORKER_ERROR/)
|
|
24
|
+
const cancelled = new AbortController(); cancelled.abort()
|
|
25
|
+
const response = await handleDebugger('get_debugger_state', { debugSessionId: 'absent' }, cancelled.signal)
|
|
26
|
+
assert.equal(response.isError, true)
|
|
27
|
+
const payload = JSON.parse(response.content[0].text)
|
|
28
|
+
assert.equal(payload.error.code, 'DEBUGGER_CANCELLED')
|
|
29
|
+
assert.deepEqual(payload.error.diagnostic, diagnostic('unknown', 'cancelled'))
|
|
30
|
+
assert.equal((await handleDebugger('get_debugger_state', {})).isError, true)
|
|
31
|
+
|
|
32
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-diagnostics-test-'))
|
|
33
|
+
let index = 0
|
|
34
|
+
async function worker(body: string, onEvent: (event: WorkerEvent) => void = () => {}) {
|
|
35
|
+
const file = path.join(directory, `worker-${index++}`)
|
|
36
|
+
await writeFile(file, `#!${process.execPath}\n${body}\n`, { mode: 0o700 })
|
|
37
|
+
return new JdiWorker(file, directory, onEvent)
|
|
38
|
+
}
|
|
39
|
+
async function fails(body: string, stage: string, reason: string) {
|
|
40
|
+
const w = await worker(body)
|
|
41
|
+
try {
|
|
42
|
+
await assert.rejects(w.call('attach', {}, 3000), (error: unknown) => {
|
|
43
|
+
assert.ok(error instanceof DebuggerError)
|
|
44
|
+
assert.equal(error.code, 'DEBUGGER_WORKER_FAILED')
|
|
45
|
+
assert.deepEqual(error.diagnostic, diagnostic(stage as typeof stages[number], reason as typeof reasons[number]))
|
|
46
|
+
assert.ok(!JSON.stringify(error).includes(secret))
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
} finally { await w.close() }
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const missing = new JdiWorker(path.join(directory, 'missing'), directory, () => {})
|
|
53
|
+
try { await assert.rejects(missing.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'spawn_failed') } finally { await missing.close() }
|
|
54
|
+
await fails(`process.stderr.write(${JSON.stringify(secret)}); process.exit(7)`, 'worker_start', 'process_exited')
|
|
55
|
+
await fails(`process.stdout.write(${JSON.stringify(secret + '\n')}); setInterval(()=>{},1000)`, 'ipc', 'invalid_message')
|
|
56
|
+
await fails(`process.stdout.write('x'.repeat(1048577)+'\\n'); setInterval(()=>{},1000)`, 'ipc', 'message_limit')
|
|
57
|
+
const attach = await worker(`process.stdin.once('data', data => { const {id}=JSON.parse(data); console.log(JSON.stringify({id,error:{code:'DEBUGGER_WORKER_FAILED',diagnostic:{stage:'attach',reason:'connection_refused',raw:${JSON.stringify(secret)}}}})) })`)
|
|
58
|
+
try { await assert.rejects(attach.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'connection_refused') } finally { await attach.close() }
|
|
59
|
+
let ready = () => {}
|
|
60
|
+
const readyPromise = new Promise<void>(resolve => { ready = resolve })
|
|
61
|
+
const brokenPipe = await worker(`require('node:fs').closeSync(0); console.log(JSON.stringify({event:'ready'})); setInterval(()=>{},1000)`, () => ready())
|
|
62
|
+
try {
|
|
63
|
+
await readyPromise
|
|
64
|
+
await assert.rejects(brokenPipe.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'write_failed')
|
|
65
|
+
} finally { await brokenPipe.close() }
|
|
66
|
+
const afterReply = await worker(`process.stdin.once('data',data=>{const {id}=JSON.parse(data); console.log(JSON.stringify({id,result:{}})); setTimeout(()=>process.exit(7),100)})`)
|
|
67
|
+
try {
|
|
68
|
+
await afterReply.call('state')
|
|
69
|
+
await assert.rejects(afterReply.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'ipc' && e.diagnostic.reason === 'process_exited')
|
|
70
|
+
} finally { await afterReply.close() }
|
|
71
|
+
const idle = await worker('process.stdin.resume()')
|
|
72
|
+
try {
|
|
73
|
+
await assert.rejects(idle.call('stack', {}, 50), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'timeout')
|
|
74
|
+
const controller = new AbortController()
|
|
75
|
+
const call = withDebuggerBudget(1000, controller.signal, () => idle.call('attach'))
|
|
76
|
+
controller.abort()
|
|
77
|
+
await assert.rejects(call, (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'attach' && e.diagnostic.reason === 'cancelled')
|
|
78
|
+
} finally { await idle.close() }
|
|
79
|
+
} finally { await rm(directory, { recursive: true, force: true }) }
|
|
80
|
+
console.log('PASS diagnostic bounds/privacy, strict IPC, real worker spawn/exit/malformed/oversize, attach failure, timeout/cancel and MCP wrapping')
|
|
@@ -38,6 +38,10 @@ try {
|
|
|
38
38
|
assert.equal(journey.steps[1].success, false)
|
|
39
39
|
assert.equal(journey.steps[1].result.failure_code, 'DEBUGGER_PAUSED')
|
|
40
40
|
assert.equal(calls, 2, 'no action queued or retried after pause')
|
|
41
|
+
_setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('resolution unavailable') } })
|
|
42
|
+
const failedResolution = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
|
|
43
|
+
assert.equal(failedResolution.error.tool, 'tap')
|
|
44
|
+
assert.match(failedResolution.error.message, /resolution unavailable/, 'resolution errors retain the MCP error envelope')
|
|
41
45
|
console.log('PASS debugger readiness without UI acquisition, device scoping, dispatch guard, mid-journey pause preservation')
|
|
42
46
|
} finally {
|
|
43
47
|
debuggerSessions.paused = originalPause; debuggerSessions.hasActiveSessions = originalActive
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { debuggerSessions, iosDebuggerSessions } from '../../../src/debugger/sessions.js'
|
|
3
|
+
import { handleToolCall } from '../../../src/server-core.js'
|
|
4
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
5
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
6
|
+
import { markDispatchStarted } from '../../../src/utils/operation-budget.js'
|
|
7
|
+
import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
|
|
8
|
+
|
|
9
|
+
const original = {
|
|
10
|
+
androidPaused: debuggerSessions.paused, androidActive: debuggerSessions.hasActiveSessions,
|
|
11
|
+
iosPaused: iosDebuggerSessions.paused, iosActive: iosDebuggerSessions.hasActiveSessions,
|
|
12
|
+
tree: ToolsObserve.getUITreeHandler, tap: ToolsInteract.tapHandler
|
|
13
|
+
}
|
|
14
|
+
let iosPaused = false, androidPaused = false, racePause = false, reads = 0, androidResolutions = 0
|
|
15
|
+
const inputs: { platform?: string; deviceId?: string }[] = []
|
|
16
|
+
const pause: any = { debug_session_id: 'ios-session', target: { app_id: 'test.app', pid: 1 }, state: 'paused', captured_at_ms: 1 }
|
|
17
|
+
const tree = (platform: string, id: string): any => ({ device: { platform, id }, resolution: { width: 100, height: 100 },
|
|
18
|
+
elements: [{ text: 'Target', resourceId: 'target', bounds: [0, 0, 40, 40], visible: true, enabled: true, clickable: true }] })
|
|
19
|
+
const payload = (response: any) => JSON.parse(response.content.find((item: any) => item.type === 'text').text)
|
|
20
|
+
const call = async (elementId: string, hints: Record<string, unknown> = {}) => payload(await handleToolCall('tap_element', {
|
|
21
|
+
elementId, verificationMode: 'none', responseMode: 'compact', ...hints
|
|
22
|
+
}))
|
|
23
|
+
_setDeviceListersForTests({
|
|
24
|
+
listAndroidDevices: async () => { androidResolutions++; return [{ id: 'android', platform: 'android' } as any] },
|
|
25
|
+
listIOSDevices: async () => ['ios-a', 'ios-b'].map(id => ({ id, platform: 'ios', booted: true } as any))
|
|
26
|
+
})
|
|
27
|
+
debuggerSessions.hasActiveSessions = () => true
|
|
28
|
+
iosDebuggerSessions.hasActiveSessions = () => true
|
|
29
|
+
debuggerSessions.paused = id => androidPaused && id === 'android' ? { ...pause, debug_session_id: 'android-session' } : null
|
|
30
|
+
iosDebuggerSessions.paused = id => iosPaused && id === 'ios-a' ? pause : null
|
|
31
|
+
ToolsInteract._resetResolvedUiElementsForTests()
|
|
32
|
+
try {
|
|
33
|
+
ToolsObserve.getUITreeHandler = async ({ platform, deviceId }) => {
|
|
34
|
+
reads++
|
|
35
|
+
if (racePause) iosPaused = true
|
|
36
|
+
return tree(platform!, deviceId!)
|
|
37
|
+
}
|
|
38
|
+
ToolsInteract.tapHandler = async ({ platform, deviceId, x, y }) => {
|
|
39
|
+
markDispatchStarted()
|
|
40
|
+
inputs.push({ platform, deviceId })
|
|
41
|
+
return { success: true, device: { platform, id: deviceId }, x, y } as any
|
|
42
|
+
}
|
|
43
|
+
const handle = async (platform: 'android' | 'ios', deviceId: string) => {
|
|
44
|
+
const result = await ToolsInteract.waitForUIHandler({ platform, deviceId, selector: { text: 'Target' }, condition: 'exists', timeout_ms: 1000 })
|
|
45
|
+
assert.equal(result.status, 'success')
|
|
46
|
+
return result.element.elementId
|
|
47
|
+
}
|
|
48
|
+
const ios = await handle('ios', 'ios-a')
|
|
49
|
+
const otherIOS = await handle('ios', 'ios-b')
|
|
50
|
+
const android = await handle('android', 'android')
|
|
51
|
+
reads = 0; androidResolutions = 0; iosPaused = true
|
|
52
|
+
for (const hints of [{}, { deviceId: 'ios-a' }, { platform: 'ios' }]) {
|
|
53
|
+
const blocked = await call(ios, hints)
|
|
54
|
+
assert.equal(blocked.failure_code, 'DEBUGGER_PAUSED')
|
|
55
|
+
assert.equal(blocked.debugger.debug_session_id, 'ios-session')
|
|
56
|
+
assert.equal(blocked.delivery_status, 'not_delivered'); assert.equal(blocked.dispatch_started, false)
|
|
57
|
+
}
|
|
58
|
+
assert.equal(reads, 0, 'A known pause must block before hierarchy reads')
|
|
59
|
+
assert.equal(inputs.length, 0); assert.equal(androidResolutions, 0, 'An iOS handle must not resolve an Android device')
|
|
60
|
+
|
|
61
|
+
iosPaused = false
|
|
62
|
+
assert.equal((await call(ios)).success, true)
|
|
63
|
+
assert.deepEqual(inputs.pop(), { platform: 'ios', deviceId: 'ios-a' })
|
|
64
|
+
racePause = true
|
|
65
|
+
const raced = await call(ios)
|
|
66
|
+
assert.equal(raced.failure_code, 'DEBUGGER_PAUSED')
|
|
67
|
+
assert.equal(raced.delivery_status, 'not_delivered'); assert.equal(inputs.length, 0, 'A pause during target refresh must block native dispatch')
|
|
68
|
+
racePause = false
|
|
69
|
+
|
|
70
|
+
assert.equal((await call(android)).success, true, 'An unrelated iOS pause must not block Android')
|
|
71
|
+
assert.deepEqual(inputs.pop(), { platform: 'android', deviceId: 'android' })
|
|
72
|
+
androidPaused = true
|
|
73
|
+
assert.equal((await call(android)).failure_code, 'DEBUGGER_PAUSED')
|
|
74
|
+
assert.equal((await call(otherIOS)).success, true, 'Other iOS devices must remain usable')
|
|
75
|
+
assert.deepEqual(inputs.pop(), { platform: 'ios', deviceId: 'ios-b' })
|
|
76
|
+
androidPaused = false; reads = 0
|
|
77
|
+
for (const hints of [{ platform: 'android', deviceId: 'android' }, { platform: 'ios', deviceId: 'ios-b' }]) {
|
|
78
|
+
assert.equal((await call(ios, hints)).failure_code, 'STALE_REFERENCE', 'Explicit mismatches retain handle validation')
|
|
79
|
+
}
|
|
80
|
+
assert.equal((await call('unknown', { platform: 'ios', deviceId: 'ios-b' })).failure_code, 'STALE_REFERENCE')
|
|
81
|
+
assert.equal(reads, 0); assert.equal(inputs.length, 0)
|
|
82
|
+
console.log('PASS stored-handle debugger guards: omitted target, no observation/input while paused, racing pause, platform/device isolation and mismatches')
|
|
83
|
+
} finally {
|
|
84
|
+
debuggerSessions.paused = original.androidPaused; debuggerSessions.hasActiveSessions = original.androidActive
|
|
85
|
+
iosDebuggerSessions.paused = original.iosPaused; iosDebuggerSessions.hasActiveSessions = original.iosActive
|
|
86
|
+
ToolsObserve.getUITreeHandler = original.tree; ToolsInteract.tapHandler = original.tap
|
|
87
|
+
ToolsInteract._resetResolvedUiElementsForTests(); _resetDeviceListersForTests()
|
|
88
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
const exec = promisify(execFile)
|
|
7
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-fake-lldb-'))
|
|
8
|
+
const python = path.join(directory, 'python')
|
|
9
|
+
const xcrun = path.join(directory, 'xcrun')
|
|
10
|
+
const log = path.join(directory, 'calls')
|
|
11
|
+
const failed = path.join(directory, 'failed')
|
|
12
|
+
try {
|
|
13
|
+
await writeFile(xcrun, `#!${process.execPath}
|
|
14
|
+
const fs=require('node:fs');fs.appendFileSync(${JSON.stringify(log)},'xcrun\\n');
|
|
15
|
+
console.log(process.argv[2]==='lldb'?'/fake/lldb-python':${JSON.stringify(python)});
|
|
16
|
+
`, { mode: 0o700 })
|
|
17
|
+
await writeFile(python, `#!${process.execPath}
|
|
18
|
+
const fs=require('node:fs');
|
|
19
|
+
if(process.argv.includes('-c')) {
|
|
20
|
+
fs.appendFileSync(${JSON.stringify(log)},'probe\\n');
|
|
21
|
+
if(!fs.existsSync(${JSON.stringify(failed)})){fs.writeFileSync(${JSON.stringify(failed)},'');process.exit(1)}
|
|
22
|
+
console.log('ready');
|
|
23
|
+
} else {
|
|
24
|
+
fs.appendFileSync(${JSON.stringify(log)},'worker\\n');
|
|
25
|
+
require('node:readline').createInterface({input:process.stdin}).on('line',line=>{
|
|
26
|
+
const request=JSON.parse(line);console.log(JSON.stringify({id:request.id,result:{detached:true}}));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
`, { mode: 0o700 })
|
|
30
|
+
const code = `
|
|
31
|
+
import assert from 'node:assert/strict';import {readFileSync} from 'node:fs';
|
|
32
|
+
// Load the TypeScript toolchain before emulating macOS so Linux selects its own esbuild.
|
|
33
|
+
const {prepareIOSWorker,createIOSWorker}=await import('./src/debugger/ios-worker.ts');
|
|
34
|
+
// Isolate platform emulation and native binaries from the unit runner process.
|
|
35
|
+
Object.defineProperty(process,'platform',{value:'darwin'});
|
|
36
|
+
const first=prepareIOSWorker();assert.strictEqual(first,prepareIOSWorker());
|
|
37
|
+
const failed=await Promise.allSettled([first,prepareIOSWorker()]);
|
|
38
|
+
assert.ok(failed.every(result=>result.status==='rejected'));
|
|
39
|
+
const retry=prepareIOSWorker();assert.notStrictEqual(first,retry);
|
|
40
|
+
assert.strictEqual(retry,prepareIOSWorker());
|
|
41
|
+
const configuration=await retry;
|
|
42
|
+
assert.equal(configuration.python,${JSON.stringify(python)});
|
|
43
|
+
assert.equal(configuration.env.PYTHONPATH,'/fake/lldb-python');
|
|
44
|
+
assert.equal(configuration.env.PYTHONNOUSERSITE,'1');
|
|
45
|
+
assert.strictEqual(await prepareIOSWorker(),configuration);
|
|
46
|
+
const worker=await createIOSWorker(()=>{});await worker.close();
|
|
47
|
+
const calls=readFileSync(${JSON.stringify(log)},'utf8').trim().split('\\n');
|
|
48
|
+
assert.equal(calls.filter(call=>call==='xcrun').length,4);
|
|
49
|
+
assert.equal(calls.filter(call=>call==='probe').length,2);
|
|
50
|
+
assert.equal(calls.filter(call=>call==='worker').length,1);
|
|
51
|
+
console.log('PASS shared iOS preparation, failed-probe retry, configuration and worker cache reuse');
|
|
52
|
+
`
|
|
53
|
+
const result = await exec(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', code], {
|
|
54
|
+
env: { ...process.env, XCRUN_PATH: xcrun }, timeout: 15000
|
|
55
|
+
})
|
|
56
|
+
console.log(result.stdout.trim())
|
|
57
|
+
} finally { await rm(directory, { recursive: true, force: true }) }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtemp, writeFile, rm, realpath } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { IOSDebugNative } from '../../../src/debugger/ios.js'
|
|
6
|
+
import { DebuggerSessions, iosDebuggerSessions, debuggerSessions } from '../../../src/debugger/sessions.js'
|
|
7
|
+
import { iosCapabilities, iosLimitations } from '../../../src/debugger/ios-worker.js'
|
|
8
|
+
import { debuggerContracts, debuggerTools } from '../../../src/server/schemas/debugger-schema.js'
|
|
9
|
+
import { withDebuggerGuard, checkDebuggerGuard, pausedReadiness } from '../../../src/debugger/guard.js'
|
|
10
|
+
import { execCommandWithDiagnostics } from '../../../src/utils/ios/utils.js'
|
|
11
|
+
import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
|
|
12
|
+
import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
|
|
13
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'ios-debugger-unit-'))
|
|
14
|
+
await writeFile(path.join(directory, 'App'), '')
|
|
15
|
+
const executable = await realpath(path.join(directory, 'App'))
|
|
16
|
+
const device = 'F9243BAF-62A8-4990-8A99-17F527F56FDA'
|
|
17
|
+
let processPath = executable, started = 'Mon Nov 4 12:00:00 2026', services = '123\t0\tUIKitApplication:dev.fixture.app[uuid]'
|
|
18
|
+
const calls: string[][] = []
|
|
19
|
+
const native = new IOSDebugNative(async (file, args) => {
|
|
20
|
+
calls.push([file, ...args])
|
|
21
|
+
if (args[0] === 'simctl' && args[1] === 'list') return JSON.stringify({ devices: { runtime: [{ udid: device, state: 'Booted' }] } })
|
|
22
|
+
if (args.includes('get_app_container')) return directory
|
|
23
|
+
if (file === '/usr/bin/plutil') return 'App'
|
|
24
|
+
if (args.includes('launchctl')) return services
|
|
25
|
+
if (args.includes('comm=')) return processPath
|
|
26
|
+
if (args.includes('lstart=')) return started
|
|
27
|
+
throw new Error('unexpected native command')
|
|
28
|
+
})
|
|
29
|
+
const base = { debugSessionId: 's', line: 1 }
|
|
30
|
+
for (const selector of [{}, { className: 'dev.App', sourceFile: '/tmp/App.swift' }, { sourceFile: 'App.swift' }]) {
|
|
31
|
+
assert.equal(debuggerContracts.set_debugger_breakpoint.schema.safeParse({ ...base, ...selector }).success, false)
|
|
32
|
+
}
|
|
33
|
+
for (const selector of [{ className: 'dev.App' }, { sourceFile: '/tmp/App.swift' }]) {
|
|
34
|
+
assert.equal(debuggerContracts.set_debugger_breakpoint.schema.safeParse({ ...base, ...selector }).success, true)
|
|
35
|
+
}
|
|
36
|
+
assert.ok(debuggerTools.find(t => t.name === 'attach_debugger')!.inputSchema.properties.platform)
|
|
37
|
+
let emit: (event: WorkerEvent) => void = () => {}
|
|
38
|
+
let closed = 0
|
|
39
|
+
const sessions = new DebuggerSessions(native, async onEvent => {
|
|
40
|
+
emit = onEvent
|
|
41
|
+
return { call: async (command, args) => {
|
|
42
|
+
if (command === 'step' && args?.threadId === 'foreign') throw new DebuggerError('DEBUGGER_STALE_REFERENCE')
|
|
43
|
+
if (command === 'attach') { assert.equal(args?.pid, 123); assert.equal(args?.executablePath, executable); emit({ event: 'stopped', stop_id: 'racing-stop', thread_id: 't' }) }
|
|
44
|
+
return { breakpoints: [] }
|
|
45
|
+
}, close: async () => { closed++ } }
|
|
46
|
+
}, async () => {}, { platform: 'ios', capabilities: iosCapabilities, limitations: iosLimitations })
|
|
47
|
+
const androidPause = debuggerSessions.paused, iosPause = iosDebuggerSessions.paused
|
|
48
|
+
try {
|
|
49
|
+
const candidates = await native.candidates(device, 'dev.fixture.app')
|
|
50
|
+
assert.equal(candidates[0].pid, 123); assert.equal(candidates[0].executable_path, executable)
|
|
51
|
+
assert.equal(candidates[0].uid, null)
|
|
52
|
+
await assert.rejects(native.candidates('booted', 'dev.fixture.app'), /INVALID_ARGUMENT/)
|
|
53
|
+
await assert.rejects(native.candidates('00000000-0000-0000-0000-000000000000', 'dev.fixture.app'), /DEBUGGER_UNSUPPORTED/)
|
|
54
|
+
processPath = '/tmp/foreign-process'
|
|
55
|
+
await assert.rejects(native.identity(device, candidates[0]), /PROCESS_CHANGED/)
|
|
56
|
+
processPath = executable; started = 'new process'
|
|
57
|
+
await assert.rejects(native.identity(device, candidates[0]), /PROCESS_CHANGED/)
|
|
58
|
+
started = candidates[0].start_identity
|
|
59
|
+
services = '999\t0\tUIKitApplication:dev.fixture.other[uuid]'
|
|
60
|
+
await assert.rejects(native.identity(device, candidates[0]), /PROCESS_CHANGED/)
|
|
61
|
+
services = '123\t0\tUIKitApplication:dev.fixture.app[uuid]'
|
|
62
|
+
const attached = await sessions.attach(device, 'dev.fixture.app')
|
|
63
|
+
assert.equal(attached.device.platform, 'ios'); assert.equal(attached.state, 'paused', 'racing attach stop is preserved')
|
|
64
|
+
assert.deepEqual(attached.cleanup.remaining_resources, ['lldb_worker'])
|
|
65
|
+
await assert.rejects(sessions.command(attached.debug_session_id, 'set', { className: 'Wrong', line: 1 }), /INVALID_ARGUMENT/)
|
|
66
|
+
emit({ event: 'stopped', stop_id: 'fresh', thread_id: 't' })
|
|
67
|
+
await assert.rejects(sessions.command(attached.debug_session_id, 'step', { stopId: 'fresh', threadId: 'foreign', kind: 'into' }), /DEBUGGER_STALE_REFERENCE/)
|
|
68
|
+
assert.equal(sessions.paused(device)?.stop?.stop_id, 'fresh', 'Rejected step preserves known suspension and guard')
|
|
69
|
+
await assert.rejects(sessions.command(attached.debug_session_id, 'locals', { stopId: 'racing-stop' }), /DEBUGGER_STALE_REFERENCE/)
|
|
70
|
+
iosDebuggerSessions.paused = id => id === 'same-id' ? { ...attached, debug_session_id: 'ios' } : null
|
|
71
|
+
debuggerSessions.paused = id => id === 'same-id' ? { ...attached, debug_session_id: 'android' } : null
|
|
72
|
+
assert.equal(pausedReadiness('same-id', 'ios')?.debug_session_id, 'ios')
|
|
73
|
+
assert.equal(pausedReadiness('same-id', 'android')?.debug_session_id, 'android')
|
|
74
|
+
await assert.rejects(withDebuggerGuard('ios', 'same-id', async () => checkDebuggerGuard(true)), /DEBUGGER_PAUSED/)
|
|
75
|
+
await withDebuggerGuard('ios', 'other', async () => checkDebuggerGuard())
|
|
76
|
+
await assert.rejects(withDebuggerGuard('ios', 'same-id', async () => execCommandWithDiagnostics(['simctl', 'launch', 'same-id', 'dev.fixture.app'])), /DEBUGGER_PAUSED/, 'Diagnostic fallback cannot resend a paused lifecycle command')
|
|
77
|
+
assert.equal((await sessions.command(attached.debug_session_id, 'detach')).cleanup.status, 'complete')
|
|
78
|
+
assert.equal(closed, 1)
|
|
79
|
+
assert.ok(!calls.some(call => call.includes('forward')), 'iOS must not allocate or remove ADB forwards')
|
|
80
|
+
const original = iosDebuggerSessions.capabilities
|
|
81
|
+
try {
|
|
82
|
+
iosDebuggerSessions.capabilities = async () => { throw new DebuggerError('DEBUGGER_UNSUPPORTED') }
|
|
83
|
+
const response = await handleDebugger('get_debugger_capabilities', { platform: 'ios', deviceId: device, appId: 'dev.fixture.app' })
|
|
84
|
+
const payload = JSON.parse(response.content[0].text)
|
|
85
|
+
assert.equal(payload.device.platform, 'ios'); assert.equal(payload.capabilities.coroutines, false)
|
|
86
|
+
assert.ok(!/JDK|Java worker|ADB/.test(payload.error.diagnostic.recovery))
|
|
87
|
+
} finally { iosDebuggerSessions.capabilities = original }
|
|
88
|
+
console.log('PASS iOS target membership, process identity, contracts, session routing, racing stops and platform guards')
|
|
89
|
+
} finally {
|
|
90
|
+
debuggerSessions.paused = androidPause; iosDebuggerSessions.paused = iosPause
|
|
91
|
+
await sessions.shutdown(); await rm(directory, { recursive: true, force: true })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Confirmed detach and process-exit evidence are required for cleanup success.
|
|
95
|
+
const { IOSWorker } = await import('../../../src/debugger/ios-worker.js')
|
|
96
|
+
let transportClosed = 0
|
|
97
|
+
const disconnected = new IOSWorker({ call: async () => { throw new Error('transport lost') }, close: async () => { transportClosed++ } }, () => false)
|
|
98
|
+
await assert.rejects(disconnected.close(), /DEBUGGER_CLEANUP_FAILED/)
|
|
99
|
+
assert.equal(transportClosed, 1)
|
|
100
|
+
const exited = new IOSWorker({ call: async () => { throw new Error('must not need detach') }, close: async () => {} }, () => true)
|
|
101
|
+
await exited.close()
|
|
102
|
+
const confirmed = new IOSWorker({ call: async () => ({ detached: true }), close: async () => {} }, () => false)
|
|
103
|
+
await confirmed.close()
|
|
104
|
+
console.log('PASS iOS cleanup requires confirmed release; worker exit alone is insufficient')
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Platform-independent regression checks for the optional LLDB worker."""
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
root = pathlib.Path(__file__).resolve().parents[3] / 'src/debugger/lldb'
|
|
7
|
+
sys.path.insert(0, str(root))
|
|
8
|
+
lldb = types.SimpleNamespace(eStateInvalid=0, eStateStopped=5, eStateExited=10, eStateDetached=9,
|
|
9
|
+
eStopReasonNone=1, eStopReasonInvalid=0, eStopReasonBreakpoint=3)
|
|
10
|
+
sys.modules['lldb'] = lldb
|
|
11
|
+
import worker
|
|
12
|
+
from inspection import Inspection
|
|
13
|
+
from breakpoints import Breakpoints
|
|
14
|
+
|
|
15
|
+
# Independent pause, idle and absolute expiry cannot be extended by heartbeats.
|
|
16
|
+
w = object.__new__(worker.Worker)
|
|
17
|
+
clock = [100.0]
|
|
18
|
+
worker.time.monotonic = lambda: clock[0]
|
|
19
|
+
w.created, w.activity, w.paused_at = 0, 100, 40
|
|
20
|
+
assert w.expired() == 'pause_limit'
|
|
21
|
+
w.paused_at = None
|
|
22
|
+
clock[0] = 400
|
|
23
|
+
assert w.expired() == 'idle_limit'
|
|
24
|
+
w.activity = 1800
|
|
25
|
+
clock[0] = 1800
|
|
26
|
+
assert w.expired() == 'session_limit'
|
|
27
|
+
|
|
28
|
+
# An internal transition with an empty thread list must not create a pause epoch.
|
|
29
|
+
i = Inspection()
|
|
30
|
+
p = types.SimpleNamespace(GetNumThreads=lambda: 0, GetState=lambda: 5, GetSelectedThread=lambda: types.SimpleNamespace(IsValid=lambda: False))
|
|
31
|
+
assert i.stopped(p) is None
|
|
32
|
+
assert i.stop_id is None
|
|
33
|
+
for method, args in [(i.thread, {'stopId': 'old', 'threadId': 'old'}),
|
|
34
|
+
(i.locals, {'stopId': 'old', 'frameId': 'old'})]:
|
|
35
|
+
try:
|
|
36
|
+
method(args)
|
|
37
|
+
raise AssertionError('Stale reference accepted')
|
|
38
|
+
except ValueError as error:
|
|
39
|
+
assert str(error) == 'DEBUGGER_STALE_REFERENCE'
|
|
40
|
+
|
|
41
|
+
# Exact source-line resolver must never request nearest-code relocation.
|
|
42
|
+
class Entry:
|
|
43
|
+
def IsValid(self): return True
|
|
44
|
+
def GetFileSpec(self): return '/tmp/App.swift'
|
|
45
|
+
def GetLine(self): return 13
|
|
46
|
+
class Location:
|
|
47
|
+
def GetAddress(self): return types.SimpleNamespace(GetLineEntry=lambda: Entry())
|
|
48
|
+
def SetEnabled(self, enabled): self.enabled = enabled
|
|
49
|
+
class Breakpoint:
|
|
50
|
+
def __init__(self): self.location = Location()
|
|
51
|
+
def __iter__(self): return iter([self.location])
|
|
52
|
+
def GetID(self): return 1
|
|
53
|
+
class Target:
|
|
54
|
+
def __init__(self): self.deleted = False; self.bp = Breakpoint()
|
|
55
|
+
def BreakpointCreateByLocation(self, *args):
|
|
56
|
+
assert args[-1] is False
|
|
57
|
+
return self.bp
|
|
58
|
+
def BreakpointDelete(self, identity): self.deleted = True
|
|
59
|
+
def FindCompileUnits(self, source): return types.SimpleNamespace(GetSize=lambda: 1)
|
|
60
|
+
lldb.SBFileSpec = lambda source: source
|
|
61
|
+
lldb.SBFileSpecList = list
|
|
62
|
+
t = Target()
|
|
63
|
+
b = Breakpoints(t)
|
|
64
|
+
r = b.set({'sourceFile': '/tmp/App.swift', 'line': 12})
|
|
65
|
+
assert r['status'] == 'rejected'
|
|
66
|
+
assert t.bp.location.enabled is False and t.deleted
|
|
67
|
+
|
|
68
|
+
# Polling must consume events before reading the process snapshot.
|
|
69
|
+
trace = []
|
|
70
|
+
class Listener:
|
|
71
|
+
def GetNextEvent(self, event): trace.append('consume'); return False
|
|
72
|
+
class Process:
|
|
73
|
+
def GetState(self): trace.append('state'); return 6
|
|
74
|
+
w.debugger = types.SimpleNamespace(GetListener=lambda: Listener())
|
|
75
|
+
w.process, w.terminal = Process(), False
|
|
76
|
+
lldb.SBEvent = object
|
|
77
|
+
w.poll()
|
|
78
|
+
assert trace == ['consume', 'state']
|
|
79
|
+
|
|
80
|
+
# EOF cleanup must release an actual target, never call Kill or expression APIs.
|
|
81
|
+
w.attached = True
|
|
82
|
+
w.inspection = Inspection()
|
|
83
|
+
w.breakpoints = types.SimpleNamespace(clear=lambda: None)
|
|
84
|
+
w.process = types.SimpleNamespace(IsValid=lambda: True, GetState=lambda: 5,
|
|
85
|
+
Detach=lambda: types.SimpleNamespace(Success=lambda: True))
|
|
86
|
+
assert w.detach() == {'detached': True}
|
|
87
|
+
assert w.terminal
|
|
88
|
+
print('PASS LLDB worker expiry, transient stops, stale references, exact lines, events and detach')
|
|
89
|
+
|
|
90
|
+
# Static Swift bool decoding must preserve boolean meaning without formatters.
|
|
91
|
+
lldb.eNoDynamicValues, lldb.eTypeIsScalar, lldb.eTypeIsInteger, lldb.eTypeIsFloat = 0, 1, 2, 4
|
|
92
|
+
class BoolValue:
|
|
93
|
+
def GetNonSyntheticValue(self): return self
|
|
94
|
+
def GetName(self): return 'cancelled'
|
|
95
|
+
def GetTypeName(self): return 'Swift.Bool'
|
|
96
|
+
def GetError(self): return types.SimpleNamespace(Success=lambda: True)
|
|
97
|
+
def GetType(self): return types.SimpleNamespace(GetTypeFlags=lambda: 0)
|
|
98
|
+
def GetValue(self): return '1'
|
|
99
|
+
def GetChildMemberWithName(self, name):
|
|
100
|
+
assert name == '_value'
|
|
101
|
+
return types.SimpleNamespace(GetNonSyntheticValue=lambda: self)
|
|
102
|
+
def IsValid(self): return True
|
|
103
|
+
values = types.SimpleNamespace(GetSize=lambda: 1, GetValueAtIndex=lambda index: BoolValue())
|
|
104
|
+
i.stop_id = '00000000-0000-0000-0000-000000000001'
|
|
105
|
+
i.frames['frame'] = types.SimpleNamespace(GetVariables=lambda *args: values)
|
|
106
|
+
assert i.locals({'stopId': i.stop_id, 'frameId': 'frame'})['locals'][0]['value'] == 'true'
|
|
107
|
+
|
|
108
|
+
# Reference summaries are stable within a stop and never expose the pointer.
|
|
109
|
+
pointer = types.SimpleNamespace(GetValue=lambda: '0x1234')
|
|
110
|
+
first = i.object_identity(pointer, 'Controller')
|
|
111
|
+
assert first == i.object_identity(pointer, 'Controller') and '1234' not in first
|
|
112
|
+
i.stop_id = '00000000-0000-0000-0000-000000000002'
|
|
113
|
+
assert first != i.object_identity(pointer, 'Controller')
|
|
114
|
+
|
|
115
|
+
# Repeated stack pages reuse references and cannot grow beyond the stop cap.
|
|
116
|
+
import inspection
|
|
117
|
+
original_location = inspection.location
|
|
118
|
+
inspection.location = lambda frame: {'source_file': '/tmp/App.swift', 'line': 1}
|
|
119
|
+
try:
|
|
120
|
+
i.invalidate()
|
|
121
|
+
i.stop_id = '00000000-0000-0000-0000-000000000001'
|
|
122
|
+
i.threads['thread'] = types.SimpleNamespace(GetNumFrames=lambda: 1001, GetFrameAtIndex=lambda index: object())
|
|
123
|
+
args = {'stopId': i.stop_id, 'threadId': 'thread', 'limit': 100}
|
|
124
|
+
assert i.stack(args)['frames'][0]['frame_id'] == i.stack(args)['frames'][0]['frame_id']
|
|
125
|
+
for offset in range(0, 1000, 100): i.stack({**args, 'offset': offset})
|
|
126
|
+
assert len(i.frames) == 1000
|
|
127
|
+
i.threads['other'] = i.threads['thread']
|
|
128
|
+
result = i.stack({**args, 'threadId': 'other'})
|
|
129
|
+
assert result['frames'] == [] and result['truncated']
|
|
130
|
+
finally:
|
|
131
|
+
inspection.location = original_location
|
|
132
|
+
print('PASS static bool decoding, opaque identities and bounded stable frame references')
|
|
133
|
+
|
|
134
|
+
# Error-reporting StepInto requires LLDB's UINT32_MAX sentinel, not line zero.
|
|
135
|
+
lldb.LLDB_INVALID_LINE_NUMBER, lldb.eOnlyDuringStepping = 4294967295, 0
|
|
136
|
+
lldb.SBError = lambda: types.SimpleNamespace(Success=lambda: True)
|
|
137
|
+
class StepThread:
|
|
138
|
+
def StepInto(self, target, end_line, error, run_mode):
|
|
139
|
+
assert target is None and end_line == 4294967295
|
|
140
|
+
w.terminal = False
|
|
141
|
+
w.inspection = Inspection()
|
|
142
|
+
w.inspection.stop_id = 'stop'
|
|
143
|
+
w.inspection.threads['thread'] = StepThread()
|
|
144
|
+
w.process = types.SimpleNamespace(GetState=lambda: 5)
|
|
145
|
+
assert w.command({'command': 'step', 'stopId': 'stop', 'threadId': 'thread', 'kind': 'into'}) == {'resumed': True}
|
|
146
|
+
assert w.inspection.stop_id is None
|
|
147
|
+
print('PASS LLDB step error overload uses the documented invalid-line sentinel')
|
|
148
|
+
|
|
149
|
+
# Signals and exceptions must not be misrepresented as completed source steps.
|
|
150
|
+
lldb.eStopReasonPlanComplete, lldb.eStopReasonSignal, lldb.eStopReasonException = 8, 5, 6
|
|
151
|
+
lldb.eStopReasonWatchpoint, lldb.eStopReasonExec = 4, 7
|
|
152
|
+
assert inspection.stop_reason(types.SimpleNamespace(GetStopReason=lambda: 5)) == 'signal'
|
|
153
|
+
assert inspection.stop_reason(types.SimpleNamespace(GetStopReason=lambda: 6)) == 'exception'
|
|
154
|
+
assert inspection.stop_reason(types.SimpleNamespace(GetStopReason=lambda: 8)) == 'step'
|
|
155
|
+
assert inspection.stop_reason(types.SimpleNamespace(GetStopReason=lambda: 99)) == 'unknown'
|
|
156
|
+
print('PASS LLDB stop reasons preserve signal/exception versus step evidence')
|
|
157
|
+
|
|
158
|
+
# Losing a previously owned process handle cannot prove detach or resume.
|
|
159
|
+
w.attached = True
|
|
160
|
+
w.process = types.SimpleNamespace(IsValid=lambda: False)
|
|
161
|
+
try:
|
|
162
|
+
w.detach()
|
|
163
|
+
raise AssertionError('Lost process handle incorrectly confirmed detach')
|
|
164
|
+
except ValueError as error:
|
|
165
|
+
assert str(error) == 'DEBUGGER_WORKER_FAILED'
|
|
166
|
+
print('PASS lost process handles preserve cleanup uncertainty')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
const result = spawnSync('python3', ['-B', fileURLToPath(new URL('./lldb-worker.py', import.meta.url))], { encoding: 'utf8', timeout: 15000 })
|
|
5
|
+
if ((result.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
|
6
|
+
console.log('UNRUN optional LLDB Python worker checks: python3 is unavailable')
|
|
7
|
+
} else {
|
|
8
|
+
assert.equal(result.status, 0, result.stderr || result.error?.message)
|
|
9
|
+
console.log(result.stdout.trim())
|
|
10
|
+
}
|
|
@@ -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')
|