mobile-debug-mcp 0.24.0 → 0.24.2

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.
@@ -63,26 +63,31 @@ export function inferScrollFailure(message: string | undefined): { failureCode:
63
63
 
64
64
  export function buildActionExecutionResult({
65
65
  actionType,
66
+ device,
66
67
  selector,
67
68
  resolved,
68
69
  success,
69
70
  uiFingerprintBefore,
70
71
  uiFingerprintAfter,
71
- failure
72
+ failure,
73
+ details
72
74
  }: {
73
75
  actionType: string
76
+ device?: ActionExecutionResult['device']
74
77
  selector: Record<string, unknown> | null
75
78
  resolved?: Partial<ActionTargetResolved> | null
76
79
  success: boolean
77
80
  uiFingerprintBefore: string | null
78
81
  uiFingerprintAfter: string | null
79
82
  failure?: { failureCode: ActionFailureCode; retryable: boolean }
83
+ details?: Record<string, unknown>
80
84
  }): ActionExecutionResult {
81
85
  const timestamp = Date.now()
82
86
  return {
83
87
  action_id: nextActionId(actionType, timestamp),
84
88
  timestamp,
85
89
  action_type: actionType,
90
+ ...(device ? { device } : {}),
86
91
  target: {
87
92
  selector,
88
93
  resolved: normalizeResolvedTarget(resolved)
@@ -90,6 +95,7 @@ export function buildActionExecutionResult({
90
95
  success,
91
96
  ...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
92
97
  ui_fingerprint_before: uiFingerprintBefore,
93
- ui_fingerprint_after: uiFingerprintAfter
98
+ ui_fingerprint_after: uiFingerprintAfter,
99
+ ...(details ? { details } : {})
94
100
  }
95
101
  }
@@ -27,11 +27,19 @@ async function handleStartApp(args: ToolCallArgs) {
27
27
  const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId)
28
28
  return wrapResponse(buildActionExecutionResult({
29
29
  actionType: 'start_app',
30
+ device: res.device,
30
31
  selector: { appId },
31
32
  success: !!res.appStarted,
32
33
  uiFingerprintBefore,
33
34
  uiFingerprintAfter,
34
- failure: res.appStarted ? undefined : inferGenericFailure((res as any).error)
35
+ failure: res.appStarted ? undefined : inferGenericFailure(res.error),
36
+ details: {
37
+ launch_time_ms: res.launchTimeMs,
38
+ ...(typeof res.output === 'string' ? { output: res.output } : {}),
39
+ ...(res.device ? { device_id: res.device.id } : {}),
40
+ ...(typeof res.error === 'string' ? { error: res.error } : {}),
41
+ ...(res.observedApp ? { observed_app: res.observedApp } : {})
42
+ }
35
43
  }))
36
44
  }
37
45
 
@@ -50,11 +58,20 @@ async function handleRestartApp(args: ToolCallArgs) {
50
58
  const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId)
51
59
  return wrapResponse(buildActionExecutionResult({
52
60
  actionType: 'restart_app',
61
+ device: res.device,
53
62
  selector: { appId },
54
63
  success: !!res.appRestarted,
55
64
  uiFingerprintBefore,
56
65
  uiFingerprintAfter,
57
- failure: res.appRestarted ? undefined : inferGenericFailure((res as any).error)
66
+ failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
67
+ details: {
68
+ launch_time_ms: res.launchTimeMs,
69
+ ...(typeof res.output === 'string' ? { output: res.output } : {}),
70
+ ...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
71
+ ...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
72
+ ...(typeof res.error === 'string' ? { error: res.error } : {}),
73
+ ...(res.observedApp ? { observed_app: res.observedApp } : {})
74
+ }
58
75
  }))
59
76
  }
60
77
 
@@ -10,8 +10,34 @@ export async function getSystemStatus() {
10
10
  const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])]
11
11
 
12
12
  const success = issues.length === 0
13
+ const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue))
14
+ const iosReady = ios.iosAvailable && ios.iosDevices > 0
15
+ const gradleReady = (gradle.issues || []).length === 0
16
+ const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked')
17
+
18
+ const androidSummary = !android.adbAvailable
19
+ ? 'ADB unavailable'
20
+ : android.devices === 0
21
+ ? 'ADB available but no Android devices connected'
22
+ : android.logsAvailable
23
+ ? `${android.devices} Android device(s) connected; log access available`
24
+ : `${android.devices} Android device(s) connected; log access unavailable`
25
+
26
+ const iosSummary = !ios.iosAvailable
27
+ ? 'xcrun unavailable'
28
+ : ios.iosDevices === 0
29
+ ? 'xcrun available but no iOS simulators booted'
30
+ : `${ios.iosDevices} iOS simulator(s) booted`
31
+
32
+ const gradleSummary = !gradle.gradleJavaHome
33
+ ? 'No explicit Gradle JDK override detected'
34
+ : gradleReady
35
+ ? `Gradle JDK configured at ${gradle.gradleJavaHome}`
36
+ : `Gradle JDK override invalid: ${gradle.gradleJavaHome}`
37
+
13
38
  return {
14
39
  success,
40
+ status: overallStatus,
15
41
  adbAvailable: android.adbAvailable,
16
42
  adbVersion: android.adbVersion,
17
43
  devices: android.devices,
@@ -25,9 +51,38 @@ export async function getSystemStatus() {
25
51
  gradleJavaHome: gradle.gradleJavaHome,
26
52
  gradleValid: gradle.gradleValid,
27
53
  gradleFilesChecked: gradle.filesChecked,
28
- gradleSuggestedFixes: gradle.suggestedFixes
54
+ gradleSuggestedFixes: gradle.suggestedFixes,
55
+ summary: {
56
+ overall: overallStatus,
57
+ android: {
58
+ ready: androidReady,
59
+ summary: androidSummary,
60
+ blockers: android.issues
61
+ },
62
+ ios: {
63
+ ready: iosReady,
64
+ summary: iosSummary,
65
+ blockers: ios.issues
66
+ },
67
+ gradle: {
68
+ ready: gradleReady,
69
+ summary: gradleSummary,
70
+ blockers: gradle.issues || [],
71
+ suggestedFixes: gradle.suggestedFixes || []
72
+ }
73
+ }
29
74
  }
30
75
  } catch (e: unknown) {
31
- return { success: false, issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))] }
76
+ return {
77
+ success: false,
78
+ status: 'blocked',
79
+ issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))],
80
+ summary: {
81
+ overall: 'blocked',
82
+ android: { ready: false, summary: 'Android status unavailable', blockers: [] },
83
+ ios: { ready: false, summary: 'iOS status unavailable', blockers: [] },
84
+ gradle: { ready: false, summary: 'Gradle status unavailable', blockers: [], suggestedFixes: [] }
85
+ }
86
+ }
32
87
  }
33
88
  }
package/src/types.ts CHANGED
@@ -10,6 +10,15 @@ export interface StartAppResponse {
10
10
  device: DeviceInfo;
11
11
  appStarted: boolean;
12
12
  launchTimeMs: number;
13
+ output?: string;
14
+ observedApp?: {
15
+ appId: string;
16
+ package?: string | null;
17
+ activity?: string | null;
18
+ screen?: string | null;
19
+ pid?: number | null;
20
+ matchedTarget?: boolean | null;
21
+ };
13
22
  error?: string;
14
23
  diagnostics?: any;
15
24
  }
@@ -25,6 +34,17 @@ export interface RestartAppResponse {
25
34
  device: DeviceInfo;
26
35
  appRestarted: boolean;
27
36
  launchTimeMs: number;
37
+ output?: string;
38
+ observedApp?: {
39
+ appId: string;
40
+ package?: string | null;
41
+ activity?: string | null;
42
+ screen?: string | null;
43
+ pid?: number | null;
44
+ matchedTarget?: boolean | null;
45
+ };
46
+ terminatedBeforeRestart?: boolean;
47
+ terminateError?: string;
28
48
  error?: string;
29
49
  diagnostics?: any;
30
50
  }
@@ -155,6 +175,7 @@ export interface ActionExecutionResult {
155
175
  action_id: string;
156
176
  timestamp: number;
157
177
  action_type: string;
178
+ device?: DeviceInfo;
158
179
  target: {
159
180
  selector: Record<string, unknown> | null;
160
181
  resolved: ActionTargetResolved | null;
@@ -164,6 +185,7 @@ export interface ActionExecutionResult {
164
185
  retryable?: boolean;
165
186
  ui_fingerprint_before?: string | null;
166
187
  ui_fingerprint_after?: string | null;
188
+ details?: Record<string, unknown>;
167
189
  }
168
190
 
169
191
  export interface TapElementResponse extends ActionExecutionResult {}
@@ -179,6 +201,11 @@ export interface ExpectScreenResponse {
179
201
  screen: string | null;
180
202
  };
181
203
  confidence: number;
204
+ comparison: {
205
+ basis: 'fingerprint' | 'screen' | 'none';
206
+ matched: boolean;
207
+ reason: string;
208
+ };
182
209
  }
183
210
 
184
211
  export interface ExpectElementVisibleResponse {
@@ -190,7 +217,16 @@ export interface ExpectElementVisibleResponse {
190
217
  contains?: boolean;
191
218
  };
192
219
  element_id: string | null;
220
+ expected_condition?: 'visible';
193
221
  element?: ActionTargetResolved | null;
222
+ observed?: {
223
+ status?: string;
224
+ matched_count?: number;
225
+ condition_satisfied?: boolean;
226
+ selected_index?: number | null;
227
+ last_matched_element?: ActionTargetResolved | null;
228
+ };
229
+ reason?: string;
194
230
  failure_code?: 'TIMEOUT' | 'ELEMENT_NOT_FOUND' | 'UNKNOWN';
195
231
  retryable?: boolean;
196
232
  }
@@ -15,7 +15,12 @@ async function run() {
15
15
  success: true,
16
16
  observed_screen: { fingerprint: 'fp_home', screen: 'com.example.HomeActivity' },
17
17
  expected_screen: { fingerprint: 'fp_home', screen: null },
18
- confidence: 1
18
+ confidence: 1,
19
+ comparison: {
20
+ basis: 'fingerprint',
21
+ matched: true,
22
+ reason: 'observed fingerprint matches expected fingerprint fp_home'
23
+ }
19
24
  })
20
25
 
21
26
  ;(Observe as any).ToolsObserve.getCurrentScreenHandler = async () => ({
@@ -26,6 +31,11 @@ async function run() {
26
31
  assert.strictEqual(expectScreen.success, true)
27
32
  assert.strictEqual(expectScreen.observed_screen.screen, 'HomeActivity')
28
33
  assert.strictEqual(expectScreen.confidence, 1)
34
+ assert.deepStrictEqual(expectScreen.comparison, {
35
+ basis: 'screen',
36
+ matched: true,
37
+ reason: 'observed screen matches expected screen HomeActivity'
38
+ })
29
39
 
30
40
  ;(ToolsInteract as any).waitForUIHandler = async () => ({
31
41
  status: 'success',
@@ -46,10 +56,18 @@ async function run() {
46
56
  assert.strictEqual(expectElementVisible.success, true)
47
57
  assert.strictEqual(expectElementVisible.element_id, 'el_ready')
48
58
  assert.strictEqual(expectElementVisible.element?.resource_id, 'rid_ready')
59
+ assert.strictEqual(expectElementVisible.expected_condition, 'visible')
60
+ assert.strictEqual(expectElementVisible.reason, 'selector is visible')
49
61
 
50
62
  ;(ToolsInteract as any).waitForUIHandler = async () => ({
51
63
  status: 'timeout',
52
- error: { code: 'ELEMENT_NOT_FOUND', message: 'Condition visible not satisfied within timeout' }
64
+ error: { code: 'ELEMENT_NOT_FOUND', message: 'Condition visible not satisfied within timeout; observed 0 match(es)' },
65
+ observed: {
66
+ matched_count: 0,
67
+ condition_satisfied: false,
68
+ selected_index: null,
69
+ last_matched_element: null
70
+ }
53
71
  })
54
72
  const timeoutResult = await ToolsInteract.expectElementVisibleHandler({
55
73
  selector: { text: 'Missing' },
@@ -59,6 +77,15 @@ async function run() {
59
77
  success: false,
60
78
  selector: { text: 'Missing' },
61
79
  element_id: null,
80
+ expected_condition: 'visible',
81
+ observed: {
82
+ status: 'timeout',
83
+ matched_count: 0,
84
+ condition_satisfied: false,
85
+ selected_index: null,
86
+ last_matched_element: null
87
+ },
88
+ reason: 'Condition visible not satisfied within timeout; observed 0 match(es)',
62
89
  failure_code: 'TIMEOUT',
63
90
  retryable: true
64
91
  })
@@ -9,22 +9,26 @@ async function runTests() {
9
9
 
10
10
  try {
11
11
  let seq1: Array<string | null> = ['B', 'B']
12
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null })
12
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null, activity: 'MainActivity' })
13
13
  const start1 = Date.now()
14
14
  const res1 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 2000, pollIntervalMs: 50 })
15
15
  const elapsed1 = Date.now() - start1
16
16
  assert.ok(res1 && (res1 as any).success === true && (res1 as any).newFingerprint === 'B', 'Immediate fingerprint change should succeed')
17
+ assert.strictEqual((res1 as any).previousFingerprint, 'A')
18
+ assert.strictEqual((res1 as any).observed_screen.activity, 'MainActivity')
17
19
  console.log('Test 1: Immediate change -> PASS', 'Elapsed:', elapsed1, 'ms')
18
20
 
19
21
  let seq2: Array<string | null> = [null, null, 'B', 'B']
20
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B' })
22
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B', activity: 'NextActivity' })
21
23
  const res2 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 3000, pollIntervalMs: 50 })
22
24
  assert.ok(res2 && (res2 as any).success === true && (res2 as any).newFingerprint === 'B', 'Transient nulls should not prevent success')
23
25
  console.log('Test 2: Transient nulls -> PASS')
24
26
 
25
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A' })
27
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A', activity: 'HomeActivity' })
26
28
  const res3 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 300, pollIntervalMs: 50 })
27
29
  assert.ok(res3 && (res3 as any).success === false && (res3 as any).reason === 'timeout', 'Unchanged fingerprint should time out')
30
+ assert.strictEqual((res3 as any).previousFingerprint, 'A')
31
+ assert.strictEqual((res3 as any).observed_screen.activity, 'HomeActivity')
28
32
  console.log('Test 3: Timeout -> PASS')
29
33
  } finally {
30
34
  ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = original
@@ -12,6 +12,12 @@ async function run() {
12
12
  const r1 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hello' }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
13
13
  const ok1 = r1 && r1.status === 'success' && r1.matched === 1 && r1.element && r1.element.text === 'Hello' && typeof r1.element.elementId === 'string'
14
14
  assert.ok(ok1, 'Exact match should satisfy exists condition')
15
+ assert.deepStrictEqual((r1 as any).requested, {
16
+ selector: { text: 'Hello' },
17
+ condition: 'exists',
18
+ match: null
19
+ })
20
+ assert.strictEqual((r1 as any).observed.matched_count, 1)
15
21
  console.log('Exact match exists:', ok1 ? 'PASS' : 'FAIL', JSON.stringify(r1, null, 2))
16
22
 
17
23
  // Test 2: contains matching
@@ -26,6 +32,9 @@ async function run() {
26
32
  const r3 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hidden' }, condition: 'visible', timeout_ms: 300, poll_interval_ms: 50, platform: 'android' })
27
33
  const ok3 = r3 && r3.status === 'timeout' && r3.error && r3.error.code === 'ELEMENT_NOT_FOUND'
28
34
  assert.ok(ok3, 'Hidden element should fail visible condition')
35
+ assert.strictEqual((r3 as any).observed.matched_count, 1)
36
+ assert.strictEqual((r3 as any).observed.condition_satisfied, false)
37
+ assert.match((r3 as any).error.message, /observed 1 match/)
29
38
  console.log('Visible negative (hidden element):', ok3 ? 'PASS' : 'FAIL', JSON.stringify(r3, null, 2))
30
39
 
31
40
  // Test 4: clickable condition
@@ -66,8 +75,18 @@ async function run() {
66
75
  const r7 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, retry: { max_attempts: 1 }, platform: 'android' })
67
76
  const ok7 = r7 && r7.status === 'timeout' && r7.error && r7.error.code === 'ELEMENT_NOT_FOUND'
68
77
  assert.ok(ok7, 'Missing selector should time out with ELEMENT_NOT_FOUND')
78
+ assert.strictEqual((r7 as any).observed.selected_index, null)
69
79
  console.log('Timeout no match:', ok7 ? 'PASS' : 'FAIL', JSON.stringify(r7, null, 2))
70
80
 
81
+ // Test 8: requested match index out of range should not report a selected index
82
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'OnlyOne', resourceId: 'rid8', bounds: [0,0,10,10], visible: true } ] })
83
+ const r8 = await ToolsInteract.waitForUIHandler({ selector: { text: 'OnlyOne' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, match: { index: 1 }, platform: 'android' })
84
+ const ok8 = r8 && r8.status === 'timeout' && r8.error && r8.error.code === 'ELEMENT_NOT_FOUND'
85
+ assert.ok(ok8, 'Out-of-range match index should time out deterministically')
86
+ assert.strictEqual((r8 as any).observed.matched_count, 1)
87
+ assert.strictEqual((r8 as any).observed.selected_index, null)
88
+ console.log('Out-of-range match index:', ok8 ? 'PASS' : 'FAIL', JSON.stringify(r8, null, 2))
89
+
71
90
  } finally {
72
91
  ;(Observe as any).ToolsObserve.getUITreeHandler = origGetUITree
73
92
  }
@@ -69,8 +69,101 @@ exit 0
69
69
  assert.ok(res2.output && typeof res2.output === 'string', 'Project dir install succeeded with output')
70
70
  }
71
71
 
72
+ const testOnlyAdbPath = path.join(binDir, 'adb-test-only')
73
+ const testOnlyAdbScript = `#!/bin/sh
74
+ if [ "$1" = "-s" ]; then
75
+ shift 2
76
+ fi
77
+
78
+ if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
79
+ case "$3" in
80
+ ro.build.version.release) echo '16' ;;
81
+ ro.product.model) echo 'sdk_gphone64_arm64' ;;
82
+ ro.kernel.qemu) echo '1' ;;
83
+ esac
84
+ exit 0
85
+ fi
86
+
87
+ if [ "$1" = "install" ]; then
88
+ if [ "$2" = "-r" ] && [ "$3" = "-t" ]; then
89
+ echo 'Performing Streamed Install'
90
+ echo 'Success'
91
+ exit 0
92
+ fi
93
+ echo 'Performing Streamed Install'
94
+ echo 'adb: failed to install test.apk: Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]' 1>&2
95
+ exit 1
96
+ fi
97
+
98
+ echo 'Success'
99
+ exit 0
100
+ `
101
+ await fs.writeFile(testOnlyAdbPath, testOnlyAdbScript, { mode: 0o755 })
102
+ process.env.ADB_PATH = testOnlyAdbPath
103
+
104
+ const { dir: d2, file: testOnlyApk } = await makeTempFile('.apk')
105
+ const testOnlyRes = await ai.installApp(testOnlyApk, 'emulator-5554')
106
+ console.log('testOnlyRes', testOnlyRes)
107
+ assert.strictEqual(testOnlyRes.installed, true, 'Test-only APK should retry with -t and install successfully')
108
+
109
+ const cleanupLog = path.join(binDir, 'pm-cleanup.log')
110
+ const pmFallbackAdbPath = path.join(binDir, 'adb-pm-fallback')
111
+ const pmFallbackAdbScript = `#!/bin/sh
112
+ if [ "$1" = "-s" ]; then
113
+ shift 2
114
+ fi
115
+
116
+ if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
117
+ case "$3" in
118
+ ro.build.version.release) echo '16' ;;
119
+ ro.product.model) echo 'sdk_gphone64_arm64' ;;
120
+ ro.kernel.qemu) echo '1' ;;
121
+ esac
122
+ exit 0
123
+ fi
124
+
125
+ if [ "$1" = "install" ]; then
126
+ echo 'adb install failed' 1>&2
127
+ exit 1
128
+ fi
129
+
130
+ if [ "$1" = "push" ]; then
131
+ echo 'pushed'
132
+ exit 0
133
+ fi
134
+
135
+ if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "install" ]; then
136
+ if [ "$4" = "-r" ] && [ "$5" = "-t" ]; then
137
+ echo 'Failure [INSTALL_FAILED_VERSION_DOWNGRADE]'
138
+ exit 1
139
+ fi
140
+ echo 'Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]'
141
+ exit 1
142
+ fi
143
+
144
+ if [ "$1" = "shell" ] && [ "$2" = "rm" ]; then
145
+ echo cleanup >> "${cleanupLog}"
146
+ exit 0
147
+ fi
148
+
149
+ echo 'unexpected args:' "$@" 1>&2
150
+ exit 1
151
+ `
152
+ await fs.writeFile(pmFallbackAdbPath, pmFallbackAdbScript, { mode: 0o755 })
153
+ process.env.ADB_PATH = pmFallbackAdbPath
154
+
155
+ const { dir: d3, file: pmFallbackApk } = await makeTempFile('.apk')
156
+ const pmFallbackRes = await ai.installApp(pmFallbackApk, 'emulator-5554')
157
+ console.log('pmFallbackRes', pmFallbackRes)
158
+ assert.strictEqual(pmFallbackRes.installed, false, 'Failed pm fallback should surface as install failure')
159
+ assert.match(pmFallbackRes.error || '', /INSTALL_FAILED_VERSION_DOWNGRADE/, 'Final pm retry failure should be reported')
160
+ const cleanupCount = (await fs.readFile(cleanupLog, 'utf8')).trim().split('\n').filter(Boolean).length
161
+ assert.strictEqual(cleanupCount, 1, 'pm fallback cleanup should run once')
162
+
72
163
  // cleanup
73
164
  await fs.rm(d1, { recursive: true, force: true }).catch(() => {})
165
+ await fs.rm(d2, { recursive: true, force: true }).catch(() => {})
166
+ await fs.rm(d3, { recursive: true, force: true }).catch(() => {})
74
167
  await fs.rm(dirGradle, { recursive: true, force: true }).catch(() => {})
75
168
 
76
169
  // restore PATH and ADB_PATH
@@ -87,4 +180,4 @@ exit 0
87
180
  }
88
181
  }
89
182
 
90
- run().catch((e) => { console.error(e); process.exit(1) })
183
+ run().catch((e) => { console.error(e); process.exit(1) })
@@ -78,38 +78,55 @@ async function run() {
78
78
  return {
79
79
  device: { platform: 'android', id: 'emulator-5554', osVersion: '14', model: 'Pixel', simulator: true },
80
80
  appStarted: true,
81
- launchTimeMs: 123
81
+ launchTimeMs: 123,
82
+ output: 'Events injected: 1',
83
+ observedApp: {
84
+ appId: 'com.example.app',
85
+ package: 'com.example.app',
86
+ activity: 'com.example.MainActivity',
87
+ screen: 'MainActivity',
88
+ matchedTarget: true
89
+ }
82
90
  } as any
83
91
  }
84
92
  const startAppResponse = await handleToolCall('start_app', { platform: 'android', appId: 'com.example.app' })
85
93
  const startAppPayload = JSON.parse((startAppResponse as any).content[0].text)
86
94
  assert.strictEqual(startAppPayload.success, true)
87
95
  assert.strictEqual(startAppPayload.action_type, 'start_app')
96
+ assert.strictEqual(startAppPayload.device.id, 'emulator-5554')
88
97
  assert.deepStrictEqual(startAppPayload.target.selector, { appId: 'com.example.app' })
98
+ assert.strictEqual(startAppPayload.details.launch_time_ms, 123)
99
+ assert.strictEqual(startAppPayload.details.observed_app.matchedTarget, true)
89
100
 
90
101
  ;(ToolsInteract as any).expectScreenHandler = async () => ({
91
102
  success: true,
92
103
  observed_screen: { fingerprint: 'fp_after', screen: 'MainActivity' },
93
104
  expected_screen: { fingerprint: 'fp_after', screen: null },
94
- confidence: 1
105
+ confidence: 1,
106
+ comparison: { basis: 'fingerprint', matched: true, reason: 'observed fingerprint matches expected fingerprint fp_after' }
95
107
  })
96
108
 
97
109
  const expectScreenResponse = await handleToolCall('expect_screen', { fingerprint: 'fp_after' })
98
110
  const expectScreenPayload = JSON.parse((expectScreenResponse as any).content[0].text)
99
111
  assert.strictEqual(expectScreenPayload.success, true)
100
112
  assert.strictEqual(expectScreenPayload.confidence, 1)
113
+ assert.strictEqual(expectScreenPayload.comparison.basis, 'fingerprint')
101
114
 
102
115
  ;(ToolsInteract as any).expectElementVisibleHandler = async () => ({
103
116
  success: true,
104
117
  selector: { text: 'Ready' },
105
118
  element_id: 'el_ready',
106
- element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 }
119
+ expected_condition: 'visible',
120
+ element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 },
121
+ observed: { status: 'success', matched_count: 1, condition_satisfied: true, selected_index: 0, last_matched_element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 } },
122
+ reason: 'selector is visible'
107
123
  })
108
124
 
109
125
  const expectElementResponse = await handleToolCall('expect_element_visible', { selector: { text: 'Ready' } })
110
126
  const expectElementPayload = JSON.parse((expectElementResponse as any).content[0].text)
111
127
  assert.strictEqual(expectElementPayload.success, true)
112
128
  assert.strictEqual(expectElementPayload.element_id, 'el_ready')
129
+ assert.strictEqual(expectElementPayload.expected_condition, 'visible')
113
130
 
114
131
  ;(ToolsObserve as any).captureScreenshotHandler = async () => ({
115
132
  device: { platform: 'ios', id: 'booted', osVersion: '18.0', model: 'Simulator', simulator: true },
@@ -6,7 +6,9 @@ import { getSystemStatus } from '../../../src/system/index.js'
6
6
  async function run() {
7
7
  const payload = await getSystemStatus()
8
8
  assert(typeof payload.success === 'boolean')
9
+ assert(typeof (payload as any).status === 'string')
9
10
  assert(Array.isArray(payload.issues))
11
+ assert((payload as any).summary && typeof (payload as any).summary.overall === 'string')
10
12
 
11
13
  const adb = ensureAdbAvailable()
12
14
  assert(adb && typeof adb.ok === 'boolean')
@@ -93,8 +93,11 @@ async function run() {
93
93
 
94
94
  const healthy = await systemStatus.getSystemStatus()
95
95
  assert.strictEqual(healthy.success, true)
96
+ assert.strictEqual((healthy as any).status, 'ready')
96
97
  assert.strictEqual(healthy.adbAvailable, true)
97
98
  assert.strictEqual(typeof healthy.adbVersion, 'string')
99
+ assert.strictEqual((healthy as any).summary.android.ready, true)
100
+ assert.strictEqual(typeof (healthy as any).summary.ios.summary, 'string')
98
101
 
99
102
  setScenario({
100
103
  ADB_VERSION_STATUS: '1',
@@ -105,6 +108,7 @@ async function run() {
105
108
  })
106
109
  const missingAdb = await systemStatus.getSystemStatus()
107
110
  assert.strictEqual(missingAdb.success, false)
111
+ assert.strictEqual((missingAdb as any).summary.android.ready, false)
108
112
  assert(missingAdb.issues.some((issue: string) => issue.includes('ADB')))
109
113
 
110
114
  setScenario({
@@ -116,6 +120,7 @@ async function run() {
116
120
  })
117
121
  const unauthorized = await systemStatus.getSystemStatus()
118
122
  assert.strictEqual(unauthorized.success, false)
123
+ assert.strictEqual((unauthorized as any).summary.android.ready, false)
119
124
  assert(unauthorized.issues.some((issue: string) => issue.includes('unauthorized')))
120
125
  assert(unauthorized.issues.some((issue: string) => issue.includes('offline')))
121
126
 
@@ -130,6 +135,7 @@ async function run() {
130
135
  assert.strictEqual(missingXcrun.iosAvailable, false)
131
136
  assert.strictEqual(missingXcrun.adbAvailable, true)
132
137
  assert.strictEqual(Array.isArray(missingXcrun.issues), true)
138
+ assert.strictEqual((missingXcrun as any).summary.ios.ready, false)
133
139
 
134
140
  console.log('system_status checks passed')
135
141
  } finally {