mobile-debug-mcp 0.32.4 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.github/copilot-instructions.md +7 -0
- package/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +21 -10
- package/README.md +69 -4
- package/agent-plugin/.claude-plugin/plugin.json +20 -0
- package/agent-plugin/.codex-plugin/plugin.json +34 -0
- package/agent-plugin/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/README.md +9 -3
- package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
- package/dist/debugger/android.js +78 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/sessions.js +262 -0
- package/dist/debugger/worker.js +165 -0
- package/dist/interact/android.js +10 -4
- package/dist/interact/conditional-action.js +68 -0
- package/dist/interact/index.js +70 -14
- package/dist/interact/ios.js +14 -8
- package/dist/manage/android.js +4 -3
- package/dist/manage/ios.js +3 -2
- package/dist/observe/android.js +25 -4
- package/dist/observe/evidence.js +280 -0
- package/dist/observe/index.js +33 -6
- package/dist/observe/ios.js +25 -6
- package/dist/observe/readiness-service.js +124 -0
- package/dist/observe/readiness.js +88 -0
- package/dist/observe/recording.js +165 -0
- package/dist/observe/snapshot-metadata.js +10 -2
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +285 -0
- package/dist/server/common.js +9 -0
- package/dist/server/condition-schema.js +32 -0
- package/dist/server/debugger-handlers.js +38 -0
- package/dist/server/debugger-schema.js +42 -0
- package/dist/server/journey-schema.js +4 -2
- package/dist/server/readiness-schema.js +12 -0
- package/dist/server/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +51 -0
- package/dist/server/tool-handlers.js +294 -105
- package/dist/server-core.js +3 -3
- package/dist/server.js +34 -0
- package/dist/system/index.js +5 -4
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +4 -4
- package/dist/utils/java.js +14 -12
- package/dist/utils/operation-budget.js +26 -2
- package/dist/utils/resolve-device.js +9 -0
- package/docs/CHANGELOG.md +15 -0
- package/docs/specs/020-conditional-actions/intent.md +62 -0
- package/docs/specs/020-conditional-actions/plan.md +214 -0
- package/docs/specs/020-conditional-actions/spec.md +301 -0
- package/docs/specs/021-app-readiness-signals/intent.md +40 -0
- package/docs/specs/021-app-readiness-signals/plan.md +102 -0
- package/docs/specs/021-app-readiness-signals/spec.md +144 -0
- package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
- package/docs/specs/022-correlated-evidence/intent.md +37 -0
- package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
- package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
- package/docs/specs/022-correlated-evidence/plan.md +171 -0
- package/docs/specs/022-correlated-evidence/spec.md +123 -0
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +40 -0
- package/docs/specs/023-android-system-traces/plan.md +117 -0
- package/docs/specs/023-android-system-traces/spec.md +120 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
- package/docs/specs/process.md +213 -0
- package/docs/tools/TOOLS.md +5 -0
- package/docs/tools/debugger.md +104 -0
- package/docs/tools/evidence.md +82 -0
- package/docs/tools/interact.md +125 -2
- package/docs/tools/observe.md +70 -0
- package/docs/tools/system-traces.md +126 -0
- package/package.json +1 -1
- package/scripts/agent-plugin.mjs +68 -8
- package/src/debugger/android.ts +65 -0
- package/src/debugger/guard.ts +26 -0
- package/src/debugger/sessions.ts +168 -0
- package/src/debugger/worker.ts +122 -0
- package/src/interact/android.ts +10 -4
- package/src/interact/conditional-action.ts +75 -0
- package/src/interact/index.ts +57 -28
- package/src/interact/ios.ts +14 -8
- package/src/manage/android.ts +4 -3
- package/src/manage/ios.ts +3 -2
- package/src/observe/android.ts +33 -6
- package/src/observe/evidence.ts +262 -0
- package/src/observe/index.ts +32 -8
- package/src/observe/ios.ts +32 -8
- package/src/observe/readiness-service.ts +94 -0
- package/src/observe/readiness.ts +84 -0
- package/src/observe/snapshot-metadata.ts +12 -1
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +218 -0
- package/src/server/common.ts +7 -0
- package/src/server/condition-schema.ts +32 -0
- package/src/server/debugger-handlers.ts +29 -0
- package/src/server/debugger-schema.ts +37 -0
- package/src/server/journey-schema.ts +4 -2
- package/src/server/readiness-schema.ts +12 -0
- package/src/server/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +49 -0
- package/src/server/tool-handlers.ts +182 -21
- package/src/server-core.ts +3 -3
- package/src/server.ts +24 -0
- package/src/system/index.ts +7 -5
- package/src/types.ts +30 -1
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +4 -4
- package/src/utils/java.ts +13 -12
- package/src/utils/operation-budget.ts +24 -2
- package/src/utils/resolve-device.ts +10 -0
- package/test/device/automated/debugger/runtime.android.test.ts +203 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
- package/test/device/manual/interact/readiness.manual.ts +99 -0
- package/test/fixtures/debugger-app/MainActivity.java +46 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +13 -0
- package/test/fixtures/debugger-app/build.ts +29 -0
- package/test/fixtures/readiness-app/MainActivity.java +23 -0
- package/test/fixtures/readiness-app/README.md +12 -0
- package/test/fixtures/readiness-app/build.ts +37 -0
- package/test/fixtures/readiness-app/main.m +27 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/guards.test.ts +46 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
- package/test/unit/observe/compact_tree_error.test.ts +24 -0
- package/test/unit/observe/conditional_snapshots.test.ts +33 -0
- package/test/unit/observe/correlated-evidence.test.ts +128 -0
- package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/readiness.test.ts +41 -0
- package/test/unit/observe/readiness_native.test.ts +68 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/server/conditional_actions.test.ts +299 -0
- package/test/unit/server/conditional_schema.test.ts +59 -0
- package/test/unit/server/correlated-evidence.test.ts +194 -0
- package/test/unit/server/readiness.test.ts +170 -0
- package/test/unit/server/selector_tap_target.test.ts +79 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tap_selector.test.ts +2 -0
- package/worker/DebuggerWorker.java +160 -0
- package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
- /package/{skills → .agents/skills}/README.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
- /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { handleDebugger } from './debugger-handlers.js';
|
|
2
|
+
import { debuggerContracts } from './debugger-schema.js';
|
|
3
|
+
import { debuggerSessions } from '../debugger/sessions.js';
|
|
4
|
+
import { withDebuggerGuard, guardDispatched } from '../debugger/guard.js';
|
|
5
|
+
import { systemTraces, TraceError } from '../observe/system-trace.js';
|
|
6
|
+
import { systemTraceTools, traceTargetSchema, traceStartSchema, traceControlSchema } from './system-trace-schema.js';
|
|
7
|
+
import { currentEvidenceCorrelationId, bindSystemTrace, withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js';
|
|
1
8
|
import { measure, withTiming } from '../utils/timing.js';
|
|
2
9
|
import { randomUUID } from 'node:crypto';
|
|
3
10
|
import { runWithBudget } from '../utils/operation-budget.js';
|
|
4
11
|
import { journeySchema, tapElementControlsSchema } from './journey-schema.js';
|
|
12
|
+
import { readinessSchema, readinessWaitSchema } from './readiness-schema.js';
|
|
13
|
+
import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js';
|
|
14
|
+
import { validateExpectedState } from './condition-schema.js';
|
|
15
|
+
import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js';
|
|
5
16
|
import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js';
|
|
6
17
|
import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js';
|
|
7
18
|
import { ToolsInteract } from '../interact/index.js';
|
|
@@ -11,6 +22,7 @@ import { ToolsNetwork } from '../network/index.js';
|
|
|
11
22
|
import { getSystemStatus } from '../system/index.js';
|
|
12
23
|
import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js';
|
|
13
24
|
import { buildActionExecutionResult, captureActionFingerprint, getArrayArg, getBooleanArg, getNumberArg, getObjectArg, getStringArg, inferGenericFailure, inferScrollFailure, requireBooleanArg, requireNumberArg, requireObjectArg, requireStringArg, wrapResponse, wrapToolError } from './common.js';
|
|
25
|
+
const systemTraceToolNames = new Set(systemTraceTools.map(tool => tool.name));
|
|
14
26
|
function interactionControls(args) {
|
|
15
27
|
const verificationMode = getStringArg(args, 'verificationMode') ?? 'full';
|
|
16
28
|
const responseMode = getStringArg(args, 'responseMode') ?? 'debug';
|
|
@@ -43,18 +55,45 @@ function compactAction(result, verificationMode, started = true, stateDelta, tot
|
|
|
43
55
|
timing: { total_ms: totalMs }
|
|
44
56
|
};
|
|
45
57
|
}
|
|
46
|
-
function
|
|
58
|
+
function correlatedHandler(name, handler, action = false, journey = false) {
|
|
47
59
|
return async (args) => {
|
|
60
|
+
const ref = validateActionRef(args.actionRef);
|
|
61
|
+
const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
|
|
62
|
+
try {
|
|
63
|
+
return await handler(args);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return error instanceof TraceError ? { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true } : wrapToolError(name, error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const item = result.content.find(item => item.type === 'text');
|
|
70
|
+
if (item?.text) {
|
|
71
|
+
const payload = JSON.parse(item.text);
|
|
72
|
+
payload.evidence = evidence;
|
|
73
|
+
item.text = JSON.stringify(payload);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function actionHandler(type, execute) {
|
|
79
|
+
return correlatedHandler(type, async (args) => {
|
|
48
80
|
const timing = {};
|
|
49
81
|
return withTiming(timing, async () => {
|
|
50
82
|
const started = performance.now();
|
|
51
83
|
const controls = interactionControls(args);
|
|
84
|
+
const expected = validateExpectedState(type, args);
|
|
85
|
+
const condition = expected ? createConditionalAction(args.platform, args.deviceId, expected) : undefined;
|
|
52
86
|
const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000);
|
|
53
87
|
let dispatchStarted = false;
|
|
54
88
|
let result;
|
|
55
89
|
const dispatchWithBudget = (timeoutMs) => runWithBudget(timeoutMs, async (budget) => {
|
|
56
90
|
try {
|
|
57
|
-
|
|
91
|
+
if (!condition)
|
|
92
|
+
return payloadOf(await execute(args));
|
|
93
|
+
return await withConditionalAction(condition, async () => {
|
|
94
|
+
const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId });
|
|
95
|
+
return withResolvedDevice(device, async () => payloadOf(await execute(args)));
|
|
96
|
+
});
|
|
58
97
|
}
|
|
59
98
|
finally {
|
|
60
99
|
dispatchStarted = budget.dispatchStarted;
|
|
@@ -68,17 +107,28 @@ function actionHandler(type, execute) {
|
|
|
68
107
|
dispatchStarted = true;
|
|
69
108
|
}
|
|
70
109
|
catch (error) {
|
|
71
|
-
if (controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
|
|
110
|
+
if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
|
|
72
111
|
return wrapToolError(type, error);
|
|
73
112
|
result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) };
|
|
74
113
|
}
|
|
114
|
+
const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status);
|
|
115
|
+
if (blocked) {
|
|
116
|
+
dispatchStarted = false;
|
|
117
|
+
result = buildActionExecutionResult({
|
|
118
|
+
actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
|
|
119
|
+
selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
|
|
120
|
+
success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
|
|
121
|
+
failure: { failureCode: conditionFailureCode(condition), retryable: false },
|
|
122
|
+
traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
|
|
123
|
+
});
|
|
124
|
+
}
|
|
75
125
|
if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? ''))
|
|
76
126
|
result.failure_code = 'ACTION_TIMEOUT';
|
|
77
127
|
if (result.failure_code === 'ACTION_TIMEOUT')
|
|
78
128
|
result.retryable = !dispatchStarted;
|
|
79
129
|
if (!result.success && typeof result.retryable !== 'boolean')
|
|
80
130
|
result.retryable = !dispatchStarted;
|
|
81
|
-
result.action_id ??= randomUUID();
|
|
131
|
+
result.action_id ??= currentEvidenceActionId() ?? randomUUID();
|
|
82
132
|
result.action_type ??= type;
|
|
83
133
|
recordObservationEvent({
|
|
84
134
|
step_id: args._stepId,
|
|
@@ -92,6 +142,7 @@ function actionHandler(type, execute) {
|
|
|
92
142
|
if (result.success && controls.verificationMode === 'light') {
|
|
93
143
|
const verificationStarted = performance.now();
|
|
94
144
|
const tree = await withObservationContext({ stepId: args._stepId, purpose: 'verification' }, () => measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () => ToolsObserve.getUITreeHandler({ platform: args.platform ?? result.device?.platform, deviceId: args.deviceId ?? result.device?.id })))).catch(() => null);
|
|
145
|
+
markEvidenceVerification();
|
|
95
146
|
observationAvailable = !!tree && !tree.error;
|
|
96
147
|
recordObservationEvent({
|
|
97
148
|
verification_budget_ms: controls.verificationTimeoutMs,
|
|
@@ -102,11 +153,21 @@ function actionHandler(type, execute) {
|
|
|
102
153
|
if (observationAvailable && tree.device)
|
|
103
154
|
delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined;
|
|
104
155
|
}
|
|
156
|
+
if (!blocked && controls.verificationMode === 'full' && dispatchStarted)
|
|
157
|
+
markEvidenceVerification();
|
|
105
158
|
const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started);
|
|
159
|
+
if (blocked)
|
|
160
|
+
compact.verification.status = 'not_requested';
|
|
106
161
|
if (result.success && controls.verificationMode === 'light' && !observationAvailable)
|
|
107
162
|
compact.verification.status = 'unavailable';
|
|
108
163
|
const evidence = {
|
|
109
164
|
...compact,
|
|
165
|
+
...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
|
|
166
|
+
? { ...condition.result, observed: condition.result.observed ? {
|
|
167
|
+
snapshot_session_id: condition.result.observed.snapshot_session_id,
|
|
168
|
+
snapshot_revision: condition.result.observed.snapshot_revision,
|
|
169
|
+
captured_at_ms: condition.result.observed.captured_at_ms
|
|
170
|
+
} : null } : condition.result } : {}),
|
|
110
171
|
...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
|
|
111
172
|
...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
|
|
112
173
|
};
|
|
@@ -115,7 +176,7 @@ function actionHandler(type, execute) {
|
|
|
115
176
|
effective_controls: { ...controls, actionTimeoutMs: args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined ? null : budgetMs, timeout_bound: !(args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined) }
|
|
116
177
|
}, controls.responseMode === 'debug');
|
|
117
178
|
});
|
|
118
|
-
};
|
|
179
|
+
}, true);
|
|
119
180
|
}
|
|
120
181
|
const handleStartApp = actionHandler('start_app', executeStartApp);
|
|
121
182
|
const handleRestartApp = actionHandler('restart_app', executeRestartApp);
|
|
@@ -131,7 +192,7 @@ async function executeStartApp(args) {
|
|
|
131
192
|
const deviceId = getStringArg(args, 'deviceId');
|
|
132
193
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
133
194
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
134
|
-
|
|
195
|
+
notifyActionStart();
|
|
135
196
|
const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')));
|
|
136
197
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
137
198
|
const result = buildActionExecutionResult({
|
|
@@ -168,7 +229,7 @@ async function executeRestartApp(args) {
|
|
|
168
229
|
const deviceId = getStringArg(args, 'deviceId');
|
|
169
230
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
170
231
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
171
|
-
|
|
232
|
+
notifyActionStart();
|
|
172
233
|
const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined;
|
|
173
234
|
const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)));
|
|
174
235
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
@@ -381,9 +442,12 @@ async function handleAdjustControl(args) {
|
|
|
381
442
|
platform,
|
|
382
443
|
deviceId
|
|
383
444
|
});
|
|
445
|
+
markEvidenceVerification();
|
|
384
446
|
return wrapResponse(res);
|
|
385
447
|
}
|
|
386
448
|
async function handleWaitForUI(args) {
|
|
449
|
+
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
|
|
450
|
+
throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
|
|
387
451
|
const selector = getObjectArg(args, 'selector');
|
|
388
452
|
const condition = getStringArg(args, 'condition') ?? 'exists';
|
|
389
453
|
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
@@ -392,10 +456,13 @@ async function handleWaitForUI(args) {
|
|
|
392
456
|
const retry = getObjectArg(args, 'retry');
|
|
393
457
|
const platform = getStringArg(args, 'platform');
|
|
394
458
|
const deviceId = getStringArg(args, 'deviceId');
|
|
395
|
-
const
|
|
459
|
+
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId });
|
|
460
|
+
const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms, device => execute(device.platform, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute();
|
|
396
461
|
return wrapResponse(res);
|
|
397
462
|
}
|
|
398
463
|
async function handleWaitForUIChange(args) {
|
|
464
|
+
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
|
|
465
|
+
throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
|
|
399
466
|
const platform = getStringArg(args, 'platform');
|
|
400
467
|
const deviceId = getStringArg(args, 'deviceId');
|
|
401
468
|
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
@@ -403,7 +470,8 @@ async function handleWaitForUIChange(args) {
|
|
|
403
470
|
const expected_change = getStringArg(args, 'expected_change');
|
|
404
471
|
const scope = getStringArg(args, 'scope');
|
|
405
472
|
const target = getStringArg(args, 'target');
|
|
406
|
-
const
|
|
473
|
+
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target });
|
|
474
|
+
const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms, device => execute(device.platform, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute();
|
|
407
475
|
return wrapResponse(res);
|
|
408
476
|
}
|
|
409
477
|
async function handleFindElement(args) {
|
|
@@ -424,7 +492,7 @@ async function executeTap(args) {
|
|
|
424
492
|
const deviceId = getStringArg(args, 'deviceId');
|
|
425
493
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
426
494
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
427
|
-
|
|
495
|
+
notifyActionStart();
|
|
428
496
|
const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }));
|
|
429
497
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
430
498
|
const result = buildActionExecutionResult({
|
|
@@ -450,14 +518,15 @@ async function executeTapElement(args) {
|
|
|
450
518
|
if (selector) {
|
|
451
519
|
const waitFor = getObjectArg(args, 'waitFor');
|
|
452
520
|
const targetStarted = performance.now();
|
|
453
|
-
const
|
|
521
|
+
const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000;
|
|
522
|
+
const resolution = await withObservationContext({ stepId: args._stepId, purpose: 'target' }, () => measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }))));
|
|
454
523
|
recordObservationEvent({ step_id: args._stepId, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status });
|
|
455
524
|
if (resolution.status !== 'success' || !resolution.element?.elementId)
|
|
456
525
|
return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true });
|
|
457
526
|
resolvedId = resolution.element.elementId;
|
|
458
527
|
freshTree = resolution._tree;
|
|
459
528
|
}
|
|
460
|
-
|
|
529
|
+
notifyActionStart();
|
|
461
530
|
if (freshTree)
|
|
462
531
|
recordObservationConsumer(freshTree, 'target', args._stepId);
|
|
463
532
|
const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }));
|
|
@@ -508,102 +577,125 @@ async function handleRunJourney(args) {
|
|
|
508
577
|
const steps = parsed.data.steps;
|
|
509
578
|
const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId });
|
|
510
579
|
const deviceId = device.id;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
try {
|
|
529
|
-
if (step.type === 'tap') {
|
|
530
|
-
result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
|
|
580
|
+
const finishTrace = parsed.data.systemTraceId
|
|
581
|
+
? await systemTraces.link(parsed.data.systemTraceId, device, currentEvidenceCorrelationId()) : undefined;
|
|
582
|
+
if (parsed.data.systemTraceId)
|
|
583
|
+
bindSystemTrace(parsed.data.systemTraceId);
|
|
584
|
+
try {
|
|
585
|
+
return await withResolvedDevice(device, async () => {
|
|
586
|
+
const started = performance.now();
|
|
587
|
+
const results = [];
|
|
588
|
+
let stoppedAt = null;
|
|
589
|
+
let pendingLightStep = null;
|
|
590
|
+
for (let index = 0; index < steps.length; index++) {
|
|
591
|
+
const step = steps[index];
|
|
592
|
+
if (platform === 'android' && debuggerSessions.paused(deviceId)) {
|
|
593
|
+
results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } });
|
|
594
|
+
stoppedAt = step.id;
|
|
595
|
+
break;
|
|
531
596
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
597
|
+
const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full';
|
|
598
|
+
const next = steps[index + 1];
|
|
599
|
+
const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
|
|
600
|
+
next.assertion?.kind === 'element_visible';
|
|
601
|
+
const stepStart = performance.now();
|
|
602
|
+
let result;
|
|
603
|
+
if (stoppedAt) {
|
|
604
|
+
results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } });
|
|
605
|
+
continue;
|
|
535
606
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
607
|
+
try {
|
|
608
|
+
if (step.type === 'tap') {
|
|
609
|
+
result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
|
|
610
|
+
}
|
|
611
|
+
else if (step.type === 'wait') {
|
|
612
|
+
result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId }));
|
|
613
|
+
result = { success: result.status === 'success', ...result };
|
|
614
|
+
}
|
|
615
|
+
else if (step.type === 'assert') {
|
|
616
|
+
result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, async () => {
|
|
617
|
+
const assertion = step.assertion;
|
|
618
|
+
if (assertion?.kind === 'element_visible')
|
|
619
|
+
result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
|
|
620
|
+
else if (assertion?.kind === 'element_absent')
|
|
621
|
+
result = await expectElementAbsent({ selector: assertion.selector, timeoutMs: step.timeoutMs ?? 5000, pollIntervalMs: step.pollIntervalMs ?? 100, platform, deviceId });
|
|
622
|
+
else if (assertion?.kind === 'screen_fingerprint')
|
|
623
|
+
result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: step.timeoutMs ?? 5000, platform, deviceId });
|
|
624
|
+
else if (assertion?.kind === 'state_equals')
|
|
625
|
+
result = await ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, exact_budget: true });
|
|
626
|
+
else
|
|
627
|
+
result = { success: false, failure_code: 'INVALID_ASSERTION' };
|
|
628
|
+
return result;
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
else if (step.type === 'start_app') {
|
|
632
|
+
result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
|
|
636
|
+
}
|
|
551
637
|
}
|
|
552
|
-
|
|
553
|
-
result =
|
|
638
|
+
catch (error) {
|
|
639
|
+
result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) };
|
|
554
640
|
}
|
|
555
|
-
|
|
556
|
-
result =
|
|
641
|
+
if (platform === 'android' && debuggerSessions.paused(deviceId)) {
|
|
642
|
+
result.success = false;
|
|
643
|
+
result.failure_code = 'DEBUGGER_PAUSED';
|
|
644
|
+
result.retryable = false;
|
|
557
645
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true };
|
|
569
|
-
pendingLightStep = results.length;
|
|
646
|
+
if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? ''))
|
|
647
|
+
result.failure_code = 'ASSERTION_TIMEOUT';
|
|
648
|
+
const success = !!result?.success;
|
|
649
|
+
if (lightCoveredByAssertion) {
|
|
650
|
+
result.verification = { mode: 'light', status: success ? 'unavailable' :
|
|
651
|
+
(['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') };
|
|
652
|
+
if (success) {
|
|
653
|
+
result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true };
|
|
654
|
+
pendingLightStep = results.length;
|
|
655
|
+
}
|
|
570
656
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
657
|
+
if (step.type === 'assert' && pendingLightStep !== null) {
|
|
658
|
+
const pending = results[pendingLightStep].result;
|
|
659
|
+
if (typeof result?.observed?.matched_count === 'number') {
|
|
660
|
+
pending.verification.status = 'observed';
|
|
661
|
+
if (pending.evidence)
|
|
662
|
+
completeJourneyVerification(pending.evidence.action_id);
|
|
663
|
+
delete pending.verification_diagnostic;
|
|
664
|
+
const delta = ToolsObserve.getLatestStateDelta(platform, deviceId);
|
|
665
|
+
if (delta) {
|
|
666
|
+
pending.state_delta = delta;
|
|
667
|
+
pending.state_delta_available = true;
|
|
668
|
+
}
|
|
581
669
|
}
|
|
670
|
+
pendingLightStep = null;
|
|
582
671
|
}
|
|
583
|
-
|
|
672
|
+
if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type))
|
|
673
|
+
result = {
|
|
674
|
+
success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
675
|
+
...(result.error?.code ? { failure_code: result.error.code } : {}),
|
|
676
|
+
...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
|
|
677
|
+
...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
|
|
678
|
+
...(result.actual_state ? { observed: result.actual_state } : {})
|
|
679
|
+
};
|
|
680
|
+
results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } });
|
|
681
|
+
if (!success)
|
|
682
|
+
stoppedAt = step.id;
|
|
584
683
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
results
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
for (const step of results)
|
|
601
|
-
for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
|
|
602
|
-
if (key !== 'total_ms' && typeof value === 'number')
|
|
603
|
-
componentTiming[key] = (componentTiming[key] ?? 0) + value;
|
|
604
|
-
}
|
|
605
|
-
return wrapResponse({ success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug');
|
|
606
|
-
});
|
|
684
|
+
const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
|
|
685
|
+
? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined;
|
|
686
|
+
const componentTiming = {};
|
|
687
|
+
for (const step of results)
|
|
688
|
+
for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
|
|
689
|
+
if (key !== 'total_ms' && typeof value === 'number')
|
|
690
|
+
componentTiming[key] = (componentTiming[key] ?? 0) + value;
|
|
691
|
+
}
|
|
692
|
+
refreshJourneyEvidence(results);
|
|
693
|
+
return wrapResponse({ ...(finishTrace ? { system_trace: await finishTrace() } : {}), success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug');
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
await finishTrace?.();
|
|
698
|
+
}
|
|
607
699
|
}
|
|
608
700
|
async function executeSwipe(args) {
|
|
609
701
|
const platform = getStringArg(args, 'platform') ?? 'android';
|
|
@@ -615,7 +707,7 @@ async function executeSwipe(args) {
|
|
|
615
707
|
const deviceId = getStringArg(args, 'deviceId');
|
|
616
708
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
617
709
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
618
|
-
|
|
710
|
+
notifyActionStart();
|
|
619
711
|
const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }));
|
|
620
712
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
621
713
|
const result = buildActionExecutionResult({
|
|
@@ -638,7 +730,7 @@ async function executeScrollToElement(args) {
|
|
|
638
730
|
const deviceId = getStringArg(args, 'deviceId');
|
|
639
731
|
const { verificationMode } = interactionControls(args);
|
|
640
732
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
641
|
-
|
|
733
|
+
notifyActionStart();
|
|
642
734
|
const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }));
|
|
643
735
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
644
736
|
const result = buildActionExecutionResult({
|
|
@@ -666,7 +758,7 @@ async function executeTypeText(args) {
|
|
|
666
758
|
const deviceId = getStringArg(args, 'deviceId');
|
|
667
759
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
668
760
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
669
|
-
|
|
761
|
+
notifyActionStart();
|
|
670
762
|
const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }));
|
|
671
763
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
672
764
|
const result = buildActionExecutionResult({
|
|
@@ -684,7 +776,7 @@ async function executePressBack(args) {
|
|
|
684
776
|
const deviceId = getStringArg(args, 'deviceId');
|
|
685
777
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
686
778
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
687
|
-
|
|
779
|
+
notifyActionStart();
|
|
688
780
|
const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }));
|
|
689
781
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
690
782
|
const result = buildActionExecutionResult({
|
|
@@ -742,7 +834,50 @@ async function handleGetNetworkActivity(args) {
|
|
|
742
834
|
const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId });
|
|
743
835
|
return wrapResponse(result);
|
|
744
836
|
}
|
|
837
|
+
async function handleSystemTrace(name, args) {
|
|
838
|
+
const schema = name === 'start_system_trace' ? traceStartSchema : name === 'get_system_trace_capabilities' ? traceTargetSchema : traceControlSchema;
|
|
839
|
+
const parsed = schema.safeParse(args);
|
|
840
|
+
if (!parsed.success)
|
|
841
|
+
throw new TraceError('INVALID_ARGUMENT');
|
|
842
|
+
if (name === 'get_system_trace_capabilities' || name === 'start_system_trace') {
|
|
843
|
+
const target = parsed.data;
|
|
844
|
+
if (target.platform !== 'android') {
|
|
845
|
+
if (name === 'start_system_trace')
|
|
846
|
+
throw new TraceError('UNSUPPORTED_PLATFORM');
|
|
847
|
+
return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: {}, reasons: ['UNSUPPORTED_PLATFORM'], enabled_sources: [], unavailable_categories: [], limitations: ['Android only'] });
|
|
848
|
+
}
|
|
849
|
+
let device;
|
|
850
|
+
try {
|
|
851
|
+
device = await resolveTargetDevice(target);
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
if (name === 'start_system_trace')
|
|
855
|
+
throw new TraceError('TRACE_UNSUPPORTED');
|
|
856
|
+
return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: { device: false }, reasons: ['DEVICE_UNAVAILABLE'], enabled_sources: [], unavailable_categories: [], limitations: ['An authorised Android device is required'] });
|
|
857
|
+
}
|
|
858
|
+
if (name === 'get_system_trace_capabilities')
|
|
859
|
+
return wrapResponse(await systemTraces.capabilities(device.id));
|
|
860
|
+
const result = await systemTraces.start(device.id, target.durationMs);
|
|
861
|
+
return { ...wrapResponse(result), ...(result.state === 'failed' ? { isError: true } : {}) };
|
|
862
|
+
}
|
|
863
|
+
const operation = name === 'stop_system_trace' ? 'stop' : name === 'cancel_system_trace' ? 'cancel' : 'status';
|
|
864
|
+
const result = await systemTraces.control(parsed.data.traceId, operation);
|
|
865
|
+
return { ...wrapResponse(result), ...(['failed', 'cancelled'].includes(result.state) ? { isError: true } : {}) };
|
|
866
|
+
}
|
|
745
867
|
export const toolHandlers = {
|
|
868
|
+
...Object.fromEntries(systemTraceTools.map(({ name }) => [name, (args) => handleSystemTrace(name, args)])),
|
|
869
|
+
get_app_readiness: async (args) => {
|
|
870
|
+
const parsed = readinessSchema.safeParse(args);
|
|
871
|
+
if (!parsed.success)
|
|
872
|
+
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
873
|
+
return wrapResponse(await getAppReadiness(parsed.data));
|
|
874
|
+
},
|
|
875
|
+
wait_for_app_ready: async (args) => {
|
|
876
|
+
const parsed = readinessWaitSchema.safeParse(args);
|
|
877
|
+
if (!parsed.success)
|
|
878
|
+
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
879
|
+
return wrapResponse(await getAppReadiness(parsed.data, true));
|
|
880
|
+
},
|
|
746
881
|
start_app: handleStartApp,
|
|
747
882
|
terminate_app: handleTerminateApp,
|
|
748
883
|
restart_app: handleRestartApp,
|
|
@@ -779,7 +914,12 @@ export const toolHandlers = {
|
|
|
779
914
|
classify_action_outcome: handleClassifyActionOutcome,
|
|
780
915
|
get_network_activity: handleGetNetworkActivity
|
|
781
916
|
};
|
|
782
|
-
|
|
917
|
+
for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
|
|
918
|
+
toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey');
|
|
919
|
+
}
|
|
920
|
+
export async function handleToolCall(name, args = {}, signal) {
|
|
921
|
+
if (Object.hasOwn(debuggerContracts, name))
|
|
922
|
+
return handleDebugger(name, args, signal);
|
|
783
923
|
const handler = toolHandlers[name];
|
|
784
924
|
if (!handler)
|
|
785
925
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -801,9 +941,58 @@ export async function handleToolCall(name, args = {}) {
|
|
|
801
941
|
};
|
|
802
942
|
return withTiming(measured, async () => {
|
|
803
943
|
try {
|
|
804
|
-
|
|
944
|
+
if (!systemTraceToolNames.has(name))
|
|
945
|
+
validateExpectedState(name, args);
|
|
946
|
+
const guarded = ['start_app', 'terminate_app', 'restart_app', 'reset_app_data', 'install_app', 'build_and_install', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'run_journey', 'wait_for_ui', 'wait_for_ui_change', 'wait_for_screen_change', 'expect_screen', 'expect_element_visible', 'expect_state'].includes(name);
|
|
947
|
+
if (guarded && args.platform !== 'ios' && debuggerSessions.hasActiveSessions()) {
|
|
948
|
+
const device = await resolveTargetDevice({ platform: 'android', deviceId: args.deviceId });
|
|
949
|
+
return withResolvedDevice(device, () => withDebuggerGuard('android', device.id, async () => {
|
|
950
|
+
const before = debuggerSessions.paused(device.id);
|
|
951
|
+
if (before)
|
|
952
|
+
return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before });
|
|
953
|
+
let response;
|
|
954
|
+
try {
|
|
955
|
+
response = await handler(args);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
if (!debuggerSessions.paused(device.id))
|
|
959
|
+
throw error;
|
|
960
|
+
response = wrapResponse({ success: false });
|
|
961
|
+
}
|
|
962
|
+
const pause = debuggerSessions.paused(device.id);
|
|
963
|
+
if (pause) {
|
|
964
|
+
const item = response.content.find(item => item.type === 'text');
|
|
965
|
+
if (item?.text) {
|
|
966
|
+
const payload = JSON.parse(item.text);
|
|
967
|
+
Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
|
|
968
|
+
dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
|
|
969
|
+
verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause });
|
|
970
|
+
item.text = JSON.stringify(payload);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return addTiming(response);
|
|
974
|
+
}));
|
|
975
|
+
}
|
|
976
|
+
const response = await handler(args);
|
|
977
|
+
if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name) && args.platform !== 'ios') {
|
|
978
|
+
const item = response.content.find(item => item.type === 'text');
|
|
979
|
+
if (item?.text) {
|
|
980
|
+
try {
|
|
981
|
+
const payload = JSON.parse(item.text);
|
|
982
|
+
const device = payload.device ?? payload.evidence?.device;
|
|
983
|
+
const id = device?.id ?? args.deviceId;
|
|
984
|
+
const pause = id && device?.platform !== 'ios' ? debuggerSessions.paused(String(id)) : null;
|
|
985
|
+
if (pause)
|
|
986
|
+
item.text = JSON.stringify({ ...payload, debugger: pause });
|
|
987
|
+
}
|
|
988
|
+
catch { /* Non-JSON observation */ }
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return addTiming(response);
|
|
805
992
|
}
|
|
806
993
|
catch (error) {
|
|
994
|
+
if (error instanceof TraceError)
|
|
995
|
+
return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true };
|
|
807
996
|
console.error(`Error executing tool ${name}:`, error);
|
|
808
997
|
return addTiming(wrapToolError(name, error));
|
|
809
998
|
}
|