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
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getSystemStatus } from './system/index.js';
|
|
|
7
7
|
const server = createServer();
|
|
8
8
|
if (process.env.MOBILE_DEBUG_MCP_STARTUP_HEALTHCHECK === '1') {
|
|
9
9
|
getSystemStatus().then((res) => {
|
|
10
|
-
console.info('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices });
|
|
10
|
+
console.info('[startup] system status summary:', { adb: 'adbAvailable' in res ? res.adbAvailable : false, ios: 'iosAvailable' in res ? res.iosAvailable : false, devices: 'devices' in res ? res.devices : 0, iosDevices: 'iosDevices' in res ? res.iosDevices : 0 });
|
|
11
11
|
}).catch((e) => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)));
|
|
12
12
|
}
|
|
13
13
|
const transport = new StdioServerTransport();
|
package/dist/system/android.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { probeInstallation, validAppId } from '../utils/app-installation.js';
|
|
1
2
|
import { execSync } from 'child_process';
|
|
2
3
|
import { ensureAdbAvailable } from '../utils/android/utils.js';
|
|
3
4
|
export async function checkAndroid() {
|
|
@@ -9,6 +10,8 @@ export async function checkAndroid() {
|
|
|
9
10
|
let logsAvailable = false;
|
|
10
11
|
let envValid = false;
|
|
11
12
|
let appInstalled = undefined;
|
|
13
|
+
let appInstallations = [];
|
|
14
|
+
let targets = [];
|
|
12
15
|
try {
|
|
13
16
|
const adbCheck = ensureAdbAvailable();
|
|
14
17
|
const adbCmd = adbCheck.adbCmd || 'adb';
|
|
@@ -19,6 +22,7 @@ export async function checkAndroid() {
|
|
|
19
22
|
try {
|
|
20
23
|
const out = execSync(`${adbCmd} devices -l`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
21
24
|
const state = parseDevices(out, issues);
|
|
25
|
+
targets = out.split('\n').map(line => line.trim().split(/\s+/)).filter(parts => parts[0] && !['List', '*'].includes(parts[0])).map(parts => ({ id: parts[0], available: parts[1] === 'device' }));
|
|
22
26
|
devices = state.devices;
|
|
23
27
|
deviceStates = state.deviceStates;
|
|
24
28
|
}
|
|
@@ -43,15 +47,15 @@ export async function checkAndroid() {
|
|
|
43
47
|
if (!envValid)
|
|
44
48
|
issues.push('ANDROID_SDK_ROOT/ANDROID_HOME missing and adb not found in PATH');
|
|
45
49
|
const pkg = process.env.MCP_TARGET_PACKAGE || process.env.MCP_TARGET_APP_ID;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
appInstallations = await Promise.all(targets.map(target => probeInstallation(adbCmd, target.id, pkg, 'android-current-user', adbAvailable && target.available)));
|
|
51
|
+
reportInstallations(appInstallations, pkg, issues);
|
|
52
|
+
appInstalled = installationSummary(appInstallations);
|
|
49
53
|
}
|
|
50
54
|
catch (e) {
|
|
51
55
|
console.debug('[get_system_status] adb availability check failed: ' + String(e));
|
|
52
56
|
issues.push('ADB check failed');
|
|
53
57
|
}
|
|
54
|
-
return { adbAvailable, adbVersion, devices, deviceStates, logsAvailable, envValid, appInstalled, issues };
|
|
58
|
+
return { adbAvailable, adbVersion, devices, deviceStates, logsAvailable, envValid, appInstalled, appInstallations, issues };
|
|
55
59
|
}
|
|
56
60
|
function parseDevices(out, issues) {
|
|
57
61
|
const lines = out.split('\n').map(l => l.trim()).filter(Boolean);
|
|
@@ -73,16 +77,16 @@ function parseDevices(out, issues) {
|
|
|
73
77
|
issues.push(`${stateCounts['offline']} device(s) offline`);
|
|
74
78
|
return { devices, deviceStates };
|
|
75
79
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
function installationSummary(checks) {
|
|
81
|
+
return checks.length === 1 && checks[0].status !== 'unknown' ? checks[0].status === 'installed' : undefined;
|
|
82
|
+
}
|
|
83
|
+
function reportInstallations(checks, pkg, issues) {
|
|
84
|
+
if (pkg !== undefined && !validAppId(pkg))
|
|
85
|
+
issues.push('Invalid target app identifier');
|
|
86
|
+
for (const check of checks) {
|
|
87
|
+
if (check.status === 'absent')
|
|
88
|
+
issues.push(`App ${check.appId} not installed on device ${check.deviceId}`);
|
|
89
|
+
if (check.status === 'unknown' && check.reason !== 'APP_ID_NOT_PROVIDED')
|
|
90
|
+
issues.push(`Installation check unavailable on device ${check.deviceId}: ${check.reason}`);
|
|
87
91
|
}
|
|
88
92
|
}
|
package/dist/system/index.js
CHANGED
|
@@ -40,7 +40,8 @@ export async function getSystemStatus() {
|
|
|
40
40
|
logsAvailable: androidRaw.logsAvailable,
|
|
41
41
|
envValid: androidRaw.envValid,
|
|
42
42
|
issues,
|
|
43
|
-
appInstalled: androidRaw.appInstalled,
|
|
43
|
+
...(androidRaw.appInstalled === undefined ? {} : { appInstalled: androidRaw.appInstalled }),
|
|
44
|
+
appInstallations: androidRaw.appInstallations,
|
|
44
45
|
iosAvailable: iosRaw.iosAvailable,
|
|
45
46
|
iosDevices: iosRaw.iosDevices,
|
|
46
47
|
gradleJavaHome: gradle.gradleJavaHome,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { promises as fsPromises } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
export { findInPath, resolveAdbCmd, getAdbCmd, ensureAdbAvailable, getAdbArgs, getAdbTimeout, execAdb, spawnAdb } from './commands.js';
|
|
4
|
-
import { execAdb } from './commands.js';
|
|
4
|
+
import { execAdb, getAdbCmd } from './commands.js';
|
|
5
|
+
import { installationFields, probeInstallation } from '../app-installation.js';
|
|
5
6
|
export { mergeEnv, prepareGradle } from './gradle.js';
|
|
6
7
|
export { parseBounds, getCenter, parseBooleanAttr, parseNumberAttr, normalizeClassName, inferAndroidRole, buildAndroidSelectorConfidence, buildAndroidSelector, buildAndroidSemantic, isSliderLikeAndroid, extractAndroidState, traverseNode } from './ui-tree.js';
|
|
7
8
|
export { parseLogLine } from './logs.js';
|
|
@@ -79,22 +80,12 @@ export async function listAndroidDevices(appId) {
|
|
|
79
80
|
execAdb(['shell', 'getprop', 'ro.kernel.qemu'], serial).catch(() => '0')
|
|
80
81
|
]);
|
|
81
82
|
const simulator = simOutput === '1';
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const pm = await execAdb(['shell', 'pm', 'path', appId], serial);
|
|
86
|
-
appInstalled = !!(pm && pm.includes('package:'));
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
console.debug(`[listAndroidDevices] pm check failed for ${serial}: ${String(e)}`);
|
|
90
|
-
appInstalled = false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { platform: 'android', id: serial, osVersion, model, simulator, appInstalled };
|
|
83
|
+
const check = await probeInstallation(getAdbCmd(), serial, appId, 'android-current-user', deviceLines.find(line => line.split(/\s+/)[0] === serial)?.split(/\s+/)[1] === 'device');
|
|
84
|
+
return { platform: 'android', id: serial, osVersion, model, simulator, ...installationFields(check) };
|
|
94
85
|
}
|
|
95
|
-
catch
|
|
96
|
-
|
|
97
|
-
return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false,
|
|
86
|
+
catch {
|
|
87
|
+
const check = await probeInstallation(getAdbCmd(), serial, appId, 'android-current-user', false);
|
|
88
|
+
return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false, ...installationFields(check) };
|
|
98
89
|
}
|
|
99
90
|
}));
|
|
100
91
|
return infos;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { execCmd } from './exec.js';
|
|
3
|
+
import { remainingBudget } from './operation-budget.js';
|
|
4
|
+
export function validAppId(appId) { return /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(appId); }
|
|
5
|
+
export function installationContext(deviceId, appId, scope) {
|
|
6
|
+
return { status: 'unknown', appId: appId ?? null, deviceId, scope, checkedAt: null, reason: appId === undefined ? 'APP_ID_NOT_PROVIDED' : 'CHECK_FAILED' };
|
|
7
|
+
}
|
|
8
|
+
export function installationFields(appInstallation) {
|
|
9
|
+
return { appInstallation, ...(appInstallation.status === 'unknown' ? {} : { appInstalled: appInstallation.status === 'installed' }) };
|
|
10
|
+
}
|
|
11
|
+
export async function probeInstallation(command, deviceId, appId, scope, available = true) {
|
|
12
|
+
const result = installationContext(deviceId, appId, scope);
|
|
13
|
+
if (appId === undefined || !validAppId(appId))
|
|
14
|
+
return result;
|
|
15
|
+
if (!available)
|
|
16
|
+
return { ...result, reason: 'DEVICE_UNAVAILABLE' };
|
|
17
|
+
try {
|
|
18
|
+
return await runProbe(command, result);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ...result, checkedAt: new Date().toISOString(), reason: 'CHECK_FAILED' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function runProbe(command, context) {
|
|
25
|
+
const android = context.scope === 'android-current-user';
|
|
26
|
+
const started = performance.now();
|
|
27
|
+
const timeout = remainingBudget(5000);
|
|
28
|
+
const args = android ? ['-s', context.deviceId, 'shell', 'pm', 'path', context.appId] : ['simctl', 'listapps', context.deviceId];
|
|
29
|
+
const output = await execCmd(command, args, { timeout });
|
|
30
|
+
const result = { ...context, checkedAt: new Date().toISOString() };
|
|
31
|
+
if (output.exitCode !== 0) {
|
|
32
|
+
if (android && !output.stdout && !output.stderr)
|
|
33
|
+
return confirmAndroidAbsence(command, result, Math.max(1, timeout - (performance.now() - started)));
|
|
34
|
+
return { ...result, reason: 'CHECK_FAILED' };
|
|
35
|
+
}
|
|
36
|
+
let status;
|
|
37
|
+
if (android) {
|
|
38
|
+
const lines = output.stdout.trim().split('\n').filter(Boolean);
|
|
39
|
+
if (lines.some(line => !/^package:\/\S+$/.test(line.trim())))
|
|
40
|
+
return { ...result, reason: 'UNRECOGNIZED_RESPONSE' };
|
|
41
|
+
status = lines.length ? 'installed' : 'absent';
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
try {
|
|
45
|
+
const budget = remainingBudget(Math.max(1, Math.floor(timeout - (performance.now() - started))));
|
|
46
|
+
if (performance.now() - started >= timeout)
|
|
47
|
+
return { ...result, reason: 'CHECK_FAILED' };
|
|
48
|
+
const json = await new Promise((resolve, reject) => {
|
|
49
|
+
const child = execFile('/usr/bin/plutil', ['-convert', 'json', '-o', '-', '--', '-'], { timeout: budget, killSignal: 'SIGKILL', maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => error ? reject(error) : resolve(stdout));
|
|
50
|
+
child.stdin?.on('error', reject);
|
|
51
|
+
child.stdin?.end(output.stdout);
|
|
52
|
+
});
|
|
53
|
+
const apps = JSON.parse(json);
|
|
54
|
+
if (!apps || typeof apps !== 'object' || Array.isArray(apps))
|
|
55
|
+
return { ...result, reason: 'UNRECOGNIZED_RESPONSE' };
|
|
56
|
+
if (Object.values(apps).some(value => !value || typeof value !== 'object' || Array.isArray(value)))
|
|
57
|
+
return { ...result, reason: 'UNRECOGNIZED_RESPONSE' };
|
|
58
|
+
status = Object.hasOwn(apps, context.appId) ? 'installed' : 'absent';
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { ...result, reason: 'UNRECOGNIZED_RESPONSE' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const { reason: _reason, ...confirmed } = result;
|
|
65
|
+
return { ...confirmed, status };
|
|
66
|
+
}
|
|
67
|
+
async function confirmAndroidAbsence(command, context, timeout) {
|
|
68
|
+
const output = await execCmd(command, ['-s', context.deviceId, 'shell', 'pm', 'list', 'packages', context.appId], { timeout });
|
|
69
|
+
const result = { ...context, checkedAt: new Date().toISOString() };
|
|
70
|
+
if (output.exitCode !== 0)
|
|
71
|
+
return { ...result, reason: 'CHECK_FAILED' };
|
|
72
|
+
const lines = output.stdout.split('\n').map(line => line.trim()).filter(Boolean);
|
|
73
|
+
if (lines.some(line => !/^package:[A-Za-z0-9_.-]+$/.test(line)))
|
|
74
|
+
return { ...result, reason: 'UNRECOGNIZED_RESPONSE' };
|
|
75
|
+
const { reason: _reason, ...confirmed } = result;
|
|
76
|
+
return { ...confirmed, status: lines.includes(`package:${context.appId}`) ? 'installed' : 'absent' };
|
|
77
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
const measurement = new AsyncLocalStorage();
|
|
4
|
+
// Private benchmark diagnostics: no arguments, device IDs or response contents.
|
|
5
|
+
export function recordBenchmarkSpan(stage, duration) {
|
|
6
|
+
const current = measurement.getStore();
|
|
7
|
+
if (!current)
|
|
8
|
+
return;
|
|
9
|
+
const end = performance.now() - current.started;
|
|
10
|
+
current.spans.push({ stage, start_ms: Math.max(0, end - duration), end_ms: end });
|
|
11
|
+
}
|
|
12
|
+
export async function benchmarkSubprocess(operation) {
|
|
13
|
+
return benchmarkStage('subprocess_ms', operation);
|
|
14
|
+
}
|
|
15
|
+
export async function benchmarkStage(stage, operation) {
|
|
16
|
+
if (!measurement.getStore())
|
|
17
|
+
return operation();
|
|
18
|
+
const started = performance.now();
|
|
19
|
+
try {
|
|
20
|
+
return await operation();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
recordBenchmarkSpan(stage, performance.now() - started);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function withBenchmarkRequest(tool, operation) {
|
|
27
|
+
if (process.env.MOBILE_DEBUG_MCP_LATENCY_METRICS !== '1')
|
|
28
|
+
return operation();
|
|
29
|
+
const current = { id: randomUUID(), started: performance.now(), spans: [] };
|
|
30
|
+
return measurement.run(current, async () => {
|
|
31
|
+
let completed = false;
|
|
32
|
+
try {
|
|
33
|
+
const result = await operation();
|
|
34
|
+
completed = true;
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
process.stderr.write(`${JSON.stringify({ event: 'latency_metric', request_id: current.id,
|
|
39
|
+
tool, handler_completed: completed, elapsed_ms: performance.now() - current.started,
|
|
40
|
+
spans: current.spans })}\n`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
package/dist/utils/exec.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { checkDebuggerGuard } from '../debugger/guard.js';
|
|
2
2
|
import { remainingBudget } from './operation-budget.js';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
+
import { benchmarkSubprocess } from './benchmark-metrics.js';
|
|
4
5
|
export async function execCmd(cmd, args, opts = {}) {
|
|
6
|
+
return benchmarkSubprocess(() => execute(cmd, args, opts));
|
|
7
|
+
}
|
|
8
|
+
async function execute(cmd, args, opts) {
|
|
5
9
|
const { env, cwd, shell } = opts;
|
|
6
10
|
const timeout = remainingBudget(opts.timeout ?? 0);
|
|
7
11
|
return new Promise((resolve, reject) => {
|
package/dist/utils/ios/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installationFields, probeInstallation } from '../app-installation.js';
|
|
1
2
|
import { checkDebuggerGuard } from '../../debugger/guard.js';
|
|
2
3
|
import { remainingBudget } from '../operation-budget.js';
|
|
3
4
|
import { execFile, spawn, execSync, spawnSync } from "child_process";
|
|
@@ -279,17 +280,9 @@ export async function listIOSDevices(appId) {
|
|
|
279
280
|
simulator: true,
|
|
280
281
|
booted: bootedSet.has(device.udid)
|
|
281
282
|
};
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.then(() => { info.appInstalled = true; })
|
|
286
|
-
.catch(() => { info.appInstalled = false; })
|
|
287
|
-
.then(() => { out.push(info); });
|
|
288
|
-
checks.push(p);
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
out.push(info);
|
|
292
|
-
}
|
|
283
|
+
out.push(info);
|
|
284
|
+
checks.push(probeInstallation(getXcrunCmd(), device.udid, appId, 'ios-simulator', info.booted === true)
|
|
285
|
+
.then(check => { Object.assign(info, installationFields(check)); }));
|
|
293
286
|
}
|
|
294
287
|
}
|
|
295
288
|
Promise.all(checks).then(() => resolve(out)).catch(() => resolve(out));
|
|
@@ -38,6 +38,7 @@ export async function listDevices(platform, appId) {
|
|
|
38
38
|
return iosDeviceLister(appId);
|
|
39
39
|
}
|
|
40
40
|
const deviceContext = new AsyncLocalStorage();
|
|
41
|
+
export function currentResolvedDevice() { return deviceContext.getStore(); }
|
|
41
42
|
export function withResolvedDevice(device, operation) {
|
|
42
43
|
bindEvidenceDevice(device);
|
|
43
44
|
return deviceContext.run(device, operation);
|
|
@@ -75,7 +76,7 @@ function selectDevice(devices, platform, appId, prefer) {
|
|
|
75
76
|
if (prefer === "emulator")
|
|
76
77
|
candidates = candidates.filter(d => d.simulator);
|
|
77
78
|
if (appId) {
|
|
78
|
-
const installed = candidates.filter(d => d.appInstalled);
|
|
79
|
+
const installed = candidates.filter(d => d.appInstallation ? d.appInstallation.status === 'installed' && d.appInstallation.appId === appId && d.appInstallation.deviceId === d.id : d.appInstalled === true);
|
|
79
80
|
if (installed.length > 0)
|
|
80
81
|
candidates = installed;
|
|
81
82
|
}
|
|
@@ -101,7 +102,7 @@ function selectDevice(devices, platform, appId, prefer) {
|
|
|
101
102
|
if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
|
|
102
103
|
return candidates[0];
|
|
103
104
|
}
|
|
104
|
-
const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: d.appInstalled }));
|
|
105
|
+
const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: d.appInstalled, appInstallation: d.appInstallation }));
|
|
105
106
|
const err = Object.assign(new Error(`Multiple matching devices found: ${JSON.stringify(list, null, 2)}`), { devices: list });
|
|
106
107
|
throw err;
|
|
107
108
|
}
|
package/dist/utils/timing.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { recordBenchmarkSpan } from './benchmark-metrics.js';
|
|
2
3
|
const timings = new AsyncLocalStorage();
|
|
3
4
|
export function withTiming(values, operation) { return timings.run(values, operation); }
|
|
4
5
|
export function recordTiming(component, milliseconds) {
|
|
6
|
+
recordBenchmarkSpan(component, milliseconds);
|
|
5
7
|
const values = timings.getStore();
|
|
6
8
|
if (values)
|
|
7
9
|
values[component] = (values[component] ?? 0) + milliseconds;
|
|
@@ -12,8 +14,6 @@ export async function measure(component, operation) {
|
|
|
12
14
|
return await operation();
|
|
13
15
|
}
|
|
14
16
|
finally {
|
|
15
|
-
|
|
16
|
-
if (values)
|
|
17
|
-
values[component] = (values[component] ?? 0) + performance.now() - start;
|
|
17
|
+
recordTiming(component, performance.now() - start);
|
|
18
18
|
}
|
|
19
19
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,31 @@ and [tool reference](tools/TOOLS.md) for the current checkout.
|
|
|
8
8
|
|
|
9
9
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
10
10
|
|
|
11
|
+
## [0.37.0]
|
|
12
|
+
|
|
13
|
+
- Updated the agent plugin to `0.0.7`, pinned to MCP `0.37.0`.
|
|
14
|
+
|
|
15
|
+
- Android debug snapshots use one fresh UI tree for the returned hierarchy and
|
|
16
|
+
screen fingerprint, removing a duplicate acquisition.
|
|
17
|
+
|
|
18
|
+
- Add an end-to-end latency benchmark with repeated mode comparisons, visible
|
|
19
|
+
failed attempts, source/build identity, and opt-in timing-only request and
|
|
20
|
+
subprocess diagnostics. Public tool contracts and verification are unchanged.
|
|
21
|
+
|
|
22
|
+
- Compact interaction/journey results now summarize observations, readiness, conditions and state deltas within explicit byte budgets. Detailed original results are available through session-local `get_interaction_diagnostics` references without replaying input. Debug output and response-mode defaults are unchanged.
|
|
23
|
+
|
|
24
|
+
- Report device-scoped app installation as installed, absent or unknown. Publish
|
|
25
|
+
`list_devices.appId`; omit legacy installation booleans for inconclusive or
|
|
26
|
+
multi-device system checks instead of reporting false absence.
|
|
27
|
+
|
|
28
|
+
- Clean up discarded iOS trace files after a recorder-stop failure when process
|
|
29
|
+
exit is confirmed, while retaining the failure and protecting live recordings.
|
|
30
|
+
|
|
31
|
+
- Add timestamped device-readiness evidence to launch/interaction results and
|
|
32
|
+
readiness waits, preserving dispatch outcomes and compact/journey evidence.
|
|
33
|
+
Android power/keyguard blockers prevent readiness success; iOS reports
|
|
34
|
+
per-signal unknown/unsupported evidence. No automatic wake or unlock.
|
|
35
|
+
|
|
11
36
|
## [0.36.0]
|
|
12
37
|
|
|
13
38
|
- Updated the agent plugin to `0.0.6`, pinned to MCP `0.36.0`.
|
|
@@ -14,5 +14,6 @@ iOS latency.
|
|
|
14
14
|
| --- | --- | --- | --- |
|
|
15
15
|
| Interaction latency | [Run the benchmark](interaction-latency/README.md) | [Android measurements](interaction-latency/final.md), [iOS interaction validation](interaction-latency/ios-validation.md) | [018](../specs/018-interaction-latency-and-batched-journeys/spec.md) |
|
|
16
16
|
| UI-tree observation | [Run the paired benchmark](ui-tree-performance/README.md) | [Comparison and limitations](ui-tree-performance/comparison.md) | [019](../specs/019-ui-tree-observation-performance/spec.md) |
|
|
17
|
+
| End-to-end interaction latency | [Run the benchmarks](end-to-end-latency/README.md) | [Provisional Android results and iOS blocker](end-to-end-latency/investigation.md) | [036](../specs/036-end-to-end-interaction-latency/spec.md) |
|
|
17
18
|
|
|
18
19
|
Fixture instructions: [latency journey app](../../test/fixtures/latency-journey-app/README.md).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# End-to-end interaction latency
|
|
2
|
+
|
|
3
|
+
[Benchmark index](../README.md) · [Specification](../../specs/036-end-to-end-interaction-latency/spec.md)
|
|
4
|
+
|
|
5
|
+
This investigation reuses the Spec 018 Android fixture and Spec 019 tree metrics.
|
|
6
|
+
It does not replace their historical results or optimization acceptance gates.
|
|
7
|
+
Requires Node >=22, Java supporting `--release 8`, Android platform 36 and
|
|
8
|
+
build-tools 36.0.0. Start an Android emulator, then run from the repository root:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm run verify
|
|
12
|
+
LATENCY_DEVICE_ID=emulator-5554 MCP_ADB_TIMEOUT=30000 npx tsx test/device/automated/latency/end-to-end.ts --smoke
|
|
13
|
+
LATENCY_DEVICE_ID=emulator-5554 MCP_ADB_TIMEOUT=30000 npx tsx test/device/automated/latency/end-to-end.ts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Only explicitly named Android emulators are accepted; physical phones are not
|
|
17
|
+
selected. Existing device resolution can enumerate other attached devices;
|
|
18
|
+
the report records discovery counts without serials. No display configuration is changed. The dedicated fixture is built,
|
|
19
|
+
installed, and reset; an installation created by this run is removed on exit.
|
|
20
|
+
A pre-existing fixture installation is left installed, with reset fixture data.
|
|
21
|
+
The harness owns isolated POSIX server process groups, closes them on exit and
|
|
22
|
+
interrupt, and verifies their termination. An unsafe timeout stops the run;
|
|
23
|
+
semantic failures are retained before the next explicitly recorded reset.
|
|
24
|
+
|
|
25
|
+
The smoke uses one attempt per case and writes `smoke.json`/`smoke.md`; it is not
|
|
26
|
+
performance evidence. The full run writes `baseline.json`/`baseline.md` after
|
|
27
|
+
each attempt, preserving partial results on failure/interruption. It performs
|
|
28
|
+
three warm-ups and ten measured attempts per case, reversing mode order every
|
|
29
|
+
round. Do not run other device workloads concurrently.
|
|
30
|
+
|
|
31
|
+
`baseline-attempt-1.json`/`.md` retain the first failed warm-up run: the emulator
|
|
32
|
+
disappeared, the next reset failed, and fixture cleanup could not be confirmed.
|
|
33
|
+
No measured samples were produced. Both owned server groups exited. The retry
|
|
34
|
+
restores the same AVD headlessly using `emulator -avd Pixel_9_Pro -port 5554
|
|
35
|
+
-no-window -no-snapshot-save`; it does not wipe data. This startup configuration
|
|
36
|
+
is distinct from the earlier development smoke and failed attempt.
|
|
37
|
+
|
|
38
|
+
Cases compare the original standalone handle flow and four full/light ×
|
|
39
|
+
debug/compact journeys, retaining all six destination assertions. Screenshot,
|
|
40
|
+
UI tree and screenshot/log-free snapshot calls are separate cases on the known
|
|
41
|
+
fixture home screen, with instrumentation on and off. Light/compact has existing
|
|
42
|
+
assertion-based observation sharing; mode differences can therefore include
|
|
43
|
+
acquisition counts as well as response construction costs. They are not a pure
|
|
44
|
+
serialization microbenchmark.
|
|
45
|
+
|
|
46
|
+
Raw reports include source/build/harness/fixture hashes, dirty paths, environment,
|
|
47
|
+
request order, byte counts, timing-only events, setup/startup durations and
|
|
48
|
+
cleanup. Device serials, raw responses and unrestricted stderr are not retained.
|
|
49
|
+
Source/build/harness changes during measurement invalidate the run. Externally
|
|
50
|
+
run builds and validation suites are separate from MCP request time. This fixture
|
|
51
|
+
uses offline SDK tools rather than Gradle. Device application work inside requests
|
|
52
|
+
is included; it is not established as pure MCP overhead.
|
|
53
|
+
|
|
54
|
+
Percentiles use nearest rank and successful attempts only; failures remain in
|
|
55
|
+
raw records and fail the run. Warm-up failures are counted separately. Ten samples
|
|
56
|
+
provide a preliminary p95, not a robust tail estimate. Inclusive component spans
|
|
57
|
+
overlap and must not be summed. Handler residual subtracts their interval union;
|
|
58
|
+
client-minus-handler includes scheduling, transport and uninstrumented work.
|
|
59
|
+
Existing serialization spans cover selected response construction, not all SDK
|
|
60
|
+
wire serialization. Tree event stages expose acquisition, retry and parse costs.
|
|
61
|
+
Subprocess spans cover `execCmd`; direct platform spawns can remain in residuals.
|
|
62
|
+
|
|
63
|
+
Compare observation `on`/`off` results to assess probe overhead; these include
|
|
64
|
+
normal device variability and are not automatically a causal overhead estimate.
|
|
65
|
+
`missing_request_metrics` is expected for `off` cases only. Instrumentation uses
|
|
66
|
+
`MOBILE_DEBUG_MCP_LATENCY_METRICS=1` and the existing
|
|
67
|
+
`MOBILE_DEBUG_MCP_UI_TREE_METRICS=1` only in instrumented benchmark servers.
|
|
68
|
+
Normal MCP contracts and diagnostic output are unchanged.
|
|
69
|
+
|
|
70
|
+
For standalone iOS observations, use an isolated booted simulator with working
|
|
71
|
+
idb discovery and English Settings. The runner confirms the Settings root
|
|
72
|
+
before each timed request and records preparation time separately.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
IOS_LATENCY_DEVICE_ID=YOUR_SIMULATOR_ID npx tsx test/device/automated/latency-ios/benchmark.ts --smoke
|
|
76
|
+
IOS_LATENCY_DEVICE_ID=YOUR_SIMULATOR_ID npx tsx test/device/automated/latency-ios/benchmark.ts
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Equivalent iOS fixture journeys depend on [#89](https://github.com/clivejefferies/mobile-debug-tools/issues/89).
|
|
80
|
+
See the [investigation report](investigation.md) for the Android measurements
|
|
81
|
+
and the observed iOS setup blocker.
|
|
82
|
+
|
|
83
|
+
For a focused [Android snapshot comparison](android-snapshot-pair.md), build
|
|
84
|
+
the pre-change checkout and preserve its emitted `dist` directory and source
|
|
85
|
+
hash. The preserved build needs its ESM `package.json` and the same installed
|
|
86
|
+
dependencies. Build the candidate in the current checkout, then run:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
LATENCY_DEVICE_ID=emulator-5554 \
|
|
90
|
+
SNAPSHOT_BASELINE_SERVER=/absolute/path/to/baseline/dist/server.js \
|
|
91
|
+
SNAPSHOT_BASELINE_SOURCE_SHA=BASELINE_SOURCE_SHA256 \
|
|
92
|
+
MCP_ADB_TIMEOUT=30000 \
|
|
93
|
+
node --import tsx test/device/automated/latency/snapshot-pair.ts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use the `treeHash` helper in `end-to-end-environment.ts` to hash baseline `src`
|
|
97
|
+
before editing it. This command writes `android-snapshot-pair.json` with the
|
|
98
|
+
attempts and acceptance result.
|