mobile-debug-mcp 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +1 -1
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +10 -2
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +10 -2
- package/dist/contracts/device-readiness.js +1 -0
- package/dist/interact/waits/ui-change-wait.js +9 -2
- package/dist/interact/waits/ui-wait.js +7 -3
- package/dist/manage/index.js +3 -0
- package/dist/observe/device-readiness.js +116 -0
- package/dist/observe/evidence.js +31 -0
- package/dist/observe/ios-trace/sessions.js +5 -3
- package/dist/observe/readiness-service.js +21 -9
- package/dist/observe/readiness.js +30 -0
- package/dist/observe/screenshot-encoding.js +4 -0
- package/dist/observe/snapshot-collection.js +7 -2
- package/dist/server/definitions/manage.js +3 -2
- package/dist/server/handlers/action-execution.js +23 -14
- package/dist/server/handlers/compact-interaction.js +106 -0
- package/dist/server/handlers/compact-state.js +47 -0
- package/dist/server/handlers/input.js +2 -0
- package/dist/server/handlers/interact.js +2 -0
- package/dist/server/handlers/interaction-diagnostics.js +62 -0
- package/dist/server/handlers/journey.js +18 -20
- package/dist/server/handlers/observe.js +2 -1
- package/dist/server/handlers/tool-handlers.js +9 -5
- package/dist/server/schemas/interaction-diagnostics.js +9 -0
- package/dist/server/tool-definitions.js +9 -4
- package/dist/server-core.js +5 -2
- package/dist/server.js +1 -1
- package/dist/system/android.js +19 -15
- package/dist/system/index.js +2 -1
- package/dist/utils/android/utils.js +7 -16
- package/dist/utils/app-installation.js +77 -0
- package/dist/utils/benchmark-metrics.js +43 -0
- package/dist/utils/exec.js +4 -0
- package/dist/utils/ios/utils.js +4 -11
- package/dist/utils/resolve-device.js +3 -2
- package/dist/utils/timing.js +3 -3
- package/docs/CHANGELOG.md +25 -0
- package/docs/benchmarks/README.md +1 -0
- package/docs/benchmarks/end-to-end-latency/README.md +98 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
- package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
- package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
- package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
- package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
- package/docs/benchmarks/end-to-end-latency/smoke.md +21 -0
- package/docs/specs/031-ios-system-traces/plan.md +28 -0
- package/docs/specs/033-device-readiness-blockers/android-evidence.json +437 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
- package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
- package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
- package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
- package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
- package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
- package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
- package/docs/specs/034-app-installation-status/intent.md +41 -0
- package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
- package/docs/specs/034-app-installation-status/plan.md +73 -0
- package/docs/specs/034-app-installation-status/spec.md +114 -0
- package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
- package/docs/specs/035-compact-interaction-results/intent.md +38 -0
- package/docs/specs/035-compact-interaction-results/plan.md +43 -0
- package/docs/specs/035-compact-interaction-results/spec.md +118 -0
- package/docs/specs/035-compact-interaction-results/verification.md +111 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
- package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
- package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
- package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
- package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
- package/docs/tools/TOOLS.md +1 -0
- package/docs/tools/compact-interactions.md +56 -0
- package/docs/tools/correlated-evidence.md +6 -0
- package/docs/tools/interact.md +17 -8
- package/docs/tools/manage.md +10 -2
- package/docs/tools/observe.md +42 -2
- package/docs/tools/system.md +6 -0
- package/package.json +1 -1
- package/src/contracts/device-readiness.ts +8 -0
- package/src/interact/waits/ui-change-wait.ts +7 -2
- package/src/interact/waits/ui-wait.ts +6 -3
- package/src/manage/index.ts +2 -0
- package/src/observe/device-readiness.ts +96 -0
- package/src/observe/evidence.ts +30 -1
- package/src/observe/ios-trace/sessions.ts +4 -2
- package/src/observe/readiness-service.ts +21 -9
- package/src/observe/readiness.ts +33 -0
- package/src/observe/screenshot-encoding.ts +4 -0
- package/src/observe/snapshot-collection.ts +7 -2
- package/src/server/common.ts +2 -2
- package/src/server/definitions/manage.ts +3 -2
- package/src/server/handlers/action-execution.ts +22 -14
- package/src/server/handlers/compact-interaction.ts +91 -0
- package/src/server/handlers/compact-state.ts +43 -0
- package/src/server/handlers/input.ts +2 -0
- package/src/server/handlers/interact.ts +2 -0
- package/src/server/handlers/interaction-diagnostics.ts +50 -0
- package/src/server/handlers/journey.ts +15 -17
- package/src/server/handlers/observe.ts +2 -1
- package/src/server/handlers/payload.ts +2 -1
- package/src/server/handlers/tool-handlers.ts +9 -5
- package/src/server/schemas/interaction-diagnostics.ts +10 -0
- package/src/server/tool-definitions.ts +8 -4
- package/src/server-core.ts +5 -2
- package/src/server.ts +1 -1
- package/src/system/android.ts +18 -11
- package/src/system/index.ts +2 -1
- package/src/types.ts +2 -0
- package/src/utils/android/utils.ts +8 -10
- package/src/utils/app-installation.ts +73 -0
- package/src/utils/benchmark-metrics.ts +42 -0
- package/src/utils/exec.ts +5 -0
- package/src/utils/ios/utils.ts +4 -10
- package/src/utils/resolve-device.ts +4 -2
- package/src/utils/timing.ts +3 -2
- package/test/device/automated/debugger/runtime.android.test.ts +5 -0
- package/test/device/automated/debugger/runtime.ios.test.ts +7 -1
- package/test/device/automated/interact/compact-results.smoke.ts +58 -0
- package/test/device/automated/latency/end-to-end-environment.ts +61 -0
- package/test/device/automated/latency/end-to-end-report.ts +90 -0
- package/test/device/automated/latency/end-to-end-session.ts +98 -0
- package/test/device/automated/latency/end-to-end.ts +149 -0
- package/test/device/automated/latency/owned-transport.ts +81 -0
- package/test/device/automated/latency/snapshot-pair.ts +143 -0
- package/test/device/automated/latency-ios/benchmark.ts +126 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
- package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
- package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +2 -2
- package/test/device/automated/observe/system-trace.smoke.ts +2 -2
- package/test/fixtures/contracts/tool-definitions.json +51 -15
- package/test/fixtures/latency-journey-app/build.ts +3 -3
- package/test/unit/debugger/handle-guards.test.ts +2 -1
- package/test/unit/interact/ui-change-readiness-interruption.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
- package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
- package/test/unit/observe/device-readiness.test.ts +97 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +23 -0
- package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
- package/test/unit/server/compact-interaction.test.ts +87 -0
- package/test/unit/server/compact-journey-evidence.test.ts +40 -0
- package/test/unit/server/compact-review-regressions.test.ts +78 -0
- package/test/unit/server/conditional_actions.test.ts +7 -3
- package/test/unit/server/correlated-evidence.test.ts +18 -6
- package/test/unit/server/end-to-end-report.test.ts +46 -0
- package/test/unit/server/input-device-readiness.test.ts +55 -0
- package/test/unit/server/interaction-diagnostics.test.ts +45 -0
- package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
- package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
- package/test/unit/server/readiness.test.ts +24 -1
- package/test/unit/server/run_journey.test.ts +2 -1
- package/test/unit/server/selector_tap_target.test.ts +2 -1
- package/test/unit/server/system-trace.test.ts +6 -4
- package/test/unit/utils/app-installation.test.ts +78 -0
- package/test/unit/utils/benchmark-metrics.test.ts +36 -0
- package/test/unit/utils/resolve_device.test.ts +11 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { observeDeviceReadiness } from '../../observe/device-readiness.js';
|
|
2
|
+
import { benchmarkStage } from '../../utils/benchmark-metrics.js';
|
|
1
3
|
import { debuggerSessions } from '../../debugger/sessions.js';
|
|
2
4
|
import { systemTraces } from '../../observe/system-trace.js';
|
|
3
|
-
import { currentEvidenceCorrelationId, bindSystemTrace, refreshJourneyEvidence, completeJourneyVerification } from '../../observe/evidence.js';
|
|
5
|
+
import { currentEvidenceCorrelationId, bindSystemTrace, withJourneyStepEvidence, refreshJourneyEvidence, completeJourneyVerification, JourneyStepError } from '../../observe/evidence.js';
|
|
4
6
|
import { runWithBudget } from '../../utils/operation-budget.js';
|
|
5
7
|
import { journeySchema } from '../schemas/journey-schema.js';
|
|
6
8
|
import { resolveTargetDevice, withResolvedDevice } from '../../utils/resolve-device.js';
|
|
@@ -25,23 +27,27 @@ async function assertStep(step, platform, deviceId) {
|
|
|
25
27
|
return ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: timeoutMs, poll_interval_ms: pollIntervalMs, exact_budget: true });
|
|
26
28
|
return { success: false, failure_code: 'INVALID_ASSERTION' };
|
|
27
29
|
}
|
|
30
|
+
async function observeStep(s, operation) {
|
|
31
|
+
return s.input.responseMode === 'compact' ? withJourneyStepEvidence(operation) : operation();
|
|
32
|
+
}
|
|
28
33
|
async function executeStep(s, step, covered) {
|
|
29
34
|
const { platform, responseMode, defaults } = s.input, deviceId = s.deviceId;
|
|
30
35
|
try {
|
|
31
36
|
if (step.type === 'tap')
|
|
32
37
|
return payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: covered ? 'none' : step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
33
|
-
if (step.type === 'wait')
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
if (step.type === 'wait')
|
|
39
|
+
return await observeStep(s, async () => {
|
|
40
|
+
const 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 })));
|
|
41
|
+
return { success: result.status === 'success', ...result };
|
|
42
|
+
});
|
|
37
43
|
if (step.type === 'assert')
|
|
38
|
-
return await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, () => assertStep(step, platform, deviceId)));
|
|
44
|
+
return await observeStep(s, () => withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, () => assertStep(step, platform, deviceId))));
|
|
39
45
|
if (step.type === 'start_app')
|
|
40
46
|
return payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
41
47
|
return { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
|
|
42
48
|
}
|
|
43
49
|
catch (error) {
|
|
44
|
-
return { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) };
|
|
50
|
+
return { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error), ...(error instanceof JourneyStepError ? { evidence: error.evidence } : {}) };
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
function lightVerification(s, step, result, covered, success) {
|
|
@@ -69,24 +75,16 @@ function lightVerification(s, step, result, covered, success) {
|
|
|
69
75
|
}
|
|
70
76
|
s.pendingLightStep = null;
|
|
71
77
|
}
|
|
72
|
-
function compactAssertion(result, success) {
|
|
73
|
-
const errorCode = typeof result.error === 'object' ? result.error?.code : undefined;
|
|
74
|
-
return { success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
75
|
-
...(errorCode ? { failure_code: errorCode } : {}),
|
|
76
|
-
...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
|
|
77
|
-
...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
|
|
78
|
-
...(result.actual_state ? { observed: result.actual_state } : {}) };
|
|
79
|
-
}
|
|
80
78
|
function coveredByAssertion(s, step, next) {
|
|
81
79
|
const verification = ('verificationMode' in step ? step.verificationMode : undefined) ?? s.input.defaults?.verificationMode ?? 'full';
|
|
82
80
|
return step.type === 'tap' && s.input.responseMode === 'compact' && verification === 'light' && next?.type === 'assert' && next.assertion.kind === 'element_visible';
|
|
83
81
|
}
|
|
84
82
|
async function runSteps(s) {
|
|
85
|
-
const { platform,
|
|
83
|
+
const { platform, steps } = s.input;
|
|
86
84
|
for (let index = 0; index < steps.length; index++) {
|
|
87
85
|
const step = steps[index];
|
|
88
86
|
if (platform === 'android' && debuggerSessions.paused(s.deviceId)) {
|
|
89
|
-
s.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' } });
|
|
87
|
+
s.results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { device_readiness: await observeDeviceReadiness({ platform, id: s.deviceId }), success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } });
|
|
90
88
|
s.stoppedAt = step.id;
|
|
91
89
|
break;
|
|
92
90
|
}
|
|
@@ -95,7 +93,9 @@ async function runSteps(s) {
|
|
|
95
93
|
s.results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } });
|
|
96
94
|
continue;
|
|
97
95
|
}
|
|
98
|
-
|
|
96
|
+
const result = await (step.type === 'assert' || step.type === 'wait'
|
|
97
|
+
? benchmarkStage(step.type === 'assert' ? 'assertion_ms' : 'wait_ms', () => executeStep(s, step, covered))
|
|
98
|
+
: executeStep(s, step, covered));
|
|
99
99
|
if (platform === 'android' && debuggerSessions.paused(s.deviceId)) {
|
|
100
100
|
result.success = false;
|
|
101
101
|
result.failure_code = 'DEBUGGER_PAUSED';
|
|
@@ -105,8 +105,6 @@ async function runSteps(s) {
|
|
|
105
105
|
result.failure_code = 'ASSERTION_TIMEOUT';
|
|
106
106
|
const success = !!result?.success;
|
|
107
107
|
lightVerification(s, step, result, covered, success);
|
|
108
|
-
if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type))
|
|
109
|
-
result = compactAssertion(result, success);
|
|
110
108
|
s.results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - started } });
|
|
111
109
|
if (!success)
|
|
112
110
|
s.stoppedAt = step.id;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ToolsObserve } from '../../observe/index.js';
|
|
2
|
+
import { benchmarkStage } from '../../utils/benchmark-metrics.js';
|
|
2
3
|
import { withObservationContext } from '../../observe/performance-metrics.js';
|
|
3
4
|
import { getBooleanArg, getNumberArg, getStringArg, requireStringArg, wrapResponse } from '../common.js';
|
|
4
5
|
export async function handleGetLogs(args) {
|
|
@@ -25,7 +26,7 @@ export async function handleCaptureScreenshot(args) {
|
|
|
25
26
|
const platform = requireStringArg(args, 'platform');
|
|
26
27
|
const deviceId = getStringArg(args, 'deviceId');
|
|
27
28
|
const started = performance.now();
|
|
28
|
-
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
29
|
+
const res = await benchmarkStage('screenshot_capture_ms', () => ToolsObserve.captureScreenshotHandler({ platform, deviceId }));
|
|
29
30
|
const screenshotMs = performance.now() - started;
|
|
30
31
|
const mime = res.screenshot_mime || 'image/png';
|
|
31
32
|
const content = [
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { finalizeCompactInteraction } from './compact-interaction.js';
|
|
2
|
+
import { handleInteractionDiagnostics } from './interaction-diagnostics.js';
|
|
3
|
+
import { observeDeviceReadiness } from '../../observe/device-readiness.js';
|
|
1
4
|
import { ToolsInteract } from '../../interact/index.js';
|
|
2
5
|
import { handleDebugger } from './debugger-handlers.js';
|
|
3
6
|
import { debuggerContracts } from '../schemas/debugger-schema.js';
|
|
@@ -33,6 +36,7 @@ export const toolHandlers = {
|
|
|
33
36
|
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
34
37
|
return wrapResponse(await getAppReadiness(parsed.data, true));
|
|
35
38
|
},
|
|
39
|
+
get_interaction_diagnostics: handleInteractionDiagnostics,
|
|
36
40
|
start_app: handleStartApp,
|
|
37
41
|
terminate_app: handleTerminateApp,
|
|
38
42
|
restart_app: handleRestartApp,
|
|
@@ -84,7 +88,7 @@ async function dispatchGuarded(device, args, handler, addTiming) {
|
|
|
84
88
|
return withResolvedDevice(device, () => withDebuggerGuard(device.platform, device.id, async () => {
|
|
85
89
|
const before = sessions.paused(device.id);
|
|
86
90
|
if (before)
|
|
87
|
-
return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before });
|
|
91
|
+
return addTiming(wrapResponse({ device_readiness: await observeDeviceReadiness(device), success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before }));
|
|
88
92
|
let response;
|
|
89
93
|
try {
|
|
90
94
|
response = await handler(args);
|
|
@@ -101,7 +105,7 @@ async function dispatchGuarded(device, args, handler, addTiming) {
|
|
|
101
105
|
const payload = JSON.parse(item.text);
|
|
102
106
|
Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
|
|
103
107
|
dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
|
|
104
|
-
verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause });
|
|
108
|
+
verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, device_readiness: await observeDeviceReadiness(device), debugger: pause });
|
|
105
109
|
item.text = JSON.stringify(payload);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
@@ -135,7 +139,7 @@ export async function handleToolCall(name, args = {}, signal) {
|
|
|
135
139
|
const measured = {};
|
|
136
140
|
const addTiming = (response) => {
|
|
137
141
|
if (!['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'wait_for_ui', 'get_ui_tree', 'capture_screenshot', 'capture_debug_snapshot'].includes(name))
|
|
138
|
-
return response;
|
|
142
|
+
return finalizeCompactInteraction(name, args, response);
|
|
139
143
|
const item = response.content.find(item => item.type === 'text');
|
|
140
144
|
if (item?.text) {
|
|
141
145
|
try {
|
|
@@ -145,7 +149,7 @@ export async function handleToolCall(name, args = {}, signal) {
|
|
|
145
149
|
}
|
|
146
150
|
catch { /* Non-JSON text responses retain their original content. */ }
|
|
147
151
|
}
|
|
148
|
-
return response;
|
|
152
|
+
return finalizeCompactInteraction(name, args, response);
|
|
149
153
|
};
|
|
150
154
|
return withTiming(measured, async () => {
|
|
151
155
|
try {
|
|
@@ -162,7 +166,7 @@ export async function handleToolCall(name, args = {}, signal) {
|
|
|
162
166
|
}
|
|
163
167
|
catch (error) {
|
|
164
168
|
if (error instanceof TraceError)
|
|
165
|
-
return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true };
|
|
169
|
+
return addTiming({ ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true });
|
|
166
170
|
console.error(`Error executing tool ${name}:`, error);
|
|
167
171
|
return addTiming(wrapToolError(name, error));
|
|
168
172
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const diagnosticsSchema = z.object({ sessionId: z.string().uuid(), resultId: z.string().uuid() }).strict();
|
|
3
|
+
export const interactionDiagnosticsTool = {
|
|
4
|
+
name: 'get_interaction_diagnostics',
|
|
5
|
+
description: 'Read the retained original interaction or journey result without replaying input or observing the device. Pass diagnostics.session_id/result_id as sessionId/resultId. Session-local memory: ten minutes, 100 results, 16 MiB total, 1 MiB per result. Expired, evicted or restarted results are unavailable. May contain private UI text and errors; compact output is not redaction.',
|
|
6
|
+
inputSchema: { type: 'object', additionalProperties: false, required: ['sessionId', 'resultId'], properties: {
|
|
7
|
+
sessionId: { type: 'string', format: 'uuid' }, resultId: { type: 'string', format: 'uuid' }
|
|
8
|
+
} }
|
|
9
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { interactionDiagnosticsTool } from './schemas/interaction-diagnostics.js';
|
|
1
2
|
import { verificationModeJson, responseModeJson } from '../contracts/execution-controls.js';
|
|
2
3
|
import { manageTools } from './definitions/manage.js';
|
|
3
4
|
import { observeTools } from './definitions/observe.js';
|
|
@@ -51,9 +52,10 @@ const baseOrder = [
|
|
|
51
52
|
export const toolDefinitions = [
|
|
52
53
|
...debuggerTools,
|
|
53
54
|
...systemTraceTools,
|
|
55
|
+
interactionDiagnosticsTool,
|
|
54
56
|
...['get_app_readiness', 'wait_for_app_ready'].map(name => ({
|
|
55
57
|
name,
|
|
56
|
-
description: `${name === 'get_app_readiness' ? 'Observe readiness with at most two fresh UI-tree samples.' : 'Wait within a single bounded budget for settled UI evidence.'} Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.`,
|
|
58
|
+
description: `${name === 'get_app_readiness' ? 'Observe readiness with at most two fresh UI-tree samples.' : 'Wait within a single bounded budget for settled UI evidence.'} Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android and supported iOS simulator debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.`,
|
|
57
59
|
inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties: {
|
|
58
60
|
...readinessPropertiesJson,
|
|
59
61
|
timeoutMs: { ...readinessPropertiesJson.timeoutMs, default: name === 'get_app_readiness' ? 15000 : 30000 },
|
|
@@ -67,12 +69,12 @@ for (const tool of toolDefinitions) {
|
|
|
67
69
|
tool.inputSchema.properties ??= {};
|
|
68
70
|
if (['wait_for_ui', 'wait_for_ui_change'].includes(tool.name)) {
|
|
69
71
|
tool.inputSchema.properties.requireReady = { type: 'boolean', default: false };
|
|
70
|
-
tool.description += '\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.';
|
|
72
|
+
tool.description += '\nDevice-readiness blockers prevent readiness success; unknown/unsupported device signals remain explicit.\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.';
|
|
71
73
|
}
|
|
72
74
|
if (tool.name === 'get_ui_tree')
|
|
73
75
|
tool.description += '\nSuccessful observations include snapshot_session_id and snapshot_revision for same-device conditional input. Compact errors preserve the error and cannot authorize an expectation.';
|
|
74
76
|
if (tool.name === 'run_journey')
|
|
75
|
-
tool.description += '\nTap steps may include expectedState from a successful same-device get_ui_tree. A failed condition blocks that input, retains fresh condition evidence
|
|
77
|
+
tool.description += '\nTap steps may include expectedState from a successful same-device get_ui_tree. A failed condition blocks that input, retains fresh condition evidence through a diagnostics reference in compact mode, and stops subsequent steps. Conditions are not journey defaults or supported on other step types.';
|
|
76
78
|
if (conditionalTools.has(tool.name)) {
|
|
77
79
|
tool.inputSchema.properties.expectedState = expectedStateJson;
|
|
78
80
|
tool.inputSchema.properties.platform ??= { type: 'string', enum: ['android'] };
|
|
@@ -83,12 +85,13 @@ for (const tool of toolDefinitions) {
|
|
|
83
85
|
deviceId: { type: 'string', minLength: 1, pattern: '\\S', not: { enum: ['booted', 'default'] } }
|
|
84
86
|
} }
|
|
85
87
|
}];
|
|
86
|
-
tool.description += '\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures
|
|
88
|
+
tool.description += '\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.';
|
|
87
89
|
}
|
|
88
90
|
else {
|
|
89
91
|
tool.inputSchema.allOf = [...(Array.isArray(tool.inputSchema.allOf) ? tool.inputSchema.allOf : []), { not: { required: ['expectedState'] } }];
|
|
90
92
|
}
|
|
91
93
|
if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back'].includes(tool.name)) {
|
|
94
|
+
tool.description += '\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.';
|
|
92
95
|
Object.assign(tool.inputSchema.properties, {
|
|
93
96
|
verificationMode: { ...verificationModeJson, default: 'full' },
|
|
94
97
|
responseMode: { ...responseModeJson, default: 'debug' },
|
|
@@ -112,6 +115,8 @@ for (const tool of toolDefinitions) {
|
|
|
112
115
|
// Correlation is explicit; an omitted reference creates an independent observation.
|
|
113
116
|
for (const tool of toolDefinitions) {
|
|
114
117
|
tool.inputSchema.properties ??= {};
|
|
118
|
+
if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'get_app_readiness', 'wait_for_app_ready'].includes(tool.name))
|
|
119
|
+
tool.description += '\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.';
|
|
115
120
|
if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream'].includes(tool.name)) {
|
|
116
121
|
Object.assign(tool.inputSchema.properties, { actionRef: {
|
|
117
122
|
type: 'object', additionalProperties: false, required: ['sessionId', 'actionId'], properties: {
|
package/dist/server-core.js
CHANGED
|
@@ -3,10 +3,11 @@ import { ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadRes
|
|
|
3
3
|
import { wrapResponse } from './server/common.js';
|
|
4
4
|
import { toolDefinitions } from './server/tool-definitions.js';
|
|
5
5
|
import { handleToolCall } from './server/handlers/tool-handlers.js';
|
|
6
|
+
import { withBenchmarkRequest, benchmarkStage } from './utils/benchmark-metrics.js';
|
|
6
7
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
8
|
export const serverInfo = {
|
|
8
9
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.
|
|
10
|
+
version: '0.37.0'
|
|
10
11
|
};
|
|
11
12
|
export function createServer() {
|
|
12
13
|
const server = new Server(serverInfo, {
|
|
@@ -29,7 +30,9 @@ export function createServer() {
|
|
|
29
30
|
}));
|
|
30
31
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
31
32
|
const { name, arguments: args } = request.params;
|
|
32
|
-
|
|
33
|
+
const operation = () => handleToolCall(name, args, extra.signal);
|
|
34
|
+
return withBenchmarkRequest(name, () => name.startsWith('expect_') || name.startsWith('wait_for_')
|
|
35
|
+
? benchmarkStage(name.startsWith('expect_') ? 'assertion_ms' : 'wait_ms', operation) : operation());
|
|
33
36
|
});
|
|
34
37
|
return server;
|
|
35
38
|
}
|