mobile-debug-mcp 0.36.0 → 0.37.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/README.md +10 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +1 -1
- 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/cursor/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +10 -2
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +10 -2
- package/dist/contracts/device-readiness.js +1 -0
- package/dist/interact/waits/ui-change-wait.js +9 -2
- package/dist/interact/waits/ui-wait.js +7 -3
- package/dist/manage/index.js +3 -0
- package/dist/observe/device-readiness.js +116 -0
- package/dist/observe/evidence.js +31 -0
- package/dist/observe/ios-trace/sessions.js +5 -3
- package/dist/observe/readiness-service.js +21 -9
- package/dist/observe/readiness.js +30 -0
- package/dist/observe/screenshot-encoding.js +4 -0
- package/dist/observe/snapshot-collection.js +7 -2
- package/dist/server/definitions/manage.js +3 -2
- package/dist/server/handlers/action-execution.js +23 -14
- package/dist/server/handlers/compact-interaction.js +106 -0
- package/dist/server/handlers/compact-state.js +47 -0
- package/dist/server/handlers/input.js +2 -0
- package/dist/server/handlers/interact.js +2 -0
- package/dist/server/handlers/interaction-diagnostics.js +62 -0
- package/dist/server/handlers/journey.js +18 -20
- package/dist/server/handlers/observe.js +2 -1
- package/dist/server/handlers/tool-handlers.js +9 -5
- package/dist/server/schemas/interaction-diagnostics.js +9 -0
- package/dist/server/tool-definitions.js +9 -4
- package/dist/server-core.js +5 -2
- package/dist/server.js +1 -1
- package/dist/system/android.js +19 -15
- package/dist/system/index.js +2 -1
- package/dist/utils/android/utils.js +7 -16
- package/dist/utils/app-installation.js +77 -0
- package/dist/utils/benchmark-metrics.js +43 -0
- package/dist/utils/exec.js +4 -0
- package/dist/utils/ios/utils.js +4 -11
- package/dist/utils/resolve-device.js +3 -2
- package/dist/utils/timing.js +3 -3
- package/docs/CHANGELOG.md +25 -0
- package/docs/benchmarks/README.md +1 -0
- package/docs/benchmarks/end-to-end-latency/README.md +98 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
- package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
- package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
- package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
- package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
- package/docs/benchmarks/end-to-end-latency/smoke.md +21 -0
- package/docs/specs/031-ios-system-traces/plan.md +28 -0
- package/docs/specs/033-device-readiness-blockers/android-evidence.json +437 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
- package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
- package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
- package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
- package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
- package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
- package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
- package/docs/specs/034-app-installation-status/intent.md +41 -0
- package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
- package/docs/specs/034-app-installation-status/plan.md +73 -0
- package/docs/specs/034-app-installation-status/spec.md +114 -0
- package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
- package/docs/specs/035-compact-interaction-results/intent.md +38 -0
- package/docs/specs/035-compact-interaction-results/plan.md +43 -0
- package/docs/specs/035-compact-interaction-results/spec.md +118 -0
- package/docs/specs/035-compact-interaction-results/verification.md +111 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
- package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
- package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
- package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
- package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
- package/docs/tools/TOOLS.md +1 -0
- package/docs/tools/compact-interactions.md +56 -0
- package/docs/tools/correlated-evidence.md +6 -0
- package/docs/tools/interact.md +17 -8
- package/docs/tools/manage.md +10 -2
- package/docs/tools/observe.md +42 -2
- package/docs/tools/system.md +6 -0
- package/package.json +1 -1
- package/src/contracts/device-readiness.ts +8 -0
- package/src/interact/waits/ui-change-wait.ts +7 -2
- package/src/interact/waits/ui-wait.ts +6 -3
- package/src/manage/index.ts +2 -0
- package/src/observe/device-readiness.ts +96 -0
- package/src/observe/evidence.ts +30 -1
- package/src/observe/ios-trace/sessions.ts +4 -2
- package/src/observe/readiness-service.ts +21 -9
- package/src/observe/readiness.ts +33 -0
- package/src/observe/screenshot-encoding.ts +4 -0
- package/src/observe/snapshot-collection.ts +7 -2
- package/src/server/common.ts +2 -2
- package/src/server/definitions/manage.ts +3 -2
- package/src/server/handlers/action-execution.ts +22 -14
- package/src/server/handlers/compact-interaction.ts +91 -0
- package/src/server/handlers/compact-state.ts +43 -0
- package/src/server/handlers/input.ts +2 -0
- package/src/server/handlers/interact.ts +2 -0
- package/src/server/handlers/interaction-diagnostics.ts +50 -0
- package/src/server/handlers/journey.ts +15 -17
- package/src/server/handlers/observe.ts +2 -1
- package/src/server/handlers/payload.ts +2 -1
- package/src/server/handlers/tool-handlers.ts +9 -5
- package/src/server/schemas/interaction-diagnostics.ts +10 -0
- package/src/server/tool-definitions.ts +8 -4
- package/src/server-core.ts +5 -2
- package/src/server.ts +1 -1
- package/src/system/android.ts +18 -11
- package/src/system/index.ts +2 -1
- package/src/types.ts +2 -0
- package/src/utils/android/utils.ts +8 -10
- package/src/utils/app-installation.ts +73 -0
- package/src/utils/benchmark-metrics.ts +42 -0
- package/src/utils/exec.ts +5 -0
- package/src/utils/ios/utils.ts +4 -10
- package/src/utils/resolve-device.ts +4 -2
- package/src/utils/timing.ts +3 -2
- package/test/device/automated/debugger/runtime.android.test.ts +5 -0
- package/test/device/automated/debugger/runtime.ios.test.ts +7 -1
- package/test/device/automated/interact/compact-results.smoke.ts +58 -0
- package/test/device/automated/latency/end-to-end-environment.ts +61 -0
- package/test/device/automated/latency/end-to-end-report.ts +90 -0
- package/test/device/automated/latency/end-to-end-session.ts +98 -0
- package/test/device/automated/latency/end-to-end.ts +149 -0
- package/test/device/automated/latency/owned-transport.ts +81 -0
- package/test/device/automated/latency/snapshot-pair.ts +143 -0
- package/test/device/automated/latency-ios/benchmark.ts +126 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
- package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
- package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +2 -2
- package/test/device/automated/observe/system-trace.smoke.ts +2 -2
- package/test/fixtures/contracts/tool-definitions.json +51 -15
- package/test/fixtures/latency-journey-app/build.ts +3 -3
- package/test/unit/debugger/handle-guards.test.ts +2 -1
- package/test/unit/interact/ui-change-readiness-interruption.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
- package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
- package/test/unit/observe/device-readiness.test.ts +97 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +23 -0
- package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
- package/test/unit/server/compact-interaction.test.ts +87 -0
- package/test/unit/server/compact-journey-evidence.test.ts +40 -0
- package/test/unit/server/compact-review-regressions.test.ts +78 -0
- package/test/unit/server/conditional_actions.test.ts +7 -3
- package/test/unit/server/correlated-evidence.test.ts +18 -6
- package/test/unit/server/end-to-end-report.test.ts +46 -0
- package/test/unit/server/input-device-readiness.test.ts +55 -0
- package/test/unit/server/interaction-diagnostics.test.ts +45 -0
- package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
- package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
- package/test/unit/server/readiness.test.ts +24 -1
- package/test/unit/server/run_journey.test.ts +2 -1
- package/test/unit/server/selector_tap_target.test.ts +2 -1
- package/test/unit/server/system-trace.test.ts +6 -4
- package/test/unit/utils/app-installation.test.ts +78 -0
- package/test/unit/utils/benchmark-metrics.test.ts +36 -0
- package/test/unit/utils/resolve_device.test.ts +11 -0
package/README.md
CHANGED
|
@@ -10,6 +10,11 @@ Android, iOS, and Kotlin Multiplatform projects are supported. Flutter and React
|
|
|
10
10
|
Native have not been tested. Platform and device limitations are described in the
|
|
11
11
|
[tool reference](docs/tools/TOOLS.md).
|
|
12
12
|
|
|
13
|
+
Use `responseMode: "compact"` for bounded interaction and journey summaries.
|
|
14
|
+
Retrieve retained details without replaying input with
|
|
15
|
+
[`get_interaction_diagnostics`](docs/tools/compact-interactions.md); debug output
|
|
16
|
+
remains available.
|
|
17
|
+
|
|
13
18
|
## What you can use it for
|
|
14
19
|
|
|
15
20
|
Mobile Debug Tools gives a coding agent access to a running app, so development
|
|
@@ -297,3 +302,8 @@ Android debugger errors include bounded, sanitized failure classifications and r
|
|
|
297
302
|
## License
|
|
298
303
|
|
|
299
304
|
[Apache-2.0](LICENSE)
|
|
305
|
+
|
|
306
|
+
Launch and interaction results distinguish dispatch from timestamped
|
|
307
|
+
[device readiness evidence](docs/tools/observe.md#device-readiness-evidence).
|
|
308
|
+
Readiness waits account for observed sleeping/locked device blockers. Unknown or
|
|
309
|
+
unsupported signals are explicit, and no automatic unlock is performed.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "mobile-debug-tools",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"description": "Mobile app debugging tools and usage guidance for Android and iOS agents.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "mobile-debug-tools contributors"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Mobile app debugging tools and usage guidance for Android and iOS agents.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "mobile-debug-tools contributors"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"command": "npx",
|
|
13
13
|
"args": [
|
|
14
14
|
"--yes",
|
|
15
|
-
"mobile-debug-mcp@0.
|
|
15
|
+
"mobile-debug-mcp@0.37.0",
|
|
16
16
|
"server"
|
|
17
17
|
]
|
|
18
18
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Mobile app debugging tools and usage guidance for Android and iOS agents.",
|
|
5
5
|
"repository": "https://github.com/clivejefferies/mobile-debug-tools",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Mobile app debugging tools and usage guidance for Android and iOS agents.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "mobile-debug-tools contributors"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"command": "npx",
|
|
15
15
|
"args": [
|
|
16
16
|
"--yes",
|
|
17
|
-
"mobile-debug-mcp@0.
|
|
17
|
+
"mobile-debug-mcp@0.37.0",
|
|
18
18
|
"server"
|
|
19
19
|
],
|
|
20
20
|
"cwd": "${CURSOR_PLUGIN_ROOT}"
|
package/agent-plugin/.mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Mobile app debugging tools and usage guidance for Android and iOS agents.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "mobile-debug-tools contributors"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"command": "npx",
|
|
15
15
|
"args": [
|
|
16
16
|
"--yes",
|
|
17
|
-
"mobile-debug-mcp@0.
|
|
17
|
+
"mobile-debug-mcp@0.37.0",
|
|
18
18
|
"server"
|
|
19
19
|
],
|
|
20
20
|
"cwd": "${CURSOR_PLUGIN_ROOT}"
|
|
@@ -57,7 +57,14 @@ required. A 2-second timeout cannot reliably accommodate a 3-second tree read.
|
|
|
57
57
|
Adjust to measured device latency and supported tool limits. Report an
|
|
58
58
|
observation timeout separately from evidence that a control is absent.
|
|
59
59
|
|
|
60
|
-
Use compact responses for routine actions.
|
|
60
|
+
Use compact responses for routine actions. Compact interaction/journey results retain
|
|
61
|
+
outcomes, verification, readiness blockers and observed_state (or explicit unknown).
|
|
62
|
+
For original errors, condition trees, state-delta elements and evidence timestamps,
|
|
63
|
+
call get_interaction_diagnostics with diagnostics.session_id/result_id as
|
|
64
|
+
sessionId/resultId. This reads retained data without replaying input. Results expire
|
|
65
|
+
after ten minutes or earlier eviction (100 results, 16 MiB total, 1 MiB per result).
|
|
66
|
+
Check diagnostics.available; unavailable details never justify automatic replay.
|
|
67
|
+
Debug mode can request additional captures at execution time. If action dispatch is reliable but
|
|
61
68
|
verification is slow or unavailable, dispatch with verification disabled and
|
|
62
69
|
verify at meaningful checkpoints with one focused accessibility assertion or
|
|
63
70
|
screenshot. Do not assume `light` verification is faster: use it only when it
|
|
@@ -103,7 +110,8 @@ actions are tap, element tap, swipe, Android typing/back, and journey tap steps.
|
|
|
103
110
|
The server takes a fresh normalized hierarchy immediately before input.
|
|
104
111
|
`PRECONDITION_FAILED` or `PRECONDITION_UNAVAILABLE` means input was blocked:
|
|
105
112
|
inspect `condition` evidence and decide again; do not blindly retry or replace
|
|
106
|
-
the expectation. Compact failures
|
|
113
|
+
the expectation. Compact failures summarize condition evidence; their diagnostics lookup may contain
|
|
114
|
+
the full UI tree and private text. Compact output is not privacy redaction.
|
|
107
115
|
Expectations expire with retained history or server restart. The extra hierarchy
|
|
108
116
|
read can take seconds; it is not atomic with input delivery and does not prove
|
|
109
117
|
the action's outcome.
|
package/agent-plugin/mcp.json
CHANGED
package/agent-plugin/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "mobile-debug-tools",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"description": "Mobile app debugging tools and usage guidance for Android and iOS agents.",
|
|
6
6
|
"repository": "https://github.com/clivejefferies/mobile-debug-tools",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -57,7 +57,14 @@ required. A 2-second timeout cannot reliably accommodate a 3-second tree read.
|
|
|
57
57
|
Adjust to measured device latency and supported tool limits. Report an
|
|
58
58
|
observation timeout separately from evidence that a control is absent.
|
|
59
59
|
|
|
60
|
-
Use compact responses for routine actions.
|
|
60
|
+
Use compact responses for routine actions. Compact interaction/journey results retain
|
|
61
|
+
outcomes, verification, readiness blockers and observed_state (or explicit unknown).
|
|
62
|
+
For original errors, condition trees, state-delta elements and evidence timestamps,
|
|
63
|
+
call get_interaction_diagnostics with diagnostics.session_id/result_id as
|
|
64
|
+
sessionId/resultId. This reads retained data without replaying input. Results expire
|
|
65
|
+
after ten minutes or earlier eviction (100 results, 16 MiB total, 1 MiB per result).
|
|
66
|
+
Check diagnostics.available; unavailable details never justify automatic replay.
|
|
67
|
+
Debug mode can request additional captures at execution time. If action dispatch is reliable but
|
|
61
68
|
verification is slow or unavailable, dispatch with verification disabled and
|
|
62
69
|
verify at meaningful checkpoints with one focused accessibility assertion or
|
|
63
70
|
screenshot. Do not assume `light` verification is faster: use it only when it
|
|
@@ -103,7 +110,8 @@ actions are tap, element tap, swipe, Android typing/back, and journey tap steps.
|
|
|
103
110
|
The server takes a fresh normalized hierarchy immediately before input.
|
|
104
111
|
`PRECONDITION_FAILED` or `PRECONDITION_UNAVAILABLE` means input was blocked:
|
|
105
112
|
inspect `condition` evidence and decide again; do not blindly retry or replace
|
|
106
|
-
the expectation. Compact failures
|
|
113
|
+
the expectation. Compact failures summarize condition evidence; their diagnostics lookup may contain
|
|
114
|
+
the full UI tree and private text. Compact output is not privacy redaction.
|
|
107
115
|
Expectations expire with retained history or server restart. The extra hierarchy
|
|
108
116
|
read can take seconds; it is not atomic with input delivery and does not prove
|
|
109
117
|
the action's outcome.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { systemClock } from '../../runtime/clock.js';
|
|
2
2
|
import { remainingBudget } from '../../utils/operation-budget.js';
|
|
3
3
|
import { checkDebuggerGuard } from '../../debugger/guard.js';
|
|
4
|
-
import { observeReadinessWait, isDeviceUnavailable } from '../../observe/readiness.js';
|
|
4
|
+
import { observeReadinessWait, observeReadinessDeviceWait, isDeviceUnavailable } from '../../observe/readiness.js';
|
|
5
5
|
import { readinessDelay } from '../../observe/readiness-service.js';
|
|
6
6
|
import { _buildUiChangeSignatures, _summarizeUiChangeDelta, _matchesUiChange, _uiChangeSignaturesEqual } from './ui-change-helpers.js';
|
|
7
7
|
import { createChangeWait, changeResult } from './ui-change-state.js';
|
|
@@ -26,12 +26,19 @@ function changed(s, signatures) {
|
|
|
26
26
|
s.since = s.clock.now();
|
|
27
27
|
}
|
|
28
28
|
const stable = s.since === null ? 0 : s.clock.now() - s.since;
|
|
29
|
-
return (s.stability === 0 || stable >= s.stability) && (!s.tracker || s.tracker.
|
|
29
|
+
return (s.stability === 0 || stable >= s.stability) && (!s.tracker || s.tracker.ready);
|
|
30
30
|
}
|
|
31
31
|
async function sample(s, deps) {
|
|
32
32
|
if (s.tracker)
|
|
33
33
|
remainingBudget();
|
|
34
34
|
const started = s.clock.now(), a = s.input;
|
|
35
|
+
await observeReadinessDeviceWait();
|
|
36
|
+
if (s.tracker?.deviceReadiness.input_ready === false) {
|
|
37
|
+
s.candidate = null;
|
|
38
|
+
s.change = null;
|
|
39
|
+
s.since = null;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
35
42
|
const tree = await deps.observe({ platform: a.platform, deviceId: a.deviceId });
|
|
36
43
|
observeReadinessWait(tree, started);
|
|
37
44
|
const scoped = deps.scope(tree, a.scope, a.target);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { systemClock } from '../../runtime/clock.js';
|
|
2
|
+
import { benchmarkStage } from '../../utils/benchmark-metrics.js';
|
|
2
3
|
import { remainingBudget } from '../../utils/operation-budget.js';
|
|
3
4
|
import { checkDebuggerGuard } from '../../debugger/guard.js';
|
|
4
|
-
import { observeReadinessWait, isDeviceUnavailable } from '../../observe/readiness.js';
|
|
5
|
+
import { observeReadinessWait, observeReadinessDeviceWait, isDeviceUnavailable } from '../../observe/readiness.js';
|
|
5
6
|
import { readinessDelay } from '../../observe/readiness-service.js';
|
|
6
7
|
import { matchingElements, evaluateCondition } from './ui-wait-condition.js';
|
|
7
8
|
import { createUIWait, validateUIWait, waitObserved, waitFailure } from './ui-wait-state.js';
|
|
8
9
|
async function delay(s, ms) {
|
|
9
|
-
await (s.tracker && s.clock === systemClock ? readinessDelay(ms) : s.clock.sleep(ms));
|
|
10
|
+
await benchmarkStage('poll_sleep_ms', () => s.tracker && s.clock === systemClock ? readinessDelay(ms) : s.clock.sleep(ms));
|
|
10
11
|
}
|
|
11
12
|
function acceptObservation(s, tree, now) {
|
|
12
13
|
const age = typeof tree?.captured_at_ms === 'number' ? now - tree.captured_at_ms : null;
|
|
@@ -15,7 +16,7 @@ function acceptObservation(s, tree, now) {
|
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
s.stable++;
|
|
18
|
-
if (s.stable < (s.args.singleObservation ? 1 : 2) || (s.tracker && s.tracker.
|
|
19
|
+
if (s.stable < (s.args.singleObservation ? 1 : 2) || (s.tracker && !s.tracker.ready))
|
|
19
20
|
return null;
|
|
20
21
|
return { status: 'success', ...(s.args.singleObservation ? { _tree: tree } : {}), matched: s.count,
|
|
21
22
|
element: s.element, metrics: { latency_ms: now - s.start, poll_count: s.polls, attempts: s.attempts }, requested: s.requested, observed: waitObserved(s, true) };
|
|
@@ -50,6 +51,9 @@ async function poll(s, deps) {
|
|
|
50
51
|
s.polls++;
|
|
51
52
|
try {
|
|
52
53
|
const started = s.clock.now(), a = s.args;
|
|
54
|
+
await observeReadinessDeviceWait();
|
|
55
|
+
if (s.tracker?.deviceReadiness.input_ready === false)
|
|
56
|
+
return null;
|
|
53
57
|
const tree = await deps.observe({ platform: a.platform, deviceId: a.deviceId });
|
|
54
58
|
observeReadinessWait(tree, started);
|
|
55
59
|
if (a.resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error)))
|
package/dist/manage/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validAppId } from '../utils/app-installation.js';
|
|
1
2
|
import { promises as fs } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js';
|
|
@@ -313,6 +314,8 @@ export class ToolsManage {
|
|
|
313
314
|
}
|
|
314
315
|
}
|
|
315
316
|
static async listDevicesHandler({ platform, appId }) {
|
|
317
|
+
if (appId !== undefined && !validAppId(appId))
|
|
318
|
+
throw new Error('Invalid appId');
|
|
316
319
|
const devices = await listDevices(platform, appId);
|
|
317
320
|
return { devices };
|
|
318
321
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { pausedReadiness } from '../debugger/guard.js';
|
|
3
|
+
import { execCmd } from '../utils/exec.js';
|
|
4
|
+
import { getAdbCmd } from '../utils/android/commands.js';
|
|
5
|
+
import { getXcrunCmd } from '../utils/ios/utils.js';
|
|
6
|
+
import { remainingBudget } from '../utils/operation-budget.js';
|
|
7
|
+
import { benchmarkStage } from '../utils/benchmark-metrics.js';
|
|
8
|
+
export function unknownDeviceReadiness(code) {
|
|
9
|
+
return { wakefulness: 'unknown', lock_state: 'unknown', input_ready: null, blockers: [], evidence: [], limitations: code ? [code] : [] };
|
|
10
|
+
}
|
|
11
|
+
function values(text, pattern) { return [...text.matchAll(pattern)].map(match => match[1]); }
|
|
12
|
+
function unique(items) { return items.length > 0 && new Set(items).size === 1 ? items[0] : null; }
|
|
13
|
+
export function parseAndroidPower(text) {
|
|
14
|
+
// Later vendor/debug sections can repeat mWakefulness as a numeric enum.
|
|
15
|
+
// Classify only the named current-state section when its boundary is present.
|
|
16
|
+
const main = text.split('Power Manager State:')[1];
|
|
17
|
+
if (main?.includes('Settings and Configuration:'))
|
|
18
|
+
text = main.split('Settings and Configuration:')[0];
|
|
19
|
+
const wake = unique(values(text, /^\s*mWakefulness=(\w+)\s*$/gm));
|
|
20
|
+
const changing = unique(values(text, /^\s*mWakefulnessChanging=(true|false)\s*$/gm));
|
|
21
|
+
if (!wake || !changing)
|
|
22
|
+
return 'unknown';
|
|
23
|
+
if (changing === 'true')
|
|
24
|
+
return 'transitioning';
|
|
25
|
+
return wake === 'Awake' ? 'awake' : ['Asleep', 'Dozing'].includes(wake) ? 'asleep' : 'unknown';
|
|
26
|
+
}
|
|
27
|
+
export function parseAndroidLock(text) {
|
|
28
|
+
const section = text.split(/\bKeyguardServiceDelegate\s*\n/)[1];
|
|
29
|
+
if (!section)
|
|
30
|
+
return 'unknown';
|
|
31
|
+
const showing = unique(values(section, /^\s*(?:showing|mIsShowing)=(true|false)\s*$/gm));
|
|
32
|
+
return showing === 'true' ? 'locked' : showing === 'false' ? 'unlocked' : 'unknown';
|
|
33
|
+
}
|
|
34
|
+
export function finishDeviceReadiness(state) {
|
|
35
|
+
if (state.wakefulness === 'asleep')
|
|
36
|
+
state.blockers.push('DEVICE_ASLEEP');
|
|
37
|
+
if (state.wakefulness === 'transitioning')
|
|
38
|
+
state.blockers.push('DEVICE_TRANSITIONING');
|
|
39
|
+
if (state.lock_state === 'locked')
|
|
40
|
+
state.blockers.push('DEVICE_LOCKED');
|
|
41
|
+
state.blockers = [...new Set(state.blockers)];
|
|
42
|
+
state.input_ready = state.blockers.length ? false : null;
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
export function parseIosLock(text) {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(text);
|
|
48
|
+
if (data.error?.domain === 'com.apple.dt.CoreDeviceError' && data.error.code === 1001 &&
|
|
49
|
+
data.error.userInfo?.CapabilityFeatureIdentifier?.string === 'com.apple.coredevice.feature.getlockstate')
|
|
50
|
+
return 'unsupported';
|
|
51
|
+
// Successful response semantics require runtime validation before classification.
|
|
52
|
+
}
|
|
53
|
+
catch { /* Non-JSON output supplies no state evidence. */ }
|
|
54
|
+
return 'unknown';
|
|
55
|
+
}
|
|
56
|
+
const probes = new AsyncLocalStorage();
|
|
57
|
+
export function withDeviceReadinessProbe(probe, operation) { return probes.run(probe, operation); }
|
|
58
|
+
const nativeProbe = (command, args, timeout) => execCmd(command, args, { timeout });
|
|
59
|
+
export async function observeDeviceReadiness(device, probe = probes.getStore() ?? nativeProbe) {
|
|
60
|
+
return benchmarkStage('device_readiness_ms', () => observe(device, probe));
|
|
61
|
+
}
|
|
62
|
+
async function observe(device, probe) {
|
|
63
|
+
const state = unknownDeviceReadiness();
|
|
64
|
+
if (!device?.id || !['android', 'ios'].includes(device.platform) || ['booted', 'default'].includes(device.id))
|
|
65
|
+
return unknownDeviceReadiness('DEVICE_TARGET_UNAVAILABLE');
|
|
66
|
+
const paused = pausedReadiness(device.id, device.platform);
|
|
67
|
+
if (paused) {
|
|
68
|
+
state.blockers.push('DEBUGGER_PAUSED');
|
|
69
|
+
state.evidence.push({ code: 'DEBUGGER_PAUSED', source: 'debugger', captured_at_ms: paused.captured_at_ms });
|
|
70
|
+
return finishDeviceReadiness(state);
|
|
71
|
+
}
|
|
72
|
+
if (device.platform === 'android') {
|
|
73
|
+
await androidProbe(state, device.id, 'power', probe);
|
|
74
|
+
await androidProbe(state, device.id, 'window', probe);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
state.limitations.push('DEVICE_WAKEFULNESS_UNVERIFIED');
|
|
78
|
+
try {
|
|
79
|
+
const timeout = remainingBudget(1000);
|
|
80
|
+
// CoreDevice requires a CLI timeout of at least five seconds; the parent
|
|
81
|
+
// subprocess deadline still caps this read at one second.
|
|
82
|
+
const result = await probe(getXcrunCmd(), ['devicectl', 'device', 'info', 'lockState', '--device', device.id, '--timeout', '5', '--json-output', '-'], timeout);
|
|
83
|
+
remainingBudget();
|
|
84
|
+
state.lock_state = parseIosLock(result.stdout);
|
|
85
|
+
if (state.lock_state === 'unsupported')
|
|
86
|
+
state.evidence.push({ code: 'DEVICE_LOCK_SIGNAL_UNSUPPORTED', source: 'coredevice', captured_at_ms: Date.now() });
|
|
87
|
+
else
|
|
88
|
+
state.limitations.push('DEVICE_LOCK_OBSERVATION_UNAVAILABLE');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
state.limitations.push('DEVICE_LOCK_OBSERVATION_FAILED');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return finishDeviceReadiness(state);
|
|
95
|
+
}
|
|
96
|
+
async function androidProbe(state, id, kind, probe) {
|
|
97
|
+
try {
|
|
98
|
+
const timeout = remainingBudget(1000);
|
|
99
|
+
const result = await probe(getAdbCmd(), ['-s', id, 'shell', 'dumpsys', kind, ...(kind === 'window' ? ['policy'] : [])], timeout);
|
|
100
|
+
remainingBudget();
|
|
101
|
+
if (result.exitCode !== 0)
|
|
102
|
+
throw new Error('PROBE_FAILED');
|
|
103
|
+
const value = kind === 'power' ? parseAndroidPower(result.stdout) : parseAndroidLock(result.stdout);
|
|
104
|
+
if (kind === 'power')
|
|
105
|
+
state.wakefulness = value;
|
|
106
|
+
else
|
|
107
|
+
state.lock_state = value;
|
|
108
|
+
if (value === 'unknown')
|
|
109
|
+
state.limitations.push(kind === 'power' ? 'DEVICE_WAKEFULNESS_UNVERIFIED' : 'DEVICE_LOCK_STATE_UNVERIFIED');
|
|
110
|
+
else
|
|
111
|
+
state.evidence.push({ code: `DEVICE_${value.toUpperCase()}`, source: `android_${kind}`, captured_at_ms: Date.now() });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
state.limitations.push(kind === 'power' ? 'DEVICE_POWER_OBSERVATION_FAILED' : 'DEVICE_LOCK_OBSERVATION_FAILED');
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/observe/evidence.js
CHANGED
|
@@ -217,11 +217,21 @@ export function refreshJourneyEvidence(steps) {
|
|
|
217
217
|
const child = current.children.find(c => c.action?.id === previous?.action_id);
|
|
218
218
|
if (child && step.result)
|
|
219
219
|
step.result.evidence = assemble(child);
|
|
220
|
+
else if (previous?.observations && step.result) {
|
|
221
|
+
const ids = new Set(previous.observations.map(o => o.evidence_id));
|
|
222
|
+
step.result.evidence = assemble({ ...current, observations: current.observations.filter(o => ids.has(o.value.evidence_id)) });
|
|
223
|
+
}
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
export function completeJourneyVerification(actionId) {
|
|
223
227
|
const child = context.getStore()?.children.find(c => c.action?.id === actionId);
|
|
224
228
|
if (child?.action) {
|
|
229
|
+
const parent = context.getStore();
|
|
230
|
+
const completed = child.action.dispatchEnd?.mono;
|
|
231
|
+
if (completed !== undefined) {
|
|
232
|
+
const after = parent.observations.filter(o => o.start.mono >= completed && sameDevice(o.value.device, child.action.device));
|
|
233
|
+
child.observations.push(...after);
|
|
234
|
+
}
|
|
225
235
|
child.verification = stamp();
|
|
226
236
|
child.action.verification = child.verification;
|
|
227
237
|
}
|
|
@@ -317,3 +327,24 @@ function recordCaptureTime(acquisition, result, start) {
|
|
|
317
327
|
acquisition.reused = source < start.wall;
|
|
318
328
|
}
|
|
319
329
|
}
|
|
330
|
+
export class JourneyStepError extends Error {
|
|
331
|
+
evidence;
|
|
332
|
+
constructor(evidence, cause) {
|
|
333
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
334
|
+
this.evidence = evidence;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Retain per-step evidence without changing the parent journey's acquisition flow. */
|
|
338
|
+
export async function withJourneyStepEvidence(operation) {
|
|
339
|
+
const current = context.getStore();
|
|
340
|
+
const start = current?.observations.length ?? 0;
|
|
341
|
+
try {
|
|
342
|
+
const result = await operation();
|
|
343
|
+
return { ...result, ...(current ? { evidence: assemble({ ...current, observations: current.observations.slice(start) }) } : {}) };
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (current)
|
|
347
|
+
throw new JourneyStepError(assemble({ ...current, observations: current.observations.slice(start) }), error);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -163,11 +163,13 @@ export class IosTraceSessions {
|
|
|
163
163
|
catch (error) {
|
|
164
164
|
this.fail(s, reasonFor(error, s.recording?.recorder.alive() ? 'TRACE_STOP_FAILED' : 'TRACE_ARTIFACT_FAILED'));
|
|
165
165
|
s.result.cleanup.status = 'failed';
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
// Stop timeouts can throw after SIGKILL has already closed the recorder.
|
|
167
|
+
// Discarding is safe only after exit; keep the failure but release owned files.
|
|
168
|
+
if ((s.discard || s.result.reason === 'STORAGE_LIMIT') && !s.recording?.recorder.alive())
|
|
169
|
+
await this.discardStoppedFiles(s);
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
|
-
async
|
|
172
|
+
async discardStoppedFiles(s) {
|
|
171
173
|
try {
|
|
172
174
|
if (s.directory)
|
|
173
175
|
await fs.rm(s.directory, { recursive: true, force: true });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unknownDeviceReadiness, observeDeviceReadiness } from './device-readiness.js';
|
|
1
2
|
import { pausedReadiness } from '../debugger/guard.js';
|
|
2
3
|
import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js';
|
|
3
4
|
import { remainingBudget, runWithBudget } from '../utils/operation-budget.js';
|
|
@@ -28,10 +29,14 @@ export async function readinessDelay(ms) {
|
|
|
28
29
|
await new Promise(resolve => setTimeout(resolve, Math.min(ms, remainingBudget(ms))));
|
|
29
30
|
remainingBudget();
|
|
30
31
|
}
|
|
32
|
+
function observationComplete(tracker, wait, count) {
|
|
33
|
+
return tracker.ready || (!wait && (tracker.summary.state === 'loading' || count === 2));
|
|
34
|
+
}
|
|
31
35
|
export async function getAppReadiness(args, wait = false) {
|
|
32
36
|
const started = performance.now();
|
|
33
37
|
let device = null;
|
|
34
38
|
let tracker = new ReadinessTracker(args.platform ?? 'android', args.deviceId ?? '');
|
|
39
|
+
tracker.deviceReadiness = unknownDeviceReadiness('DEVICE_TARGET_UNAVAILABLE');
|
|
35
40
|
let failureCode;
|
|
36
41
|
let debugPause = null;
|
|
37
42
|
await runWithBudget(args.timeoutMs ?? (wait ? 30000 : 15000), async () => {
|
|
@@ -45,9 +50,17 @@ export async function getAppReadiness(args, wait = false) {
|
|
|
45
50
|
remainingBudget();
|
|
46
51
|
debugPause = pausedReadiness(device.id, device.platform);
|
|
47
52
|
if (debugPause) {
|
|
53
|
+
tracker.deviceReadiness = await observeDeviceReadiness(device);
|
|
48
54
|
failureCode = wait ? 'DEBUGGER_PAUSED' : undefined;
|
|
49
55
|
return;
|
|
50
56
|
}
|
|
57
|
+
await tracker.observeDevice();
|
|
58
|
+
if (tracker.deviceReadiness.input_ready === false) {
|
|
59
|
+
if (!wait)
|
|
60
|
+
return;
|
|
61
|
+
await readinessDelay(args.pollIntervalMs ?? 500);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
51
64
|
const captureStarted = Date.now();
|
|
52
65
|
try {
|
|
53
66
|
const tree = await ToolsObserve.getUITreeHandler({ platform: device.platform, deviceId: device.id });
|
|
@@ -68,9 +81,7 @@ export async function getAppReadiness(args, wait = false) {
|
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
count++;
|
|
71
|
-
if (tracker
|
|
72
|
-
return;
|
|
73
|
-
if (!wait && count === 2)
|
|
84
|
+
if (observationComplete(tracker, wait, count))
|
|
74
85
|
return;
|
|
75
86
|
await readinessDelay(wait ? args.pollIntervalMs ?? 500 : args.stabilityMs ?? 500);
|
|
76
87
|
}
|
|
@@ -86,17 +97,18 @@ export async function getAppReadiness(args, wait = false) {
|
|
|
86
97
|
}
|
|
87
98
|
});
|
|
88
99
|
if (debugPause)
|
|
89
|
-
return { success: !wait, device, readiness: debugPause, elapsed_ms: performance.now() - started, ...(wait ? { failure_code: 'DEBUGGER_PAUSED' } : {}) };
|
|
100
|
+
return { success: !wait, device, readiness: debugPause, device_readiness: tracker.deviceReadiness, elapsed_ms: performance.now() - started, ...(wait ? { failure_code: 'DEBUGGER_PAUSED' } : {}) };
|
|
90
101
|
const state = tracker.summary.state;
|
|
91
|
-
const success = !failureCode && (wait ?
|
|
102
|
+
const success = !failureCode && (wait ? tracker.ready : state !== 'unknown' || tracker.deviceReadiness.input_ready === false);
|
|
92
103
|
return {
|
|
93
|
-
success, device, readiness: tracker.summary, elapsed_ms: performance.now() - started,
|
|
104
|
+
success, device, readiness: tracker.summary, device_readiness: tracker.deviceReadiness, elapsed_ms: performance.now() - started,
|
|
94
105
|
...(!success ? { failure_code: failureCode ?? (wait ? 'READINESS_TIMEOUT' : 'READINESS_UNAVAILABLE') } : {})
|
|
95
106
|
};
|
|
96
107
|
}
|
|
97
108
|
export async function withReadyWait(target, timeoutMs, operation, failure) {
|
|
98
109
|
const started = performance.now();
|
|
99
110
|
let tracker = new ReadinessTracker(target.platform ?? 'android', target.deviceId ?? '');
|
|
111
|
+
tracker.deviceReadiness = unknownDeviceReadiness('DEVICE_TARGET_UNAVAILABLE');
|
|
100
112
|
let resolved = false;
|
|
101
113
|
return runWithBudget(timeoutMs, async () => {
|
|
102
114
|
try {
|
|
@@ -110,9 +122,9 @@ export async function withReadyWait(target, timeoutMs, operation, failure) {
|
|
|
110
122
|
}
|
|
111
123
|
catch {
|
|
112
124
|
const alreadyFailed = result && (('status' in result && result.status !== 'success') || ('success' in result && result.success === false));
|
|
113
|
-
return { ...(alreadyFailed ? result : failure('TIMEOUT', performance.now() - started, tracker.summary.sample_count)), readiness: tracker.deadline() };
|
|
125
|
+
return { ...(alreadyFailed ? result : failure('TIMEOUT', performance.now() - started, tracker.summary.sample_count)), readiness: tracker.deadline(), device_readiness: tracker.deviceReadiness };
|
|
114
126
|
}
|
|
115
|
-
return { ...result, readiness: tracker.summary };
|
|
127
|
+
return { ...result, readiness: tracker.summary, device_readiness: tracker.deviceReadiness };
|
|
116
128
|
}
|
|
117
129
|
catch (error) {
|
|
118
130
|
const expired = budgetExpired(error);
|
|
@@ -120,7 +132,7 @@ export async function withReadyWait(target, timeoutMs, operation, failure) {
|
|
|
120
132
|
tracker.deadline();
|
|
121
133
|
else
|
|
122
134
|
tracker.unavailable();
|
|
123
|
-
return { ...failure(!expired && (!resolved || isDeviceUnavailable(error)) ? 'DEVICE_UNAVAILABLE' : 'TIMEOUT', performance.now() - started, tracker.summary.sample_count), readiness: tracker.summary };
|
|
135
|
+
return { ...failure(!expired && (!resolved || isDeviceUnavailable(error)) ? 'DEVICE_UNAVAILABLE' : 'TIMEOUT', performance.now() - started, tracker.summary.sample_count), readiness: tracker.summary, device_readiness: tracker.deviceReadiness };
|
|
124
136
|
}
|
|
125
137
|
});
|
|
126
138
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unknownDeviceReadiness, observeDeviceReadiness, finishDeviceReadiness } from './device-readiness.js';
|
|
1
2
|
import { pausedReadiness } from '../debugger/guard.js';
|
|
2
3
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
4
|
import { remainingBudget } from '../utils/operation-budget.js';
|
|
@@ -7,10 +8,38 @@ const capabilities = { loading: true, transitioning: true, settled: true, unresp
|
|
|
7
8
|
export function isDeviceUnavailable(error) {
|
|
8
9
|
return /device.*(?:not found|offline|disconnected)|no devices found|READINESS_DEVICE_UNAVAILABLE/i.test(String(error));
|
|
9
10
|
}
|
|
11
|
+
function retainBlockedEvidence(previous, next) {
|
|
12
|
+
const retained = [];
|
|
13
|
+
if (['unknown', 'unsupported'].includes(next.wakefulness) && ['asleep', 'transitioning'].includes(previous.wakefulness)) {
|
|
14
|
+
next.wakefulness = previous.wakefulness;
|
|
15
|
+
retained.push('DEVICE_ASLEEP', 'DEVICE_TRANSITIONING');
|
|
16
|
+
}
|
|
17
|
+
if (['unknown', 'unsupported'].includes(next.lock_state) && previous.lock_state === 'locked') {
|
|
18
|
+
next.lock_state = previous.lock_state;
|
|
19
|
+
retained.push('DEVICE_LOCKED');
|
|
20
|
+
}
|
|
21
|
+
if (retained.length) {
|
|
22
|
+
next.evidence = [...previous.evidence.filter(item => retained.includes(item.code)), ...next.evidence].slice(-8);
|
|
23
|
+
next.limitations = [...new Set([...next.limitations, 'DEVICE_BLOCKER_EVIDENCE_RETAINED'])];
|
|
24
|
+
}
|
|
25
|
+
return finishDeviceReadiness(next);
|
|
26
|
+
}
|
|
10
27
|
export class ReadinessTracker {
|
|
11
28
|
platform;
|
|
12
29
|
deviceId;
|
|
13
30
|
stabilityMs;
|
|
31
|
+
deviceReadiness = unknownDeviceReadiness();
|
|
32
|
+
get ready() { return this.summary.state === 'settled' && this.deviceReadiness.input_ready !== false; }
|
|
33
|
+
async observeDevice() {
|
|
34
|
+
const next = await observeDeviceReadiness({ platform: this.platform, id: this.deviceId });
|
|
35
|
+
this.deviceReadiness = retainBlockedEvidence(this.deviceReadiness, next);
|
|
36
|
+
if (this.deviceReadiness.input_ready === false) {
|
|
37
|
+
this.signature = null;
|
|
38
|
+
this.stableSince = 0;
|
|
39
|
+
this.summary = { ...this.summary, state: 'unknown', basis: 'insufficient', stable_for_ms: 0,
|
|
40
|
+
limitations: [...new Set([...this.summary.limitations, 'DEVICE_BLOCKED'])] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
signature = null;
|
|
15
44
|
stableSince = 0;
|
|
16
45
|
summary;
|
|
@@ -105,3 +134,4 @@ function readinessState(loading, previous, unchanged, stableFor, stabilityMs) {
|
|
|
105
134
|
function readinessCode(nativeLoading, textLoading, state) {
|
|
106
135
|
return nativeLoading ? 'NATIVE_LOADING_INDICATOR' : textLoading ? 'LOADING_LABEL' : state === 'transitioning' ? 'SNAPSHOT_CHANGED' : state === 'settled' ? 'SNAPSHOT_STABLE' : 'INSUFFICIENT_SAMPLES';
|
|
107
136
|
}
|
|
137
|
+
export async function observeReadinessDeviceWait() { await waits.getStore()?.observeDevice(); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { benchmarkStage } from '../utils/benchmark-metrics.js';
|
|
1
2
|
async function convert(buffer, platform) {
|
|
2
3
|
const { default: sharp } = await import('sharp');
|
|
3
4
|
const image = sharp(buffer);
|
|
@@ -22,6 +23,9 @@ async function convert(buffer, platform) {
|
|
|
22
23
|
return { hasAlpha, webp, jpeg };
|
|
23
24
|
}
|
|
24
25
|
export async function encodeScreenshot(buffer, platform) {
|
|
26
|
+
return benchmarkStage('screenshot_encoding_ms', () => encode(buffer, platform));
|
|
27
|
+
}
|
|
28
|
+
async function encode(buffer, platform) {
|
|
25
29
|
const png = buffer.toString('base64');
|
|
26
30
|
try {
|
|
27
31
|
const { hasAlpha, webp, jpeg } = await convert(buffer, platform);
|
|
@@ -3,6 +3,7 @@ import { measure } from '../utils/timing.js';
|
|
|
3
3
|
import { deriveSnapshotMetadata } from './snapshot-metadata.js';
|
|
4
4
|
import { deriveSnapshotSemantic } from './snapshot-semantic.js';
|
|
5
5
|
import { snapshotLog, streamLog } from './snapshot-logs.js';
|
|
6
|
+
import { collectGetScreenFingerprint } from './android/fingerprint.js';
|
|
6
7
|
async function settle(tasks) {
|
|
7
8
|
const results = await Promise.allSettled(Object.values(tasks));
|
|
8
9
|
return Object.fromEntries(Object.keys(tasks).map((key, index) => [key, results[index]]));
|
|
@@ -15,11 +16,15 @@ function apply(result, success, failure) {
|
|
|
15
16
|
}
|
|
16
17
|
async function collect(raw, input, deps) {
|
|
17
18
|
const { platform, deviceId, includeScreenshot = true, includeLogs = true, sessionId, logLines = 200 } = input;
|
|
19
|
+
const androidTree = platform === 'android' ? deps.getUITreeHandler({ platform, deviceId }) : undefined;
|
|
18
20
|
const results = await settle({
|
|
19
21
|
screenshot: includeScreenshot ? measure('screenshot_ms', () => deps.captureScreenshotHandler({ platform, deviceId })) : Promise.resolve(null),
|
|
20
22
|
currentScreen: !platform || platform === 'android' ? deps.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
|
|
21
|
-
fingerprint:
|
|
22
|
-
|
|
23
|
+
fingerprint: androidTree ? collectGetScreenFingerprint({
|
|
24
|
+
getUITree: () => androidTree,
|
|
25
|
+
getCurrentScreen: id => deps.getCurrentScreenHandler({ deviceId: id })
|
|
26
|
+
}, deviceId) : deps.getScreenFingerprintHandler({ platform, deviceId }),
|
|
27
|
+
uiTree: androidTree ?? deps.getUITreeHandler({ platform, deviceId }),
|
|
23
28
|
readLogStream: includeLogs ? deps.readLogStreamHandler({ platform, deviceId, sessionId: sessionId || 'default', limit: logLines }) : Promise.resolve({ entries: [] })
|
|
24
29
|
});
|
|
25
30
|
let entries = [];
|