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/src/interact/index.ts
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';
|
|
@@ -605,6 +609,7 @@ export class ToolsInteract {
|
|
|
605
609
|
}
|
|
606
610
|
|
|
607
611
|
private static async _captureFingerprint(platform: 'android' | 'ios', deviceId?: string): Promise<string | null> {
|
|
612
|
+
if (isConditionBlocked()) return null
|
|
608
613
|
try {
|
|
609
614
|
const fingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
610
615
|
return fingerprint?.fingerprint ?? null
|
|
@@ -2345,13 +2350,13 @@ export class ToolsInteract {
|
|
|
2345
2350
|
break
|
|
2346
2351
|
}
|
|
2347
2352
|
}
|
|
2348
|
-
} catch (e) { console.error('Error scoring element:', e) }
|
|
2353
|
+
} catch (e) { checkDebuggerGuard(); console.error('Error scoring element:', e) }
|
|
2349
2354
|
}
|
|
2350
2355
|
if (iterationImprovedBest) {
|
|
2351
2356
|
bestIterationCandidates = iterationCandidates.slice()
|
|
2352
2357
|
}
|
|
2353
2358
|
}
|
|
2354
|
-
} catch (e) { console.error('Error fetching UI tree:', e) }
|
|
2359
|
+
} catch (e) { checkDebuggerGuard(); console.error('Error fetching UI tree:', e) }
|
|
2355
2360
|
if (shouldStop || Date.now() > deadline) break
|
|
2356
2361
|
await new Promise(r => setTimeout(r, 100))
|
|
2357
2362
|
}
|
|
@@ -2439,7 +2444,7 @@ export class ToolsInteract {
|
|
|
2439
2444
|
}
|
|
2440
2445
|
}
|
|
2441
2446
|
}
|
|
2442
|
-
} catch (e) { console.error('Error resolving ancestor:', e) }
|
|
2447
|
+
} catch (e) { checkDebuggerGuard(); console.error('Error resolving ancestor:', e) }
|
|
2443
2448
|
|
|
2444
2449
|
if (!best) return { found: false, error: 'Element not found' }
|
|
2445
2450
|
|
|
@@ -2491,7 +2496,8 @@ export class ToolsInteract {
|
|
|
2491
2496
|
return { found: true, element: outEl, score: scoreVal, confidence: scoreVal, resolution }
|
|
2492
2497
|
}
|
|
2493
2498
|
|
|
2494
|
-
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 }: { selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }, condition?: 'exists'|'not_exists'|'visible'|'clickable', timeout_ms?: number, poll_interval_ms?: number, match?: { index?: number }, retry?: { max_attempts?: number, backoff_ms?: number }, platform?: 'android'|'ios', deviceId?: string, singleObservation?: boolean, singleAttempt?: boolean, rejectAmbiguous?: boolean }) {
|
|
2499
|
+
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 }: { selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }, condition?: 'exists'|'not_exists'|'visible'|'clickable', timeout_ms?: number, poll_interval_ms?: number, match?: { index?: number }, retry?: { max_attempts?: number, backoff_ms?: number }, platform?: 'android'|'ios', deviceId?: string, singleObservation?: boolean, singleAttempt?: boolean, rejectAmbiguous?: boolean, resolveTapTarget?: boolean }) {
|
|
2500
|
+
const readinessTracker = currentReadinessWait()
|
|
2495
2501
|
const overallStart = Date.now()
|
|
2496
2502
|
const requestedIndex = typeof match?.index === 'number' ? match.index : null
|
|
2497
2503
|
const requested = {
|
|
@@ -2559,7 +2565,11 @@ export class ToolsInteract {
|
|
|
2559
2565
|
remainingBudget()
|
|
2560
2566
|
totalPollCount++
|
|
2561
2567
|
try {
|
|
2568
|
+
const acquisitionStarted = Date.now()
|
|
2562
2569
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId }) as any
|
|
2570
|
+
observeReadinessWait(tree, acquisitionStarted)
|
|
2571
|
+
// A failed acquisition cannot establish that the requested tap target is absent.
|
|
2572
|
+
if (resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error))) throw new Error('ACTION_TIMEOUT')
|
|
2563
2573
|
const elements = (tree && Array.isArray(tree.elements)) ? tree.elements as any[] : []
|
|
2564
2574
|
|
|
2565
2575
|
const matches: { el: any, idx: number }[] = []
|
|
@@ -2633,7 +2643,8 @@ export class ToolsInteract {
|
|
|
2633
2643
|
conditionMet = visibleFlag
|
|
2634
2644
|
} else conditionMet = false
|
|
2635
2645
|
} else if (condition === 'clickable') {
|
|
2636
|
-
matchedElement = chosen ? (ToolsInteract.
|
|
2646
|
+
matchedElement = chosen ? (resolveTapTarget && (!ToolsInteract._isVisibleElement(chosen.el) || chosen.el.enabled === false)
|
|
2647
|
+
? chosen : ToolsInteract._resolveActionableAncestor(elements, chosen as { el: UiElement, idx: number }) || chosen) : null
|
|
2637
2648
|
if (matchedElement) {
|
|
2638
2649
|
const b = matchedElement.el.bounds
|
|
2639
2650
|
const visibleFlag = !!matchedElement.el.visible && Array.isArray(b) && b.length >= 4 && (b[2] > b[0] && b[3] > b[1])
|
|
@@ -2643,6 +2654,13 @@ export class ToolsInteract {
|
|
|
2643
2654
|
} else conditionMet = false
|
|
2644
2655
|
}
|
|
2645
2656
|
|
|
2657
|
+
// Waiting for a label to exist/be visible is distinct from resolving its
|
|
2658
|
+
// executable control. Reuse the clickable-wait ancestor policy without
|
|
2659
|
+
// promoting hidden or disabled labels into an unrelated enabled target.
|
|
2660
|
+
if (resolveTapTarget && conditionMet && matchedElement &&
|
|
2661
|
+
ToolsInteract._isVisibleElement(matchedElement.el) && matchedElement.el.enabled !== false) {
|
|
2662
|
+
matchedElement = ToolsInteract._resolveActionableAncestor(elements, matchedElement) ?? matchedElement
|
|
2663
|
+
}
|
|
2646
2664
|
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android')
|
|
2647
2665
|
const resolvedDeviceId = tree?.device?.id || deviceId
|
|
2648
2666
|
lastMatchedCount = matchedCount
|
|
@@ -2656,7 +2674,7 @@ export class ToolsInteract {
|
|
|
2656
2674
|
if (conditionMet && snapshotFresh) {
|
|
2657
2675
|
if (matchedAt === null) matchedAt = now
|
|
2658
2676
|
stableMatchCount++
|
|
2659
|
-
if (stableMatchCount >= stableObservationCount) {
|
|
2677
|
+
if (stableMatchCount >= stableObservationCount && (!readinessTracker || readinessTracker.summary.state === 'settled')) {
|
|
2660
2678
|
const latency_ms = now - overallStart
|
|
2661
2679
|
const outEl = lastMatchedElement
|
|
2662
2680
|
|
|
@@ -2680,20 +2698,23 @@ export class ToolsInteract {
|
|
|
2680
2698
|
matchedAt = null
|
|
2681
2699
|
}
|
|
2682
2700
|
|
|
2683
|
-
} catch (e) {
|
|
2701
|
+
} catch (e) { checkDebuggerGuard();
|
|
2702
|
+
// Deadline expiry does not invalidate the last successfully observed state.
|
|
2684
2703
|
if (e instanceof Error && e.message === 'ACTION_TIMEOUT') throw e
|
|
2704
|
+
if (readinessTracker && !String(e).includes('READINESS_')) readinessTracker.unavailable()
|
|
2705
|
+
if (readinessTracker && isDeviceUnavailable(e)) throw e
|
|
2685
2706
|
// Non-fatal per-poll error; record and continue
|
|
2686
|
-
console.warn('waitForUI: poll error (non-fatal):', e instanceof Error ? e.message : String(e))
|
|
2707
|
+
if (!readinessTracker) console.warn('waitForUI: poll error (non-fatal):', e instanceof Error ? e.message : String(e))
|
|
2687
2708
|
}
|
|
2688
2709
|
|
|
2689
2710
|
if (singleAttempt) break
|
|
2690
2711
|
// Sleep until next poll
|
|
2691
|
-
await new Promise(r => setTimeout(r, effectivePoll || 50))
|
|
2712
|
+
await (readinessTracker ? readinessDelay(effectivePoll || 50) : new Promise(r => setTimeout(r, effectivePoll || 50)))
|
|
2692
2713
|
}
|
|
2693
2714
|
|
|
2694
2715
|
// Attempt timed out; if more attempts allowed, backoff then retry
|
|
2695
2716
|
if (attempts < maxAttempts) {
|
|
2696
|
-
if (backoff > 0) await new Promise(r => setTimeout(r, backoff))
|
|
2717
|
+
if (backoff > 0) await (readinessTracker ? readinessDelay(backoff) : new Promise(r => setTimeout(r, backoff)))
|
|
2697
2718
|
continue
|
|
2698
2719
|
}
|
|
2699
2720
|
|
|
@@ -2710,24 +2731,25 @@ export class ToolsInteract {
|
|
|
2710
2731
|
: ` observed ${lastMatchedCount} match(es)`
|
|
2711
2732
|
return {
|
|
2712
2733
|
status: 'timeout',
|
|
2713
|
-
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
2734
|
+
error: { code: readinessTracker ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
2714
2735
|
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
2715
2736
|
requested,
|
|
2716
2737
|
observed
|
|
2717
2738
|
}
|
|
2718
2739
|
}
|
|
2719
2740
|
|
|
2720
|
-
} catch (err) {
|
|
2721
|
-
if (err instanceof Error && err.message === 'ACTION_TIMEOUT') throw err
|
|
2741
|
+
} catch (err) { checkDebuggerGuard();
|
|
2742
|
+
if (!readinessTracker && err instanceof Error && err.message === 'ACTION_TIMEOUT') throw err
|
|
2743
|
+
if (readinessTracker && isDeviceUnavailable(err)) throw err
|
|
2722
2744
|
const elapsed = Date.now() - overallStart
|
|
2723
2745
|
return {
|
|
2724
2746
|
status: 'timeout',
|
|
2725
|
-
error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
|
|
2747
|
+
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) },
|
|
2726
2748
|
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
2727
2749
|
requested,
|
|
2728
2750
|
observed: {
|
|
2729
2751
|
matched_count: lastMatchedCount,
|
|
2730
|
-
condition_satisfied: false,
|
|
2752
|
+
condition_satisfied: readinessTracker ? lastConditionSatisfied : false,
|
|
2731
2753
|
selected_index: lastMatchedElement?.index ?? null,
|
|
2732
2754
|
last_matched_element: lastMatchedElement
|
|
2733
2755
|
}
|
|
@@ -2784,9 +2806,9 @@ export class ToolsInteract {
|
|
|
2784
2806
|
}
|
|
2785
2807
|
lastFingerprint = confirmFp
|
|
2786
2808
|
continue
|
|
2787
|
-
} catch (e) { console.error('Error confirming fingerprint:', e); continue }
|
|
2809
|
+
} catch (e) { checkDebuggerGuard(); console.error('Error confirming fingerprint:', e); continue }
|
|
2788
2810
|
}
|
|
2789
|
-
} catch (e) { console.error('Error getting screen fingerprint:', e) }
|
|
2811
|
+
} catch (e) { checkDebuggerGuard(); console.error('Error getting screen fingerprint:', e) }
|
|
2790
2812
|
|
|
2791
2813
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
2792
2814
|
}
|
|
@@ -2821,6 +2843,7 @@ export class ToolsInteract {
|
|
|
2821
2843
|
scope?: 'screen' | 'subtree',
|
|
2822
2844
|
target?: string | null
|
|
2823
2845
|
}): Promise<WaitForUIChangeResponse> {
|
|
2846
|
+
const readinessTracker = currentReadinessWait()
|
|
2824
2847
|
const start = Date.now()
|
|
2825
2848
|
const pollIntervalMs = 300
|
|
2826
2849
|
const stabilityWindow = Math.max(0, typeof stability_window_ms === 'number' ? stability_window_ms : 300)
|
|
@@ -2844,7 +2867,10 @@ export class ToolsInteract {
|
|
|
2844
2867
|
|
|
2845
2868
|
while (Date.now() - start < timeout_ms) {
|
|
2846
2869
|
try {
|
|
2870
|
+
if (readinessTracker) remainingBudget()
|
|
2871
|
+
const acquisitionStarted = Date.now()
|
|
2847
2872
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId }) as any
|
|
2873
|
+
observeReadinessWait(tree, acquisitionStarted)
|
|
2848
2874
|
const scopedTree = ToolsInteract._resolveUiChangeScope(tree, scope, target)
|
|
2849
2875
|
if (scopedTree.error) {
|
|
2850
2876
|
lastScopeResolution = scopedTree.resolution
|
|
@@ -2892,7 +2918,7 @@ export class ToolsInteract {
|
|
|
2892
2918
|
}
|
|
2893
2919
|
|
|
2894
2920
|
const stableForMs = candidateSinceMs === null ? 0 : Date.now() - candidateSinceMs
|
|
2895
|
-
if (stabilityWindow === 0 || stableForMs >= stabilityWindow) {
|
|
2921
|
+
if ((stabilityWindow === 0 || stableForMs >= stabilityWindow) && (!readinessTracker || readinessTracker.summary.state === 'settled')) {
|
|
2896
2922
|
return {
|
|
2897
2923
|
success: true,
|
|
2898
2924
|
observed_change: candidateObservedChange ?? observedChange,
|
|
@@ -2915,11 +2941,14 @@ export class ToolsInteract {
|
|
|
2915
2941
|
candidateSinceMs = null
|
|
2916
2942
|
}
|
|
2917
2943
|
}
|
|
2918
|
-
} catch {
|
|
2919
|
-
|
|
2944
|
+
} catch (error) { checkDebuggerGuard();
|
|
2945
|
+
if (readinessTracker && !String(error).includes('READINESS_')) readinessTracker.unavailable()
|
|
2946
|
+
if (readinessTracker && isDeviceUnavailable(error)) throw error
|
|
2947
|
+
if (readinessTracker && String(error).includes('ACTION_TIMEOUT')) { readinessTracker.deadline(); break }
|
|
2948
|
+
// Default waits retain best-effort polling.
|
|
2920
2949
|
}
|
|
2921
2950
|
|
|
2922
|
-
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
2951
|
+
await (readinessTracker ? readinessDelay(pollIntervalMs) : new Promise(resolve => setTimeout(resolve, pollIntervalMs)))
|
|
2923
2952
|
}
|
|
2924
2953
|
|
|
2925
2954
|
return {
|
|
@@ -3505,7 +3534,7 @@ export class ToolsInteract {
|
|
|
3505
3534
|
const msgs = ToolsInteract._logsToMessages(logsArr)
|
|
3506
3535
|
baselineLastLine = msgs.length ? msgs[msgs.length - 1] : null
|
|
3507
3536
|
}
|
|
3508
|
-
} catch (err) {
|
|
3537
|
+
} catch (err) { checkDebuggerGuard();
|
|
3509
3538
|
try { console.warn('waitForUI: failed to get baseline data (non-fatal):', err instanceof Error ? err.message : String(err)) } catch { }
|
|
3510
3539
|
}
|
|
3511
3540
|
|
|
@@ -3555,8 +3584,8 @@ export class ToolsInteract {
|
|
|
3555
3584
|
stableDuration = 0
|
|
3556
3585
|
lastObservedState = false
|
|
3557
3586
|
}
|
|
3558
|
-
} catch (err) { console.error('waitForUI(ui) find error:', err) }
|
|
3559
|
-
} catch (err) { console.error('waitForUI(ui) outer error:', err) }
|
|
3587
|
+
} catch (err) { checkDebuggerGuard(); console.error('waitForUI(ui) find error:', err) }
|
|
3588
|
+
} catch (err) { checkDebuggerGuard(); console.error('waitForUI(ui) outer error:', err) }
|
|
3560
3589
|
} else if (type === 'log') {
|
|
3561
3590
|
try {
|
|
3562
3591
|
// Logs: presence semantics only (match 'present'). Stability not applicable (immediate)
|
|
@@ -3586,7 +3615,7 @@ export class ToolsInteract {
|
|
|
3586
3615
|
return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: 0, matchedLog: { message: line }, matchSource: 'log-snapshot', timestamp: now2, type: 'log', observed_state: true }
|
|
3587
3616
|
}
|
|
3588
3617
|
}
|
|
3589
|
-
} catch (err) { console.error('waitForUI(log) error:', err) }
|
|
3618
|
+
} catch (err) { checkDebuggerGuard(); console.error('waitForUI(log) error:', err) }
|
|
3590
3619
|
} else if (type === 'screen') {
|
|
3591
3620
|
try {
|
|
3592
3621
|
const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
@@ -3608,7 +3637,7 @@ export class ToolsInteract {
|
|
|
3608
3637
|
lastObservedState = false
|
|
3609
3638
|
}
|
|
3610
3639
|
}
|
|
3611
|
-
} catch (err) { console.error('waitForUI(screen) error:', err) }
|
|
3640
|
+
} catch (err) { checkDebuggerGuard(); console.error('waitForUI(screen) error:', err) }
|
|
3612
3641
|
} else if (type === 'idle') {
|
|
3613
3642
|
try {
|
|
3614
3643
|
const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
@@ -3627,7 +3656,7 @@ export class ToolsInteract {
|
|
|
3627
3656
|
return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: idleMs, matchSource: 'idle-stable', timestamp: now2, type: 'idle', observed_state: lastObservedState ?? null }
|
|
3628
3657
|
}
|
|
3629
3658
|
}
|
|
3630
|
-
} catch (err) { console.error('waitForUI(idle) error:', err) }
|
|
3659
|
+
} catch (err) { checkDebuggerGuard(); console.error('waitForUI(idle) error:', err) }
|
|
3631
3660
|
}
|
|
3632
3661
|
|
|
3633
3662
|
// Respect poll interval and avoid tight loop
|
|
@@ -3643,7 +3672,7 @@ export class ToolsInteract {
|
|
|
3643
3672
|
// time ensures the latest exported ToolsObserve object is used.
|
|
3644
3673
|
const Obs = await import('../observe/index.js')
|
|
3645
3674
|
snapshot = await (Obs as any).ToolsObserve.captureDebugSnapshotHandler({ reason: `wait_for_ui timeout for ${type}`, includeLogs: true, platform, deviceId })
|
|
3646
|
-
} catch (err) {
|
|
3675
|
+
} catch (err) { checkDebuggerGuard();
|
|
3647
3676
|
snapshot = { error: err instanceof Error ? err.message : String(err) }
|
|
3648
3677
|
}
|
|
3649
3678
|
}
|
package/src/interact/ios.ts
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 { TapResponse, SwipeResponse } from "../types.js"
|
|
@@ -31,9 +33,11 @@ export class iOSInteract {
|
|
|
31
33
|
args.push('--udid', targetUdid);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
await
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
await beforeInputDispatch('ios', device.id)
|
|
37
|
+
const timeout = remainingBudget() || undefined
|
|
38
|
+
markDispatchStarted()
|
|
39
|
+
await evidenceDispatch({ platform: 'ios', id: device.id }, () => new Promise<void>((resolve, reject) => {
|
|
40
|
+
const proc = spawn(getIdbCmd(), args, { timeout });
|
|
37
41
|
let stderr = '';
|
|
38
42
|
proc.stderr.on('data', d => stderr += d.toString());
|
|
39
43
|
proc.on('close', code => {
|
|
@@ -41,7 +45,7 @@ export class iOSInteract {
|
|
|
41
45
|
else reject(new Error(`idb ui tap failed: ${stderr}`));
|
|
42
46
|
});
|
|
43
47
|
proc.on('error', err => reject(err));
|
|
44
|
-
});
|
|
48
|
+
}));
|
|
45
49
|
|
|
46
50
|
return { device, success: true, x, y };
|
|
47
51
|
} catch (e) {
|
|
@@ -73,9 +77,11 @@ export class iOSInteract {
|
|
|
73
77
|
args.push('--udid', targetUdid);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
await
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
await beforeInputDispatch('ios', device.id)
|
|
81
|
+
const timeout = remainingBudget() || undefined
|
|
82
|
+
markDispatchStarted()
|
|
83
|
+
await evidenceDispatch({ platform: 'ios', id: device.id }, () => new Promise<void>((resolve, reject) => {
|
|
84
|
+
const proc = spawn(getIdbCmd(), args, { timeout });
|
|
79
85
|
let stderr = '';
|
|
80
86
|
proc.stderr.on('data', d => stderr += d.toString());
|
|
81
87
|
proc.on('close', code => {
|
|
@@ -83,7 +89,7 @@ export class iOSInteract {
|
|
|
83
89
|
else reject(new Error(`idb ui swipe failed: ${stderr}`));
|
|
84
90
|
});
|
|
85
91
|
proc.on('error', err => reject(err));
|
|
86
|
-
});
|
|
92
|
+
}));
|
|
87
93
|
|
|
88
94
|
return { device, success: true, start: [x1, y1], end: [x2, y2], duration };
|
|
89
95
|
} catch (e) {
|
package/src/manage/android.ts
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'
|
|
@@ -156,7 +157,7 @@ export class AndroidManage {
|
|
|
156
157
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
157
158
|
try {
|
|
158
159
|
markDispatchStarted()
|
|
159
|
-
const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId, { timeout: timeoutMs })
|
|
160
|
+
const output = await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId, { timeout: timeoutMs }))
|
|
160
161
|
const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null)
|
|
161
162
|
return {
|
|
162
163
|
device: deviceInfo,
|
|
@@ -182,7 +183,7 @@ export class AndroidManage {
|
|
|
182
183
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
183
184
|
try {
|
|
184
185
|
markDispatchStarted()
|
|
185
|
-
await execAdb(['shell', 'am', 'force-stop', appId], deviceId)
|
|
186
|
+
await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'am', 'force-stop', appId], deviceId))
|
|
186
187
|
return { device: deviceInfo, appTerminated: true }
|
|
187
188
|
} catch (e: unknown) {
|
|
188
189
|
const diagnostics = hasFiniteBudget() ? undefined : execAdbWithDiagnostics(['shell', 'am', 'force-stop', appId], deviceId)
|
|
@@ -215,7 +216,7 @@ export class AndroidManage {
|
|
|
215
216
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
216
217
|
try {
|
|
217
218
|
markDispatchStarted()
|
|
218
|
-
const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId)
|
|
219
|
+
const output = await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'pm', 'clear', appId], deviceId))
|
|
219
220
|
return { device: deviceInfo, dataCleared: output === 'Success' }
|
|
220
221
|
} catch (e: unknown) {
|
|
221
222
|
const diag = execAdbWithDiagnostics(['shell', 'pm', 'clear', appId], deviceId)
|
package/src/manage/ios.ts
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"
|
|
@@ -316,7 +317,7 @@ export class iOSManage {
|
|
|
316
317
|
validateBundleId(bundleId)
|
|
317
318
|
try {
|
|
318
319
|
markDispatchStarted()
|
|
319
|
-
const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
320
|
+
const result = await evidenceDispatch({ platform: 'ios', id: deviceId }, () => execCommand(['simctl', 'launch', deviceId, bundleId], deviceId))
|
|
320
321
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
321
322
|
const fingerprint = observe ? await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null) : null
|
|
322
323
|
const pidMatch = result.output.match(/:\s*(\d+)\s*$/)
|
|
@@ -343,7 +344,7 @@ export class iOSManage {
|
|
|
343
344
|
validateBundleId(bundleId)
|
|
344
345
|
try {
|
|
345
346
|
markDispatchStarted()
|
|
346
|
-
await execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId)
|
|
347
|
+
await evidenceDispatch({ platform: 'ios', id: deviceId }, () => execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId))
|
|
347
348
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
348
349
|
return { device, appTerminated: true }
|
|
349
350
|
} catch (e: unknown) {
|
package/src/observe/android.ts
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 { GetLogsResponse, CaptureAndroidScreenResponse, GetUITreeResponse, GetCurrentScreenResponse, UIElement, DeviceInfo } from "../types.js"
|
|
@@ -11,14 +12,20 @@ import { deriveSnapshotMetadata } from "./snapshot-metadata.js"
|
|
|
11
12
|
import { remainingBudget } from '../utils/operation-budget.js'
|
|
12
13
|
import { createObservationMeasurement } from './performance-metrics.js'
|
|
13
14
|
|
|
14
|
-
const activeLogStreams: Map<string, { proc: any, file: string }> = new Map()
|
|
15
|
+
const activeLogStreams: Map<string, { proc: any, file: string, device?: { platform: 'android' | 'ios'; id: string } }> = new Map()
|
|
15
16
|
|
|
16
17
|
export class AndroidObserve {
|
|
18
|
+
getLogStreamDevice(sessionId: string = 'default') { return activeLogStreams.get(sessionId)?.device ?? null }
|
|
19
|
+
|
|
17
20
|
async getDeviceMetadata(appId: string, deviceId?: string): Promise<DeviceInfo> {
|
|
18
21
|
return getAndroidDeviceMetadata(appId, deviceId);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
async getUITree(deviceId?: string): Promise<GetUITreeResponse> {
|
|
25
|
+
return captureEvidence('ui_tree', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async collectGetUITree(deviceId?: string): Promise<GetUITreeResponse> {
|
|
22
29
|
const measurement = createObservationMeasurement('android')
|
|
23
30
|
let metadata
|
|
24
31
|
try {
|
|
@@ -122,6 +129,10 @@ export class AndroidObserve {
|
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
async getLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
|
|
132
|
+
return captureEvidence('logs', { platform: 'android', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
|
|
125
136
|
const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters
|
|
126
137
|
const metadata = await getAndroidDeviceMetadata(appId || "", deviceId)
|
|
127
138
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
@@ -221,6 +232,10 @@ export class AndroidObserve {
|
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
async captureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
|
|
235
|
+
return captureEvidence('screenshot', { platform: 'android', id: deviceId ?? '' }, () => this.collectCaptureScreen(deviceId))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async collectCaptureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
|
|
224
239
|
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
225
240
|
const deviceInfo: DeviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
226
241
|
|
|
@@ -349,6 +364,10 @@ export class AndroidObserve {
|
|
|
349
364
|
}
|
|
350
365
|
|
|
351
366
|
async getCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> {
|
|
367
|
+
return captureEvidence('activity', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetCurrentScreen(deviceId))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async collectGetCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> {
|
|
352
371
|
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
353
372
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
354
373
|
|
|
@@ -410,13 +429,17 @@ export class AndroidObserve {
|
|
|
410
429
|
}
|
|
411
430
|
}
|
|
412
431
|
|
|
413
|
-
async getScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string }> {
|
|
432
|
+
async getScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
|
|
433
|
+
return captureEvidence('fingerprint', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private async collectGetScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
|
|
414
437
|
try {
|
|
415
438
|
const tree = await this.getUITree(deviceId)
|
|
416
439
|
if (!tree || (tree as any).error) return { fingerprint: null, error: (tree as any).error }
|
|
417
440
|
|
|
418
441
|
const current = await this.getCurrentScreen(deviceId).catch(() => null)
|
|
419
|
-
return computeScreenFingerprint(tree, current, 'android', 50)
|
|
442
|
+
return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms }
|
|
420
443
|
} catch (e) {
|
|
421
444
|
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
|
|
422
445
|
}
|
|
@@ -436,7 +459,7 @@ export class AndroidObserve {
|
|
|
436
459
|
activeLogStreams.delete(sessionId)
|
|
437
460
|
}
|
|
438
461
|
|
|
439
|
-
const args = ['logcat', `--pid=${pid}`, filter]
|
|
462
|
+
const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter]
|
|
440
463
|
const proc = spawn(getAdbCmd(), args)
|
|
441
464
|
|
|
442
465
|
const tmpDir = process.env.TMPDIR || '/tmp'
|
|
@@ -463,10 +486,10 @@ export class AndroidObserve {
|
|
|
463
486
|
|
|
464
487
|
proc.on('close', () => {
|
|
465
488
|
stream.end()
|
|
466
|
-
activeLogStreams.delete(sessionId)
|
|
489
|
+
if (activeLogStreams.get(sessionId)?.proc === proc) activeLogStreams.delete(sessionId)
|
|
467
490
|
})
|
|
468
491
|
|
|
469
|
-
activeLogStreams.set(sessionId, { proc, file })
|
|
492
|
+
activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } })
|
|
470
493
|
return { success: true, stream_started: true }
|
|
471
494
|
} catch (e) {
|
|
472
495
|
return { success: false, error: e instanceof Error ? e.message : String(e) }
|
|
@@ -482,6 +505,10 @@ export class AndroidObserve {
|
|
|
482
505
|
}
|
|
483
506
|
|
|
484
507
|
async readLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
|
|
508
|
+
return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
|
|
485
512
|
const entry = activeLogStreams.get(sessionId)
|
|
486
513
|
let file: string | undefined
|
|
487
514
|
if (entry && entry.file) file = entry.file
|