mobile-debug-mcp 0.31.0 → 0.32.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/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +19 -10
- package/dist/observe/android.js +6 -1
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +7 -2
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +358 -55
- package/dist/server-core.js +1 -1
- package/dist/utils/exec.js +4 -2
- package/dist/utils/ios/utils.js +5 -2
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +3 -0
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
- package/docs/tools/interact.md +30 -0
- package/package.json +1 -1
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +18 -10
- package/src/observe/android.ts +5 -1
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +6 -2
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +324 -53
- package/src/server-core.ts +1 -1
- package/src/types.ts +31 -2
- package/src/utils/exec.ts +4 -2
- package/src/utils/ios/utils.ts +5 -2
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
- package/test/fixtures/latency-journey-app/README.md +10 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +32 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +90 -0
- package/test/unit/server/tap_selector.test.ts +68 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
let nextRevision = 1;
|
|
2
3
|
const snapshotStateByDevice = new Map();
|
|
3
4
|
function normalize(value) {
|
|
4
5
|
if (value === null || value === undefined)
|
|
@@ -43,17 +44,19 @@ function stableElementIdentity(element, index) {
|
|
|
43
44
|
index
|
|
44
45
|
})).digest('hex')}`;
|
|
45
46
|
}
|
|
46
|
-
function
|
|
47
|
+
function buildElementRecords(tree) {
|
|
47
48
|
const signatures = new Map();
|
|
49
|
+
const elementsByIdentity = new Map();
|
|
48
50
|
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
49
|
-
for (let index = 0; index < elements.length; index++) {
|
|
51
|
+
for (let index = 0; index < Math.min(elements.length, 500); index++) {
|
|
50
52
|
const element = elements[index];
|
|
51
53
|
if (!element)
|
|
52
54
|
continue;
|
|
53
55
|
const identity = stableElementIdentity(element, index);
|
|
54
56
|
signatures.set(identity, crypto.createHash('sha1').update(JSON.stringify(stableElementSignature(element))).digest('hex'));
|
|
57
|
+
elementsByIdentity.set(identity, structuredClone(element));
|
|
55
58
|
}
|
|
56
|
-
return signatures;
|
|
59
|
+
return { signatures, elementsByIdentity };
|
|
57
60
|
}
|
|
58
61
|
function summarizeSnapshotDelta(previous, currentElements) {
|
|
59
62
|
if (!previous)
|
|
@@ -118,24 +121,47 @@ export function detectLoadingState(tree, source) {
|
|
|
118
121
|
}
|
|
119
122
|
export function deriveSnapshotMetadata(deviceKey, tree, source, signatureOverride) {
|
|
120
123
|
const signature = signatureOverride ?? computeSnapshotSignature(tree);
|
|
121
|
-
const
|
|
124
|
+
const existing = snapshotStateByDevice.get(deviceKey);
|
|
125
|
+
const previous = existing && Date.now() - existing.updatedAt <= 10 * 60 * 1000 ? existing : undefined;
|
|
126
|
+
if (!previous && existing)
|
|
127
|
+
snapshotStateByDevice.delete(deviceKey);
|
|
122
128
|
const hasValidTree = !!tree && !tree.error;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
const records = hasValidTree ? buildElementRecords(tree) : null;
|
|
130
|
+
const currentElementSignatures = records?.signatures ?? previous?.elementSignatures ?? new Map();
|
|
131
|
+
const currentElements = records?.elementsByIdentity ?? previous?.elements ?? new Map();
|
|
132
|
+
const revision = previous && (signature === null || previous.signature === signature) ? previous.revision : nextRevision++;
|
|
133
|
+
const history = previous?.history ?? new Map();
|
|
134
|
+
const historyTimes = previous?.historyTimes ?? new Map();
|
|
135
|
+
const oversized = previous?.oversized ?? new Map();
|
|
136
|
+
if (hasValidTree) {
|
|
137
|
+
history.set(revision, currentElements);
|
|
138
|
+
if (!historyTimes.has(revision))
|
|
139
|
+
historyTimes.set(revision, Date.now());
|
|
140
|
+
if ((tree.elements?.length ?? 0) > 500)
|
|
141
|
+
oversized.set(revision, (tree.elements?.length ?? 0) - 500);
|
|
142
|
+
}
|
|
143
|
+
for (const [id, timestamp] of historyTimes) {
|
|
144
|
+
if (Date.now() - timestamp > 10 * 60 * 1000) {
|
|
145
|
+
history.delete(id);
|
|
146
|
+
historyTimes.delete(id);
|
|
147
|
+
oversized.delete(id);
|
|
133
148
|
}
|
|
134
149
|
}
|
|
150
|
+
while (history.size > 8) {
|
|
151
|
+
const oldest = history.keys().next().value;
|
|
152
|
+
if (oldest === undefined)
|
|
153
|
+
break;
|
|
154
|
+
history.delete(oldest);
|
|
155
|
+
historyTimes.delete(oldest);
|
|
156
|
+
oversized.delete(oldest);
|
|
157
|
+
}
|
|
135
158
|
snapshotStateByDevice.set(deviceKey, {
|
|
136
159
|
revision,
|
|
137
160
|
signature,
|
|
138
|
-
elementSignatures: currentElementSignatures
|
|
161
|
+
elementSignatures: currentElementSignatures,
|
|
162
|
+
elements: currentElements,
|
|
163
|
+
history, historyTimes, oversized, previousRevision: previous?.revision ?? null,
|
|
164
|
+
updatedAt: Date.now()
|
|
139
165
|
});
|
|
140
166
|
return {
|
|
141
167
|
snapshot_revision: revision,
|
|
@@ -144,6 +170,59 @@ export function deriveSnapshotMetadata(deviceKey, tree, source, signatureOverrid
|
|
|
144
170
|
loading_state: detectLoadingState(tree, source)
|
|
145
171
|
};
|
|
146
172
|
}
|
|
173
|
+
export function getStateDelta(deviceKey, baseRevision, currentRevision) {
|
|
174
|
+
const state = snapshotStateByDevice.get(deviceKey);
|
|
175
|
+
if (!state || Date.now() - (state.historyTimes.get(baseRevision) ?? 0) > 10 * 60 * 1000 || Date.now() - state.updatedAt > 10 * 60 * 1000)
|
|
176
|
+
return null;
|
|
177
|
+
const base = state.history.get(baseRevision);
|
|
178
|
+
const current = state?.history.get(currentRevision);
|
|
179
|
+
if (!base || !current)
|
|
180
|
+
return null;
|
|
181
|
+
const added = [];
|
|
182
|
+
const changed = [];
|
|
183
|
+
const removed = [];
|
|
184
|
+
for (const [identity, element] of current) {
|
|
185
|
+
const before = base.get(identity);
|
|
186
|
+
if (!before)
|
|
187
|
+
added.push(element);
|
|
188
|
+
else if (JSON.stringify(stableElementSignature(before)) !== JSON.stringify(stableElementSignature(element)))
|
|
189
|
+
changed.push(element);
|
|
190
|
+
}
|
|
191
|
+
let index = 0;
|
|
192
|
+
for (const [identity, element] of base) {
|
|
193
|
+
if (!current.has(identity))
|
|
194
|
+
removed.push({ stable_id: element.stable_id ?? undefined, resourceId: element.resourceId ?? undefined, contentDescription: element.contentDescription ?? undefined, text: element.text ?? undefined, index });
|
|
195
|
+
index++;
|
|
196
|
+
}
|
|
197
|
+
const all = added.length + changed.length + removed.length;
|
|
198
|
+
const limit = 200;
|
|
199
|
+
return {
|
|
200
|
+
base_snapshot_revision: baseRevision,
|
|
201
|
+
snapshot_revision: currentRevision,
|
|
202
|
+
added: added.slice(0, limit),
|
|
203
|
+
changed: changed.slice(0, Math.max(0, limit - added.length)),
|
|
204
|
+
removed: removed.slice(0, Math.max(0, limit - added.length - changed.length)),
|
|
205
|
+
truncated: all > limit || state.oversized.has(baseRevision) || state.oversized.has(currentRevision),
|
|
206
|
+
...((all > limit || state.oversized.has(baseRevision) || state.oversized.has(currentRevision))
|
|
207
|
+
? { omitted_changes: Math.max(all - limit, state.oversized.get(baseRevision) ?? 0, state.oversized.get(currentRevision) ?? 0) } : {})
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function getLatestStateDelta(deviceKey) {
|
|
211
|
+
const state = snapshotStateByDevice.get(deviceKey);
|
|
212
|
+
if (!state || state.previousRevision === null)
|
|
213
|
+
return null;
|
|
214
|
+
return getStateDelta(deviceKey, state.previousRevision, state.revision);
|
|
215
|
+
}
|
|
216
|
+
export function retainConnectedDeviceSnapshots(platform, deviceIds) {
|
|
217
|
+
const keys = new Set(deviceIds.map(id => `${platform}:${id}`));
|
|
218
|
+
for (const key of snapshotStateByDevice.keys())
|
|
219
|
+
if (key.startsWith(`${platform}:`) && !keys.has(key))
|
|
220
|
+
snapshotStateByDevice.delete(key);
|
|
221
|
+
}
|
|
222
|
+
export function clearDeviceSnapshots(deviceKey) {
|
|
223
|
+
snapshotStateByDevice.delete(deviceKey);
|
|
224
|
+
}
|
|
147
225
|
export function resetSnapshotMetadataForTests() {
|
|
148
226
|
snapshotStateByDevice.clear();
|
|
227
|
+
nextRevision = 1;
|
|
149
228
|
}
|
package/dist/server/common.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { recordTiming } from '../utils/timing.js';
|
|
1
2
|
import { ToolsObserve } from '../observe/index.js';
|
|
2
3
|
export const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
3
4
|
export const DEFAULT_MAX_RETRY_DEPTH = 3;
|
|
4
|
-
export function wrapResponse(data) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
export function wrapResponse(data, includeSerializationTiming = true) {
|
|
6
|
+
const start = performance.now();
|
|
7
|
+
let text = JSON.stringify(data, null, 2);
|
|
8
|
+
const elapsed = performance.now() - start;
|
|
9
|
+
const value = data;
|
|
10
|
+
if (includeSerializationTiming && value && typeof value === 'object' && value.timing && typeof value.timing === 'object') {
|
|
11
|
+
value.timing.serialization_ms = (value.timing.serialization_ms ?? 0) + elapsed;
|
|
12
|
+
recordTiming('serialization_ms', elapsed);
|
|
13
|
+
text = JSON.stringify(value, null, 2);
|
|
14
|
+
}
|
|
15
|
+
return { content: [{ type: 'text', text }] };
|
|
11
16
|
}
|
|
12
17
|
export function getStringArg(args, key) {
|
|
13
18
|
const value = args[key];
|
|
@@ -230,6 +235,7 @@ function mapFailureCodeToFailureClass(code) {
|
|
|
230
235
|
case 'STALE_REFERENCE':
|
|
231
236
|
return 'TargetResolutionFailure';
|
|
232
237
|
case 'ELEMENT_NOT_INTERACTABLE':
|
|
238
|
+
case 'ACTION_TIMEOUT':
|
|
233
239
|
return 'ExecutionFailure';
|
|
234
240
|
case 'TIMEOUT':
|
|
235
241
|
case 'ACTION_REJECTED':
|
|
@@ -238,6 +244,7 @@ function mapFailureCodeToFailureClass(code) {
|
|
|
238
244
|
return 'ExecutionFailure';
|
|
239
245
|
case 'VERIFICATION_FAILED':
|
|
240
246
|
case 'EXPECT_STATE_MISMATCH':
|
|
247
|
+
case 'ASSERTION_TIMEOUT':
|
|
241
248
|
return 'VerificationFailure';
|
|
242
249
|
case 'CONTROL_CONVERGENCE_FAILED':
|
|
243
250
|
return 'ControlConvergenceFailure';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const selector = z.object({
|
|
3
|
+
text: z.string().min(1).optional(), resource_id: z.string().min(1).optional(),
|
|
4
|
+
accessibility_id: z.string().min(1).optional(), contains: z.boolean().optional()
|
|
5
|
+
}).strict().refine(value => !!(value.text || value.resource_id || value.accessibility_id), 'Selector requires a target');
|
|
6
|
+
const verificationMode = z.enum(['none', 'light', 'full']);
|
|
7
|
+
const timeoutMs = z.number().int().min(100).max(60000).optional();
|
|
8
|
+
const pollIntervalMs = z.number().int().min(50).max(1000).optional();
|
|
9
|
+
const waitFor = z.object({
|
|
10
|
+
condition: z.enum(['exists', 'visible', 'clickable']).optional(),
|
|
11
|
+
timeoutMs: z.number().int().min(100).max(10000).optional(), pollIntervalMs,
|
|
12
|
+
match: z.object({ index: z.number().int().nonnegative().optional() }).strict().optional()
|
|
13
|
+
}).strict();
|
|
14
|
+
const target = { selector: selector.optional(), elementId: z.string().min(1).optional() };
|
|
15
|
+
const exactlyOneTarget = (value) => Number(!!value.selector) + Number(!!value.elementId) === 1;
|
|
16
|
+
const assertion = z.union([
|
|
17
|
+
z.object({ kind: z.literal('element_visible'), selector }).strict(),
|
|
18
|
+
z.object({ kind: z.literal('element_absent'), selector }).strict(),
|
|
19
|
+
z.object({ kind: z.literal('screen_fingerprint'), fingerprint: z.string().min(1) }).strict(),
|
|
20
|
+
z.object({ kind: z.literal('state_equals'), property: z.string().min(1), expected: z.union([z.boolean(), z.number().finite(), z.string()]), ...target }).strict().refine(exactlyOneTarget)
|
|
21
|
+
]);
|
|
22
|
+
const id = z.string().min(1);
|
|
23
|
+
const step = z.union([
|
|
24
|
+
z.object({ id, type: z.literal('start_app'), appId: z.string().min(1), verificationMode: verificationMode.optional() }).strict(),
|
|
25
|
+
z.object({ id, type: z.literal('tap'), ...target, waitFor: waitFor.optional(), verificationMode: verificationMode.optional() }).strict().refine(exactlyOneTarget),
|
|
26
|
+
z.object({ id, type: z.literal('wait'), selector, condition: z.enum(['exists', 'not_exists', 'visible', 'clickable']).optional(), timeoutMs, pollIntervalMs }).strict(),
|
|
27
|
+
z.object({ id, type: z.literal('assert'), assertion, timeoutMs, pollIntervalMs }).strict()
|
|
28
|
+
]);
|
|
29
|
+
export const tapElementControlsSchema = z.object({
|
|
30
|
+
elementId: z.string().min(1).optional(), selector: selector.optional(), waitFor: waitFor.optional()
|
|
31
|
+
}).strict().refine(value => Number(!!value.elementId) + Number(!!value.selector) === 1 && (!value.elementId || !value.waitFor));
|
|
32
|
+
export const journeySchema = z.object({
|
|
33
|
+
platform: z.enum(['android', 'ios']), deviceId: z.string().min(1).optional(),
|
|
34
|
+
responseMode: z.enum(['compact', 'debug']).default('compact'), captureOnFailure: z.boolean().default(false),
|
|
35
|
+
defaults: z.object({ verificationMode: verificationMode.optional(), actionTimeoutMs: z.number().int().finite().optional(), verificationTimeoutMs: z.number().int().finite().optional() }).strict().optional(),
|
|
36
|
+
steps: z.array(step).min(1).max(50)
|
|
37
|
+
}).strict().refine(value => new Set(value.steps.map(step => step.id)).size === value.steps.length, 'Step IDs must be unique');
|
|
38
|
+
const selectorJson = { type: 'object', additionalProperties: false, properties: {
|
|
39
|
+
text: { type: 'string', minLength: 1 }, resource_id: { type: 'string', minLength: 1 }, accessibility_id: { type: 'string', minLength: 1 }, contains: { type: 'boolean' }
|
|
40
|
+
}, anyOf: ['text', 'resource_id', 'accessibility_id'].map(key => ({ required: [key] })) };
|
|
41
|
+
const targetJson = { selector: selectorJson, elementId: { type: 'string', minLength: 1 } };
|
|
42
|
+
const oneTargetJson = [{ required: ['selector'], not: { required: ['elementId'] } }, { required: ['elementId'], not: { required: ['selector'] } }];
|
|
43
|
+
const intervalJson = { timeoutMs: { type: 'integer', minimum: 100, maximum: 60000 }, pollIntervalMs: { type: 'integer', minimum: 50, maximum: 1000 } };
|
|
44
|
+
const modeJson = { type: 'string', enum: ['none', 'light', 'full'] };
|
|
45
|
+
const objectJson = (properties, required, extra = {}) => ({ type: 'object', additionalProperties: false, properties, required, ...extra });
|
|
46
|
+
export const journeyStepsJson = {
|
|
47
|
+
type: 'array', minItems: 1, maxItems: 50, items: { oneOf: [
|
|
48
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'start_app' }, appId: { type: 'string', minLength: 1 }, verificationMode: modeJson }, ['id', 'type', 'appId']),
|
|
49
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'tap' }, ...targetJson, verificationMode: modeJson,
|
|
50
|
+
waitFor: objectJson({ ...intervalJson, timeoutMs: { type: 'integer', minimum: 100, maximum: 10000 }, condition: { enum: ['exists', 'visible', 'clickable'] }, match: objectJson({ index: { type: 'integer', minimum: 0 } }, []) }, [])
|
|
51
|
+
}, ['id', 'type'], { oneOf: oneTargetJson }),
|
|
52
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'wait' }, selector: selectorJson, condition: { enum: ['exists', 'not_exists', 'visible', 'clickable'] }, ...intervalJson }, ['id', 'type', 'selector']),
|
|
53
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'assert' }, ...intervalJson, assertion: { oneOf: [
|
|
54
|
+
objectJson({ kind: { const: 'element_visible' }, selector: selectorJson }, ['kind', 'selector']),
|
|
55
|
+
objectJson({ kind: { const: 'element_absent' }, selector: selectorJson }, ['kind', 'selector']),
|
|
56
|
+
objectJson({ kind: { const: 'screen_fingerprint' }, fingerprint: { type: 'string', minLength: 1 } }, ['kind', 'fingerprint']),
|
|
57
|
+
objectJson({ kind: { const: 'state_equals' }, ...targetJson, property: { type: 'string', minLength: 1 }, expected: { type: ['boolean', 'number', 'string'] } }, ['kind', 'property', 'expected'], { oneOf: oneTargetJson })
|
|
58
|
+
] } }, ['id', 'type', 'assertion'])
|
|
59
|
+
] }
|
|
60
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { journeyStepsJson } from './journey-schema.js';
|
|
1
2
|
export const toolDefinitions = [
|
|
2
3
|
{
|
|
3
4
|
name: 'start_app',
|
|
@@ -47,7 +48,11 @@ Failure Handling:
|
|
|
47
48
|
deviceId: {
|
|
48
49
|
type: 'string',
|
|
49
50
|
description: 'Device UDID (iOS) or Serial (Android). Defaults to booted/connected.'
|
|
50
|
-
}
|
|
51
|
+
},
|
|
52
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
53
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
54
|
+
actionTimeoutMs: { type: 'number', minimum: 100, maximum: 30000 },
|
|
55
|
+
verificationTimeoutMs: { type: 'number', minimum: 100, maximum: 60000 }
|
|
51
56
|
},
|
|
52
57
|
required: ['platform', 'appId']
|
|
53
58
|
}
|
|
@@ -250,6 +255,7 @@ Failure Handling:
|
|
|
250
255
|
properties: {
|
|
251
256
|
reason: { type: 'string', description: 'Optional reason for snapshot' },
|
|
252
257
|
includeLogs: { type: 'boolean', description: 'Whether to include logs', default: true },
|
|
258
|
+
includeScreenshot: { type: 'boolean', description: 'Whether to collect screenshot bytes', default: true },
|
|
253
259
|
logLines: { type: 'number', description: 'Maximum number of log lines to include', default: 200 },
|
|
254
260
|
platform: { type: 'string', enum: ['android', 'ios'], description: 'Optional platform override' },
|
|
255
261
|
appId: { type: 'string', description: 'Optional appId to scope logs (package/bundle id)' },
|
|
@@ -307,7 +313,9 @@ Failure Handling:
|
|
|
307
313
|
deviceId: {
|
|
308
314
|
type: 'string',
|
|
309
315
|
description: 'Device Serial (Android) or UDID (iOS). Defaults to connected/booted device.'
|
|
310
|
-
}
|
|
316
|
+
},
|
|
317
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
318
|
+
sinceSnapshotRevision: { type: 'number', minimum: 1 }
|
|
311
319
|
},
|
|
312
320
|
required: ['platform']
|
|
313
321
|
}
|
|
@@ -729,7 +737,11 @@ Failure Handling:
|
|
|
729
737
|
deviceId: {
|
|
730
738
|
type: 'string',
|
|
731
739
|
description: 'Device Serial/UDID. Defaults to connected/booted device.'
|
|
732
|
-
}
|
|
740
|
+
},
|
|
741
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
742
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
743
|
+
actionTimeoutMs: { type: 'number', minimum: 100, maximum: 30000 },
|
|
744
|
+
verificationTimeoutMs: { type: 'number', minimum: 100, maximum: 60000 }
|
|
733
745
|
},
|
|
734
746
|
required: ['x', 'y']
|
|
735
747
|
}
|
|
@@ -783,9 +795,14 @@ This tool reports execution success only. Verification must be done with a separ
|
|
|
783
795
|
elementId: {
|
|
784
796
|
type: 'string',
|
|
785
797
|
description: 'A unique element identifier returned by wait_for_ui'
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
798
|
+
},
|
|
799
|
+
selector: { type: 'object', properties: { text: { type: 'string' }, resource_id: { type: 'string' }, accessibility_id: { type: 'string' }, contains: { type: 'boolean' } } },
|
|
800
|
+
waitFor: { type: 'object', properties: { condition: { type: 'string', enum: ['exists', 'visible', 'clickable'] }, timeoutMs: { type: 'number', minimum: 100, maximum: 10000 }, pollIntervalMs: { type: 'number', minimum: 50, maximum: 1000 }, match: { type: 'object', properties: { index: { type: 'number' } } } } },
|
|
801
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
802
|
+
deviceId: { type: 'string' },
|
|
803
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
804
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' }
|
|
805
|
+
}
|
|
789
806
|
}
|
|
790
807
|
},
|
|
791
808
|
{
|
|
@@ -1062,6 +1079,20 @@ BEHAVIOUR after outcome:
|
|
|
1062
1079
|
required: ['uiChanged']
|
|
1063
1080
|
}
|
|
1064
1081
|
},
|
|
1082
|
+
{
|
|
1083
|
+
name: 'run_journey',
|
|
1084
|
+
description: 'Run an ordered, bounded sequence of start_app, tap, wait, and deterministic assertion steps against one device. Stops after the first failed step.',
|
|
1085
|
+
inputSchema: {
|
|
1086
|
+
type: 'object',
|
|
1087
|
+
properties: {
|
|
1088
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
1089
|
+
deviceId: { type: 'string' },
|
|
1090
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'compact' },
|
|
1091
|
+
steps: { type: 'array', minItems: 1, maxItems: 50, items: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string', enum: ['start_app', 'tap', 'wait', 'assert'] } }, required: ['id', 'type'] } }
|
|
1092
|
+
},
|
|
1093
|
+
required: ['platform', 'steps']
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1065
1096
|
{
|
|
1066
1097
|
name: 'get_network_activity',
|
|
1067
1098
|
description: `Returns structured network events captured from platform logs since the last action.
|
|
@@ -1095,3 +1126,25 @@ Returns { requests: [], count: 0 } when no credible network signals are found.`,
|
|
|
1095
1126
|
}
|
|
1096
1127
|
}
|
|
1097
1128
|
];
|
|
1129
|
+
// Keep the additive execution controls consistent across all action tools.
|
|
1130
|
+
for (const tool of toolDefinitions) {
|
|
1131
|
+
if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back'].includes(tool.name)) {
|
|
1132
|
+
Object.assign(tool.inputSchema.properties, {
|
|
1133
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
1134
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
1135
|
+
actionTimeoutMs: { type: 'integer', description: 'Dispatch budget in milliseconds, clamped to 100–30000.' },
|
|
1136
|
+
verificationTimeoutMs: { type: 'integer', description: 'Light observation budget in milliseconds, clamped to 100–60000.' }
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (tool.name === 'tap_element')
|
|
1140
|
+
Object.assign(tool.inputSchema, { oneOf: [{ required: ['elementId'], not: { required: ['selector'] } }, { required: ['selector'], not: { required: ['elementId'] } }] });
|
|
1141
|
+
if (tool.name === 'run_journey')
|
|
1142
|
+
Object.assign(tool.inputSchema.properties, {
|
|
1143
|
+
steps: journeyStepsJson,
|
|
1144
|
+
captureOnFailure: { type: 'boolean', default: false },
|
|
1145
|
+
defaults: { type: 'object', additionalProperties: false, properties: {
|
|
1146
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'] },
|
|
1147
|
+
actionTimeoutMs: { type: 'integer' }, verificationTimeoutMs: { type: 'integer' }
|
|
1148
|
+
} }
|
|
1149
|
+
});
|
|
1150
|
+
}
|