mobile-debug-mcp 0.32.4 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.github/copilot-instructions.md +7 -0
- package/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +21 -10
- package/README.md +69 -4
- package/agent-plugin/.claude-plugin/plugin.json +20 -0
- package/agent-plugin/.codex-plugin/plugin.json +34 -0
- package/agent-plugin/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/README.md +9 -3
- package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
- package/dist/debugger/android.js +78 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/sessions.js +262 -0
- package/dist/debugger/worker.js +165 -0
- package/dist/interact/android.js +10 -4
- package/dist/interact/conditional-action.js +68 -0
- package/dist/interact/index.js +70 -14
- package/dist/interact/ios.js +14 -8
- package/dist/manage/android.js +4 -3
- package/dist/manage/ios.js +3 -2
- package/dist/observe/android.js +25 -4
- package/dist/observe/evidence.js +280 -0
- package/dist/observe/index.js +33 -6
- package/dist/observe/ios.js +25 -6
- package/dist/observe/readiness-service.js +124 -0
- package/dist/observe/readiness.js +88 -0
- package/dist/observe/recording.js +165 -0
- package/dist/observe/snapshot-metadata.js +10 -2
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +285 -0
- package/dist/server/common.js +9 -0
- package/dist/server/condition-schema.js +32 -0
- package/dist/server/debugger-handlers.js +38 -0
- package/dist/server/debugger-schema.js +42 -0
- package/dist/server/journey-schema.js +4 -2
- package/dist/server/readiness-schema.js +12 -0
- package/dist/server/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +51 -0
- package/dist/server/tool-handlers.js +294 -105
- package/dist/server-core.js +3 -3
- package/dist/server.js +34 -0
- package/dist/system/index.js +5 -4
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +4 -4
- package/dist/utils/java.js +14 -12
- package/dist/utils/operation-budget.js +26 -2
- package/dist/utils/resolve-device.js +9 -0
- package/docs/CHANGELOG.md +15 -0
- package/docs/specs/020-conditional-actions/intent.md +62 -0
- package/docs/specs/020-conditional-actions/plan.md +214 -0
- package/docs/specs/020-conditional-actions/spec.md +301 -0
- package/docs/specs/021-app-readiness-signals/intent.md +40 -0
- package/docs/specs/021-app-readiness-signals/plan.md +102 -0
- package/docs/specs/021-app-readiness-signals/spec.md +144 -0
- package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
- package/docs/specs/022-correlated-evidence/intent.md +37 -0
- package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
- package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
- package/docs/specs/022-correlated-evidence/plan.md +171 -0
- package/docs/specs/022-correlated-evidence/spec.md +123 -0
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +40 -0
- package/docs/specs/023-android-system-traces/plan.md +117 -0
- package/docs/specs/023-android-system-traces/spec.md +120 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
- package/docs/specs/process.md +213 -0
- package/docs/tools/TOOLS.md +5 -0
- package/docs/tools/debugger.md +104 -0
- package/docs/tools/evidence.md +82 -0
- package/docs/tools/interact.md +125 -2
- package/docs/tools/observe.md +70 -0
- package/docs/tools/system-traces.md +126 -0
- package/package.json +1 -1
- package/scripts/agent-plugin.mjs +68 -8
- package/src/debugger/android.ts +65 -0
- package/src/debugger/guard.ts +26 -0
- package/src/debugger/sessions.ts +168 -0
- package/src/debugger/worker.ts +122 -0
- package/src/interact/android.ts +10 -4
- package/src/interact/conditional-action.ts +75 -0
- package/src/interact/index.ts +57 -28
- package/src/interact/ios.ts +14 -8
- package/src/manage/android.ts +4 -3
- package/src/manage/ios.ts +3 -2
- package/src/observe/android.ts +33 -6
- package/src/observe/evidence.ts +262 -0
- package/src/observe/index.ts +32 -8
- package/src/observe/ios.ts +32 -8
- package/src/observe/readiness-service.ts +94 -0
- package/src/observe/readiness.ts +84 -0
- package/src/observe/snapshot-metadata.ts +12 -1
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +218 -0
- package/src/server/common.ts +7 -0
- package/src/server/condition-schema.ts +32 -0
- package/src/server/debugger-handlers.ts +29 -0
- package/src/server/debugger-schema.ts +37 -0
- package/src/server/journey-schema.ts +4 -2
- package/src/server/readiness-schema.ts +12 -0
- package/src/server/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +49 -0
- package/src/server/tool-handlers.ts +182 -21
- package/src/server-core.ts +3 -3
- package/src/server.ts +24 -0
- package/src/system/index.ts +7 -5
- package/src/types.ts +30 -1
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +4 -4
- package/src/utils/java.ts +13 -12
- package/src/utils/operation-budget.ts +24 -2
- package/src/utils/resolve-device.ts +10 -0
- package/test/device/automated/debugger/runtime.android.test.ts +203 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
- package/test/device/manual/interact/readiness.manual.ts +99 -0
- package/test/fixtures/debugger-app/MainActivity.java +46 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +13 -0
- package/test/fixtures/debugger-app/build.ts +29 -0
- package/test/fixtures/readiness-app/MainActivity.java +23 -0
- package/test/fixtures/readiness-app/README.md +12 -0
- package/test/fixtures/readiness-app/build.ts +37 -0
- package/test/fixtures/readiness-app/main.m +27 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/guards.test.ts +46 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
- package/test/unit/observe/compact_tree_error.test.ts +24 -0
- package/test/unit/observe/conditional_snapshots.test.ts +33 -0
- package/test/unit/observe/correlated-evidence.test.ts +128 -0
- package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/readiness.test.ts +41 -0
- package/test/unit/observe/readiness_native.test.ts +68 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/server/conditional_actions.test.ts +299 -0
- package/test/unit/server/conditional_schema.test.ts +59 -0
- package/test/unit/server/correlated-evidence.test.ts +194 -0
- package/test/unit/server/readiness.test.ts +170 -0
- package/test/unit/server/selector_tap_target.test.ts +79 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tap_selector.test.ts +2 -0
- package/worker/DebuggerWorker.java +160 -0
- package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
- /package/{skills → .agents/skills}/README.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
- /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
package/dist/interact/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { checkDebuggerGuard } from '../debugger/guard.js';
|
|
2
|
+
import { currentReadinessWait, observeReadinessWait, isDeviceUnavailable } from '../observe/readiness.js';
|
|
3
|
+
import { readinessDelay } from '../observe/readiness-service.js';
|
|
4
|
+
import { isConditionBlocked } from './conditional-action.js';
|
|
1
5
|
import { remainingBudget } from '../utils/operation-budget.js';
|
|
2
6
|
import { createHash } from 'crypto';
|
|
3
7
|
import { AndroidInteract } from './android.js';
|
|
@@ -419,6 +423,8 @@ export class ToolsInteract {
|
|
|
419
423
|
}
|
|
420
424
|
}
|
|
421
425
|
static async _captureFingerprint(platform, deviceId) {
|
|
426
|
+
if (isConditionBlocked())
|
|
427
|
+
return null;
|
|
422
428
|
try {
|
|
423
429
|
const fingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
424
430
|
return fingerprint?.fingerprint ?? null;
|
|
@@ -1907,6 +1913,7 @@ export class ToolsInteract {
|
|
|
1907
1913
|
}
|
|
1908
1914
|
}
|
|
1909
1915
|
catch (e) {
|
|
1916
|
+
checkDebuggerGuard();
|
|
1910
1917
|
console.error('Error scoring element:', e);
|
|
1911
1918
|
}
|
|
1912
1919
|
}
|
|
@@ -1916,6 +1923,7 @@ export class ToolsInteract {
|
|
|
1916
1923
|
}
|
|
1917
1924
|
}
|
|
1918
1925
|
catch (e) {
|
|
1926
|
+
checkDebuggerGuard();
|
|
1919
1927
|
console.error('Error fetching UI tree:', e);
|
|
1920
1928
|
}
|
|
1921
1929
|
if (shouldStop || Date.now() > deadline)
|
|
@@ -2018,6 +2026,7 @@ export class ToolsInteract {
|
|
|
2018
2026
|
}
|
|
2019
2027
|
}
|
|
2020
2028
|
catch (e) {
|
|
2029
|
+
checkDebuggerGuard();
|
|
2021
2030
|
console.error('Error resolving ancestor:', e);
|
|
2022
2031
|
}
|
|
2023
2032
|
if (!best)
|
|
@@ -2068,7 +2077,8 @@ export class ToolsInteract {
|
|
|
2068
2077
|
};
|
|
2069
2078
|
return { found: true, element: outEl, score: scoreVal, confidence: scoreVal, resolution };
|
|
2070
2079
|
}
|
|
2071
|
-
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId, singleObservation = false, singleAttempt = false, rejectAmbiguous = false }) {
|
|
2080
|
+
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId, singleObservation = false, singleAttempt = false, rejectAmbiguous = false, resolveTapTarget = false }) {
|
|
2081
|
+
const readinessTracker = currentReadinessWait();
|
|
2072
2082
|
const overallStart = Date.now();
|
|
2073
2083
|
const requestedIndex = typeof match?.index === 'number' ? match.index : null;
|
|
2074
2084
|
const requested = {
|
|
@@ -2127,7 +2137,12 @@ export class ToolsInteract {
|
|
|
2127
2137
|
remainingBudget();
|
|
2128
2138
|
totalPollCount++;
|
|
2129
2139
|
try {
|
|
2140
|
+
const acquisitionStarted = Date.now();
|
|
2130
2141
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
2142
|
+
observeReadinessWait(tree, acquisitionStarted);
|
|
2143
|
+
// A failed acquisition cannot establish that the requested tap target is absent.
|
|
2144
|
+
if (resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error)))
|
|
2145
|
+
throw new Error('ACTION_TIMEOUT');
|
|
2131
2146
|
const elements = (tree && Array.isArray(tree.elements)) ? tree.elements : [];
|
|
2132
2147
|
const matches = [];
|
|
2133
2148
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -2213,7 +2228,8 @@ export class ToolsInteract {
|
|
|
2213
2228
|
conditionMet = false;
|
|
2214
2229
|
}
|
|
2215
2230
|
else if (condition === 'clickable') {
|
|
2216
|
-
matchedElement = chosen ? (ToolsInteract.
|
|
2231
|
+
matchedElement = chosen ? (resolveTapTarget && (!ToolsInteract._isVisibleElement(chosen.el) || chosen.el.enabled === false)
|
|
2232
|
+
? chosen : ToolsInteract._resolveActionableAncestor(elements, chosen) || chosen) : null;
|
|
2217
2233
|
if (matchedElement) {
|
|
2218
2234
|
const b = matchedElement.el.bounds;
|
|
2219
2235
|
const visibleFlag = !!matchedElement.el.visible && Array.isArray(b) && b.length >= 4 && (b[2] > b[0] && b[3] > b[1]);
|
|
@@ -2224,6 +2240,13 @@ export class ToolsInteract {
|
|
|
2224
2240
|
else
|
|
2225
2241
|
conditionMet = false;
|
|
2226
2242
|
}
|
|
2243
|
+
// Waiting for a label to exist/be visible is distinct from resolving its
|
|
2244
|
+
// executable control. Reuse the clickable-wait ancestor policy without
|
|
2245
|
+
// promoting hidden or disabled labels into an unrelated enabled target.
|
|
2246
|
+
if (resolveTapTarget && conditionMet && matchedElement &&
|
|
2247
|
+
ToolsInteract._isVisibleElement(matchedElement.el) && matchedElement.el.enabled !== false) {
|
|
2248
|
+
matchedElement = ToolsInteract._resolveActionableAncestor(elements, matchedElement) ?? matchedElement;
|
|
2249
|
+
}
|
|
2227
2250
|
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
2228
2251
|
const resolvedDeviceId = tree?.device?.id || deviceId;
|
|
2229
2252
|
lastMatchedCount = matchedCount;
|
|
@@ -2236,7 +2259,7 @@ export class ToolsInteract {
|
|
|
2236
2259
|
if (matchedAt === null)
|
|
2237
2260
|
matchedAt = now;
|
|
2238
2261
|
stableMatchCount++;
|
|
2239
|
-
if (stableMatchCount >= stableObservationCount) {
|
|
2262
|
+
if (stableMatchCount >= stableObservationCount && (!readinessTracker || readinessTracker.summary.state === 'settled')) {
|
|
2240
2263
|
const latency_ms = now - overallStart;
|
|
2241
2264
|
const outEl = lastMatchedElement;
|
|
2242
2265
|
return {
|
|
@@ -2261,20 +2284,27 @@ export class ToolsInteract {
|
|
|
2261
2284
|
}
|
|
2262
2285
|
}
|
|
2263
2286
|
catch (e) {
|
|
2287
|
+
checkDebuggerGuard();
|
|
2288
|
+
// Deadline expiry does not invalidate the last successfully observed state.
|
|
2264
2289
|
if (e instanceof Error && e.message === 'ACTION_TIMEOUT')
|
|
2265
2290
|
throw e;
|
|
2291
|
+
if (readinessTracker && !String(e).includes('READINESS_'))
|
|
2292
|
+
readinessTracker.unavailable();
|
|
2293
|
+
if (readinessTracker && isDeviceUnavailable(e))
|
|
2294
|
+
throw e;
|
|
2266
2295
|
// Non-fatal per-poll error; record and continue
|
|
2267
|
-
|
|
2296
|
+
if (!readinessTracker)
|
|
2297
|
+
console.warn('waitForUI: poll error (non-fatal):', e instanceof Error ? e.message : String(e));
|
|
2268
2298
|
}
|
|
2269
2299
|
if (singleAttempt)
|
|
2270
2300
|
break;
|
|
2271
2301
|
// Sleep until next poll
|
|
2272
|
-
await new Promise(r => setTimeout(r, effectivePoll || 50));
|
|
2302
|
+
await (readinessTracker ? readinessDelay(effectivePoll || 50) : new Promise(r => setTimeout(r, effectivePoll || 50)));
|
|
2273
2303
|
}
|
|
2274
2304
|
// Attempt timed out; if more attempts allowed, backoff then retry
|
|
2275
2305
|
if (attempts < maxAttempts) {
|
|
2276
2306
|
if (backoff > 0)
|
|
2277
|
-
await new Promise(r => setTimeout(r, backoff));
|
|
2307
|
+
await (readinessTracker ? readinessDelay(backoff) : new Promise(r => setTimeout(r, backoff)));
|
|
2278
2308
|
continue;
|
|
2279
2309
|
}
|
|
2280
2310
|
// Final failure for this call
|
|
@@ -2290,7 +2320,7 @@ export class ToolsInteract {
|
|
|
2290
2320
|
: ` observed ${lastMatchedCount} match(es)`;
|
|
2291
2321
|
return {
|
|
2292
2322
|
status: 'timeout',
|
|
2293
|
-
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
2323
|
+
error: { code: readinessTracker ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
2294
2324
|
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
2295
2325
|
requested,
|
|
2296
2326
|
observed
|
|
@@ -2298,17 +2328,20 @@ export class ToolsInteract {
|
|
|
2298
2328
|
}
|
|
2299
2329
|
}
|
|
2300
2330
|
catch (err) {
|
|
2301
|
-
|
|
2331
|
+
checkDebuggerGuard();
|
|
2332
|
+
if (!readinessTracker && err instanceof Error && err.message === 'ACTION_TIMEOUT')
|
|
2333
|
+
throw err;
|
|
2334
|
+
if (readinessTracker && isDeviceUnavailable(err))
|
|
2302
2335
|
throw err;
|
|
2303
2336
|
const elapsed = Date.now() - overallStart;
|
|
2304
2337
|
return {
|
|
2305
2338
|
status: 'timeout',
|
|
2306
|
-
error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
|
|
2339
|
+
error: { code: readinessTracker && String(err).includes('ACTION_TIMEOUT') ? 'TIMEOUT' : 'INTERNAL_ERROR', message: readinessTracker ? 'Readiness predicate wait did not complete' : err instanceof Error ? err.message : String(err) },
|
|
2307
2340
|
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
2308
2341
|
requested,
|
|
2309
2342
|
observed: {
|
|
2310
2343
|
matched_count: lastMatchedCount,
|
|
2311
|
-
condition_satisfied: false,
|
|
2344
|
+
condition_satisfied: readinessTracker ? lastConditionSatisfied : false,
|
|
2312
2345
|
selected_index: lastMatchedElement?.index ?? null,
|
|
2313
2346
|
last_matched_element: lastMatchedElement
|
|
2314
2347
|
}
|
|
@@ -2370,12 +2403,14 @@ export class ToolsInteract {
|
|
|
2370
2403
|
continue;
|
|
2371
2404
|
}
|
|
2372
2405
|
catch (e) {
|
|
2406
|
+
checkDebuggerGuard();
|
|
2373
2407
|
console.error('Error confirming fingerprint:', e);
|
|
2374
2408
|
continue;
|
|
2375
2409
|
}
|
|
2376
2410
|
}
|
|
2377
2411
|
}
|
|
2378
2412
|
catch (e) {
|
|
2413
|
+
checkDebuggerGuard();
|
|
2379
2414
|
console.error('Error getting screen fingerprint:', e);
|
|
2380
2415
|
}
|
|
2381
2416
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
@@ -2393,6 +2428,7 @@ export class ToolsInteract {
|
|
|
2393
2428
|
};
|
|
2394
2429
|
}
|
|
2395
2430
|
static async waitForUIChangeHandler({ platform, deviceId, timeout_ms = 60000, stability_window_ms = 300, expected_change, scope = 'screen', target = null }) {
|
|
2431
|
+
const readinessTracker = currentReadinessWait();
|
|
2396
2432
|
const start = Date.now();
|
|
2397
2433
|
const pollIntervalMs = 300;
|
|
2398
2434
|
const stabilityWindow = Math.max(0, typeof stability_window_ms === 'number' ? stability_window_ms : 300);
|
|
@@ -2415,7 +2451,11 @@ export class ToolsInteract {
|
|
|
2415
2451
|
};
|
|
2416
2452
|
while (Date.now() - start < timeout_ms) {
|
|
2417
2453
|
try {
|
|
2454
|
+
if (readinessTracker)
|
|
2455
|
+
remainingBudget();
|
|
2456
|
+
const acquisitionStarted = Date.now();
|
|
2418
2457
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
2458
|
+
observeReadinessWait(tree, acquisitionStarted);
|
|
2419
2459
|
const scopedTree = ToolsInteract._resolveUiChangeScope(tree, scope, target);
|
|
2420
2460
|
if (scopedTree.error) {
|
|
2421
2461
|
lastScopeResolution = scopedTree.resolution;
|
|
@@ -2461,7 +2501,7 @@ export class ToolsInteract {
|
|
|
2461
2501
|
candidateSinceMs = Date.now();
|
|
2462
2502
|
}
|
|
2463
2503
|
const stableForMs = candidateSinceMs === null ? 0 : Date.now() - candidateSinceMs;
|
|
2464
|
-
if (stabilityWindow === 0 || stableForMs >= stabilityWindow) {
|
|
2504
|
+
if ((stabilityWindow === 0 || stableForMs >= stabilityWindow) && (!readinessTracker || readinessTracker.summary.state === 'settled')) {
|
|
2465
2505
|
return {
|
|
2466
2506
|
success: true,
|
|
2467
2507
|
observed_change: candidateObservedChange ?? observedChange,
|
|
@@ -2486,10 +2526,19 @@ export class ToolsInteract {
|
|
|
2486
2526
|
}
|
|
2487
2527
|
}
|
|
2488
2528
|
}
|
|
2489
|
-
catch {
|
|
2490
|
-
|
|
2529
|
+
catch (error) {
|
|
2530
|
+
checkDebuggerGuard();
|
|
2531
|
+
if (readinessTracker && !String(error).includes('READINESS_'))
|
|
2532
|
+
readinessTracker.unavailable();
|
|
2533
|
+
if (readinessTracker && isDeviceUnavailable(error))
|
|
2534
|
+
throw error;
|
|
2535
|
+
if (readinessTracker && String(error).includes('ACTION_TIMEOUT')) {
|
|
2536
|
+
readinessTracker.deadline();
|
|
2537
|
+
break;
|
|
2538
|
+
}
|
|
2539
|
+
// Default waits retain best-effort polling.
|
|
2491
2540
|
}
|
|
2492
|
-
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
2541
|
+
await (readinessTracker ? readinessDelay(pollIntervalMs) : new Promise(resolve => setTimeout(resolve, pollIntervalMs)));
|
|
2493
2542
|
}
|
|
2494
2543
|
return {
|
|
2495
2544
|
success: false,
|
|
@@ -2999,6 +3048,7 @@ export class ToolsInteract {
|
|
|
2999
3048
|
}
|
|
3000
3049
|
}
|
|
3001
3050
|
catch (err) {
|
|
3051
|
+
checkDebuggerGuard();
|
|
3002
3052
|
try {
|
|
3003
3053
|
console.warn('waitForUI: failed to get baseline data (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
3004
3054
|
}
|
|
@@ -3053,10 +3103,12 @@ export class ToolsInteract {
|
|
|
3053
3103
|
}
|
|
3054
3104
|
}
|
|
3055
3105
|
catch (err) {
|
|
3106
|
+
checkDebuggerGuard();
|
|
3056
3107
|
console.error('waitForUI(ui) find error:', err);
|
|
3057
3108
|
}
|
|
3058
3109
|
}
|
|
3059
3110
|
catch (err) {
|
|
3111
|
+
checkDebuggerGuard();
|
|
3060
3112
|
console.error('waitForUI(ui) outer error:', err);
|
|
3061
3113
|
}
|
|
3062
3114
|
}
|
|
@@ -3090,6 +3142,7 @@ export class ToolsInteract {
|
|
|
3090
3142
|
}
|
|
3091
3143
|
}
|
|
3092
3144
|
catch (err) {
|
|
3145
|
+
checkDebuggerGuard();
|
|
3093
3146
|
console.error('waitForUI(log) error:', err);
|
|
3094
3147
|
}
|
|
3095
3148
|
}
|
|
@@ -3118,6 +3171,7 @@ export class ToolsInteract {
|
|
|
3118
3171
|
}
|
|
3119
3172
|
}
|
|
3120
3173
|
catch (err) {
|
|
3174
|
+
checkDebuggerGuard();
|
|
3121
3175
|
console.error('waitForUI(screen) error:', err);
|
|
3122
3176
|
}
|
|
3123
3177
|
}
|
|
@@ -3142,6 +3196,7 @@ export class ToolsInteract {
|
|
|
3142
3196
|
}
|
|
3143
3197
|
}
|
|
3144
3198
|
catch (err) {
|
|
3199
|
+
checkDebuggerGuard();
|
|
3145
3200
|
console.error('waitForUI(idle) error:', err);
|
|
3146
3201
|
}
|
|
3147
3202
|
}
|
|
@@ -3159,6 +3214,7 @@ export class ToolsInteract {
|
|
|
3159
3214
|
snapshot = await Obs.ToolsObserve.captureDebugSnapshotHandler({ reason: `wait_for_ui timeout for ${type}`, includeLogs: true, platform, deviceId });
|
|
3160
3215
|
}
|
|
3161
3216
|
catch (err) {
|
|
3217
|
+
checkDebuggerGuard();
|
|
3162
3218
|
snapshot = { error: err instanceof Error ? err.message : String(err) };
|
|
3163
3219
|
}
|
|
3164
3220
|
}
|
package/dist/interact/ios.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { evidenceDispatch } from '../observe/evidence.js';
|
|
2
|
+
import { beforeInputDispatch } from './conditional-action.js';
|
|
1
3
|
import { remainingBudget, markDispatchStarted } from '../utils/operation-budget.js';
|
|
2
4
|
import { spawn } from "child_process";
|
|
3
5
|
import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "../utils/ios/utils.js";
|
|
@@ -24,9 +26,11 @@ export class iOSInteract {
|
|
|
24
26
|
if (targetUdid) {
|
|
25
27
|
args.push('--udid', targetUdid);
|
|
26
28
|
}
|
|
27
|
-
await
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
await beforeInputDispatch('ios', device.id);
|
|
30
|
+
const timeout = remainingBudget() || undefined;
|
|
31
|
+
markDispatchStarted();
|
|
32
|
+
await evidenceDispatch({ platform: 'ios', id: device.id }, () => new Promise((resolve, reject) => {
|
|
33
|
+
const proc = spawn(getIdbCmd(), args, { timeout });
|
|
30
34
|
let stderr = '';
|
|
31
35
|
proc.stderr.on('data', d => stderr += d.toString());
|
|
32
36
|
proc.on('close', code => {
|
|
@@ -36,7 +40,7 @@ export class iOSInteract {
|
|
|
36
40
|
reject(new Error(`idb ui tap failed: ${stderr}`));
|
|
37
41
|
});
|
|
38
42
|
proc.on('error', err => reject(err));
|
|
39
|
-
});
|
|
43
|
+
}));
|
|
40
44
|
return { device, success: true, x, y };
|
|
41
45
|
}
|
|
42
46
|
catch (e) {
|
|
@@ -64,9 +68,11 @@ export class iOSInteract {
|
|
|
64
68
|
if (targetUdid) {
|
|
65
69
|
args.push('--udid', targetUdid);
|
|
66
70
|
}
|
|
67
|
-
await
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
await beforeInputDispatch('ios', device.id);
|
|
72
|
+
const timeout = remainingBudget() || undefined;
|
|
73
|
+
markDispatchStarted();
|
|
74
|
+
await evidenceDispatch({ platform: 'ios', id: device.id }, () => new Promise((resolve, reject) => {
|
|
75
|
+
const proc = spawn(getIdbCmd(), args, { timeout });
|
|
70
76
|
let stderr = '';
|
|
71
77
|
proc.stderr.on('data', d => stderr += d.toString());
|
|
72
78
|
proc.on('close', code => {
|
|
@@ -76,7 +82,7 @@ export class iOSInteract {
|
|
|
76
82
|
reject(new Error(`idb ui swipe failed: ${stderr}`));
|
|
77
83
|
});
|
|
78
84
|
proc.on('error', err => reject(err));
|
|
79
|
-
});
|
|
85
|
+
}));
|
|
80
86
|
return { device, success: true, start: [x1, y1], end: [x2, y2], duration };
|
|
81
87
|
}
|
|
82
88
|
catch (e) {
|
package/dist/manage/android.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evidenceDispatch } from '../observe/evidence.js';
|
|
1
2
|
import { hasFiniteBudget, markDispatchStarted, remainingBudget, runWithBudget } from '../utils/operation-budget.js';
|
|
2
3
|
import { promises as fs } from 'fs';
|
|
3
4
|
import { spawn } from 'child_process';
|
|
@@ -152,7 +153,7 @@ export class AndroidManage {
|
|
|
152
153
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
153
154
|
try {
|
|
154
155
|
markDispatchStarted();
|
|
155
|
-
const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId, { timeout: timeoutMs });
|
|
156
|
+
const output = await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId, { timeout: timeoutMs }));
|
|
156
157
|
const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null);
|
|
157
158
|
return {
|
|
158
159
|
device: deviceInfo,
|
|
@@ -178,7 +179,7 @@ export class AndroidManage {
|
|
|
178
179
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
179
180
|
try {
|
|
180
181
|
markDispatchStarted();
|
|
181
|
-
await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
|
|
182
|
+
await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'am', 'force-stop', appId], deviceId));
|
|
182
183
|
return { device: deviceInfo, appTerminated: true };
|
|
183
184
|
}
|
|
184
185
|
catch (e) {
|
|
@@ -211,7 +212,7 @@ export class AndroidManage {
|
|
|
211
212
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
212
213
|
try {
|
|
213
214
|
markDispatchStarted();
|
|
214
|
-
const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
|
|
215
|
+
const output = await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'pm', 'clear', appId], deviceId));
|
|
215
216
|
return { device: deviceInfo, dataCleared: output === 'Success' };
|
|
216
217
|
}
|
|
217
218
|
catch (e) {
|
package/dist/manage/ios.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evidenceDispatch } from '../observe/evidence.js';
|
|
1
2
|
import { hasFiniteBudget, markDispatchStarted, remainingBudget, runWithBudget } from '../utils/operation-budget.js';
|
|
2
3
|
import { promises as fs } from "fs";
|
|
3
4
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -293,7 +294,7 @@ export class iOSManage {
|
|
|
293
294
|
validateBundleId(bundleId);
|
|
294
295
|
try {
|
|
295
296
|
markDispatchStarted();
|
|
296
|
-
const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
297
|
+
const result = await evidenceDispatch({ platform: 'ios', id: deviceId }, () => execCommand(['simctl', 'launch', deviceId, bundleId], deviceId));
|
|
297
298
|
const device = await getIOSDeviceMetadata(deviceId);
|
|
298
299
|
const fingerprint = observe ? await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null) : null;
|
|
299
300
|
const pidMatch = result.output.match(/:\s*(\d+)\s*$/);
|
|
@@ -320,7 +321,7 @@ export class iOSManage {
|
|
|
320
321
|
validateBundleId(bundleId);
|
|
321
322
|
try {
|
|
322
323
|
markDispatchStarted();
|
|
323
|
-
await execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId);
|
|
324
|
+
await evidenceDispatch({ platform: 'ios', id: deviceId }, () => execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId));
|
|
324
325
|
const device = await getIOSDeviceMetadata(deviceId);
|
|
325
326
|
return { device, appTerminated: true };
|
|
326
327
|
}
|
package/dist/observe/android.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureEvidence } from './evidence.js';
|
|
1
2
|
import { spawn } from "child_process";
|
|
2
3
|
import { XMLParser } from "fast-xml-parser";
|
|
3
4
|
import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, getScreenResolution, traverseNode, parseLogLine } from "../utils/android/utils.js";
|
|
@@ -11,10 +12,14 @@ import { remainingBudget } from '../utils/operation-budget.js';
|
|
|
11
12
|
import { createObservationMeasurement } from './performance-metrics.js';
|
|
12
13
|
const activeLogStreams = new Map();
|
|
13
14
|
export class AndroidObserve {
|
|
15
|
+
getLogStreamDevice(sessionId = 'default') { return activeLogStreams.get(sessionId)?.device ?? null; }
|
|
14
16
|
async getDeviceMetadata(appId, deviceId) {
|
|
15
17
|
return getAndroidDeviceMetadata(appId, deviceId);
|
|
16
18
|
}
|
|
17
19
|
async getUITree(deviceId) {
|
|
20
|
+
return captureEvidence('ui_tree', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId));
|
|
21
|
+
}
|
|
22
|
+
async collectGetUITree(deviceId) {
|
|
18
23
|
const measurement = createObservationMeasurement('android');
|
|
19
24
|
let metadata;
|
|
20
25
|
try {
|
|
@@ -113,6 +118,9 @@ export class AndroidObserve {
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
async getLogs(filters = {}) {
|
|
121
|
+
return captureEvidence('logs', { platform: 'android', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters));
|
|
122
|
+
}
|
|
123
|
+
async collectGetLogs(filters = {}) {
|
|
116
124
|
const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters;
|
|
117
125
|
const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
|
|
118
126
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
@@ -213,6 +221,9 @@ export class AndroidObserve {
|
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
async captureScreen(deviceId) {
|
|
224
|
+
return captureEvidence('screenshot', { platform: 'android', id: deviceId ?? '' }, () => this.collectCaptureScreen(deviceId));
|
|
225
|
+
}
|
|
226
|
+
async collectCaptureScreen(deviceId) {
|
|
216
227
|
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
217
228
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
218
229
|
return new Promise((resolve, reject) => {
|
|
@@ -331,6 +342,9 @@ export class AndroidObserve {
|
|
|
331
342
|
});
|
|
332
343
|
}
|
|
333
344
|
async getCurrentScreen(deviceId) {
|
|
345
|
+
return captureEvidence('activity', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetCurrentScreen(deviceId));
|
|
346
|
+
}
|
|
347
|
+
async collectGetCurrentScreen(deviceId) {
|
|
334
348
|
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
335
349
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
336
350
|
try {
|
|
@@ -385,12 +399,15 @@ export class AndroidObserve {
|
|
|
385
399
|
}
|
|
386
400
|
}
|
|
387
401
|
async getScreenFingerprint(deviceId) {
|
|
402
|
+
return captureEvidence('fingerprint', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId));
|
|
403
|
+
}
|
|
404
|
+
async collectGetScreenFingerprint(deviceId) {
|
|
388
405
|
try {
|
|
389
406
|
const tree = await this.getUITree(deviceId);
|
|
390
407
|
if (!tree || tree.error)
|
|
391
408
|
return { fingerprint: null, error: tree.error };
|
|
392
409
|
const current = await this.getCurrentScreen(deviceId).catch(() => null);
|
|
393
|
-
return computeScreenFingerprint(tree, current, 'android', 50);
|
|
410
|
+
return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms };
|
|
394
411
|
}
|
|
395
412
|
catch (e) {
|
|
396
413
|
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
|
|
@@ -411,7 +428,7 @@ export class AndroidObserve {
|
|
|
411
428
|
catch { }
|
|
412
429
|
activeLogStreams.delete(sessionId);
|
|
413
430
|
}
|
|
414
|
-
const args = ['logcat', `--pid=${pid}`, filter];
|
|
431
|
+
const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter];
|
|
415
432
|
const proc = spawn(getAdbCmd(), args);
|
|
416
433
|
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
417
434
|
const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
|
|
@@ -434,9 +451,10 @@ export class AndroidObserve {
|
|
|
434
451
|
});
|
|
435
452
|
proc.on('close', () => {
|
|
436
453
|
stream.end();
|
|
437
|
-
activeLogStreams.
|
|
454
|
+
if (activeLogStreams.get(sessionId)?.proc === proc)
|
|
455
|
+
activeLogStreams.delete(sessionId);
|
|
438
456
|
});
|
|
439
|
-
activeLogStreams.set(sessionId, { proc, file });
|
|
457
|
+
activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } });
|
|
440
458
|
return { success: true, stream_started: true };
|
|
441
459
|
}
|
|
442
460
|
catch (e) {
|
|
@@ -455,6 +473,9 @@ export class AndroidObserve {
|
|
|
455
473
|
return { success: true };
|
|
456
474
|
}
|
|
457
475
|
async readLogStream(sessionId = 'default', limit = 100, since) {
|
|
476
|
+
return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since));
|
|
477
|
+
}
|
|
478
|
+
async collectReadLogStream(sessionId = 'default', limit = 100, since) {
|
|
458
479
|
const entry = activeLogStreams.get(sessionId);
|
|
459
480
|
let file;
|
|
460
481
|
if (entry && entry.file)
|