mobile-debug-mcp 0.16.0 → 0.17.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.
@@ -82,4 +82,107 @@ export class ToolsObserve {
82
82
  // Both observes implement getScreenFingerprint
83
83
  return await observe.getScreenFingerprint(resolved.id);
84
84
  }
85
+ static async captureDebugSnapshotHandler({ reason, includeLogs = true, logLines = 200, platform, appId, deviceId, sessionId } = {}) {
86
+ const timestamp = Date.now();
87
+ const out = { timestamp, reason: reason || '', activity: null, fingerprint: null, screenshot: null, ui_tree: null, logs: [] };
88
+ // Parallel fetches for performance: screenshot, current screen, fingerprint, ui tree, and log stream/get logs
89
+ const sid = sessionId || 'default';
90
+ const tasks = {
91
+ screenshot: ToolsObserve.captureScreenshotHandler({ platform, deviceId }),
92
+ currentScreen: (!platform || platform === 'android') ? ToolsObserve.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
93
+ fingerprint: ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }),
94
+ uiTree: ToolsObserve.getUITreeHandler({ platform, deviceId }),
95
+ readLogStream: includeLogs ? ToolsObserve.readLogStreamHandler({ platform, sessionId: sid, limit: logLines }) : Promise.resolve({ entries: [] }),
96
+ };
97
+ const results = await Promise.allSettled(Object.values(tasks));
98
+ const keys = Object.keys(tasks);
99
+ // Map results back to keys
100
+ for (let i = 0; i < results.length; i++) {
101
+ const key = keys[i];
102
+ const res = results[i];
103
+ if (res.status === 'fulfilled') {
104
+ const val = res.value;
105
+ if (key === 'screenshot') {
106
+ out.screenshot = val && val.screenshot ? val.screenshot : null;
107
+ }
108
+ else if (key === 'currentScreen') {
109
+ out.activity = val && ((val.activity || val.shortActivity)) ? (val.activity || val.shortActivity) : out.activity || '';
110
+ }
111
+ else if (key === 'fingerprint') {
112
+ if (val && val.fingerprint)
113
+ out.fingerprint = val.fingerprint;
114
+ if (val && val.activity)
115
+ out.activity = out.activity || val.activity;
116
+ if (val && val.error)
117
+ out.fingerprint_error = val.error;
118
+ }
119
+ else if (key === 'uiTree') {
120
+ out.ui_tree = val;
121
+ if (val && val.error)
122
+ out.ui_tree_error = val.error;
123
+ }
124
+ else if (key === 'readLogStream') {
125
+ // handle below after evaluating fallback
126
+ // temporarily attach to out._streamEntries
127
+ out._streamEntries = val && val.entries ? val.entries : [];
128
+ }
129
+ }
130
+ else {
131
+ const errMsg = res.reason instanceof Error ? res.reason.message : String(res.reason);
132
+ if (key === 'screenshot')
133
+ out.screenshot_error = errMsg;
134
+ if (key === 'currentScreen')
135
+ out.activity_error = errMsg;
136
+ if (key === 'fingerprint') {
137
+ out.fingerprint = null;
138
+ out.fingerprint_error = errMsg;
139
+ }
140
+ if (key === 'uiTree') {
141
+ out.ui_tree = null;
142
+ out.ui_tree_error = errMsg;
143
+ }
144
+ if (key === 'readLogStream') {
145
+ out._streamEntries = [];
146
+ out.logs_error = errMsg;
147
+ }
148
+ }
149
+ }
150
+ // Logs: prefer stream entries, fallback to snapshot logs when empty
151
+ if (includeLogs) {
152
+ try {
153
+ let entries = Array.isArray(out._streamEntries) ? out._streamEntries : [];
154
+ if (!entries || entries.length === 0) {
155
+ const gl = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines: logLines });
156
+ const raw = (gl && gl.logs) ? gl.logs : [];
157
+ entries = raw.slice(-Math.max(0, logLines)).map(line => {
158
+ const level = /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(line) ? 'ERROR' : /\b(WARN| W )\b/i.test(line) ? 'WARN' : 'INFO';
159
+ return { timestamp: null, level, message: line };
160
+ });
161
+ }
162
+ else {
163
+ entries = entries.map(ent => {
164
+ const msg = (ent && (ent.message || ent.msg)) ? (ent.message || ent.msg) : (typeof ent === 'string' ? ent : JSON.stringify(ent));
165
+ const levelRaw = (ent && (ent.level || ent.levelName || ent._level)) ? (ent.level || ent.levelName || ent._level) : '';
166
+ const level = (levelRaw && String(levelRaw)).toString().toUpperCase() || (/\bERROR\b/i.test(msg) ? 'ERROR' : /\bWARN\b/i.test(msg) ? 'WARN' : 'INFO');
167
+ let tsNum = null;
168
+ const maybeIso = ent && (ent._iso || ent.timestamp);
169
+ if (maybeIso && typeof maybeIso === 'string') {
170
+ const d = new Date(maybeIso);
171
+ if (!isNaN(d.getTime()))
172
+ tsNum = d.getTime();
173
+ }
174
+ return { timestamp: tsNum, level, message: msg };
175
+ });
176
+ }
177
+ out.logs = entries;
178
+ }
179
+ catch (e) {
180
+ out.logs = [];
181
+ out.logs_error = e instanceof Error ? e.message : String(e);
182
+ }
183
+ }
184
+ // Clean up internal temporary field
185
+ delete out._streamEntries;
186
+ return out;
187
+ }
85
188
  }
package/dist/server.js CHANGED
@@ -195,6 +195,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
195
195
  required: ["platform"]
196
196
  }
197
197
  },
198
+ {
199
+ name: "capture_debug_snapshot",
200
+ description: "Capture a complete debug snapshot (screenshot, ui tree, activity, fingerprint, logs). Returns structured JSON.",
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ reason: { type: "string", description: "Optional reason for snapshot" },
205
+ includeLogs: { type: "boolean", description: "Whether to include logs", default: true },
206
+ logLines: { type: "number", description: "Maximum number of log lines to include", default: 200 },
207
+ platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override" },
208
+ appId: { type: "string", description: "Optional appId to scope logs (package/bundle id)" },
209
+ deviceId: { type: "string", description: "Optional device serial/udid" },
210
+ sessionId: { type: "string", description: "Optional log stream session id to prefer" }
211
+ }
212
+ }
213
+ },
198
214
  {
199
215
  name: "start_log_stream",
200
216
  description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
@@ -556,6 +572,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
556
572
  ]
557
573
  };
558
574
  }
575
+ if (name === "capture_debug_snapshot") {
576
+ const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args;
577
+ const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId });
578
+ return wrapResponse(res);
579
+ }
559
580
  if (name === "get_ui_tree") {
560
581
  const { platform, deviceId } = args;
561
582
  const res = await ToolsObserve.getUITreeHandler({ platform, deviceId });
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.17.0]
6
+ - Added `capture_debug_snapshot` observe tool: captures a full debugging snapshot including screenshot (base64), UI tree, current activity (Android), screen fingerprint, and recent logs (prefers active log stream, falls back to snapshot logs). Returns a single structured JSON object and includes per-part error fields for partial failures. Implemented as `ToolsObserve.captureDebugSnapshotHandler` and registered in the server.
7
+
5
8
  ## [0.16.0]
6
9
  - Added `wait_for_screen_change` interact tool: polls the platform-specific `get_screen_fingerprint` until it differs from a provided `previousFingerprint`, with configurable `timeoutMs` and `pollIntervalMs` and an optional stability confirmation poll to avoid reacting to transient UI flickers. Implemented at the interact layer and delegates fingerprinting to the observe implementations (Android/iOS).
7
10
  - Added unit tests covering immediate change, transient null fingerprints, stability confirmation and timeout behavior: `test/interact/unit/wait_for_screen_change.test.ts`.
@@ -76,6 +76,50 @@ Response:
76
76
 
77
77
  ---
78
78
 
79
+ ## capture_debug_snapshot
80
+ Capture a complete debug snapshot of the app state for diagnostics and post-mortem analysis.
81
+
82
+ Input:
83
+
84
+ ```json
85
+ {
86
+ "reason": "optional string describing why snapshot is taken",
87
+ "includeLogs": true,
88
+ "logLines": 200,
89
+ "platform": "android | ios",
90
+ "appId": "optional package/bundle id to scope logs",
91
+ "deviceId": "optional device serial/udid",
92
+ "sessionId": "optional log stream session id to prefer"
93
+ }
94
+ ```
95
+
96
+ Behavior:
97
+ - Captures screenshot (base64), current activity (Android), screen fingerprint, full UI tree, and recent logs.
98
+ - Prefers active log stream entries (read_log_stream) and falls back to get_logs when no active stream is available.
99
+ - Returns partial data when components fail and includes per-part error fields (e.g. `screenshot_error`, `ui_tree_error`).
100
+ - Caps logs to `logLines` entries and prefers recent entries.
101
+ - Fast by default: does not wait for new logs and avoids long blocking operations.
102
+
103
+ Response (example):
104
+
105
+ ```json
106
+ {
107
+ "timestamp": 1710000000,
108
+ "reason": "Crash after tapping checkout",
109
+ "activity": "CheckoutActivity",
110
+ "fingerprint": "abc123",
111
+ "screenshot": "<base64 PNG string>",
112
+ "ui_tree": { ... },
113
+ "logs": [ { "timestamp": 1710000000, "level": "ERROR", "message": "NullPointerException at CheckoutViewModel" } ]
114
+ }
115
+ ```
116
+
117
+ Notes:
118
+ - Useful immediately after detecting crashes or unexpected UI behaviour.
119
+ - Do not expect perfect data during a crash; tool is designed to return best-effort context and include errors for failed parts.
120
+
121
+ ---
122
+
79
123
  ## get_screen_fingerprint
80
124
  Generate a stable fingerprint representing the visible screen. Useful for detecting navigation changes, preventing loops, and synchronisation.
81
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,6 +5,8 @@ export { AndroidInteract, iOSInteract };
5
5
  import { resolveTargetDevice } from '../utils/resolve-device.js'
6
6
  import { ToolsObserve } from '../observe/index.js'
7
7
 
8
+ interface ScreenFingerprintResponse { fingerprint: string | null }
9
+
8
10
  export class ToolsInteract {
9
11
 
10
12
  private static async getInteractionService(platform?: 'android' | 'ios', deviceId?: string) {
@@ -51,8 +53,8 @@ export class ToolsInteract {
51
53
 
52
54
  while (Date.now() - start < timeoutMs) {
53
55
  try {
54
- const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
55
- const fp = (res as any)?.fingerprint ?? null
56
+ const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
57
+ const fp = res?.fingerprint ?? null
56
58
  if (fp === null || fp === undefined) {
57
59
  lastFingerprint = null
58
60
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
@@ -64,9 +66,9 @@ export class ToolsInteract {
64
66
  if (fp !== previousFingerprint) {
65
67
  // Stability confirmation
66
68
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
67
- try {
68
- const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
69
- const confirmFp = (confirmRes as any)?.fingerprint ?? null
69
+ try {
70
+ const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
71
+ const confirmFp = confirmRes?.fingerprint ?? null
70
72
  if (confirmFp === fp) {
71
73
  return { success: true, newFingerprint: fp, elapsedMs: Date.now() - start }
72
74
  }
@@ -89,4 +89,92 @@ export class ToolsObserve {
89
89
  // Both observes implement getScreenFingerprint
90
90
  return await (observe as any).getScreenFingerprint(resolved.id)
91
91
  }
92
+
93
+ static async captureDebugSnapshotHandler({ reason, includeLogs = true, logLines = 200, platform, appId, deviceId, sessionId }: { reason?: string; includeLogs?: boolean; logLines?: number; platform?: 'android' | 'ios'; appId?: string; deviceId?: string; sessionId?: string } = {}) {
94
+ const timestamp = Date.now()
95
+ const out: any = { timestamp, reason: reason || '', activity: null, fingerprint: null, screenshot: null, ui_tree: null, logs: [] }
96
+
97
+ // Parallel fetches for performance: screenshot, current screen, fingerprint, ui tree, and log stream/get logs
98
+ const sid = sessionId || 'default'
99
+ const tasks = {
100
+ screenshot: ToolsObserve.captureScreenshotHandler({ platform, deviceId }),
101
+ currentScreen: (!platform || platform === 'android') ? ToolsObserve.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
102
+ fingerprint: ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }),
103
+ uiTree: ToolsObserve.getUITreeHandler({ platform, deviceId }),
104
+ readLogStream: includeLogs ? ToolsObserve.readLogStreamHandler({ platform, sessionId: sid, limit: logLines }) : Promise.resolve({ entries: [] }),
105
+ }
106
+
107
+ const results = await Promise.allSettled(Object.values(tasks))
108
+ const keys = Object.keys(tasks)
109
+
110
+ // Map results back to keys
111
+ for (let i = 0; i < results.length; i++) {
112
+ const key = keys[i]
113
+ const res = results[i] as PromiseSettledResult<any>
114
+ if (res.status === 'fulfilled') {
115
+ const val = res.value
116
+ if (key === 'screenshot') {
117
+ out.screenshot = val && val.screenshot ? val.screenshot : null
118
+ } else if (key === 'currentScreen') {
119
+ out.activity = val && ((val.activity || val.shortActivity)) ? (val.activity || val.shortActivity) : out.activity || ''
120
+ } else if (key === 'fingerprint') {
121
+ if (val && val.fingerprint) out.fingerprint = val.fingerprint
122
+ if (val && val.activity) out.activity = out.activity || val.activity
123
+ if (val && val.error) out.fingerprint_error = val.error
124
+ } else if (key === 'uiTree') {
125
+ out.ui_tree = val
126
+ if (val && val.error) out.ui_tree_error = val.error
127
+ } else if (key === 'readLogStream') {
128
+ // handle below after evaluating fallback
129
+ // temporarily attach to out._streamEntries
130
+ out._streamEntries = val && val.entries ? val.entries : []
131
+ }
132
+ } else {
133
+ const errMsg = res.reason instanceof Error ? res.reason.message : String(res.reason)
134
+ if (key === 'screenshot') out.screenshot_error = errMsg
135
+ if (key === 'currentScreen') out.activity_error = errMsg
136
+ if (key === 'fingerprint') { out.fingerprint = null; out.fingerprint_error = errMsg }
137
+ if (key === 'uiTree') { out.ui_tree = null; out.ui_tree_error = errMsg }
138
+ if (key === 'readLogStream') { out._streamEntries = [] ; out.logs_error = errMsg }
139
+ }
140
+ }
141
+
142
+ // Logs: prefer stream entries, fallback to snapshot logs when empty
143
+ if (includeLogs) {
144
+ try {
145
+ let entries: any[] = Array.isArray(out._streamEntries) ? out._streamEntries : []
146
+ if (!entries || entries.length === 0) {
147
+ const gl = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines: logLines })
148
+ const raw: string[] = (gl && (gl as any).logs) ? (gl as any).logs : []
149
+ entries = raw.slice(-Math.max(0, logLines)).map(line => {
150
+ const level = /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(line) ? 'ERROR' : /\b(WARN| W )\b/i.test(line) ? 'WARN' : 'INFO'
151
+ return { timestamp: null, level, message: line }
152
+ })
153
+ } else {
154
+ entries = entries.map(ent => {
155
+ const msg = (ent && (ent.message || ent.msg)) ? (ent.message || ent.msg) : (typeof ent === 'string' ? ent : JSON.stringify(ent))
156
+ const levelRaw = (ent && (ent.level || ent.levelName || ent._level)) ? (ent.level || ent.levelName || ent._level) : ''
157
+ const level = (levelRaw && String(levelRaw)).toString().toUpperCase() || (/\bERROR\b/i.test(msg) ? 'ERROR' : /\bWARN\b/i.test(msg) ? 'WARN' : 'INFO')
158
+ let tsNum: number | null = null
159
+ const maybeIso = ent && ((ent._iso || ent.timestamp) as any)
160
+ if (maybeIso && typeof maybeIso === 'string') {
161
+ const d = new Date(maybeIso)
162
+ if (!isNaN(d.getTime())) tsNum = d.getTime()
163
+ }
164
+ return { timestamp: tsNum, level, message: msg }
165
+ })
166
+ }
167
+
168
+ out.logs = entries
169
+ } catch (e) {
170
+ out.logs = []
171
+ out.logs_error = e instanceof Error ? e.message : String(e)
172
+ }
173
+ }
174
+
175
+ // Clean up internal temporary field
176
+ delete out._streamEntries
177
+
178
+ return out
179
+ }
92
180
  }
package/src/server.ts CHANGED
@@ -215,6 +215,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
215
215
  required: ["platform"]
216
216
  }
217
217
  },
218
+ {
219
+ name: "capture_debug_snapshot",
220
+ description: "Capture a complete debug snapshot (screenshot, ui tree, activity, fingerprint, logs). Returns structured JSON."
221
+ ,
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ reason: { type: "string", description: "Optional reason for snapshot" },
226
+ includeLogs: { type: "boolean", description: "Whether to include logs", default: true },
227
+ logLines: { type: "number", description: "Maximum number of log lines to include", default: 200 },
228
+ platform: { type: "string", enum: ["android","ios"], description: "Optional platform override" },
229
+ appId: { type: "string", description: "Optional appId to scope logs (package/bundle id)" },
230
+ deviceId: { type: "string", description: "Optional device serial/udid" },
231
+ sessionId: { type: "string", description: "Optional log stream session id to prefer" }
232
+ }
233
+ }
234
+ },
218
235
  {
219
236
  name: "start_log_stream",
220
237
  description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
@@ -594,6 +611,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
594
611
  }
595
612
  }
596
613
 
614
+ if (name === "capture_debug_snapshot") {
615
+ const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args as any
616
+ const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId })
617
+ return wrapResponse(res)
618
+ }
619
+
597
620
  if (name === "get_ui_tree") {
598
621
  const { platform, deviceId } = args as any
599
622
  const res = await ToolsObserve.getUITreeHandler({ platform, deviceId })
@@ -0,0 +1,89 @@
1
+ import { ToolsObserve } from '../../../src/observe/index.js'
2
+
3
+ async function run() {
4
+ console.log('Starting capture_debug_snapshot unit tests...')
5
+
6
+ // Save original ToolsObserve handlers
7
+ const origCaptureHandler = (ToolsObserve as any).captureScreenshotHandler
8
+ const origGetCurrentHandler = (ToolsObserve as any).getCurrentScreenHandler
9
+ const origGetFpHandler = (ToolsObserve as any).getScreenFingerprintHandler
10
+ const origGetTreeHandler = (ToolsObserve as any).getUITreeHandler
11
+ const origReadLogStreamHandler = (ToolsObserve as any).readLogStreamHandler
12
+ const origGetLogsHandler = (ToolsObserve as any).getLogsHandler
13
+
14
+ try {
15
+ // --- Test 1: all components succeed and logs come from stream ---
16
+ ;(ToolsObserve as any).captureScreenshotHandler = async function() {
17
+ return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screenshot: 'BASE64PNG', resolution: { width: 1080, height: 1920 } }
18
+ }
19
+ ;(ToolsObserve as any).getCurrentScreenHandler = async function() {
20
+ return { device: { platform: 'android', id: 'mock' }, package: 'com.example', activity: 'com.example.Main', shortActivity: 'Main' }
21
+ }
22
+ ;(ToolsObserve as any).getScreenFingerprintHandler = async function() {
23
+ return { fingerprint: 'abc123', activity: 'Main' }
24
+ }
25
+ ;(ToolsObserve as any).getUITreeHandler = async function() {
26
+ return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] }
27
+ }
28
+ ;(ToolsObserve as any).readLogStreamHandler = async function() {
29
+ return { entries: [ { timestamp: '2026-03-23T20:00:00.000Z', level: 'ERROR', message: 'Boom' } ], crash_summary: { crash_detected: true } }
30
+ }
31
+ ;(ToolsObserve as any).getLogsHandler = async function() {
32
+ return { device: { platform: 'android', id: 'mock' }, logs: [], logCount: 0 }
33
+ }
34
+
35
+ const res1: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 50, sessionId: 's1' })
36
+ console.log('res1:', JSON.stringify(res1, null, 2))
37
+ const pass1 = res1 && res1.screenshot === 'BASE64PNG' && res1.activity && res1.fingerprint === 'abc123' && Array.isArray(res1.logs) && res1.logs.length === 1
38
+ console.log('Test 1:', pass1 ? 'PASS' : 'FAIL')
39
+
40
+ // Restore handlers before next test
41
+ ;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
42
+ ;(ToolsObserve as any).getCurrentScreenHandler = origGetCurrentHandler
43
+ ;(ToolsObserve as any).getScreenFingerprintHandler = origGetFpHandler
44
+ ;(ToolsObserve as any).getUITreeHandler = origGetTreeHandler
45
+ ;(ToolsObserve as any).readLogStreamHandler = origReadLogStreamHandler
46
+ ;(ToolsObserve as any).getLogsHandler = origGetLogsHandler
47
+
48
+ // --- Test 2: screenshot and ui tree fail; logs fallback to getLogs ---
49
+ ;(ToolsObserve as any).captureScreenshotHandler = async function() { throw new Error('screencap failed') }
50
+ ;(ToolsObserve as any).getUITreeHandler = async function() { throw new Error('uie_error') }
51
+ ;(ToolsObserve as any).readLogStreamHandler = async function() { return { entries: [] } }
52
+ ;(ToolsObserve as any).getLogsHandler = async function() { return { device: { platform: 'android', id: 'mock' }, logs: ['INFO starting','ERROR crash here'], logCount: 2 } }
53
+
54
+ const res2: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 10, appId: 'com.example' })
55
+ console.log('res2:', JSON.stringify(res2, null, 2))
56
+ const pass2 = res2 && res2.screenshot_error && res2.ui_tree_error && Array.isArray(res2.logs) && res2.logs.length === 2
57
+ console.log('Test 2:', pass2 ? 'PASS' : 'FAIL')
58
+
59
+ // Restore handlers before next test
60
+ ;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
61
+ ;(ToolsObserve as any).getCurrentScreenHandler = origGetCurrentHandler
62
+ ;(ToolsObserve as any).getScreenFingerprintHandler = origGetFpHandler
63
+ ;(ToolsObserve as any).getUITreeHandler = origGetTreeHandler
64
+ ;(ToolsObserve as any).readLogStreamHandler = origReadLogStreamHandler
65
+ ;(ToolsObserve as any).getLogsHandler = origGetLogsHandler
66
+
67
+ // --- Test 3: includeLogs=false should omit logs ---
68
+ ;(ToolsObserve as any).captureScreenshotHandler = async function() { return { device: { platform: 'android', id: 'mock' }, screenshot: null } }
69
+ ;(ToolsObserve as any).getCurrentScreenHandler = async function() { return { device: { platform: 'android', id: 'mock' }, package: '', activity: '', shortActivity: '' } }
70
+ ;(ToolsObserve as any).getScreenFingerprintHandler = async function() { return { fingerprint: null } }
71
+ ;(ToolsObserve as any).getUITreeHandler = async function() { return { device: { platform: 'android', id: 'mock' }, screen: '', resolution: { width: 0, height: 0 }, elements: [] } }
72
+ ;(ToolsObserve as any).readLogStreamHandler = async function() { return { entries: [] } }
73
+
74
+ const res3: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: false })
75
+ console.log('res3:', JSON.stringify(res3, null, 2))
76
+ const pass3 = res3 && typeof res3.logs !== 'undefined' && res3.logs.length === 0
77
+ console.log('Test 3:', pass3 ? 'PASS' : 'FAIL')
78
+
79
+ } finally {
80
+ ;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
81
+ ;(ToolsObserve as any).getCurrentScreenHandler = origGetCurrentHandler
82
+ ;(ToolsObserve as any).getScreenFingerprintHandler = origGetFpHandler
83
+ ;(ToolsObserve as any).getUITreeHandler = origGetTreeHandler
84
+ ;(ToolsObserve as any).readLogStreamHandler = origReadLogStreamHandler
85
+ ;(ToolsObserve as any).getLogsHandler = origGetLogsHandler
86
+ }
87
+ }
88
+
89
+ run().catch(console.error)
@@ -11,5 +11,6 @@ import '../manage/unit/diagnostics.test.ts'
11
11
  import '../manage/unit/detection.test.ts'
12
12
  import '../manage/unit/mcp_disable_autodetect.test.ts'
13
13
  import '../interact/unit/wait_for_screen_change.test.ts'
14
+ import '../observe/unit/capture_debug_snapshot.test.ts'
14
15
 
15
16
  console.log('Unit tests loaded.')