mobile-debug-mcp 0.32.2 → 0.33.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/{skills → .agents/skills}/mcp-builder/SKILL.md +1 -2
- package/{skills → .agents/skills}/spec-review/SKILL.md +1 -1
- package/{skills → .agents/skills}/test-authoring/SKILL.md +1 -1
- package/.claude-plugin/marketplace.json +14 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.github/copilot-instructions.md +7 -0
- package/AGENTS.md +21 -10
- package/README.md +67 -5
- 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/LICENSE +201 -0
- package/agent-plugin/README.md +29 -0
- package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
- package/agent-plugin/mcp.json +11 -0
- package/agent-plugin/plugin.json +29 -0
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +124 -0
- package/dist/interact/android.js +10 -4
- package/dist/interact/conditional-action.js +68 -0
- package/dist/interact/index.js +54 -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 +27 -22
- package/dist/observe/evidence.js +273 -0
- package/dist/observe/index.js +33 -6
- package/dist/observe/ios.js +25 -6
- package/dist/observe/readiness-service.js +115 -0
- package/dist/observe/readiness.js +81 -0
- package/dist/observe/recording.js +165 -0
- package/dist/observe/snapshot-metadata.js +10 -2
- package/dist/server/common.js +9 -0
- package/dist/server/condition-schema.js +32 -0
- package/dist/server/journey-schema.js +3 -2
- package/dist/server/readiness-schema.js +12 -0
- package/dist/server/tool-definitions.js +46 -0
- package/dist/server/tool-handlers.js +99 -17
- package/dist/server-core.js +1 -1
- package/dist/utils/ios/utils.js +4 -4
- package/dist/utils/operation-budget.js +24 -2
- package/dist/utils/resolve-device.js +7 -0
- package/docs/CHANGELOG.md +16 -2
- package/docs/benchmarks/ui-tree-performance/README.md +4 -3
- 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/process.md +213 -0
- package/docs/tools/TOOLS.md +3 -0
- package/docs/tools/evidence.md +79 -0
- package/docs/tools/interact.md +125 -2
- package/docs/tools/observe.md +64 -2
- package/package.json +4 -2
- package/scripts/agent-plugin.mjs +98 -0
- package/src/interact/android.ts +10 -4
- package/src/interact/conditional-action.ts +75 -0
- package/src/interact/index.ts +42 -14
- 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 +35 -21
- package/src/observe/evidence.ts +255 -0
- package/src/observe/index.ts +32 -8
- package/src/observe/ios.ts +32 -8
- package/src/observe/readiness-service.ts +89 -0
- package/src/observe/readiness.ts +78 -0
- package/src/observe/snapshot-metadata.ts +12 -1
- package/src/server/common.ts +7 -0
- package/src/server/condition-schema.ts +32 -0
- package/src/server/journey-schema.ts +3 -2
- package/src/server/readiness-schema.ts +12 -0
- package/src/server/tool-definitions.ts +44 -0
- package/src/server/tool-handlers.ts +93 -17
- package/src/server-core.ts +1 -1
- package/src/types.ts +30 -1
- package/src/utils/ios/utils.ts +4 -4
- package/src/utils/operation-budget.ts +22 -2
- package/src/utils/resolve-device.ts +8 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -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/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/unit/observe/android_tree_acquisition.test.ts +6 -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-getlogs.test.ts +3 -3
- package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/readiness.test.ts +39 -0
- package/test/unit/observe/readiness_native.test.ts +68 -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/tap_selector.test.ts +2 -0
- package/skills/mobile-debug-tools-usage/SKILL.md +0 -74
- package/src/observe/android-tree-bridge.ts +0 -178
- package/test/device/automated/ui-tree-performance/bridge-probe.ts +0 -197
- package/test/fixtures/ui-tree-bridge/AndroidManifest.xml +0 -6
- package/test/fixtures/ui-tree-bridge/build.ts +0 -34
- package/test/fixtures/ui-tree-bridge/src/TreeInstrumentation.java +0 -126
- package/test/unit/observe/android_tree_bridge_cleanup.test.ts +0 -95
- /package/{skills → .agents/skills}/README.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/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/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,12 @@
|
|
|
1
|
+
import { withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js';
|
|
1
2
|
import { measure, withTiming } from '../utils/timing.js';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { runWithBudget } from '../utils/operation-budget.js';
|
|
4
5
|
import { journeySchema, tapElementControlsSchema } from './journey-schema.js';
|
|
6
|
+
import { readinessSchema, readinessWaitSchema } from './readiness-schema.js';
|
|
7
|
+
import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js';
|
|
8
|
+
import { validateExpectedState } from './condition-schema.js';
|
|
9
|
+
import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js';
|
|
5
10
|
import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js';
|
|
6
11
|
import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js';
|
|
7
12
|
import { ToolsInteract } from '../interact/index.js';
|
|
@@ -43,18 +48,45 @@ function compactAction(result, verificationMode, started = true, stateDelta, tot
|
|
|
43
48
|
timing: { total_ms: totalMs }
|
|
44
49
|
};
|
|
45
50
|
}
|
|
46
|
-
function
|
|
51
|
+
function correlatedHandler(name, handler, action = false, journey = false) {
|
|
47
52
|
return async (args) => {
|
|
53
|
+
const ref = validateActionRef(args.actionRef);
|
|
54
|
+
const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
|
|
55
|
+
try {
|
|
56
|
+
return await handler(args);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return wrapToolError(name, error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const item = result.content.find(item => item.type === 'text');
|
|
63
|
+
if (item?.text) {
|
|
64
|
+
const payload = JSON.parse(item.text);
|
|
65
|
+
payload.evidence = evidence;
|
|
66
|
+
item.text = JSON.stringify(payload);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function actionHandler(type, execute) {
|
|
72
|
+
return correlatedHandler(type, async (args) => {
|
|
48
73
|
const timing = {};
|
|
49
74
|
return withTiming(timing, async () => {
|
|
50
75
|
const started = performance.now();
|
|
51
76
|
const controls = interactionControls(args);
|
|
77
|
+
const expected = validateExpectedState(type, args);
|
|
78
|
+
const condition = expected ? createConditionalAction(args.platform, args.deviceId, expected) : undefined;
|
|
52
79
|
const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000);
|
|
53
80
|
let dispatchStarted = false;
|
|
54
81
|
let result;
|
|
55
82
|
const dispatchWithBudget = (timeoutMs) => runWithBudget(timeoutMs, async (budget) => {
|
|
56
83
|
try {
|
|
57
|
-
|
|
84
|
+
if (!condition)
|
|
85
|
+
return payloadOf(await execute(args));
|
|
86
|
+
return await withConditionalAction(condition, async () => {
|
|
87
|
+
const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId });
|
|
88
|
+
return withResolvedDevice(device, async () => payloadOf(await execute(args)));
|
|
89
|
+
});
|
|
58
90
|
}
|
|
59
91
|
finally {
|
|
60
92
|
dispatchStarted = budget.dispatchStarted;
|
|
@@ -68,17 +100,28 @@ function actionHandler(type, execute) {
|
|
|
68
100
|
dispatchStarted = true;
|
|
69
101
|
}
|
|
70
102
|
catch (error) {
|
|
71
|
-
if (controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
|
|
103
|
+
if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
|
|
72
104
|
return wrapToolError(type, error);
|
|
73
105
|
result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) };
|
|
74
106
|
}
|
|
107
|
+
const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status);
|
|
108
|
+
if (blocked) {
|
|
109
|
+
dispatchStarted = false;
|
|
110
|
+
result = buildActionExecutionResult({
|
|
111
|
+
actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
|
|
112
|
+
selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
|
|
113
|
+
success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
|
|
114
|
+
failure: { failureCode: conditionFailureCode(condition), retryable: false },
|
|
115
|
+
traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
|
|
116
|
+
});
|
|
117
|
+
}
|
|
75
118
|
if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? ''))
|
|
76
119
|
result.failure_code = 'ACTION_TIMEOUT';
|
|
77
120
|
if (result.failure_code === 'ACTION_TIMEOUT')
|
|
78
121
|
result.retryable = !dispatchStarted;
|
|
79
122
|
if (!result.success && typeof result.retryable !== 'boolean')
|
|
80
123
|
result.retryable = !dispatchStarted;
|
|
81
|
-
result.action_id ??= randomUUID();
|
|
124
|
+
result.action_id ??= currentEvidenceActionId() ?? randomUUID();
|
|
82
125
|
result.action_type ??= type;
|
|
83
126
|
recordObservationEvent({
|
|
84
127
|
step_id: args._stepId,
|
|
@@ -92,6 +135,7 @@ function actionHandler(type, execute) {
|
|
|
92
135
|
if (result.success && controls.verificationMode === 'light') {
|
|
93
136
|
const verificationStarted = performance.now();
|
|
94
137
|
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);
|
|
138
|
+
markEvidenceVerification();
|
|
95
139
|
observationAvailable = !!tree && !tree.error;
|
|
96
140
|
recordObservationEvent({
|
|
97
141
|
verification_budget_ms: controls.verificationTimeoutMs,
|
|
@@ -102,11 +146,21 @@ function actionHandler(type, execute) {
|
|
|
102
146
|
if (observationAvailable && tree.device)
|
|
103
147
|
delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined;
|
|
104
148
|
}
|
|
149
|
+
if (!blocked && controls.verificationMode === 'full' && dispatchStarted)
|
|
150
|
+
markEvidenceVerification();
|
|
105
151
|
const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started);
|
|
152
|
+
if (blocked)
|
|
153
|
+
compact.verification.status = 'not_requested';
|
|
106
154
|
if (result.success && controls.verificationMode === 'light' && !observationAvailable)
|
|
107
155
|
compact.verification.status = 'unavailable';
|
|
108
156
|
const evidence = {
|
|
109
157
|
...compact,
|
|
158
|
+
...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
|
|
159
|
+
? { ...condition.result, observed: condition.result.observed ? {
|
|
160
|
+
snapshot_session_id: condition.result.observed.snapshot_session_id,
|
|
161
|
+
snapshot_revision: condition.result.observed.snapshot_revision,
|
|
162
|
+
captured_at_ms: condition.result.observed.captured_at_ms
|
|
163
|
+
} : null } : condition.result } : {}),
|
|
110
164
|
...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
|
|
111
165
|
...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
|
|
112
166
|
};
|
|
@@ -115,7 +169,7 @@ function actionHandler(type, execute) {
|
|
|
115
169
|
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
170
|
}, controls.responseMode === 'debug');
|
|
117
171
|
});
|
|
118
|
-
};
|
|
172
|
+
}, true);
|
|
119
173
|
}
|
|
120
174
|
const handleStartApp = actionHandler('start_app', executeStartApp);
|
|
121
175
|
const handleRestartApp = actionHandler('restart_app', executeRestartApp);
|
|
@@ -131,7 +185,7 @@ async function executeStartApp(args) {
|
|
|
131
185
|
const deviceId = getStringArg(args, 'deviceId');
|
|
132
186
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
133
187
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
134
|
-
|
|
188
|
+
notifyActionStart();
|
|
135
189
|
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
190
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
137
191
|
const result = buildActionExecutionResult({
|
|
@@ -168,7 +222,7 @@ async function executeRestartApp(args) {
|
|
|
168
222
|
const deviceId = getStringArg(args, 'deviceId');
|
|
169
223
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
170
224
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
171
|
-
|
|
225
|
+
notifyActionStart();
|
|
172
226
|
const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined;
|
|
173
227
|
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
228
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
@@ -381,9 +435,12 @@ async function handleAdjustControl(args) {
|
|
|
381
435
|
platform,
|
|
382
436
|
deviceId
|
|
383
437
|
});
|
|
438
|
+
markEvidenceVerification();
|
|
384
439
|
return wrapResponse(res);
|
|
385
440
|
}
|
|
386
441
|
async function handleWaitForUI(args) {
|
|
442
|
+
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
|
|
443
|
+
throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
|
|
387
444
|
const selector = getObjectArg(args, 'selector');
|
|
388
445
|
const condition = getStringArg(args, 'condition') ?? 'exists';
|
|
389
446
|
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
@@ -392,10 +449,13 @@ async function handleWaitForUI(args) {
|
|
|
392
449
|
const retry = getObjectArg(args, 'retry');
|
|
393
450
|
const platform = getStringArg(args, 'platform');
|
|
394
451
|
const deviceId = getStringArg(args, 'deviceId');
|
|
395
|
-
const
|
|
452
|
+
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId });
|
|
453
|
+
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
454
|
return wrapResponse(res);
|
|
397
455
|
}
|
|
398
456
|
async function handleWaitForUIChange(args) {
|
|
457
|
+
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
|
|
458
|
+
throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
|
|
399
459
|
const platform = getStringArg(args, 'platform');
|
|
400
460
|
const deviceId = getStringArg(args, 'deviceId');
|
|
401
461
|
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
@@ -403,7 +463,8 @@ async function handleWaitForUIChange(args) {
|
|
|
403
463
|
const expected_change = getStringArg(args, 'expected_change');
|
|
404
464
|
const scope = getStringArg(args, 'scope');
|
|
405
465
|
const target = getStringArg(args, 'target');
|
|
406
|
-
const
|
|
466
|
+
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target });
|
|
467
|
+
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
468
|
return wrapResponse(res);
|
|
408
469
|
}
|
|
409
470
|
async function handleFindElement(args) {
|
|
@@ -424,7 +485,7 @@ async function executeTap(args) {
|
|
|
424
485
|
const deviceId = getStringArg(args, 'deviceId');
|
|
425
486
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
426
487
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
427
|
-
|
|
488
|
+
notifyActionStart();
|
|
428
489
|
const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }));
|
|
429
490
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
430
491
|
const result = buildActionExecutionResult({
|
|
@@ -450,14 +511,15 @@ async function executeTapElement(args) {
|
|
|
450
511
|
if (selector) {
|
|
451
512
|
const waitFor = getObjectArg(args, 'waitFor');
|
|
452
513
|
const targetStarted = performance.now();
|
|
453
|
-
const
|
|
514
|
+
const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000;
|
|
515
|
+
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
516
|
recordObservationEvent({ step_id: args._stepId, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status });
|
|
455
517
|
if (resolution.status !== 'success' || !resolution.element?.elementId)
|
|
456
518
|
return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true });
|
|
457
519
|
resolvedId = resolution.element.elementId;
|
|
458
520
|
freshTree = resolution._tree;
|
|
459
521
|
}
|
|
460
|
-
|
|
522
|
+
notifyActionStart();
|
|
461
523
|
if (freshTree)
|
|
462
524
|
recordObservationConsumer(freshTree, 'target', args._stepId);
|
|
463
525
|
const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }));
|
|
@@ -563,7 +625,8 @@ async function handleRunJourney(args) {
|
|
|
563
625
|
result.failure_code = 'ASSERTION_TIMEOUT';
|
|
564
626
|
const success = !!result?.success;
|
|
565
627
|
if (lightCoveredByAssertion) {
|
|
566
|
-
result.verification = { mode: 'light', status: success ? 'unavailable' :
|
|
628
|
+
result.verification = { mode: 'light', status: success ? 'unavailable' :
|
|
629
|
+
(['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') };
|
|
567
630
|
if (success) {
|
|
568
631
|
result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true };
|
|
569
632
|
pendingLightStep = results.length;
|
|
@@ -573,6 +636,8 @@ async function handleRunJourney(args) {
|
|
|
573
636
|
const pending = results[pendingLightStep].result;
|
|
574
637
|
if (typeof result?.observed?.matched_count === 'number') {
|
|
575
638
|
pending.verification.status = 'observed';
|
|
639
|
+
if (pending.evidence)
|
|
640
|
+
completeJourneyVerification(pending.evidence.action_id);
|
|
576
641
|
delete pending.verification_diagnostic;
|
|
577
642
|
const delta = ToolsObserve.getLatestStateDelta(platform, deviceId);
|
|
578
643
|
if (delta) {
|
|
@@ -602,6 +667,7 @@ async function handleRunJourney(args) {
|
|
|
602
667
|
if (key !== 'total_ms' && typeof value === 'number')
|
|
603
668
|
componentTiming[key] = (componentTiming[key] ?? 0) + value;
|
|
604
669
|
}
|
|
670
|
+
refreshJourneyEvidence(results);
|
|
605
671
|
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
672
|
});
|
|
607
673
|
}
|
|
@@ -615,7 +681,7 @@ async function executeSwipe(args) {
|
|
|
615
681
|
const deviceId = getStringArg(args, 'deviceId');
|
|
616
682
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
617
683
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
618
|
-
|
|
684
|
+
notifyActionStart();
|
|
619
685
|
const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }));
|
|
620
686
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
621
687
|
const result = buildActionExecutionResult({
|
|
@@ -638,7 +704,7 @@ async function executeScrollToElement(args) {
|
|
|
638
704
|
const deviceId = getStringArg(args, 'deviceId');
|
|
639
705
|
const { verificationMode } = interactionControls(args);
|
|
640
706
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
641
|
-
|
|
707
|
+
notifyActionStart();
|
|
642
708
|
const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }));
|
|
643
709
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
|
|
644
710
|
const result = buildActionExecutionResult({
|
|
@@ -666,7 +732,7 @@ async function executeTypeText(args) {
|
|
|
666
732
|
const deviceId = getStringArg(args, 'deviceId');
|
|
667
733
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
668
734
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
669
|
-
|
|
735
|
+
notifyActionStart();
|
|
670
736
|
const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }));
|
|
671
737
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
672
738
|
const result = buildActionExecutionResult({
|
|
@@ -684,7 +750,7 @@ async function executePressBack(args) {
|
|
|
684
750
|
const deviceId = getStringArg(args, 'deviceId');
|
|
685
751
|
const { verificationMode, actionTimeoutMs } = interactionControls(args);
|
|
686
752
|
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
687
|
-
|
|
753
|
+
notifyActionStart();
|
|
688
754
|
const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }));
|
|
689
755
|
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
|
|
690
756
|
const result = buildActionExecutionResult({
|
|
@@ -743,6 +809,18 @@ async function handleGetNetworkActivity(args) {
|
|
|
743
809
|
return wrapResponse(result);
|
|
744
810
|
}
|
|
745
811
|
export const toolHandlers = {
|
|
812
|
+
get_app_readiness: async (args) => {
|
|
813
|
+
const parsed = readinessSchema.safeParse(args);
|
|
814
|
+
if (!parsed.success)
|
|
815
|
+
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
816
|
+
return wrapResponse(await getAppReadiness(parsed.data));
|
|
817
|
+
},
|
|
818
|
+
wait_for_app_ready: async (args) => {
|
|
819
|
+
const parsed = readinessWaitSchema.safeParse(args);
|
|
820
|
+
if (!parsed.success)
|
|
821
|
+
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
822
|
+
return wrapResponse(await getAppReadiness(parsed.data, true));
|
|
823
|
+
},
|
|
746
824
|
start_app: handleStartApp,
|
|
747
825
|
terminate_app: handleTerminateApp,
|
|
748
826
|
restart_app: handleRestartApp,
|
|
@@ -779,6 +857,9 @@ export const toolHandlers = {
|
|
|
779
857
|
classify_action_outcome: handleClassifyActionOutcome,
|
|
780
858
|
get_network_activity: handleGetNetworkActivity
|
|
781
859
|
};
|
|
860
|
+
for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
|
|
861
|
+
toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey');
|
|
862
|
+
}
|
|
782
863
|
export async function handleToolCall(name, args = {}) {
|
|
783
864
|
const handler = toolHandlers[name];
|
|
784
865
|
if (!handler)
|
|
@@ -801,6 +882,7 @@ export async function handleToolCall(name, args = {}) {
|
|
|
801
882
|
};
|
|
802
883
|
return withTiming(measured, async () => {
|
|
803
884
|
try {
|
|
885
|
+
validateExpectedState(name, args);
|
|
804
886
|
return addTiming(await handler(args));
|
|
805
887
|
}
|
|
806
888
|
catch (error) {
|
package/dist/server-core.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
|
|
|
6
6
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
7
|
export const serverInfo = {
|
|
8
8
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.
|
|
9
|
+
version: '0.33.0'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|
package/dist/utils/ios/utils.js
CHANGED
|
@@ -75,7 +75,7 @@ export async function isIDBInstalled() {
|
|
|
75
75
|
if (!cmd)
|
|
76
76
|
return false;
|
|
77
77
|
try {
|
|
78
|
-
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
|
|
78
|
+
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: remainingBudget(5000), killSignal: 'SIGKILL' });
|
|
79
79
|
return true;
|
|
80
80
|
}
|
|
81
81
|
catch (e) {
|
|
@@ -197,7 +197,7 @@ export async function findAppBundle(dir) {
|
|
|
197
197
|
export async function getIOSDeviceMetadata(deviceId = "booted") {
|
|
198
198
|
return new Promise((resolve) => {
|
|
199
199
|
// If deviceId is provided (and not "booted"), attempt to find that device among booted simulators.
|
|
200
|
-
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], (err, stdout) => {
|
|
200
|
+
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], { timeout: remainingBudget(), killSignal: 'SIGKILL' }, (err, stdout) => {
|
|
201
201
|
const fallback = {
|
|
202
202
|
platform: "ios",
|
|
203
203
|
id: deviceId,
|
|
@@ -240,7 +240,7 @@ export async function getIOSDeviceMetadata(deviceId = "booted") {
|
|
|
240
240
|
export async function listIOSDevices(appId) {
|
|
241
241
|
return new Promise((resolve) => {
|
|
242
242
|
// Query all devices and separately query booted devices to mark them
|
|
243
|
-
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], (err, stdout) => {
|
|
243
|
+
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], { timeout: remainingBudget(), killSignal: 'SIGKILL' }, (err, stdout) => {
|
|
244
244
|
if (err || !stdout)
|
|
245
245
|
return resolve([]);
|
|
246
246
|
try {
|
|
@@ -249,7 +249,7 @@ export async function listIOSDevices(appId) {
|
|
|
249
249
|
const out = [];
|
|
250
250
|
const checks = [];
|
|
251
251
|
// Get booted devices set
|
|
252
|
-
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], (err2, stdout2) => {
|
|
252
|
+
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], { timeout: remainingBudget(), killSignal: 'SIGKILL' }, (err2, stdout2) => {
|
|
253
253
|
const bootedSet = new Set();
|
|
254
254
|
if (!err2 && stdout2) {
|
|
255
255
|
try {
|
|
@@ -8,13 +8,35 @@ export function runWithBudget(timeoutMs, operation) {
|
|
|
8
8
|
}
|
|
9
9
|
export function remainingBudget(fallback = 0) {
|
|
10
10
|
const budget = budgets.getStore();
|
|
11
|
-
|
|
11
|
+
let deadline = budget?.deadline ?? Infinity;
|
|
12
|
+
for (let ancestor = budget?.parent; ancestor; ancestor = ancestor.parent)
|
|
13
|
+
deadline = Math.min(deadline, ancestor.deadline);
|
|
14
|
+
if (!Number.isFinite(deadline))
|
|
12
15
|
return fallback;
|
|
13
|
-
const remaining = Math.floor(
|
|
16
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
14
17
|
if (remaining <= 0)
|
|
15
18
|
throw new Error('ACTION_TIMEOUT');
|
|
16
19
|
return fallback > 0 ? Math.min(fallback, remaining) : remaining;
|
|
17
20
|
}
|
|
21
|
+
// Pause only this action's budget. Any enclosing operation still bounds the read.
|
|
22
|
+
export async function runWithSeparateBudget(timeoutMs, operation) {
|
|
23
|
+
remainingBudget();
|
|
24
|
+
const action = budgets.getStore();
|
|
25
|
+
const started = performance.now();
|
|
26
|
+
const parent = action?.parent;
|
|
27
|
+
const budget = { deadline: Math.min(parent?.deadline ?? Infinity, started + timeoutMs), dispatchStarted: false, parent };
|
|
28
|
+
try {
|
|
29
|
+
return await budgets.run(budget, async () => {
|
|
30
|
+
const result = await operation();
|
|
31
|
+
remainingBudget();
|
|
32
|
+
return result;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (action)
|
|
37
|
+
action.deadline += performance.now() - started;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
18
40
|
export function markDispatchStarted() {
|
|
19
41
|
remainingBudget();
|
|
20
42
|
let budget = budgets.getStore();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindEvidenceDevice } from '../observe/evidence.js';
|
|
1
2
|
import { retainConnectedDeviceSnapshots } from '../observe/snapshot-metadata.js';
|
|
2
3
|
import { measure } from './timing.js';
|
|
3
4
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -37,9 +38,15 @@ export async function listDevices(platform, appId) {
|
|
|
37
38
|
}
|
|
38
39
|
const deviceContext = new AsyncLocalStorage();
|
|
39
40
|
export function withResolvedDevice(device, operation) {
|
|
41
|
+
bindEvidenceDevice(device);
|
|
40
42
|
return deviceContext.run(device, operation);
|
|
41
43
|
}
|
|
42
44
|
export async function resolveTargetDevice(opts) {
|
|
45
|
+
const device = await resolveDevice(opts);
|
|
46
|
+
bindEvidenceDevice(device);
|
|
47
|
+
return device;
|
|
48
|
+
}
|
|
49
|
+
async function resolveDevice(opts) {
|
|
43
50
|
const { platform, appId, prefer, deviceId } = opts;
|
|
44
51
|
const current = deviceContext.getStore();
|
|
45
52
|
if (current && current.platform === platform && (!deviceId || current.id === deviceId))
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
- Made readiness unit-test polling time deterministic while retaining exact sample-count and timeout assertions, eliminating host timer-rounding flakes.
|
|
7
|
+
- Isolated snapshot unit tests from physical device discovery and native handlers so they run without connected devices. Readiness deadline expiry now retains the last valid observation instead of intermittently resetting loading to unknown.
|
|
8
|
+
|
|
9
|
+
## [0.33.0]
|
|
10
|
+
- Fixed selector taps timing out before one normal Android UI-tree acquisition: a default single lookup now uses the action budget (5 seconds). Selector taps with exists/visible waits resolve actionable containers consistently with clickable waits; hidden/disabled labels remain protected. Acquisition timeouts retain `ACTION_TIMEOUT` instead of reporting a missing element.
|
|
11
|
+
- Added correlated action/observation evidence, explicit `actionRef`, phase/acquisition timestamps, conservative freshness, and bounded metadata history. Journey steps share correlation IDs; snapshot components and stream reads retain their actual device scope. Device log times remain explicitly incomparable with host dispatch times; tracing is unsupported.
|
|
12
|
+
- Added evidence-backed foreground UI readiness observations, bounded readiness waits, and opt-in `requireReady` for existing UI waits. Tree responses include single-sample readiness without extra reads. Unresponsive/debugger-paused signals are explicitly unsupported; iOS probes and metadata reads now respect active operation budgets.
|
|
13
|
+
- Added optional conditional input to `tap`, `tap_element`, `swipe`, Android `type_text`/`press_back`, and journey tap steps. A fresh snapshot check at native dispatch blocks stale or uncheckable expectations and returns condition evidence, including fresh trees in compact failures.
|
|
14
|
+
- Added process-scoped `snapshot_session_id` metadata to UI-tree responses so conditional expectations cannot be reused across server restarts. Successful repeated observations refresh retained revision history timestamps.
|
|
15
|
+
|
|
16
|
+
## [0.32.4]
|
|
17
|
+
- Removed the optional Android UI-tree bridge runtime and its APK build tooling. Android UI-tree reads use `uiautomator dump` through ADB.
|
|
18
|
+
|
|
5
19
|
## [0.32.2]
|
|
6
20
|
- bug fix
|
|
7
21
|
|
|
@@ -26,7 +40,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
26
40
|
## [0.30.0]
|
|
27
41
|
- Folded RFC 013 synchronization semantics into the main spec and aligned the interact docs with the shipped `wait_for_ui_change` behavior.
|
|
28
42
|
- Updated `wait_for_ui_change` to use a 300ms stabilization default and to reset stabilization on new in-place mutations.
|
|
29
|
-
- Validated the in-place UI mutation flow on
|
|
43
|
+
- Validated the in-place UI mutation flow on an Android emulator, including a delayed state-change case.
|
|
30
44
|
|
|
31
45
|
## [0.29.0]
|
|
32
46
|
- Added empty resource handlers and declared the `resources` capability so Codex MCP discovery can complete the handshake against the published npm package.
|
|
@@ -57,7 +71,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
57
71
|
- RFC-003 wait/synchronization contract with `snapshot_revision`, `captured_at_ms`, and `loading_state`
|
|
58
72
|
- Added `wait_for_ui_change` for stable in-place UI mutations
|
|
59
73
|
- Updated `get_ui_tree` and `capture_debug_snapshot` to surface snapshot metadata
|
|
60
|
-
-
|
|
74
|
+
- Validated the new UI-change flow on an Android emulator
|
|
61
75
|
|
|
62
76
|
## [0.25.1]
|
|
63
77
|
- Platform-native element identity metadata for UI targeting
|
|
@@ -50,9 +50,10 @@ The report commit SHAs are available locally as `codex/spec-019-baseline` and
|
|
|
50
50
|
`codex/spec-019-candidate`; create clean, built checkouts of those refs to
|
|
51
51
|
reproduce the paired measurement.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
`LATENCY_DEVICE_ID=emulator-5554 node --import tsx test/device/automated/ui-tree-performance/bridge-probe.ts`.
|
|
55
|
-
Its `bridge-probe.json` compares ten bridge and legacy reads, checks a network
|
|
53
|
+
The historical `bridge-probe.json` compares ten bridge and legacy reads, checks a network
|
|
56
54
|
update, and checks a timer that changes visually without emitting an
|
|
57
55
|
accessibility event. The timer check catches a stale accessibility cache even
|
|
58
56
|
when every bridge request calls `getRootInActiveWindow()`.
|
|
57
|
+
The bridge APK source, generator, and build/install probe have been removed;
|
|
58
|
+
this report is retained as historical evidence, not a runnable benchmark in
|
|
59
|
+
the current checkout.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Intent: conditional actions
|
|
2
|
+
|
|
3
|
+
Originator: repository maintainer (user in the current conversation).
|
|
4
|
+
Date: 2026-10-29.
|
|
5
|
+
Status: approved.
|
|
6
|
+
|
|
7
|
+
## Original problem
|
|
8
|
+
|
|
9
|
+
“Conditional actions: Check expected state immediately before acting; stop and
|
|
10
|
+
return fresh evidence if it changed.”
|
|
11
|
+
|
|
12
|
+
Source: user request in the current conversation, following adoption of the
|
|
13
|
+
repository development process.
|
|
14
|
+
|
|
15
|
+
## Desired outcome
|
|
16
|
+
|
|
17
|
+
An MCP client can make an action conditional on an expected UI state. The server
|
|
18
|
+
checks that expectation against a fresh observation of the same target device
|
|
19
|
+
immediately before dispatching the action. If the expectation no longer holds,
|
|
20
|
+
the server does not dispatch the action and returns fresh evidence so the client
|
|
21
|
+
can reassess its next step.
|
|
22
|
+
|
|
23
|
+
Affected users and systems: MCP clients/agents, interaction tool dispatch,
|
|
24
|
+
state observation, and Android/iOS adapters for supported actions.
|
|
25
|
+
|
|
26
|
+
## Observable success criteria
|
|
27
|
+
|
|
28
|
+
- A matching expectation allows the requested action to proceed normally.
|
|
29
|
+
- A mismatching expectation prevents dispatch and clearly reports that the
|
|
30
|
+
action was not performed, together with the newly observed state.
|
|
31
|
+
- If the expected state cannot be checked reliably, the action is not dispatched;
|
|
32
|
+
the response explains the observation failure without inventing evidence.
|
|
33
|
+
- The check and action target the same device.
|
|
34
|
+
- A mismatch does not automatically retry the action or adopt the new state.
|
|
35
|
+
- Existing calls without a condition preserve their behaviour.
|
|
36
|
+
|
|
37
|
+
## Constraints and exclusions
|
|
38
|
+
|
|
39
|
+
- Reuse existing action, observation, and response conventions where suitable.
|
|
40
|
+
- Define precise supported conditions, actions, evidence, and platform coverage
|
|
41
|
+
in the spec after approval of this intent.
|
|
42
|
+
- A pre-dispatch check cannot guarantee the UI remains unchanged between the
|
|
43
|
+
observation and native input delivery; do not claim atomic device execution.
|
|
44
|
+
- Automatic replanning, rollback, and post-action success verification are
|
|
45
|
+
outside this feature's scope.
|
|
46
|
+
- Preserve unrelated checkout changes.
|
|
47
|
+
|
|
48
|
+
## Assumptions and open questions for specification
|
|
49
|
+
|
|
50
|
+
- Proposed compatibility approach: conditions are optional on supported actions.
|
|
51
|
+
- Determine the initial action set, including whether journey steps are covered.
|
|
52
|
+
- Determine whether expectations use screen identity, element properties,
|
|
53
|
+
snapshot revisions, or a supported combination.
|
|
54
|
+
- Define fresh mismatch evidence and how it is preserved in compact responses.
|
|
55
|
+
- Existing screen fingerprints deliberately omit some changing values; assess
|
|
56
|
+
their suitability rather than assuming they detect every relevant state change.
|
|
57
|
+
|
|
58
|
+
## Approval
|
|
59
|
+
|
|
60
|
+
Approved by the originator in the current conversation on 2026-10-29: “yes”,
|
|
61
|
+
in direct response to the complete intent summary and linked artifact. Approval
|
|
62
|
+
covers the captured intent; specification approval remains a separate gate.
|