mobile-debug-mcp 0.35.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/.cursor-plugin/marketplace.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/README.md +96 -24
- 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 +37 -8
- 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 +261 -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 +46 -18
- package/dist/contracts/device-readiness.js +1 -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/sessions.js +33 -16
- package/dist/debugger/worker.js +6 -4
- package/dist/interact/index.js +12 -2
- 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/manage/ios.js +2 -0
- package/dist/observe/device-readiness.js +116 -0
- package/dist/observe/evidence.js +31 -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 +246 -0
- package/dist/observe/readiness-service.js +22 -10
- package/dist/observe/readiness.js +34 -4
- package/dist/observe/screenshot-encoding.js +4 -0
- package/dist/observe/snapshot-collection.js +7 -2
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +4 -8
- 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/debugger-handlers.js +27 -8
- package/dist/server/handlers/diagnostic.js +12 -4
- 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 +28 -14
- package/dist/server/schemas/debugger-schema.js +10 -7
- package/dist/server/schemas/interaction-diagnostics.js +9 -0
- package/dist/server/schemas/system-trace-schema.js +14 -9
- package/dist/server/tool-definitions.js +9 -4
- package/dist/server-core.js +5 -2
- package/dist/server.js +5 -5
- 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 +7 -11
- package/dist/utils/resolve-device.js +3 -2
- package/dist/utils/timing.js +3 -3
- package/docs/CHANGELOG.md +48 -1
- package/docs/agent-plugin-installation-verification.md +39 -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/code-structure.md +3 -1
- 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 +166 -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/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 +3 -2
- package/docs/tools/compact-interactions.md +56 -0
- package/docs/tools/correlated-evidence.md +6 -0
- package/docs/tools/debugger.md +45 -5
- 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-traces.md +81 -14
- package/docs/tools/system.md +6 -0
- package/package.json +2 -1
- package/scripts/agent-plugin.mjs +55 -5
- package/src/contracts/device-readiness.ts +8 -0
- package/src/debugger/android.ts +1 -1
- package/src/debugger/guard.ts +5 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- 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/sessions.ts +24 -12
- package/src/debugger/worker.ts +7 -4
- package/src/interact/index.ts +13 -2
- 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/manage/ios.ts +2 -0
- package/src/observe/device-readiness.ts +96 -0
- package/src/observe/evidence.ts +30 -1
- 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 +206 -0
- package/src/observe/readiness-service.ts +22 -10
- package/src/observe/readiness.ts +37 -4
- package/src/observe/screenshot-encoding.ts +4 -0
- package/src/observe/snapshot-collection.ts +7 -2
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +4 -4
- 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/debugger-handlers.ts +28 -9
- package/src/server/handlers/diagnostic.ts +11 -4
- 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 +29 -14
- package/src/server/schemas/debugger-schema.ts +10 -7
- package/src/server/schemas/interaction-diagnostics.ts +10 -0
- package/src/server/schemas/system-trace-schema.ts +15 -9
- package/src/server/tool-definitions.ts +8 -4
- package/src/server-core.ts +5 -2
- package/src/server.ts +4 -4
- 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 +7 -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 +227 -0
- 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 +119 -0
- package/test/device/automated/observe/system-trace.smoke.ts +2 -2
- package/test/fixtures/contracts/tool-definitions.json +243 -47
- 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/build.ts +3 -3
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/handle-guards.test.ts +89 -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/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-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +154 -0
- package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
- package/test/unit/observe/readiness.test.ts +1 -1
- 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/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 +18 -7
- 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
|
@@ -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 = [];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { TraceError } from './system-trace-error.js';
|
|
2
|
+
import { IosTraceSessions } from './ios-trace/sessions.js';
|
|
3
|
+
import { IosTraceNative } from './ios-trace/native.js';
|
|
4
|
+
export class SystemTraceRouter {
|
|
5
|
+
android;
|
|
6
|
+
ios;
|
|
7
|
+
native;
|
|
8
|
+
routes = new Map();
|
|
9
|
+
pending = new Set();
|
|
10
|
+
closing = false;
|
|
11
|
+
constructor(android, ios = new IosTraceSessions(), native = new IosTraceNative()) {
|
|
12
|
+
this.android = android;
|
|
13
|
+
this.ios = ios;
|
|
14
|
+
this.native = native;
|
|
15
|
+
}
|
|
16
|
+
capabilities(device) { return this.android.capabilities(device); }
|
|
17
|
+
iosCapabilities(device, appId, template) { return this.native.capabilities(device, appId, template); }
|
|
18
|
+
backend(platform) { return platform === 'ios' ? this.ios : this.android; }
|
|
19
|
+
track(result) {
|
|
20
|
+
this.routes.set(result.trace_id, { platform: result.device.platform, device: result.device.id,
|
|
21
|
+
active: result.cleanup.status !== 'complete', touched: performance.now() });
|
|
22
|
+
const terminal = [...this.routes.entries()].filter(([, r]) => !r.active);
|
|
23
|
+
for (const [index, [id, r]] of terminal.entries())
|
|
24
|
+
if (performance.now() - r.touched > 600000 || index < terminal.length - 100)
|
|
25
|
+
this.routes.delete(id);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
async start(device, duration, ios) {
|
|
29
|
+
if (this.closing)
|
|
30
|
+
throw new TraceError('TRACE_BUSY');
|
|
31
|
+
// Refresh automatically finalised reservations without trusting cached state.
|
|
32
|
+
await Promise.all([...this.routes.entries()].filter(([, r]) => r.active).map(async ([id, r]) => {
|
|
33
|
+
try {
|
|
34
|
+
this.track(await this.backend(r.platform).control(id, 'status'));
|
|
35
|
+
}
|
|
36
|
+
catch { /* uncertain cleanup continues to reserve capacity */ }
|
|
37
|
+
}));
|
|
38
|
+
const key = (ios ? 'ios:' : 'android:') + device;
|
|
39
|
+
const active = [...this.routes.values()].filter(r => r.active);
|
|
40
|
+
if (this.closing || this.pending.has(key) || active.some(r => r.platform + ':' + r.device === key) || active.length + this.pending.size >= 4)
|
|
41
|
+
throw new TraceError('TRACE_BUSY');
|
|
42
|
+
this.pending.add(key);
|
|
43
|
+
try {
|
|
44
|
+
if (!ios)
|
|
45
|
+
return this.track(await this.android.start(device, duration));
|
|
46
|
+
const cap = await this.native.capabilities(device, ios.appId, ios.template);
|
|
47
|
+
if (!cap.supported || !cap.device)
|
|
48
|
+
throw new TraceError('TRACE_UNSUPPORTED');
|
|
49
|
+
if (this.closing)
|
|
50
|
+
throw new TraceError('TRACE_BUSY');
|
|
51
|
+
return this.track(await this.ios.start(cap.device, ios.appId, duration));
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
this.pending.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async control(id, operation) {
|
|
58
|
+
const route = this.routes.get(id);
|
|
59
|
+
if (!route)
|
|
60
|
+
throw new TraceError('TRACE_NOT_FOUND');
|
|
61
|
+
return this.track(await this.backend(route.platform).control(id, operation));
|
|
62
|
+
}
|
|
63
|
+
async link(id, device, correlation) {
|
|
64
|
+
const route = this.routes.get(id);
|
|
65
|
+
if (!route || route.platform !== device.platform || route.device !== device.id)
|
|
66
|
+
throw new TraceError('TRACE_NOT_FOUND');
|
|
67
|
+
return this.backend(route.platform).link(id, device, correlation);
|
|
68
|
+
}
|
|
69
|
+
async shutdown() {
|
|
70
|
+
this.closing = true;
|
|
71
|
+
this.native.shutdown();
|
|
72
|
+
const results = await Promise.allSettled([this.android.shutdown(), this.ios.shutdown()]);
|
|
73
|
+
if (results.some(result => result.status === 'rejected'))
|
|
74
|
+
throw new TraceError('TRACE_STOP_FAILED');
|
|
75
|
+
const remaining = [];
|
|
76
|
+
for (const result of results)
|
|
77
|
+
if (result.status === 'fulfilled')
|
|
78
|
+
remaining.push(...result.value);
|
|
79
|
+
return remaining;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SystemTraceRouter } from './system-trace-router.js';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { promises as fs } from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -5,13 +6,8 @@ import path from 'node:path';
|
|
|
5
6
|
import { AndroidTraceNative, TRACE_MAX_BYTES } from './system-trace-android.js';
|
|
6
7
|
import { evidenceSessionId } from './evidence.js';
|
|
7
8
|
import { runWithBudget, remainingBudget } from '../utils/operation-budget.js';
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
constructor(code) {
|
|
11
|
-
super(code);
|
|
12
|
-
this.code = code;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
9
|
+
export { TraceError } from './system-trace-error.js';
|
|
10
|
+
import { TraceError } from './system-trace-error.js';
|
|
15
11
|
const clock = {
|
|
16
12
|
wall: Date.now, mono: () => performance.now(), sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
17
13
|
schedule: (fn, ms) => { const t = setTimeout(fn, ms); t.unref(); return t; }, clear: clearTimeout
|
|
@@ -291,4 +287,4 @@ export class SystemTraceSessions {
|
|
|
291
287
|
});
|
|
292
288
|
}
|
|
293
289
|
}
|
|
294
|
-
export const systemTraces = new SystemTraceSessions();
|
|
290
|
+
export const systemTraces = new SystemTraceRouter(new SystemTraceSessions());
|
|
@@ -183,11 +183,12 @@ Failure Handling:
|
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
name: 'list_devices',
|
|
186
|
-
description: 'List
|
|
186
|
+
description: 'List Android devices and iOS simulators. Supply appId for device-specific installation status; unknown checks do not imply absence.',
|
|
187
187
|
inputSchema: {
|
|
188
188
|
type: 'object',
|
|
189
189
|
properties: {
|
|
190
|
-
platform: { type: 'string', enum: ['android', 'ios'] }
|
|
190
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
191
|
+
appId: { type: 'string', pattern: '^[A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)*$', description: 'Optional package or bundle identifier to check on each device.' }
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observeDeviceReadiness, unknownDeviceReadiness } from '../../observe/device-readiness.js';
|
|
1
2
|
import { payloadOf } from './payload.js';
|
|
2
3
|
import { correlatedHandler } from './correlation.js';
|
|
3
4
|
import { interactionControls } from '../../contracts/execution-controls.js';
|
|
@@ -7,13 +8,14 @@ import { randomUUID } from 'node:crypto';
|
|
|
7
8
|
import { runWithBudget } from '../../utils/operation-budget.js';
|
|
8
9
|
import { validateExpectedState } from '../schemas/condition-schema.js';
|
|
9
10
|
import { conditionFailureCode, createConditionalAction, withConditionalAction } from '../../interact/conditional-action.js';
|
|
10
|
-
import { resolveTargetDevice, withResolvedDevice } from '../../utils/resolve-device.js';
|
|
11
|
+
import { resolveTargetDevice, withResolvedDevice, currentResolvedDevice } from '../../utils/resolve-device.js';
|
|
11
12
|
import { ToolsObserve } from '../../observe/index.js';
|
|
12
13
|
import { recordObservationEvent, withObservationContext } from '../../observe/performance-metrics.js';
|
|
13
14
|
import { buildActionExecutionResult, wrapResponse, wrapToolError } from '../common.js';
|
|
14
15
|
export function compactAction(result, verificationMode, started = true, stateDelta, totalMs = 0) {
|
|
15
16
|
return {
|
|
16
17
|
action_id: result.action_id,
|
|
18
|
+
...(result.device_readiness ? { device_readiness: result.device_readiness } : {}),
|
|
17
19
|
action_type: result.action_type,
|
|
18
20
|
success: !!result.success,
|
|
19
21
|
lifecycle_state: result.success ? (verificationMode === 'full' ? result.lifecycle_state : 'pending_verification') : 'failed',
|
|
@@ -39,15 +41,27 @@ function createAction(type, args, timing) {
|
|
|
39
41
|
delta: undefined, observationAvailable: false
|
|
40
42
|
};
|
|
41
43
|
}
|
|
44
|
+
async function executeWithDeviceState(args, execute) {
|
|
45
|
+
const result = payloadOf(await execute(args));
|
|
46
|
+
result.device_readiness = await observeDeviceReadiness(result.device ?? currentResolvedDevice());
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function actionError(type, error) {
|
|
50
|
+
const response = wrapToolError(type, error);
|
|
51
|
+
const item = response.content.find(item => item.type === 'text');
|
|
52
|
+
if (item?.text)
|
|
53
|
+
item.text = JSON.stringify({ ...JSON.parse(item.text), device_readiness: unknownDeviceReadiness('DEVICE_TARGET_UNAVAILABLE') });
|
|
54
|
+
return response;
|
|
55
|
+
}
|
|
42
56
|
async function dispatchAction(s, execute) {
|
|
43
57
|
const { type, args, controls, condition, budgetMs } = s;
|
|
44
58
|
const dispatchWithBudget = (timeoutMs) => runWithBudget(timeoutMs, async (budget) => {
|
|
45
59
|
try {
|
|
46
60
|
if (!condition)
|
|
47
|
-
return
|
|
61
|
+
return await executeWithDeviceState(args, execute);
|
|
48
62
|
return await withConditionalAction(condition, async () => {
|
|
49
63
|
const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId });
|
|
50
|
-
return withResolvedDevice(device, async () =>
|
|
64
|
+
return withResolvedDevice(device, async () => executeWithDeviceState(args, execute));
|
|
51
65
|
});
|
|
52
66
|
}
|
|
53
67
|
finally {
|
|
@@ -63,8 +77,8 @@ async function dispatchAction(s, execute) {
|
|
|
63
77
|
}
|
|
64
78
|
catch (error) {
|
|
65
79
|
if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
|
|
66
|
-
return
|
|
67
|
-
s.result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) };
|
|
80
|
+
return actionError(type, error);
|
|
81
|
+
s.result = { device_readiness: unknownDeviceReadiness('DEVICE_OBSERVATION_UNAVAILABLE'), success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) };
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
84
|
function rejectCondition(s) {
|
|
@@ -72,6 +86,7 @@ function rejectCondition(s) {
|
|
|
72
86
|
s.blocked = !!condition && ['mismatch', 'unavailable'].includes(condition.result.status);
|
|
73
87
|
if (s.blocked && condition) {
|
|
74
88
|
s.dispatchStarted = false;
|
|
89
|
+
const deviceReadiness = s.result.device_readiness;
|
|
75
90
|
s.result = buildActionExecutionResult({
|
|
76
91
|
actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
|
|
77
92
|
selector: s.result?.target?.selector ?? null, resolved: s.result?.target?.resolved,
|
|
@@ -79,6 +94,7 @@ function rejectCondition(s) {
|
|
|
79
94
|
failure: { failureCode: conditionFailureCode(condition), retryable: false },
|
|
80
95
|
traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
|
|
81
96
|
});
|
|
97
|
+
s.result.device_readiness = deviceReadiness;
|
|
82
98
|
}
|
|
83
99
|
}
|
|
84
100
|
function normalizeAction(s) {
|
|
@@ -129,19 +145,12 @@ function actionResponse(s) {
|
|
|
129
145
|
const evidence = {
|
|
130
146
|
...compact,
|
|
131
147
|
...(condition ? {
|
|
132
|
-
condition:
|
|
133
|
-
? {
|
|
134
|
-
...condition.result, observed: condition.result.observed ? {
|
|
135
|
-
snapshot_session_id: condition.result.observed.snapshot_session_id,
|
|
136
|
-
snapshot_revision: condition.result.observed.snapshot_revision,
|
|
137
|
-
captured_at_ms: condition.result.observed.captured_at_ms
|
|
138
|
-
} : null
|
|
139
|
-
} : condition.result
|
|
148
|
+
condition: condition.result
|
|
140
149
|
} : {}),
|
|
141
150
|
...(controls.verificationMode === 'light' ? { state_delta_available: !!s.delta } : {}),
|
|
142
151
|
...(s.result.success && controls.verificationMode === 'light' && !s.observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
|
|
143
152
|
};
|
|
144
|
-
return wrapResponse(
|
|
153
|
+
return wrapResponse({
|
|
145
154
|
...s.result, ...evidence, timing: { ...timing, total_ms: performance.now() - started },
|
|
146
155
|
effective_controls: { ...controls, actionTimeoutMs: s.unbounded ? null : budgetMs, timeout_bound: !(s.unbounded) }
|
|
147
156
|
}, controls.responseMode === 'debug');
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { evidenceSessionId } from '../../observe/evidence.js';
|
|
2
|
+
import { interactionDiagnostics } from './interaction-diagnostics.js';
|
|
3
|
+
import { object, pick, boundedValue, boundedState, observedState, deltaSummary } from './compact-state.js';
|
|
4
|
+
export const compactInteractionTools = new Set(['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'run_journey']);
|
|
5
|
+
const bytes = (value) => Buffer.byteLength(JSON.stringify(value));
|
|
6
|
+
// IDs are preserved verbatim; their allowance counts the UTF-8 wire encoding.
|
|
7
|
+
const idBytes = (value) => bytes(String(value ?? '')) - 2;
|
|
8
|
+
const deviceBytes = (evidence) => idBytes(object(object(evidence).device).id);
|
|
9
|
+
function compactEvidence(value) {
|
|
10
|
+
const e = object(value);
|
|
11
|
+
return { ...pick(e, ['session_id', 'correlation_id', 'action_id', 'device']),
|
|
12
|
+
...(e.association_reason ? pick(e, ['association', 'association_reason']) : {}),
|
|
13
|
+
...(object(e.trace_context).trace_id ? { trace_id: object(e.trace_context).trace_id } : {}) };
|
|
14
|
+
}
|
|
15
|
+
function conditionSummary(value) {
|
|
16
|
+
const c = object(value);
|
|
17
|
+
return { ...pick(c, ['status', 'reason']), observed: c.observed == null ? null : pick(c.observed, ['snapshot_session_id', 'snapshot_revision', 'captured_at_ms']) };
|
|
18
|
+
}
|
|
19
|
+
function addCompactError(result, error, name) {
|
|
20
|
+
const message = typeof error === 'string' ? error : object(error).message;
|
|
21
|
+
result.error = { ...pick(error, ['code']), message: boundedValue(message) };
|
|
22
|
+
result.action_type ??= name;
|
|
23
|
+
result.verification ??= { status: 'unavailable' };
|
|
24
|
+
if (object(result.error).message !== message)
|
|
25
|
+
result.truncated = true;
|
|
26
|
+
}
|
|
27
|
+
function compactResult(source, name, limit = 1280) {
|
|
28
|
+
const evidence = object(source.evidence);
|
|
29
|
+
const result = {
|
|
30
|
+
...pick(source, ['action_id', 'action_type', 'success', 'lifecycle_state', 'failure_code', 'retryable', 'delivery_status', 'dispatch_started', 'verification', 'verification_diagnostic', 'state_delta_available']),
|
|
31
|
+
observed_state: !['wait', 'assert'].includes(name) && source.dispatch_started !== true
|
|
32
|
+
? { status: 'unknown', reason: 'NO_POST_ACTION_OBSERVATION' } : observedState(evidence, source.dispatch_started === true),
|
|
33
|
+
evidence: compactEvidence(evidence), diagnostics: interactionDiagnostics.retain(source)
|
|
34
|
+
};
|
|
35
|
+
result.success ??= false;
|
|
36
|
+
result.verification ??= { status: source.dispatch_started === false ? 'not_requested' : 'unavailable' };
|
|
37
|
+
if (source.error)
|
|
38
|
+
addCompactError(result, source.error, name);
|
|
39
|
+
if (source.device_readiness)
|
|
40
|
+
result.device_readiness = pick(source.device_readiness, ['wakefulness', 'lock_state', 'input_ready', 'blockers', 'limitations']);
|
|
41
|
+
if (source.condition)
|
|
42
|
+
result.condition = conditionSummary(source.condition);
|
|
43
|
+
if (source.state_delta)
|
|
44
|
+
result.state_delta = deltaSummary(source.state_delta);
|
|
45
|
+
if (source.timing)
|
|
46
|
+
result.timing = pick(source.timing, ['total_ms']);
|
|
47
|
+
if (object(source.observed).matched_count !== undefined)
|
|
48
|
+
result.matched_count = object(source.observed).matched_count;
|
|
49
|
+
if (source.observed_fingerprint !== undefined)
|
|
50
|
+
result.matched = !!source.success;
|
|
51
|
+
const state = source.actual_state !== undefined ? source.actual_state : source.observed_state;
|
|
52
|
+
if (state !== undefined) {
|
|
53
|
+
result.observed = boundedState(state);
|
|
54
|
+
if (JSON.stringify(result.observed) !== JSON.stringify(state))
|
|
55
|
+
result.truncated = true;
|
|
56
|
+
}
|
|
57
|
+
result.evidence = { session_id: evidenceSessionId(), ...object(result.evidence) };
|
|
58
|
+
enforceBudget(result, limit + deviceBytes(evidence));
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function enforceBudget(result, limit) {
|
|
62
|
+
if (bytes(result) <= limit)
|
|
63
|
+
return;
|
|
64
|
+
result.truncated = true;
|
|
65
|
+
for (const key of ['state_delta', 'observed', 'matched_count', 'matched', 'state_delta_available', 'timing']) {
|
|
66
|
+
delete result[key];
|
|
67
|
+
if (bytes(result) <= limit)
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (result.condition)
|
|
71
|
+
result.condition = pick(result.condition, ['status', 'reason']);
|
|
72
|
+
result.observed_state = { status: 'unknown', reason: 'OMITTED' };
|
|
73
|
+
if (result.failure_code)
|
|
74
|
+
delete result.error;
|
|
75
|
+
else if (result.error)
|
|
76
|
+
result.error = pick(result.error, ['code']);
|
|
77
|
+
}
|
|
78
|
+
function compactJourney(source) {
|
|
79
|
+
const steps = Array.isArray(source.steps) ? source.steps.map(object) : [];
|
|
80
|
+
const result = { ...pick(source, ['stopped_at_step_id']), success: source.success === true,
|
|
81
|
+
evidence: compactEvidence(source.evidence), diagnostics: interactionDiagnostics.retain(source),
|
|
82
|
+
observed_state: observedState(source.evidence), timing: pick(source.timing, ['total_ms']),
|
|
83
|
+
steps: steps.map(step => ({ ...pick(step, ['id', 'type', 'status', 'success', 'reason']),
|
|
84
|
+
result: step.result ? compactResult(object(step.result), String(step.type), 1080) : null })) };
|
|
85
|
+
if (source.error) {
|
|
86
|
+
const message = object(source.error).message;
|
|
87
|
+
const bounded = boundedValue(message);
|
|
88
|
+
result.error = { ...pick(source.error, ['code']), message: bounded };
|
|
89
|
+
if (bounded !== message)
|
|
90
|
+
result.truncated = true;
|
|
91
|
+
}
|
|
92
|
+
if (source.system_trace)
|
|
93
|
+
result.system_trace = pick(source.system_trace, ['trace_id', 'state', 'coverage', 'clock_alignment']);
|
|
94
|
+
enforceBudget(result, 1280 + 1280 * steps.length + steps.reduce((sum, s) => sum + idBytes(s.id) + deviceBytes(object(s.result).evidence), 0) + idBytes(source.stopped_at_step_id) + deviceBytes(source.evidence));
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
export function finalizeCompactInteraction(name, args, response) {
|
|
98
|
+
if (!compactInteractionTools.has(name) || (args.responseMode ?? (name === 'run_journey' ? 'compact' : 'debug')) !== 'compact')
|
|
99
|
+
return response;
|
|
100
|
+
const item = response.content.find(item => item.type === 'text');
|
|
101
|
+
if (item?.text) {
|
|
102
|
+
const source = object(JSON.parse(item.text));
|
|
103
|
+
item.text = JSON.stringify(name === 'run_journey' ? compactJourney(source) : compactResult(source, name));
|
|
104
|
+
}
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function object(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
3
|
+
}
|
|
4
|
+
export function pick(value, keys) {
|
|
5
|
+
const source = object(value);
|
|
6
|
+
return Object.fromEntries(keys.filter(key => source[key] !== undefined).map(key => [key, source[key]]));
|
|
7
|
+
}
|
|
8
|
+
export function boundedValue(value) {
|
|
9
|
+
if (typeof value !== 'string')
|
|
10
|
+
return ['number', 'boolean'].includes(typeof value) || value === null ? value : undefined;
|
|
11
|
+
let text = '';
|
|
12
|
+
for (const char of value) {
|
|
13
|
+
if (Buffer.byteLength(text + char) > 128)
|
|
14
|
+
break;
|
|
15
|
+
text += char;
|
|
16
|
+
}
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
export function observedState(evidence, dispatchStarted = false) {
|
|
20
|
+
const e = object(evidence), device = object(e.device);
|
|
21
|
+
const observations = Array.isArray(e.observations) ? e.observations.map(object) : [];
|
|
22
|
+
const candidates = observations.filter(o => ['ui_tree', 'fingerprint', 'screenshot'].includes(String(o.kind)));
|
|
23
|
+
const last = candidates.at(-1);
|
|
24
|
+
if (!last)
|
|
25
|
+
return { status: 'unknown', reason: 'NO_POST_ACTION_OBSERVATION' };
|
|
26
|
+
const summary = pick(last, ['kind', 'source_captured_at_ms', 'freshness', 'snapshot_session_id', 'snapshot_revision']);
|
|
27
|
+
summary.captured_at_ms = summary.source_captured_at_ms ?? null;
|
|
28
|
+
delete summary.source_captured_at_ms;
|
|
29
|
+
const same = object(last.device).id === device.id && object(last.device).platform === device.platform && !!device.id;
|
|
30
|
+
const post = typeof e.dispatch_completed_at_ms === 'number' && typeof last.capture_started_at_ms === 'number' && last.capture_started_at_ms >= e.dispatch_completed_at_ms;
|
|
31
|
+
if (typeof last.source_captured_at_ms !== 'number' || last.source_captured_at_ms <= 0)
|
|
32
|
+
return { status: 'unknown', reason: 'CAPTURE_TIME_UNKNOWN', ...summary };
|
|
33
|
+
if (last.status === 'captured' && last.freshness === 'fresh' && same && (!dispatchStarted || post))
|
|
34
|
+
return { status: 'observed', ...summary };
|
|
35
|
+
return { status: 'unknown', reason: !same ? 'DEVICE_MISMATCH' : !post && dispatchStarted ? 'NO_POST_ACTION_OBSERVATION' : 'OBSERVATION_UNAVAILABLE', ...summary };
|
|
36
|
+
}
|
|
37
|
+
export function deltaSummary(value) {
|
|
38
|
+
const delta = object(value);
|
|
39
|
+
return { ...pick(delta, ['base_snapshot_revision', 'snapshot_revision', 'truncated', 'omitted_changes']),
|
|
40
|
+
...Object.fromEntries(['added', 'removed', 'changed'].map(key => [key, Array.isArray(delta[key]) ? delta[key].length : 0])) };
|
|
41
|
+
}
|
|
42
|
+
export function boundedState(value) {
|
|
43
|
+
if (!value || typeof value !== 'object')
|
|
44
|
+
return boundedValue(value);
|
|
45
|
+
const fields = pick(value, ['property', 'value', 'raw_value', 'checked', 'selected', 'expanded', 'enabled', 'text_value']);
|
|
46
|
+
return Object.fromEntries(Object.entries(fields).map(([key, item]) => [key, boundedValue(item)]).filter(([, item]) => item !== undefined));
|
|
47
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { iosCapabilities } from '../../debugger/ios-worker.js';
|
|
1
2
|
import { debuggerResponse } from './debugger-response.js';
|
|
2
3
|
import { classify } from '../../debugger/diagnostics.js';
|
|
3
4
|
import { debuggerContracts } from '../schemas/debugger-schema.js';
|
|
4
|
-
import { debuggerSessions, debuggerCapabilities } from '../../debugger/sessions.js';
|
|
5
|
+
import { debuggerSessions, iosDebuggerSessions, debuggerCapabilities } from '../../debugger/sessions.js';
|
|
5
6
|
import { DebuggerError, withDebuggerBudget } from '../../debugger/worker.js';
|
|
6
7
|
import { wrapResponse } from '../common.js';
|
|
7
8
|
function failure(code, summary = {}, diagnostic, command = '', mode = 'debug') {
|
|
@@ -20,15 +21,17 @@ export async function handleDebugger(name, args, signal) {
|
|
|
20
21
|
const { responseMode, ...input } = parsed.data;
|
|
21
22
|
const a = input;
|
|
22
23
|
const command = debuggerContracts[name].command;
|
|
24
|
+
const sessions = selectSessions(a);
|
|
23
25
|
try {
|
|
24
26
|
if (signal?.aborted)
|
|
25
27
|
throw new DebuggerError('DEBUGGER_CANCELLED');
|
|
26
|
-
const result = await withDebuggerBudget(a.timeoutMs, signal, async () => command === 'capabilities' ? await
|
|
27
|
-
command === 'attach' ? await
|
|
28
|
-
await
|
|
28
|
+
const result = await withDebuggerBudget(a.timeoutMs, signal, async () => command === 'capabilities' ? await sessions.capabilities(a.deviceId, a.appId, a.processId) :
|
|
29
|
+
command === 'attach' ? await sessions.attach(a.deviceId, a.appId, a.processId, a.timeoutMs) :
|
|
30
|
+
await sessions.command(a.debugSessionId, command, { ...a, signal: command === 'events' ? signal : undefined }, a.timeoutMs));
|
|
29
31
|
if (signal?.aborted && command === 'capabilities')
|
|
30
32
|
throw new DebuggerError('DEBUGGER_CANCELLED');
|
|
31
|
-
|
|
33
|
+
const payload = sessions === iosDebuggerSessions ? iosDiagnostics(result) : result;
|
|
34
|
+
return { ...wrapResponse(debuggerResponse(command, responseMode, payload)), ...(!result.success ? { isError: true } : {}) };
|
|
32
35
|
}
|
|
33
36
|
catch (e) {
|
|
34
37
|
const error = classify(e, 'unknown', 'DEBUGGER_WORKER_FAILED');
|
|
@@ -36,13 +39,29 @@ export async function handleDebugger(name, args, signal) {
|
|
|
36
39
|
let summary = {};
|
|
37
40
|
if (a.debugSessionId) {
|
|
38
41
|
try {
|
|
39
|
-
summary =
|
|
42
|
+
summary = sessions.summary(a.debugSessionId);
|
|
40
43
|
}
|
|
41
44
|
catch { /* Unknown session */ }
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
const response = failure(code, {
|
|
47
|
+
...(sessions === iosDebuggerSessions ? { capabilities: iosCapabilities } : {}),
|
|
44
48
|
debug_session_id: a.debugSessionId ?? null,
|
|
45
|
-
device: a.deviceId ? { platform: 'android', id: a.deviceId } : null, ...summary
|
|
49
|
+
device: a.deviceId ? { platform: a.platform ?? 'android', id: a.deviceId } : null, ...summary
|
|
46
50
|
}, code === 'DEBUGGER_CLEANUP_FAILED' ? summary.error?.diagnostic : error.diagnostic, command, responseMode);
|
|
51
|
+
if (sessions === iosDebuggerSessions)
|
|
52
|
+
response.content[0].text = JSON.stringify(iosDiagnostics(JSON.parse(response.content[0].text)));
|
|
53
|
+
return response;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
56
|
+
function selectSessions(a) {
|
|
57
|
+
return a.platform === 'ios' || (a.debugSessionId && iosDebuggerSessions.owns(a.debugSessionId)) ? iosDebuggerSessions : debuggerSessions;
|
|
58
|
+
}
|
|
59
|
+
function iosRecovery(d) {
|
|
60
|
+
return { ...d, recovery: d.recovery.replace('JDK 17+ availability, including java, javac and jdk.jdi.', 'Xcode and its Swift-enabled LLDB/Python availability.').replace('JDK compiler and worker source/cache', 'LLDB worker source').replace('Java worker', 'LLDB worker').replace('ADB server', 'simulator') };
|
|
61
|
+
}
|
|
62
|
+
function iosDiagnostics(value) {
|
|
63
|
+
const result = { ...value };
|
|
64
|
+
const error = value.error;
|
|
65
|
+
return { ...result, ...(error?.diagnostic ? { error: { ...error, diagnostic: iosRecovery(error.diagnostic) } } : {}),
|
|
66
|
+
...(Array.isArray(value.diagnostics) ? { diagnostics: value.diagnostics.map(iosRecovery) } : {}) };
|
|
67
|
+
}
|
|
@@ -44,10 +44,10 @@ export async function handleSystemTrace(name, args) {
|
|
|
44
44
|
throw new TraceError('INVALID_ARGUMENT');
|
|
45
45
|
if (name === 'get_system_trace_capabilities' || name === 'start_system_trace') {
|
|
46
46
|
const target = parsed.data;
|
|
47
|
-
if (target.platform
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
47
|
+
if (target.platform === 'android' && (target.appId !== undefined || target.template !== undefined))
|
|
48
|
+
throw new TraceError('INVALID_ARGUMENT');
|
|
49
|
+
if (target.platform === 'ios') {
|
|
50
|
+
return handleIosTrace(name, target);
|
|
51
51
|
}
|
|
52
52
|
let device;
|
|
53
53
|
try {
|
|
@@ -67,3 +67,11 @@ export async function handleSystemTrace(name, args) {
|
|
|
67
67
|
const result = await systemTraces.control(parsed.data.traceId, operation);
|
|
68
68
|
return { ...wrapResponse(result), ...(['failed', 'cancelled'].includes(result.state) ? { isError: true } : {}) };
|
|
69
69
|
}
|
|
70
|
+
async function handleIosTrace(name, target) {
|
|
71
|
+
if (!target.deviceId || (name === 'start_system_trace' && !target.appId))
|
|
72
|
+
throw new TraceError('INVALID_ARGUMENT');
|
|
73
|
+
if (name === 'get_system_trace_capabilities')
|
|
74
|
+
return wrapResponse(await systemTraces.iosCapabilities(target.deviceId, target.appId, target.template));
|
|
75
|
+
const result = await systemTraces.start(target.deviceId, target.durationMs, { appId: target.appId, template: target.template });
|
|
76
|
+
return { ...wrapResponse(result), ...(result.state === 'failed' ? { isError: true } : {}) };
|
|
77
|
+
}
|
|
@@ -18,6 +18,7 @@ export async function executeTap(args) {
|
|
|
18
18
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
19
19
|
const result = buildActionExecutionResult({
|
|
20
20
|
actionType: 'tap',
|
|
21
|
+
device: res.device,
|
|
21
22
|
sourceModule: 'server',
|
|
22
23
|
selector: { x, y },
|
|
23
24
|
success: !!res.success,
|
|
@@ -68,6 +69,7 @@ export async function executeSwipe(args) {
|
|
|
68
69
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
69
70
|
const result = buildActionExecutionResult({
|
|
70
71
|
actionType: 'swipe',
|
|
72
|
+
device: res.device,
|
|
71
73
|
sourceModule: 'server',
|
|
72
74
|
selector: { x1, y1, x2, y2, duration },
|
|
73
75
|
success: !!res.success,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observeDeviceReadiness } from '../../observe/device-readiness.js';
|
|
1
2
|
import { markEvidenceVerification } from '../../observe/evidence.js';
|
|
2
3
|
import { withReadyWait } from '../../observe/readiness-service.js';
|
|
3
4
|
import { ToolsInteract } from '../../interact/index.js';
|
|
@@ -68,6 +69,7 @@ export async function handleAdjustControl(args) {
|
|
|
68
69
|
platform,
|
|
69
70
|
deviceId
|
|
70
71
|
});
|
|
72
|
+
res.device_readiness = await observeDeviceReadiness(res.device);
|
|
71
73
|
markEvidenceVerification();
|
|
72
74
|
return wrapResponse(res);
|
|
73
75
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { evidenceSessionId } from '../../observe/evidence.js';
|
|
3
|
+
import { diagnosticsSchema } from '../schemas/interaction-diagnostics.js';
|
|
4
|
+
import { wrapResponse } from '../common.js';
|
|
5
|
+
const scheduleExpiry = expire => {
|
|
6
|
+
const timer = setTimeout(expire, 600000);
|
|
7
|
+
timer.unref();
|
|
8
|
+
return () => clearTimeout(timer);
|
|
9
|
+
};
|
|
10
|
+
export class InteractionDiagnostics {
|
|
11
|
+
now;
|
|
12
|
+
sessionId;
|
|
13
|
+
schedule;
|
|
14
|
+
entries = new Map();
|
|
15
|
+
bytes = 0;
|
|
16
|
+
constructor(now = () => performance.now(), sessionId = evidenceSessionId(), schedule = scheduleExpiry) {
|
|
17
|
+
this.now = now;
|
|
18
|
+
this.sessionId = sessionId;
|
|
19
|
+
this.schedule = schedule;
|
|
20
|
+
}
|
|
21
|
+
remove(id) {
|
|
22
|
+
const entry = this.entries.get(id);
|
|
23
|
+
if (entry) {
|
|
24
|
+
this.bytes -= entry.bytes;
|
|
25
|
+
entry.cancel();
|
|
26
|
+
}
|
|
27
|
+
this.entries.delete(id);
|
|
28
|
+
}
|
|
29
|
+
prune() {
|
|
30
|
+
for (const [id, entry] of this.entries)
|
|
31
|
+
if (this.now() - entry.completed >= 600000)
|
|
32
|
+
this.remove(id);
|
|
33
|
+
}
|
|
34
|
+
retain(result) {
|
|
35
|
+
this.prune();
|
|
36
|
+
const resultId = randomUUID(), text = JSON.stringify(result), bytes = Buffer.byteLength(text);
|
|
37
|
+
const ref = { session_id: this.sessionId, result_id: resultId };
|
|
38
|
+
if (bytes > 1024 * 1024)
|
|
39
|
+
return { ...ref, available: false, reason: 'RESULT_TOO_LARGE' };
|
|
40
|
+
while (this.entries.size >= 100 || this.bytes + bytes > 16 * 1024 * 1024)
|
|
41
|
+
this.remove(this.entries.keys().next().value);
|
|
42
|
+
this.entries.set(resultId, { text, completed: this.now(), bytes, cancel: this.schedule(() => this.remove(resultId)) });
|
|
43
|
+
this.bytes += bytes;
|
|
44
|
+
return { ...ref, available: true };
|
|
45
|
+
}
|
|
46
|
+
lookup(sessionId, resultId) {
|
|
47
|
+
this.prune();
|
|
48
|
+
if (sessionId !== this.sessionId)
|
|
49
|
+
return { success: false, error: { code: 'SESSION_MISMATCH', message: 'Use the session_id returned with this result.' } };
|
|
50
|
+
const entry = this.entries.get(resultId);
|
|
51
|
+
if (!entry)
|
|
52
|
+
return { success: false, error: { code: 'RESULT_UNAVAILABLE', message: 'Result expired, was evicted, is unknown, or belongs to a previous server process. Do not replay input automatically.' } };
|
|
53
|
+
return { success: true, result: JSON.parse(entry.text) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const interactionDiagnostics = new InteractionDiagnostics();
|
|
57
|
+
export async function handleInteractionDiagnostics(args) {
|
|
58
|
+
const parsed = diagnosticsSchema.safeParse(args);
|
|
59
|
+
return wrapResponse(parsed.success ? interactionDiagnostics.lookup(parsed.data.sessionId, parsed.data.resultId) : {
|
|
60
|
+
success: false, error: { code: 'INVALID_ARGUMENT', message: 'Expected only sessionId and resultId UUID strings.' }
|
|
61
|
+
}, false);
|
|
62
|
+
}
|