mobile-debug-mcp 0.35.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/.cursor-plugin/marketplace.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/README.md +96 -24
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- 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/README.md +37 -8
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +261 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +46 -18
- package/dist/contracts/device-readiness.js +1 -0
- package/dist/debugger/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/sessions.js +33 -16
- package/dist/debugger/worker.js +6 -4
- package/dist/interact/index.js +12 -2
- 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/manage/ios.js +2 -0
- package/dist/observe/device-readiness.js +116 -0
- package/dist/observe/evidence.js +31 -0
- package/dist/observe/ios-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +246 -0
- package/dist/observe/readiness-service.js +22 -10
- package/dist/observe/readiness.js +34 -4
- package/dist/observe/screenshot-encoding.js +4 -0
- package/dist/observe/snapshot-collection.js +7 -2
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +4 -8
- 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/debugger-handlers.js +27 -8
- package/dist/server/handlers/diagnostic.js +12 -4
- 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 +28 -14
- package/dist/server/schemas/debugger-schema.js +10 -7
- package/dist/server/schemas/interaction-diagnostics.js +9 -0
- package/dist/server/schemas/system-trace-schema.js +14 -9
- package/dist/server/tool-definitions.js +9 -4
- package/dist/server-core.js +5 -2
- package/dist/server.js +5 -5
- 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 +7 -11
- package/dist/utils/resolve-device.js +3 -2
- package/dist/utils/timing.js +3 -3
- package/docs/CHANGELOG.md +48 -1
- package/docs/agent-plugin-installation-verification.md +39 -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/code-structure.md +3 -1
- package/docs/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +166 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -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 +3 -2
- package/docs/tools/compact-interactions.md +56 -0
- package/docs/tools/correlated-evidence.md +6 -0
- package/docs/tools/debugger.md +45 -5
- 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-traces.md +81 -14
- package/docs/tools/system.md +6 -0
- package/package.json +2 -1
- package/scripts/agent-plugin.mjs +55 -5
- package/src/contracts/device-readiness.ts +8 -0
- package/src/debugger/android.ts +1 -1
- package/src/debugger/guard.ts +5 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/sessions.ts +24 -12
- package/src/debugger/worker.ts +7 -4
- package/src/interact/index.ts +13 -2
- 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/manage/ios.ts +2 -0
- package/src/observe/device-readiness.ts +96 -0
- package/src/observe/evidence.ts +30 -1
- package/src/observe/ios-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +206 -0
- package/src/observe/readiness-service.ts +22 -10
- package/src/observe/readiness.ts +37 -4
- package/src/observe/screenshot-encoding.ts +4 -0
- package/src/observe/snapshot-collection.ts +7 -2
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +4 -4
- 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/debugger-handlers.ts +28 -9
- package/src/server/handlers/diagnostic.ts +11 -4
- 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 +29 -14
- package/src/server/schemas/debugger-schema.ts +10 -7
- package/src/server/schemas/interaction-diagnostics.ts +10 -0
- package/src/server/schemas/system-trace-schema.ts +15 -9
- package/src/server/tool-definitions.ts +8 -4
- package/src/server-core.ts +5 -2
- package/src/server.ts +4 -4
- 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 +7 -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 +227 -0
- 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 +119 -0
- package/test/device/automated/observe/system-trace.smoke.ts +2 -2
- package/test/fixtures/contracts/tool-definitions.json +243 -47
- package/test/fixtures/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/fixtures/latency-journey-app/build.ts +3 -3
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/handle-guards.test.ts +89 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -0
- 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-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +154 -0
- package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
- package/test/unit/observe/readiness.test.ts +1 -1
- package/test/unit/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -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 +18 -7
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { ReadinessTracker, withReadinessTracker } from '../../../src/observe/readiness.js'
|
|
3
|
+
import { withDeviceReadinessProbe } from '../../../src/observe/device-readiness.js'
|
|
4
|
+
import { getSnapshotSessionId } from '../../../src/observe/snapshot-metadata.js'
|
|
5
|
+
import { waitForUIChange } from '../../../src/interact/waits/ui-change-wait.js'
|
|
6
|
+
import { resolveUiChangeScope } from '../../../src/interact/targeting/target-scope.js'
|
|
7
|
+
import type { Clock } from '../../../src/runtime/clock.js'
|
|
8
|
+
|
|
9
|
+
async function run() {
|
|
10
|
+
let now = 10000
|
|
11
|
+
const clock: Clock = { now: () => now, sleep: async ms => { now += ms } }
|
|
12
|
+
class Tracker extends ReadinessTracker {
|
|
13
|
+
override observe(tree: unknown, started: number) { return super.observe(tree, started, now) }
|
|
14
|
+
}
|
|
15
|
+
const tracker = new Tracker('android', 'fixture', 100)
|
|
16
|
+
const readTimes: number[] = []
|
|
17
|
+
const result = await withDeviceReadinessProbe(async (_command, args) => ({
|
|
18
|
+
exitCode: 0, stderr: '', stdout: args.includes('power')
|
|
19
|
+
? `mWakefulness=${now >= 10600 && now < 11200 ? 'Asleep' : 'Awake'}\nmWakefulnessChanging=false`
|
|
20
|
+
: 'KeyguardServiceDelegate\n showing=false\n mIsShowing=false'
|
|
21
|
+
}), () => withReadinessTracker(tracker, () => waitForUIChange({ platform: 'android', deviceId: 'fixture', timeout_ms: 3000, stability_window_ms: 1200, expected_change: 'text_change' }, {
|
|
22
|
+
clock,
|
|
23
|
+
observe: async () => {
|
|
24
|
+
const text = readTimes.length ? 'After' : 'Before'
|
|
25
|
+
readTimes.push(now)
|
|
26
|
+
return { device: { platform: 'android', id: 'fixture' }, screen: '', resolution: { width: 100, height: 100 },
|
|
27
|
+
elements: [{ text, type: 'TextView', stable_id: 'label', visible: true, bounds: [0, 0, 100, 40] }],
|
|
28
|
+
captured_at_ms: now, snapshot_revision: readTimes.length, snapshot_session_id: getSnapshotSessionId() }
|
|
29
|
+
},
|
|
30
|
+
scope: (tree, scope, target) => resolveUiChangeScope(tree, scope, target, () => null)
|
|
31
|
+
})))
|
|
32
|
+
assert.equal(result.success, true)
|
|
33
|
+
assert.deepEqual(readTimes, [10000, 10300, 11200, 11500, 11800, 12100, 12400])
|
|
34
|
+
assert.equal(result.elapsed_ms, 2400, 'the full change stability window starts again after the blocker')
|
|
35
|
+
console.log('PASS readiness interruption resets UI-change stability, excluding blocked time')
|
|
36
|
+
}
|
|
37
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
2
2
|
import assert from 'assert'
|
|
3
3
|
import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
|
|
4
|
+
import { computeScreenFingerprint } from '../../../src/utils/ui/index.js'
|
|
5
|
+
import type { DeviceInfo } from '../../../src/types.js'
|
|
4
6
|
|
|
5
7
|
async function run() {
|
|
6
8
|
console.log('Starting capture_debug_snapshot unit tests...')
|
|
@@ -13,7 +15,7 @@ async function run() {
|
|
|
13
15
|
const origReadLogStreamHandler = (ToolsObserve as any).readLogStreamHandler
|
|
14
16
|
const origGetLogsHandler = (ToolsObserve as any).getLogsHandler
|
|
15
17
|
|
|
16
|
-
const device = { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }
|
|
18
|
+
const device: DeviceInfo = { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }
|
|
17
19
|
let resolutions = 0
|
|
18
20
|
_setDeviceListersForTests({
|
|
19
21
|
listAndroidDevices: async () => { resolutions++; return [device] },
|
|
@@ -30,17 +32,23 @@ async function run() {
|
|
|
30
32
|
assertTarget(args)
|
|
31
33
|
return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screenshot: 'BASE64PNG', resolution: { width: 1080, height: 1920 } }
|
|
32
34
|
}
|
|
35
|
+
const current = { device, package: 'com.example', activity: 'com.example.Main', shortActivity: 'Main' }
|
|
36
|
+
const postTreeActivity = { ...current, activity: 'com.example.After' }
|
|
37
|
+
let activityCalls = 0
|
|
33
38
|
;(ToolsObserve as any).getCurrentScreenHandler = async function(args: any) {
|
|
34
39
|
assert.strictEqual(args.deviceId, device.id)
|
|
35
|
-
return
|
|
40
|
+
return ++activityCalls === 1 ? current : postTreeActivity
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
let independentFingerprints = 0
|
|
43
|
+
;(ToolsObserve as any).getScreenFingerprintHandler = async function() {
|
|
44
|
+
independentFingerprints++
|
|
45
|
+
return { fingerprint: 'independent' }
|
|
40
46
|
}
|
|
47
|
+
let treeReads = 0
|
|
41
48
|
;(ToolsObserve as any).getUITreeHandler = async function(args: any) {
|
|
42
49
|
assertTarget(args)
|
|
43
|
-
|
|
50
|
+
treeReads++
|
|
51
|
+
return { device, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] }
|
|
44
52
|
}
|
|
45
53
|
;(ToolsObserve as any).readLogStreamHandler = async function(args: any) {
|
|
46
54
|
assertTarget(args)
|
|
@@ -53,8 +61,11 @@ async function run() {
|
|
|
53
61
|
|
|
54
62
|
const res1: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 50, sessionId: 's1' })
|
|
55
63
|
console.log('res1:', JSON.stringify(res1, null, 2))
|
|
56
|
-
const pass1 = res1 && res1.raw && res1.raw.screenshot === 'BASE64PNG' && res1.raw.activity
|
|
64
|
+
const pass1 = res1 && res1.raw && res1.raw.screenshot === 'BASE64PNG' && res1.raw.activity === current.activity && Array.isArray(res1.raw.logs) && res1.raw.logs.length === 1
|
|
57
65
|
assert.ok(pass1, 'captureDebugSnapshot should aggregate successful handler results')
|
|
66
|
+
assert.equal(treeReads, 1)
|
|
67
|
+
assert.equal(independentFingerprints, 0)
|
|
68
|
+
assert.equal(res1.raw.fingerprint, computeScreenFingerprint(res1.raw.ui_tree, postTreeActivity, 'android').fingerprint)
|
|
58
69
|
assert.strictEqual(res1.semantic.screen, 'Main')
|
|
59
70
|
assert.strictEqual(res1.semantic.confidence >= 0.7, true)
|
|
60
71
|
assert.deepStrictEqual(res1.semantic.actions_available, null)
|
|
@@ -63,7 +74,7 @@ async function run() {
|
|
|
63
74
|
|
|
64
75
|
// --- Test 2: screenshot and ui tree fail; logs fallback to getLogs ---
|
|
65
76
|
;(ToolsObserve as any).captureScreenshotHandler = async function(args: any) { assertTarget(args); throw new Error('screencap failed') }
|
|
66
|
-
;(ToolsObserve as any).getUITreeHandler = async function(args: any) { assertTarget(args); throw new Error('
|
|
77
|
+
;(ToolsObserve as any).getUITreeHandler = async function(args: any) { assertTarget(args); treeReads++; throw new Error('ACTION_TIMEOUT') }
|
|
67
78
|
;(ToolsObserve as any).readLogStreamHandler = async function(args: any) { assertTarget(args); return { entries: [] } }
|
|
68
79
|
;(ToolsObserve as any).getLogsHandler = async function(args: any) { assertTarget(args); return { device: { platform: 'android', id: 'mock' }, logs: ['INFO starting','ERROR crash here'], logCount: 2 } }
|
|
69
80
|
|
|
@@ -71,33 +82,76 @@ async function run() {
|
|
|
71
82
|
console.log('res2:', JSON.stringify(res2, null, 2))
|
|
72
83
|
const pass2 = res2 && res2.raw && res2.raw.screenshot_error && res2.raw.ui_tree_error && Array.isArray(res2.raw.logs) && res2.raw.logs.length === 2
|
|
73
84
|
assert.ok(pass2, 'captureDebugSnapshot should surface partial failures and fallback logs')
|
|
85
|
+
assert.equal(res2.raw.ui_tree_error, 'ACTION_TIMEOUT')
|
|
86
|
+
assert.equal(res2.raw.fingerprint_error, 'ACTION_TIMEOUT')
|
|
87
|
+
assert.equal(res2.raw.fingerprint, null)
|
|
88
|
+
assert.equal(treeReads, 2)
|
|
74
89
|
console.log('Test 2:', pass2 ? 'PASS' : 'FAIL')
|
|
75
90
|
|
|
76
91
|
// --- Test 3: includeLogs=false should omit logs ---
|
|
77
92
|
;(ToolsObserve as any).captureScreenshotHandler = async function(args: any) { assertTarget(args); return { device: { platform: 'android', id: 'mock' }, screenshot: null } }
|
|
78
|
-
;(ToolsObserve as any).getCurrentScreenHandler = async function(args: any) { assert.strictEqual(args.deviceId, device.id);
|
|
79
|
-
;(ToolsObserve as any).
|
|
80
|
-
|
|
93
|
+
;(ToolsObserve as any).getCurrentScreenHandler = async function(args: any) { assert.strictEqual(args.deviceId, device.id); throw new Error('activity unavailable') }
|
|
94
|
+
;(ToolsObserve as any).getUITreeHandler = async function(args: any) {
|
|
95
|
+
assertTarget(args)
|
|
96
|
+
const revision = ++treeReads
|
|
97
|
+
return { device, screen: '', snapshot_revision: revision, captured_at_ms: 1000 + revision,
|
|
98
|
+
resolution: { width: 1080, height: 1920 }, elements: [{ visible: true, text: `State ${revision}` }] }
|
|
99
|
+
}
|
|
81
100
|
;(ToolsObserve as any).readLogStreamHandler = async function(args: any) { assertTarget(args); return { entries: [] } }
|
|
82
101
|
|
|
83
102
|
const res3: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: false })
|
|
84
103
|
console.log('res3:', JSON.stringify(res3, null, 2))
|
|
85
104
|
const pass3 = res3 && res3.raw && typeof res3.raw.logs !== 'undefined' && res3.raw.logs.length === 0
|
|
86
105
|
assert.ok(pass3, 'captureDebugSnapshot should return an empty logs array when includeLogs is false')
|
|
106
|
+
assert.equal(res3.raw.activity_error, 'activity unavailable')
|
|
107
|
+
assert.equal(res3.raw.fingerprint, computeScreenFingerprint(res3.raw.ui_tree, null, 'android').fingerprint)
|
|
87
108
|
console.log('Test 3:', pass3 ? 'PASS' : 'FAIL')
|
|
88
109
|
|
|
89
110
|
let screenshotCalls = 0
|
|
90
111
|
;(ToolsObserve as any).captureScreenshotHandler = async function(args: any) { assertTarget(args); screenshotCalls++; return { screenshot: 'unexpected' } }
|
|
91
|
-
const res4: any = await ToolsObserve.captureDebugSnapshotHandler({
|
|
112
|
+
const res4: any = await ToolsObserve.captureDebugSnapshotHandler({ includeLogs: false, includeScreenshot: false })
|
|
92
113
|
assert.strictEqual(screenshotCalls, 0, 'includeScreenshot=false must not invoke screenshot capture')
|
|
93
114
|
assert.strictEqual(res4.raw.screenshot, null)
|
|
94
115
|
assert.strictEqual(resolutions, 4, 'Each snapshot resolves its own target once')
|
|
116
|
+
assert.equal(treeReads, 4)
|
|
117
|
+
assert.notEqual(res4.raw.fingerprint, res3.raw.fingerprint)
|
|
118
|
+
assert.equal(res4.raw.snapshot_revision, 4)
|
|
119
|
+
assert.equal(res4.raw.captured_at_ms, 1004)
|
|
120
|
+
assert.equal(independentFingerprints, 0)
|
|
95
121
|
|
|
96
122
|
// Discovery failure must not collect from an arbitrary/default device.
|
|
97
123
|
_setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('fixture discovery unavailable') } })
|
|
98
124
|
await assert.rejects(() => ToolsObserve.captureDebugSnapshotHandler({ platform: 'android' }), /fixture discovery unavailable/)
|
|
99
125
|
assert.strictEqual(screenshotCalls, 0, 'No component capture should run without a resolved target')
|
|
100
126
|
|
|
127
|
+
const other: DeviceInfo = { ...device, id: 'other-fixture' }
|
|
128
|
+
_setDeviceListersForTests({ listAndroidDevices: async () => [device, other] })
|
|
129
|
+
;(ToolsObserve as any).getUITreeHandler = async ({ deviceId }: { deviceId: string }) => ({
|
|
130
|
+
device: deviceId === device.id ? device : other, screen: '',
|
|
131
|
+
resolution: { width: 100, height: 200 }, elements: [{ visible: true, text: deviceId }]
|
|
132
|
+
})
|
|
133
|
+
;(ToolsObserve as any).getCurrentScreenHandler = async ({ deviceId }: { deviceId: string }) => ({
|
|
134
|
+
activity: 'com.example.Main', device: deviceId === device.id ? device : other
|
|
135
|
+
})
|
|
136
|
+
const [first, second] = await Promise.all([device, other].map(target => ToolsObserve.captureDebugSnapshotHandler({
|
|
137
|
+
platform: 'android', deviceId: target.id, includeLogs: false, includeScreenshot: false
|
|
138
|
+
})))
|
|
139
|
+
assert.equal(first.raw.ui_tree?.device.id, device.id)
|
|
140
|
+
assert.equal(second.raw.ui_tree?.device.id, other.id)
|
|
141
|
+
assert.notEqual(first.raw.fingerprint, second.raw.fingerprint)
|
|
142
|
+
assert.equal(independentFingerprints, 0)
|
|
143
|
+
|
|
144
|
+
const ios: DeviceInfo = { ...device, platform: 'ios', id: 'ios-fixture' }
|
|
145
|
+
_setDeviceListersForTests({ listIOSDevices: async () => [ios] })
|
|
146
|
+
;(ToolsObserve as any).getUITreeHandler = async ({ platform, deviceId }: { platform: string; deviceId: string }) => {
|
|
147
|
+
assert.equal(platform, 'ios')
|
|
148
|
+
assert.equal(deviceId, ios.id)
|
|
149
|
+
return { device: ios, screen: '', resolution: { width: 100, height: 200 }, elements: [] }
|
|
150
|
+
}
|
|
151
|
+
const iosResult = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'ios', deviceId: ios.id, includeLogs: false, includeScreenshot: false })
|
|
152
|
+
assert.equal(iosResult.raw.fingerprint, 'independent')
|
|
153
|
+
assert.equal(independentFingerprints, 1)
|
|
154
|
+
|
|
101
155
|
} finally {
|
|
102
156
|
_resetDeviceListersForTests()
|
|
103
157
|
;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { cleanupReadinessDevice } from '../../device/automated/observe/device-readiness-cleanup.js'
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
for (const failedStep of [0, 1, 2]) {
|
|
6
|
+
const commands: string[][] = []
|
|
7
|
+
let closed = 0
|
|
8
|
+
const commandFailure = new Error('disconnected')
|
|
9
|
+
const closeFailure = new Error('close failed')
|
|
10
|
+
const failures = await cleanupReadinessDevice(true, (...args) => {
|
|
11
|
+
commands.push(args)
|
|
12
|
+
if (commands.length - 1 === failedStep) throw commandFailure
|
|
13
|
+
}, async () => { closed++; throw closeFailure })
|
|
14
|
+
assert.equal(commands.length, 3, 'a failed restoration must not skip later steps')
|
|
15
|
+
assert.equal(commands[2].at(-1), '3', 'Home is attempted')
|
|
16
|
+
assert.equal(closed, 1, 'client close always attempted')
|
|
17
|
+
assert.deepEqual(failures, [commandFailure, closeFailure])
|
|
18
|
+
}
|
|
19
|
+
let closed = false
|
|
20
|
+
assert.deepEqual(await cleanupReadinessDevice(false, () => { assert.fail('restoration disabled') }, async () => { closed = true }), [])
|
|
21
|
+
assert.equal(closed, true)
|
|
22
|
+
console.log('PASS readiness cleanup continues after each ADB failure and closes the client')
|
|
23
|
+
}
|
|
24
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { handleAdjustControl } from '../../../src/server/handlers/interact.js'
|
|
2
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
3
|
+
import type { AdjustControlResponse } from '../../../src/types.js'
|
|
4
|
+
import { ReadinessTracker } from '../../../src/observe/readiness.js'
|
|
5
|
+
import { getSnapshotSessionId } from '../../../src/observe/snapshot-metadata.js'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
import { parseAndroidPower, parseAndroidLock, observeDeviceReadiness, parseIosLock, withDeviceReadinessProbe } from '../../../src/observe/device-readiness.js'
|
|
8
|
+
import { registerDebuggerPauseProvider } from '../../../src/debugger/guard.js'
|
|
9
|
+
import { runWithBudget } from '../../../src/utils/operation-budget.js'
|
|
10
|
+
import { compactAction, actionHandler } from '../../../src/server/handlers/action-execution.js'
|
|
11
|
+
import { wrapResponse } from '../../../src/server/common.js'
|
|
12
|
+
import { withResolvedDevice } from '../../../src/utils/resolve-device.js'
|
|
13
|
+
|
|
14
|
+
const power = (state: string, changing = 'false') => `mWakefulness=${state}\nmWakefulnessChanging=${changing}`
|
|
15
|
+
const lock = (state: string) => `KeyguardServiceDelegate\n showing=${state}\n KeyguardStateMonitor\n mIsShowing=${state}`
|
|
16
|
+
for (const [text, expected] of [[power('Awake'), 'awake'], [power('Asleep'), 'asleep'], [power('Dozing'), 'asleep'], [power('Awake', 'true'), 'transitioning'], [power('Dreaming'), 'unknown'], ['mWakefulness=Awake', 'unknown'], [power('Awake') + '\nmWakefulness=Asleep', 'unknown']]) assert.equal(parseAndroidPower(text), expected)
|
|
17
|
+
assert.equal(parseAndroidPower('Power Manager State:\n' + power('Dozing') + '\nSettings and Configuration:\nmWakefulness=3'), 'asleep', 'vendor debug fields outside current-state section cannot conflict with it')
|
|
18
|
+
assert.equal(parseAndroidLock(lock('true')), 'locked')
|
|
19
|
+
assert.equal(parseAndroidLock(lock('false')), 'unlocked')
|
|
20
|
+
for (const text of ['', 'deviceHasKeyguard=true', 'showing=false', lock('true').replace('mIsShowing=true', 'mIsShowing=false')]) assert.equal(parseAndroidLock(text), 'unknown')
|
|
21
|
+
const unsupported = JSON.stringify({ error: { domain: 'com.apple.dt.CoreDeviceError', code: 1001, userInfo: { CapabilityFeatureIdentifier: { string: 'com.apple.coredevice.feature.getlockstate' } } } })
|
|
22
|
+
assert.equal(parseIosLock(unsupported), 'unsupported')
|
|
23
|
+
assert.equal(parseIosLock(unsupported.replace('getlockstate', 'other')), 'unknown')
|
|
24
|
+
assert.equal(parseIosLock('{"result":{"passcodeRequired":false}}'), 'unknown', 'unverified success semantics cannot establish unlocked')
|
|
25
|
+
const device = { platform: 'android', id: 'fixture', model: 'Fixture', osVersion: '16', simulator: true }
|
|
26
|
+
async function run() {
|
|
27
|
+
const calls: string[][] = []
|
|
28
|
+
const probe = async (_command: string, args: string[], timeout: number) => {
|
|
29
|
+
calls.push(args)
|
|
30
|
+
assert.ok(timeout > 0 && timeout <= 1000)
|
|
31
|
+
return { exitCode: 0, stdout: args.includes('power') ? power('Asleep') : lock('true'), stderr: '' }
|
|
32
|
+
}
|
|
33
|
+
const started = Date.now()
|
|
34
|
+
const state = await runWithBudget(500, () => observeDeviceReadiness(device, probe))
|
|
35
|
+
assert.deepEqual(state.blockers, ['DEVICE_ASLEEP', 'DEVICE_LOCKED'])
|
|
36
|
+
assert.equal(state.input_ready, false)
|
|
37
|
+
assert.ok(state.evidence.every(item => item.captured_at_ms >= started))
|
|
38
|
+
assert.ok(calls.every(args => args[0] === '-s' && args[1] === device.id))
|
|
39
|
+
assert.ok(!JSON.stringify(state).includes('mWakefulness'))
|
|
40
|
+
const partial = await observeDeviceReadiness(device, async (...args) => {
|
|
41
|
+
if (args[1].includes('power')) throw new Error('private diagnostic')
|
|
42
|
+
return probe(...args)
|
|
43
|
+
})
|
|
44
|
+
assert.equal(partial.wakefulness, 'unknown')
|
|
45
|
+
assert.equal(partial.lock_state, 'locked')
|
|
46
|
+
assert.ok(!JSON.stringify(partial).includes('private diagnostic'))
|
|
47
|
+
const recovered = await observeDeviceReadiness(device, async (_cmd, args) => ({ exitCode: 0, stdout: args.includes('power') ? power('Awake') : lock('false'), stderr: '' }))
|
|
48
|
+
assert.equal(recovered.input_ready, null)
|
|
49
|
+
assert.deepEqual(recovered.blockers, [])
|
|
50
|
+
const tracker = new ReadinessTracker(device.platform, device.id, 100)
|
|
51
|
+
const tree = { device, captured_at_ms: Date.now(), snapshot_revision: 1, snapshot_session_id: getSnapshotSessionId(), resolution: { width: 100, height: 100 }, elements: [{ type: 'Button', visible: true, text: 'Ready' }] }
|
|
52
|
+
tracker.observe(tree, tree.captured_at_ms, 0)
|
|
53
|
+
assert.equal(tracker.observe(tree, tree.captured_at_ms, 100).state, 'settled')
|
|
54
|
+
await withDeviceReadinessProbe(probe, () => tracker.observeDevice())
|
|
55
|
+
assert.equal(tracker.ready, false)
|
|
56
|
+
await withDeviceReadinessProbe(async (_cmd, args) => ({ exitCode: 0, stdout: args.includes('power') ? power('Awake') : lock('false'), stderr: '' }), () => tracker.observeDevice())
|
|
57
|
+
assert.equal(tracker.observe(tree, tree.captured_at_ms, 500).state, 'unknown', 'blocker breaks UI stability continuity')
|
|
58
|
+
assert.equal(tracker.observe(tree, tree.captured_at_ms, 600).state, 'settled')
|
|
59
|
+
const before = calls.length
|
|
60
|
+
const expired = await runWithBudget(0, () => observeDeviceReadiness(device, probe))
|
|
61
|
+
assert.equal(calls.length, before, 'no probe starts after expiry')
|
|
62
|
+
assert.equal(expired.wakefulness, 'unknown')
|
|
63
|
+
assert.equal((await observeDeviceReadiness(null, probe)).limitations[0], 'DEVICE_TARGET_UNAVAILABLE')
|
|
64
|
+
const ios = await observeDeviceReadiness({ platform: 'ios', id: 'ios-fixture' }, async (_cmd, args, timeout) => { assert.equal(args[args.indexOf('--timeout') + 1], '5'); assert.ok(timeout <= 1000); return { exitCode: 1, stdout: unsupported, stderr: '' } })
|
|
65
|
+
assert.equal(ios.lock_state, 'unsupported')
|
|
66
|
+
assert.equal(ios.wakefulness, 'unknown')
|
|
67
|
+
registerDebuggerPauseProvider(() => ({ debug_session_id: 'paused', captured_at_ms: started, target: {} }))
|
|
68
|
+
try {
|
|
69
|
+
const paused = await observeDeviceReadiness(device, probe)
|
|
70
|
+
assert.deepEqual(paused.blockers, ['DEBUGGER_PAUSED'])
|
|
71
|
+
assert.equal(calls.length, before)
|
|
72
|
+
} finally { registerDebuggerPauseProvider(() => null) }
|
|
73
|
+
assert.deepEqual(compactAction({ success: true, device_readiness: state }, 'none').device_readiness, state)
|
|
74
|
+
for (const responseMode of ['debug', 'compact']) {
|
|
75
|
+
const response = await withDeviceReadinessProbe(probe, () => withResolvedDevice(device, () => actionHandler('start_app', async () => wrapResponse({ success: true, device }))( { platform: 'android', deviceId: device.id, responseMode, verificationMode: 'none', actionTimeoutMs: 500 })))
|
|
76
|
+
const result = JSON.parse(response.content[0].text!)
|
|
77
|
+
assert.equal(result.success, true, 'blocker observation preserves dispatch outcome')
|
|
78
|
+
assert.equal(result.verification.status, 'not_requested')
|
|
79
|
+
assert.deepEqual(result.device_readiness.blockers, ['DEVICE_ASLEEP', 'DEVICE_LOCKED'])
|
|
80
|
+
}
|
|
81
|
+
const failedProbe = await withDeviceReadinessProbe(async () => { throw new Error('private'); }, () => withResolvedDevice(device, () => actionHandler('tap', async () => wrapResponse({ success: true, device }))({ responseMode: 'compact', verificationMode: 'none', actionTimeoutMs: 200 })))
|
|
82
|
+
const unknown = JSON.parse(failedProbe.content[0].text!)
|
|
83
|
+
assert.equal(unknown.success, true)
|
|
84
|
+
assert.equal(unknown.device_readiness.wakefulness, 'unknown')
|
|
85
|
+
assert.equal(unknown.device_readiness.lock_state, 'unknown')
|
|
86
|
+
assert.ok(!JSON.stringify(unknown.device_readiness).includes('private'))
|
|
87
|
+
const adjust = ToolsInteract.adjustControlHandler
|
|
88
|
+
ToolsInteract.adjustControlHandler = async () => ({ success: true, device } as AdjustControlResponse)
|
|
89
|
+
try {
|
|
90
|
+
const response = await withDeviceReadinessProbe(probe, () => handleAdjustControl({ selector: { text: 'Volume' }, targetValue: 50 }))
|
|
91
|
+
const result = JSON.parse(response.content[0].text!)
|
|
92
|
+
assert.equal(result.success, true)
|
|
93
|
+
assert.equal(result.device_readiness.input_ready, false, 'adjust_control carries the same blocker contract')
|
|
94
|
+
} finally { ToolsInteract.adjustControlHandler = adjust }
|
|
95
|
+
console.log('Device readiness: parsing, probes, budgets, recovery, debugger and action contracts PASS')
|
|
96
|
+
}
|
|
97
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { iosCapabilities, resolveIosApp } from '../../../src/observe/ios-trace/capabilities.js'
|
|
6
|
+
import { validateArtifact, validateToc, traceSize, IOS_TRACE_MAX_BYTES } from '../../../src/observe/ios-trace/artifact.js'
|
|
7
|
+
import { IosTraceNative } from '../../../src/observe/ios-trace/native.js'
|
|
8
|
+
import { spawnTrace, stopTraceChild, classifyTraceFailure, type TraceSpawn, type TraceCommandResult } from '../../../src/observe/ios-trace/process.js'
|
|
9
|
+
const app={app_id:'fixture',pid:123,executable:'/Fixture.app/Fixture',started_at:'Wed Nov 4 10:00:00 2026'}
|
|
10
|
+
const toc=(id='sim',pid=123,template='Time Profiler')=>`<trace-toc><run><info><target><device uuid="${id}"/><process pid="${pid}"/></target><summary><template-name>${template}</template-name></summary></info><processes><process pid="${pid}" path="/Fixture.app/Fixture"/></processes><data><table schema="time-sample"/></data></run></trace-toc>`
|
|
11
|
+
function fake(){
|
|
12
|
+
const calls:{command:string;args:string[];signals:NodeJS.Signals[]}[]=[],lines:string[]=[]
|
|
13
|
+
let mismatch=false,failExport=false,failReady=false
|
|
14
|
+
const spawn:TraceSpawn=(command,args)=>{
|
|
15
|
+
const call={command,args,signals:[] as NodeJS.Signals[]};calls.push(call)
|
|
16
|
+
const recording=args.includes('record'),listener=command.endsWith('/notification'),pending=recording||listener
|
|
17
|
+
let alive=pending,resolve!:(r:TraceCommandResult)=>void
|
|
18
|
+
const done=new Promise<TraceCommandResult>(r=>{resolve=r})
|
|
19
|
+
const execute=async()=>{
|
|
20
|
+
let output=''
|
|
21
|
+
if(args.includes('get_app_container'))output='/Fixture.app'
|
|
22
|
+
else if(args.includes('launchctl'))output='123 0 UIKitApplication:fixture[abc]'
|
|
23
|
+
else if(command==='/bin/ps')output=args.includes('lstart=')?app.started_at:mismatch?'/Other.app/Other':app.executable
|
|
24
|
+
else if(args.includes('clang'))await fs.writeFile(args[args.indexOf('-o')+1],'binary')
|
|
25
|
+
else if(args.includes('xcodebuild'))output='Xcode 27.2'
|
|
26
|
+
else if(args.includes('templates'))output='== Standard Templates ==\nTime Profiler\nSystem Trace\nAnimation Hitches'
|
|
27
|
+
else if(args.includes('xctrace')&&args.includes('devices'))output='fixture (sim)'
|
|
28
|
+
else if(args.includes('devices'))output=JSON.stringify({devices:{'iOS-26-4':[{udid:'sim',state:'Booted',name:'fixture',isAvailable:true}]}})
|
|
29
|
+
else if(args.includes('export')){
|
|
30
|
+
if(failExport){resolve({code:2,output:'private stderr',timedOut:false});return}
|
|
31
|
+
const file=args[args.indexOf('--output')+1]
|
|
32
|
+
await fs.writeFile(file,args.includes('--toc')?toc():'<trace-query-result><node><schema name="time-sample"/><row><sample-time>1</sample-time></row></node></trace-query-result>')
|
|
33
|
+
}
|
|
34
|
+
if(!pending)resolve({code:0,output,timedOut:false})
|
|
35
|
+
}
|
|
36
|
+
void execute()
|
|
37
|
+
return {done,alive:()=>alive,signal:s=>{call.signals.push(s);alive=false;resolve({code:0,output:'Stopped',timedOut:false})},line:async(line)=>{lines.push(line);if(failReady&&line==='READY')throw new Error('READINESS_TIMEOUT')}}
|
|
38
|
+
}
|
|
39
|
+
return {spawn,calls,lines,flags:(v:{mismatch?:boolean;export?:boolean;ready?:boolean})=>{mismatch=v.mismatch??false;failExport=v.export??false;failReady=v.ready??false}}
|
|
40
|
+
}
|
|
41
|
+
async function run(){
|
|
42
|
+
const f=fake(),directory=await fs.mkdtemp(path.join(os.tmpdir(),'ios-trace-unit-'))
|
|
43
|
+
try{
|
|
44
|
+
assert.deepEqual(await resolveIosApp('sim','fixture',f.spawn),app)
|
|
45
|
+
f.flags({mismatch:true});await assert.rejects(()=>resolveIosApp('sim','fixture',f.spawn),/APP_IDENTITY_UNAVAILABLE/);f.flags({})
|
|
46
|
+
const cap=await iosCapabilities('sim','fixture','Time Profiler',f.spawn,'darwin')
|
|
47
|
+
assert.equal(cap.supported,true);assert.equal(cap.prerequisites.permissions,'unknown');assert.equal(cap.templates.filter(t=>t.supported).length,1)
|
|
48
|
+
assert.equal((await iosCapabilities('sim',undefined,'System Trace',f.spawn,'darwin')).supported,false)
|
|
49
|
+
assert.equal((await iosCapabilities('physical',undefined,'Time Profiler',f.spawn,'darwin')).supported,false)
|
|
50
|
+
assert.equal((await iosCapabilities('sim',undefined,'Time Profiler',f.spawn,'linux')).reasons[0],'MACOS_REQUIRED')
|
|
51
|
+
assert.ok(!f.calls.some(c=>c.args.includes('record')),'Discovery must not record')
|
|
52
|
+
validateToc(toc(),'sim',app)
|
|
53
|
+
assert.throws(()=>validateToc(toc(),'sim',{...app,executable:'/Other.app/Other'}),/ARTIFACT_APP_MISMATCH/)
|
|
54
|
+
assert.equal(classifyTraceFailure({code:1,output:'secret path: permission denied',timedOut:false}),'PROFILING_PERMISSION_REQUIRED')
|
|
55
|
+
assert.equal(classifyTraceFailure({code:1,output:'Cannot find process for provided pid: 123',timedOut:false}),'APP_NOT_RUNNING')
|
|
56
|
+
for(const xml of [toc('other'),toc('sim',456),toc('sim',123,'System Trace'),'<invalid>'])assert.throws(()=>validateToc(xml,'sim',app))
|
|
57
|
+
const file=path.join(directory,'capture.trace');await fs.mkdir(file);await fs.writeFile(path.join(file,'data'),'samples')
|
|
58
|
+
const artifact=await validateArtifact(file,'sim',app,f.spawn)
|
|
59
|
+
assert.equal(artifact.validation.sample_rows,1);assert.equal(artifact.kind,'directory');assert.equal((await fs.stat(path.join(file,'data'))).mode&0o777,0o600)
|
|
60
|
+
await assert.rejects(()=>fs.stat(path.join(directory,'samples.xml')),{code:'ENOENT'})
|
|
61
|
+
f.flags({export:true});await assert.rejects(()=>validateArtifact(file,'sim',app,f.spawn));f.flags({})
|
|
62
|
+
const outside=path.join(directory,'link');await fs.symlink(file,outside);await assert.rejects(()=>traceSize(outside),/UNSAFE_ARTIFACT/)
|
|
63
|
+
const huge=path.join(directory,'huge');await fs.writeFile(huge,'');await fs.truncate(huge,IOS_TRACE_MAX_BYTES+1)
|
|
64
|
+
assert.ok(await traceSize(huge)>IOS_TRACE_MAX_BYTES)
|
|
65
|
+
const native=new IosTraceNative(f.spawn)
|
|
66
|
+
const recording=await native.start('sim',app,directory,30000,()=>{})
|
|
67
|
+
assert.deepEqual(f.lines,['ARMED','READY'])
|
|
68
|
+
const recorder=f.calls.find(c=>c.args.includes('record'))!,listener=f.calls.find(c=>c.command.endsWith('/notification'))!
|
|
69
|
+
assert.equal(recorder.args[recorder.args.indexOf('--device')+1],'sim')
|
|
70
|
+
assert.equal(recorder.args[recorder.args.indexOf('--attach')+1],'123')
|
|
71
|
+
assert.equal(recorder.args[recorder.args.indexOf('--notify-tracing-started')+1],listener.args[0])
|
|
72
|
+
assert.ok(!recorder.args.includes('--no-prompt'));await native.stop(recording);assert.deepEqual(recorder.signals,['SIGINT'])
|
|
73
|
+
const failed=fake();failed.flags({ready:true});const other=new IosTraceNative(failed.spawn)
|
|
74
|
+
await assert.rejects(()=>other.start('sim',app,directory,30000,()=>{}),/READINESS_TIMEOUT/)
|
|
75
|
+
assert.ok(failed.calls.find(c=>c.args.includes('record'))?.signals.includes('SIGINT'))
|
|
76
|
+
native.shutdown();other.shutdown()
|
|
77
|
+
const child=spawnTrace(process.execPath,['-e','process.stdout.write("READY\\n");setInterval(()=>{},1000)'],2000)
|
|
78
|
+
await child.line('READY',1000);await stopTraceChild(child,1000);assert.equal(child.alive(),false)
|
|
79
|
+
const signals:NodeJS.Signals[]=[]
|
|
80
|
+
const stuck={done:new Promise<TraceCommandResult>(()=>{}),alive:()=>true,line:async()=>{},signal:(s:NodeJS.Signals)=>{signals.push(s)}}
|
|
81
|
+
await assert.rejects(()=>stopTraceChild(stuck,1),/ACTION_TIMEOUT/)
|
|
82
|
+
assert.deepEqual(signals,['SIGINT','SIGKILL'])
|
|
83
|
+
const noisy=await spawnTrace(process.execPath,['-e','process.stdout.write("x".repeat(100000))'],2000).done
|
|
84
|
+
assert.equal(noisy.output.length,65536)
|
|
85
|
+
const timeout=await spawnTrace(process.execPath,['-e','setInterval(()=>{},1000)'],30).done
|
|
86
|
+
assert.equal(timeout.timedOut,true)
|
|
87
|
+
console.log('PASS iOS native discovery, notification, ownership, artifact schemas and bounded children')
|
|
88
|
+
}finally{await fs.rm(directory,{recursive:true,force:true})}
|
|
89
|
+
}
|
|
90
|
+
run().catch(error=>{console.error(error);process.exitCode=1})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { IosTraceSessions } from '../../../src/observe/ios-trace/sessions.js'
|
|
5
|
+
import { IosTraceNative, type IosRecording } from '../../../src/observe/ios-trace/native.js'
|
|
6
|
+
import type { TraceClock } from '../../../src/observe/system-trace.js'
|
|
7
|
+
import type { TraceCommandResult } from '../../../src/observe/ios-trace/process.js'
|
|
8
|
+
|
|
9
|
+
const target = { platform: 'ios' as const, id: 'sim', simulator: true, osVersion: '26.4', model: 'fixture' }
|
|
10
|
+
function fixture() {
|
|
11
|
+
let monitorTick = 0, exitOnMonitor = false, exportMessage = 'EXPORT_FAILED'
|
|
12
|
+
let now = 1000, failStart = false, failStop = false, failExport = false, monitor = '', stops = 0, starts = 0, validations = 0
|
|
13
|
+
const timers = new Map<ReturnType<typeof setTimeout>, () => void>(), recordings: IosRecording[] = [], directories: string[] = []
|
|
14
|
+
const time: TraceClock = { wall: () => now, mono: () => now, sleep: async ms => {now += ms},
|
|
15
|
+
schedule: fn => { const id = {} as ReturnType<typeof setTimeout>; timers.set(id,fn); return id }, clear: id => {timers.delete(id)} }
|
|
16
|
+
class Native extends IosTraceNative {
|
|
17
|
+
resolve = async () => ({ app_id:'fixture', pid:100, executable:'/fixture.app/Fixture',started_at:'fixture-start' })
|
|
18
|
+
async start(_d: string, _app: unknown, directory: string, _duration: number, own: (r: IosRecording) => void) {
|
|
19
|
+
starts++; directories.push(directory)
|
|
20
|
+
let alive = true, resolve!: (v: TraceCommandResult) => void
|
|
21
|
+
const done = new Promise<TraceCommandResult>(r => {resolve=r})
|
|
22
|
+
const recording: IosRecording = {file:path.join(directory,'capture.trace'), ready_at_ms:now,
|
|
23
|
+
recorder:{done, alive:()=>alive, line:async()=>{},signal:()=>{alive=false;resolve({code:0,output:'Reached specified time limit',timedOut:false})}}}
|
|
24
|
+
own(recording); recordings.push(recording)
|
|
25
|
+
await fs.mkdir(recording.file)
|
|
26
|
+
await fs.writeFile(path.join(recording.file,'data'),'sample')
|
|
27
|
+
if(failStart) throw new Error('READINESS_TIMEOUT')
|
|
28
|
+
return recording
|
|
29
|
+
}
|
|
30
|
+
async stop(r: IosRecording) {stops++; if(failStop) throw new Error('ACTION_TIMEOUT');r.recorder.signal('SIGINT');return r.recorder.done}
|
|
31
|
+
async monitor(device: string, app: {pid:number;executable:string}, recording: IosRecording) {
|
|
32
|
+
assert.equal(device,target.id);assert.equal(app.pid,100);assert.equal(app.executable,'/fixture.app/Fixture')
|
|
33
|
+
assert.equal(recording,recordings.at(-1),'Retries must retain the same owned recording')
|
|
34
|
+
now+=monitorTick
|
|
35
|
+
if(exitOnMonitor)recording.recorder.signal('SIGINT')
|
|
36
|
+
if(monitor)throw new Error(monitor)
|
|
37
|
+
}
|
|
38
|
+
async validate(r: IosRecording) {validations++;if(failExport)throw new Error(exportMessage);return {path:r.file,format:'instruments' as const,kind:'directory' as const,size_bytes:6,validation:{readable:true,schema:'time-sample',sample_rows:1}}}
|
|
39
|
+
shutdown() {for(const r of recordings)r.recorder.signal('SIGINT')}
|
|
40
|
+
}
|
|
41
|
+
const sessions = new IosTraceSessions(new Native(),time)
|
|
42
|
+
return {sessions,recordings,timers,flags:(v:{start?:boolean;stop?:boolean;export?:boolean;monitor?:string;monitorTick?:number;exitOnMonitor?:boolean;exportMessage?:string})=>{failStart=v.start??false;failStop=v.stop??false;failExport=v.export??false;monitor=v.monitor??'';monitorTick=v.monitorTick??0;exitOnMonitor=v.exitOnMonitor??false;exportMessage=v.exportMessage??'EXPORT_FAILED'},
|
|
43
|
+
tick:(ms:number)=>{now+=ms},counts:()=>({stops,starts,validations}),
|
|
44
|
+
cleanup:async()=>{await sessions.shutdown();for(const d of directories)await fs.rm(d,{recursive:true,force:true})}}
|
|
45
|
+
}
|
|
46
|
+
async function run() {
|
|
47
|
+
const f=fixture()
|
|
48
|
+
try {
|
|
49
|
+
const start=await f.sessions.start(target,'fixture')
|
|
50
|
+
await assert.rejects(()=>f.sessions.start(target,'fixture'),/TRACE_BUSY/)
|
|
51
|
+
assert.equal(start.state,'recording');assert.equal(start.recording_ready_at_ms,1000)
|
|
52
|
+
const finish=await f.sessions.link(start.trace_id,target,'journey')
|
|
53
|
+
for(const device of [{platform:'android',id:'sim'},{platform:'ios',id:'other'}])await assert.rejects(()=>f.sessions.link(start.trace_id,device,'wrong'),/TRACE_NOT_FOUND/)
|
|
54
|
+
const [a,b]=await Promise.all([f.sessions.control(start.trace_id,'stop'),f.sessions.control(start.trace_id,'stop')])
|
|
55
|
+
assert.equal(a.state,'completed');assert.deepEqual(a,b);assert.deepEqual(a.correlation_ids,['journey']);assert.equal((await finish()).coverage,'partial')
|
|
56
|
+
assert.equal(f.counts().validations,1)
|
|
57
|
+
await assert.rejects(()=>f.sessions.link(start.trace_id,target,'stale'),/TRACE_NOT_FOUND/)
|
|
58
|
+
await assert.rejects(()=>f.sessions.control('foreign','stop'),/TRACE_NOT_FOUND/)
|
|
59
|
+
assert.equal((await f.sessions.control(start.trace_id,'cancel')).artifact?.path,a.artifact?.path)
|
|
60
|
+
const cancelled=await f.sessions.start(target,'fixture')
|
|
61
|
+
const dir=path.dirname(f.recordings[1].file)
|
|
62
|
+
assert.equal((await f.sessions.control(cancelled.trace_id,'cancel')).state,'cancelled')
|
|
63
|
+
await assert.rejects(()=>fs.stat(dir),{code:'ENOENT'})
|
|
64
|
+
const expires=await f.sessions.start(target,'fixture',1000);f.tick(1001)
|
|
65
|
+
const expired=await f.sessions.control(expires.trace_id,'status')
|
|
66
|
+
assert.equal(expired.state,'completed');assert.equal(expired.completion_reason,'duration_limit')
|
|
67
|
+
f.tick(600001);await assert.rejects(()=>f.sessions.control(start.trace_id,'status'),/TRACE_NOT_FOUND/)
|
|
68
|
+
}finally{await f.cleanup()}
|
|
69
|
+
for(const failure of ['start','stop','export','monitor'] as const){
|
|
70
|
+
const f=fixture()
|
|
71
|
+
try{
|
|
72
|
+
if(failure==='start')f.flags({start:true})
|
|
73
|
+
const s=await f.sessions.start(target,'fixture')
|
|
74
|
+
if(failure==='start'){assert.equal(s.state,'failed');assert.equal(s.reason,'READINESS_TIMEOUT');assert.equal(s.cleanup.status,'complete');assert.equal(f.recordings[0].recorder.alive(),false);continue}
|
|
75
|
+
f.flags(failure==='monitor'?{monitor:'STORAGE_LIMIT'}:{[failure]:true})
|
|
76
|
+
const failed=await f.sessions.control(s.trace_id,failure==='monitor'?'status':'stop')
|
|
77
|
+
assert.equal(failed.state,'failed');assert.equal(failed.artifact,null)
|
|
78
|
+
if(failure==='monitor'){assert.equal(failed.reason,'STORAGE_LIMIT');assert.equal(failed.cleanup.status,'complete');continue}
|
|
79
|
+
f.flags({})
|
|
80
|
+
assert.equal((await f.sessions.control(s.trace_id,'stop')).state,'completed')
|
|
81
|
+
assert.equal(f.counts().starts,1,'Retry must not start a new recording')
|
|
82
|
+
}finally{await f.cleanup()}
|
|
83
|
+
}
|
|
84
|
+
const exportClassification=fixture()
|
|
85
|
+
try {
|
|
86
|
+
const s=await exportClassification.sessions.start(target,'fixture')
|
|
87
|
+
exportClassification.flags({export:true,exportMessage:'COMMAND_FAILED'})
|
|
88
|
+
const failed=await exportClassification.sessions.control(s.trace_id,'stop')
|
|
89
|
+
assert.equal(failed.reason,'TRACE_ARTIFACT_FAILED','Monitor classification must not alter export errors')
|
|
90
|
+
assert.match(failed.recovery!,/Retry stop/)
|
|
91
|
+
}finally{await exportClassification.cleanup()}
|
|
92
|
+
for(const reason of ['COMMAND_FAILED','ACTION_TIMEOUT','TARGET_DISCONNECTED','private unexpected output']) {
|
|
93
|
+
const f=fixture()
|
|
94
|
+
try {
|
|
95
|
+
const s=await f.sessions.start(target,'fixture')
|
|
96
|
+
f.flags({monitor:reason})
|
|
97
|
+
for(let n=0;n<2;n++)assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording')
|
|
98
|
+
assert.equal(f.counts().stops,0)
|
|
99
|
+
f.flags({});assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording')
|
|
100
|
+
f.flags({monitor:reason})
|
|
101
|
+
for(let n=0;n<2;n++)assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording','Successful poll must reset consecutive failure count')
|
|
102
|
+
const failed=await f.sessions.control(s.trace_id,'status')
|
|
103
|
+
assert.equal(failed.state,'failed');assert.equal(failed.reason,reason==='private unexpected output'?'TRACE_MONITOR_FAILED':reason)
|
|
104
|
+
assert.equal(failed.cleanup.status,'complete');assert.equal(failed.artifact,null)
|
|
105
|
+
assert.equal(f.counts().starts,1);assert.equal(f.recordings[0].recorder.alive(),false)
|
|
106
|
+
assert.ok(!JSON.stringify(failed).includes('private unexpected output'))
|
|
107
|
+
}finally{await f.cleanup()}
|
|
108
|
+
}
|
|
109
|
+
for(const reason of ['APP_IDENTITY_CHANGED','APP_NOT_RUNNING']) {
|
|
110
|
+
const f=fixture()
|
|
111
|
+
try {
|
|
112
|
+
const s=await f.sessions.start(target,'fixture');f.flags({monitor:reason})
|
|
113
|
+
const failed=await f.sessions.control(s.trace_id,'status')
|
|
114
|
+
assert.equal(failed.state,'failed');assert.equal(failed.reason,reason);assert.equal(failed.cleanup.status,'complete')
|
|
115
|
+
}finally{await f.cleanup()}
|
|
116
|
+
}
|
|
117
|
+
for(const ended of ['deadline','recorder'] as const) {
|
|
118
|
+
const f=fixture()
|
|
119
|
+
try {
|
|
120
|
+
const s=await f.sessions.start(target,'fixture',1000)
|
|
121
|
+
f.flags({monitor:'COMMAND_FAILED',monitorTick:ended==='deadline'?1001:0,exitOnMonitor:ended==='recorder'})
|
|
122
|
+
const result=await f.sessions.control(s.trace_id,'status')
|
|
123
|
+
assert.equal(result.state,'completed','A tolerated failed poll must still reconcile duration/recorder completion')
|
|
124
|
+
assert.equal(result.completion_reason,'duration_limit');assert.ok(result.artifact)
|
|
125
|
+
}finally{await f.cleanup()}
|
|
126
|
+
}
|
|
127
|
+
const cancelledStopFailure=fixture()
|
|
128
|
+
try {
|
|
129
|
+
const started=await cancelledStopFailure.sessions.start(target,'fixture')
|
|
130
|
+
const directory=path.dirname(cancelledStopFailure.recordings[0].file)
|
|
131
|
+
cancelledStopFailure.flags({stop:true})
|
|
132
|
+
const failed=await cancelledStopFailure.sessions.control(started.trace_id,'cancel')
|
|
133
|
+
assert.equal(failed.cleanup.status,'failed')
|
|
134
|
+
await fs.stat(directory) // Never remove a directory while the recorder may still write.
|
|
135
|
+
cancelledStopFailure.flags({})
|
|
136
|
+
assert.equal((await cancelledStopFailure.sessions.control(started.trace_id,'cancel')).cleanup.status,'complete')
|
|
137
|
+
} finally {await cancelledStopFailure.cleanup()}
|
|
138
|
+
const exitedStopFailure=fixture()
|
|
139
|
+
try {
|
|
140
|
+
const started=await exitedStopFailure.sessions.start(target,'fixture')
|
|
141
|
+
const directory=path.dirname(exitedStopFailure.recordings[0].file)
|
|
142
|
+
exitedStopFailure.flags({stop:true})
|
|
143
|
+
assert.deepEqual(await exitedStopFailure.sessions.shutdown(),[], 'a stop failure after confirmed recorder exit must not strand discarded files')
|
|
144
|
+
const result=await exitedStopFailure.sessions.control(started.trace_id,'status')
|
|
145
|
+
assert.equal(result.state,'failed', 'cleanup must not hide the recorder failure')
|
|
146
|
+
assert.equal(result.reason,'ACTION_TIMEOUT')
|
|
147
|
+
assert.equal(result.cleanup.status,'complete')
|
|
148
|
+
await assert.rejects(()=>fs.stat(directory),{code:'ENOENT'})
|
|
149
|
+
} finally {await exitedStopFailure.cleanup()}
|
|
150
|
+
const shutdown=fixture()
|
|
151
|
+
try{const s=await shutdown.sessions.start(target,'fixture');assert.deepEqual(await shutdown.sessions.shutdown(),[]);assert.equal((await shutdown.sessions.control(s.trace_id,'status')).state,'cancelled');await assert.rejects(()=>shutdown.sessions.start(target,'fixture'),/TRACE_BUSY/)}finally{await shutdown.cleanup()}
|
|
152
|
+
console.log('PASS iOS trace lifecycle, linkage, failure cleanup, retries and expiry')
|
|
153
|
+
}
|
|
154
|
+
run().catch(error=>{console.error(error);process.exitCode=1})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { ReadinessTracker } from '../../../src/observe/readiness.js'
|
|
3
|
+
import { withDeviceReadinessProbe } from '../../../src/observe/device-readiness.js'
|
|
4
|
+
import { runWithBudget } from '../../../src/utils/operation-budget.js'
|
|
5
|
+
|
|
6
|
+
async function run() {
|
|
7
|
+
let power = 'Asleep', lock = 'true', fail = false
|
|
8
|
+
const tracker = new ReadinessTracker('android', 'fixture')
|
|
9
|
+
const observe = () => withDeviceReadinessProbe(async (_command, args) => {
|
|
10
|
+
if (fail) throw new Error('native failure')
|
|
11
|
+
return { exitCode: 0, stderr: '', stdout: args.includes('power')
|
|
12
|
+
? (power ? `mWakefulness=${power}\nmWakefulnessChanging=false` : '')
|
|
13
|
+
: (lock ? `KeyguardServiceDelegate\n showing=${lock}\n mIsShowing=${lock}` : '') }
|
|
14
|
+
}, () => tracker.observeDevice())
|
|
15
|
+
await observe()
|
|
16
|
+
const blockedEvidence = structuredClone(tracker.deviceReadiness.evidence)
|
|
17
|
+
fail = true
|
|
18
|
+
await observe()
|
|
19
|
+
assert.deepEqual(tracker.deviceReadiness.blockers, ['DEVICE_ASLEEP', 'DEVICE_LOCKED'])
|
|
20
|
+
assert.deepEqual(tracker.deviceReadiness.evidence, blockedEvidence, 'retained evidence keeps its original timestamps')
|
|
21
|
+
assert.ok(tracker.deviceReadiness.limitations.includes('DEVICE_POWER_OBSERVATION_FAILED'))
|
|
22
|
+
assert.ok(tracker.deviceReadiness.limitations.includes('DEVICE_LOCK_OBSERVATION_FAILED'))
|
|
23
|
+
assert.ok(tracker.deviceReadiness.limitations.includes('DEVICE_BLOCKER_EVIDENCE_RETAINED'))
|
|
24
|
+
await runWithBudget(0, observe)
|
|
25
|
+
tracker.deadline()
|
|
26
|
+
assert.deepEqual(tracker.deviceReadiness.blockers, ['DEVICE_ASLEEP', 'DEVICE_LOCKED'])
|
|
27
|
+
assert.deepEqual(tracker.deviceReadiness.evidence, blockedEvidence)
|
|
28
|
+
fail = false; power = 'Awake'; lock = ''
|
|
29
|
+
await observe()
|
|
30
|
+
assert.deepEqual(tracker.deviceReadiness.blockers, ['DEVICE_LOCKED'], 'fresh awake evidence clears only the power blocker')
|
|
31
|
+
assert.equal(tracker.deviceReadiness.input_ready, false)
|
|
32
|
+
power = ''; lock = 'false'
|
|
33
|
+
await observe()
|
|
34
|
+
assert.deepEqual(tracker.deviceReadiness.blockers, [], 'fresh unlocked evidence clears the remaining lock blocker')
|
|
35
|
+
assert.equal(tracker.deviceReadiness.wakefulness, 'unknown', 'do not retain previously clear observations')
|
|
36
|
+
assert.equal(tracker.deviceReadiness.input_ready, null)
|
|
37
|
+
assert.ok(!tracker.deviceReadiness.limitations.includes('DEVICE_BLOCKER_EVIDENCE_RETAINED'))
|
|
38
|
+
console.log('PASS inconclusive/expired probes retain blockers per axis; only fresh clear signals remove them')
|
|
39
|
+
}
|
|
40
|
+
run().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -36,6 +36,6 @@ assert.equal(tracker.summary.capabilities.debugger_paused, true)
|
|
|
36
36
|
assert.ok(tracker.deadline().limitations.includes('DEADLINE_EXCEEDED'))
|
|
37
37
|
assert.equal(new ReadinessTracker('android', 'fixture').observe(sample(), 10000, 9000).state, 'unknown', 'no cross-request history')
|
|
38
38
|
assert.ok(!JSON.stringify(tracker.summary).includes('Ready'), 'evidence contains codes rather than private labels')
|
|
39
|
-
assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused,
|
|
39
|
+
assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused, true)
|
|
40
40
|
assert.ok(tracker.summary.limitations.includes('MANAGED_DEBUGGER_ONLY'))
|
|
41
41
|
console.log('Readiness classifier: PASS')
|