mobile-debug-mcp 0.21.5 → 0.23.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.
Files changed (90) hide show
  1. package/AGENTS.md +74 -0
  2. package/README.md +24 -5
  3. package/dist/interact/classify.js +35 -0
  4. package/dist/interact/index.js +220 -13
  5. package/dist/network/index.js +232 -0
  6. package/dist/observe/ios.js +10 -3
  7. package/dist/server-core.js +822 -0
  8. package/dist/server.js +6 -693
  9. package/dist/utils/resolve-device.js +15 -3
  10. package/docs/CHANGELOG.md +10 -1
  11. package/docs/tools/interact.md +69 -30
  12. package/package.json +3 -3
  13. package/skills/README.md +35 -0
  14. package/skills/test-authoring/SKILL.md +57 -0
  15. package/skills/test-authoring/references/repo-test-layout.md +47 -0
  16. package/skills/test-authoring/references/test-authoring-workflow.md +73 -0
  17. package/skills/test-authoring/references/test-quality-checklist.md +39 -0
  18. package/src/interact/classify.ts +64 -0
  19. package/src/interact/index.ts +250 -13
  20. package/src/network/index.ts +268 -0
  21. package/src/observe/ios.ts +12 -3
  22. package/src/server-core.ts +879 -0
  23. package/src/server.ts +8 -754
  24. package/src/types.ts +10 -1
  25. package/src/utils/resolve-device.ts +19 -3
  26. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +30 -0
  27. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +30 -0
  28. package/test/{observe/device → device/automated/observe}/get_logs.android.smoke.ts +1 -1
  29. package/test/{observe/device → device/automated/observe}/get_logs.ios.smoke.ts +1 -1
  30. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +31 -0
  31. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +31 -0
  32. package/test/device/index.ts +52 -0
  33. package/test/{interact/device/smoke-test.ts → device/manual/interact/app_lifecycle.manual.ts} +5 -5
  34. package/test/{manage/device/run-build-install-ios.ts → device/manual/manage/build_install_ios.manual.ts} +1 -1
  35. package/test/{manage/device → device/manual/manage}/install.integration.ts +6 -6
  36. package/test/{manage/device/run-install-android.ts → device/manual/manage/install_android.manual.ts} +1 -1
  37. package/test/{manage/device/run-install-ios.ts → device/manual/manage/install_ios.manual.ts} +1 -1
  38. package/test/device/manual/observe/capture_screenshot.manual.ts +29 -0
  39. package/test/{helpers/run-get-logs.ts → device/manual/observe/get_logs.manual.ts} +1 -1
  40. package/test/device/manual/observe/get_ui_tree.manual.ts +29 -0
  41. package/test/{observe/device/logstream-real.ts → device/manual/observe/logstream.manual.ts} +1 -1
  42. package/test/{observe/device/run-screen-fingerprint.ts → device/manual/observe/screen_fingerprint.manual.ts} +1 -1
  43. package/test/{observe/device/run-scroll-test-android.ts → device/manual/observe/scroll_to_element_android.manual.ts} +1 -1
  44. package/test/{observe/device/test-ui-tree.ts → device/manual/observe/ui_tree.manual.ts} +6 -6
  45. package/test/unit/index.ts +47 -27
  46. package/test/unit/interact/classify_action_outcome.test.ts +110 -0
  47. package/test/unit/interact/handler_shapes.test.ts +55 -0
  48. package/test/unit/interact/tap_element.test.ts +170 -0
  49. package/test/unit/interact/wait_for_screen_change.test.ts +34 -0
  50. package/test/{interact/unit → unit/interact}/wait_for_ui_contract.test.ts +11 -10
  51. package/test/unit/interact/wait_for_ui_selector_matching.test.ts +76 -0
  52. package/test/unit/manage/handler_shapes.test.ts +43 -0
  53. package/test/unit/network/get_network_activity.test.ts +181 -0
  54. package/test/{observe/unit → unit/observe}/capture_debug_snapshot.test.ts +5 -1
  55. package/test/{observe/unit → unit/observe}/find_element.test.ts +12 -6
  56. package/test/unit/observe/get_screen_fingerprint.test.ts +71 -0
  57. package/test/unit/observe/ios-getlogs.test.ts +53 -0
  58. package/test/unit/observe/scroll_to_element.test.ts +127 -0
  59. package/test/unit/server/contract.test.ts +45 -0
  60. package/test/unit/server/response_shapes.test.ts +93 -0
  61. package/test/unit/system/adb_version.test.ts +35 -0
  62. package/test/unit/system/get_system_status.test.ts +20 -0
  63. package/test/unit/system/system_status.test.ts +141 -0
  64. package/test/{utils → unit/utils}/detect_java.test.ts +1 -1
  65. package/test/unit/utils/exec.test.ts +51 -0
  66. package/test/unit/utils/resolve_device.test.ts +63 -0
  67. package/tsconfig.json +2 -2
  68. package/test/interact/device/run-real-test.ts +0 -3
  69. package/test/interact/unit/wait_for_screen_change.test.ts +0 -32
  70. package/test/interact/unit/wait_for_ui.test.ts +0 -76
  71. package/test/interact/unit/wait_for_ui_new.test.ts +0 -57
  72. package/test/observe/device/wait_for_element_real.ts +0 -3
  73. package/test/observe/unit/get_screen_fingerprint.test.ts +0 -69
  74. package/test/observe/unit/ios-getlogs.test.ts +0 -67
  75. package/test/observe/unit/scroll_to_element.test.ts +0 -129
  76. package/test/observe/unit/wait_for_element_mock.ts +0 -2
  77. package/test/observe/unit/wait_for_ui_edge_cases.test.ts +0 -41
  78. package/test/observe/unit/wait_for_ui_stability.test.ts +0 -30
  79. package/test/system/adb_version.test.ts +0 -25
  80. package/test/system/get_system_status.test.ts +0 -52
  81. package/test/system/system_status.test.ts +0 -109
  82. /package/test/{manage/unit → unit/manage}/build.test.ts +0 -0
  83. /package/test/{manage/unit → unit/manage}/build_and_install.test.ts +0 -0
  84. /package/test/{manage/unit → unit/manage}/detection.test.ts +0 -0
  85. /package/test/{manage/unit → unit/manage}/diagnostics.test.ts +0 -0
  86. /package/test/{manage/unit → unit/manage}/install.test.ts +0 -0
  87. /package/test/{manage/unit → unit/manage}/mcp_disable_autodetect.test.ts +0 -0
  88. /package/test/{observe/unit → unit/observe}/get_logs.test.ts +0 -0
  89. /package/test/{observe/unit → unit/observe}/logparse.test.ts +0 -0
  90. /package/test/{observe/unit → unit/observe}/logstream.test.ts +0 -0
@@ -1,69 +0,0 @@
1
- import { AndroidObserve } from '../../../src/observe/index.js'
2
-
3
- async function run() {
4
- console.log('Starting get_screen_fingerprint unit tests...')
5
-
6
- const origGet = (AndroidObserve as any).prototype.getUITree
7
- const origCurrent = (AndroidObserve as any).prototype.getCurrentScreen
8
-
9
- // Test 1: stable identical screens produce same fingerprint
10
- ;(AndroidObserve as any).prototype.getUITree = async function() {
11
- return {
12
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
13
- screen: '',
14
- resolution: { width: 1080, height: 1920 },
15
- elements: [
16
- { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
17
- { text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' }
18
- ]
19
- }
20
- }
21
-
22
- ;(AndroidObserve as any).prototype.getCurrentScreen = async function() {
23
- return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, package: 'com.example', activity: 'com.example.MainActivity', shortActivity: 'MainActivity' }
24
- }
25
-
26
- const ai = new AndroidObserve()
27
- const a = await ai.getScreenFingerprint('mock')
28
- const b = await ai.getScreenFingerprint('mock')
29
- console.log('Test 1:', a.fingerprint === b.fingerprint ? 'PASS' : 'FAIL')
30
-
31
- // Test 2: change in UI text changes fingerprint
32
- ;(AndroidObserve as any).prototype.getUITree = async function() {
33
- return {
34
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
35
- screen: '',
36
- resolution: { width: 1080, height: 1920 },
37
- elements: [
38
- { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
39
- { text: 'Profile', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' }
40
- ]
41
- }
42
- }
43
-
44
- const c = await ai.getScreenFingerprint('mock')
45
- console.log('Test 2:', a.fingerprint !== c.fingerprint ? 'PASS' : 'FAIL')
46
-
47
- // Test 3: dynamic text ignored (timestamp) should not change fingerprint
48
- ;(AndroidObserve as any).prototype.getUITree = async function() {
49
- return {
50
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
51
- screen: '',
52
- resolution: { width: 1080, height: 1920 },
53
- elements: [
54
- { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
55
- { text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' },
56
- { text: '12:34', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [900,10,1080,40], resourceId: null }
57
- ]
58
- }
59
- }
60
-
61
- const d = await ai.getScreenFingerprint('mock')
62
- console.log('Test 3:', a.fingerprint === d.fingerprint ? 'PASS' : 'FAIL')
63
-
64
- // Restore
65
- ;(AndroidObserve as any).prototype.getUITree = origGet
66
- ;(AndroidObserve as any).prototype.getCurrentScreen = origCurrent
67
- }
68
-
69
- run().catch(console.error)
@@ -1,67 +0,0 @@
1
- import { iOSObserve } from '../../../src/observe/ios'
2
- import assert from 'assert'
3
-
4
- // Lightweight unit tests: verify predicate construction and meta extraction logic using internal functions
5
- // Since getLogs executes xcrun, run tests in SKIP_DEVICE_TESTS=1 environment by stubbing execCommand where possible.
6
-
7
- import * as iosUtils from '../../../src/utils/ios/utils'
8
-
9
- function stubExecCommand(original: any, expectedArgsChecker: (args: string[]) => boolean, output: string) {
10
- return async function (args: string[], deviceId?: string) {
11
- if (!expectedArgsChecker(args)) throw new Error('Unexpected args: ' + JSON.stringify(args))
12
- return { output, device: { platform: 'ios', id: deviceId || 'booted' } }
13
- }
14
- }
15
-
16
- describe('iOS getLogs predicate and meta', () => {
17
- let obs: iOSObserve
18
- beforeEach(() => {
19
- obs = new iOSObserve()
20
- })
21
-
22
- it('uses simple process name predicate when appId provided', async () => {
23
- const bundle = 'com.ideamechanics.modul8'
24
- // stub execCommand twice: first for pgrep, second for log show
25
- const pgrepOutput = '12345\n'
26
- const logOutput = '2026-03-31 09:21:20.085 Module[12345:678] <Info> Modul8: Test message'
27
-
28
- const orig = (iosUtils as any).execCommand
29
- try {
30
- (iosUtils as any).execCommand = stubExecCommand(orig, (args) => args.includes('pgrep'), pgrepOutput)
31
- // second replacement for the log show call
32
- let called = false
33
- (iosUtils as any).execCommand = async function (args: string[]) {
34
- if (args.includes('pgrep')) return { output: pgrepOutput, device: { platform: 'ios', id: 'booted' } }
35
- if (args.includes('log') && args.includes('show')) { called = true; return { output: logOutput, device: { platform: 'ios', id: 'booted' } } }
36
- throw new Error('Unexpected args: ' + JSON.stringify(args))
37
- }
38
-
39
- const res = await obs.getLogs({ appId: bundle, deviceId: 'booted' })
40
- assert(res.meta.processNameUsed === 'modul8' || res.meta.processNameUsed === 'Modul8' || !!res.meta.processNameUsed)
41
- assert(res.meta.detectedPid === 12345)
42
- assert(res.source === 'pid')
43
- assert(res.logCount === 1)
44
- assert(res.logs[0].message.includes('Test message'))
45
- assert(called, 'log show must have been called')
46
- } finally {
47
- (iosUtils as any).execCommand = orig
48
- }
49
- })
50
-
51
- it('falls back to broad when no appId', async () => {
52
- const logOutput = '2026-03-31 09:21:20.085 SomeOther[222:333] <Info> Other: Hello'
53
- const orig = (iosUtils as any).execCommand
54
- try {
55
- (iosUtils as any).execCommand = async function (args: string[]) {
56
- if (args.includes('log') && args.includes('show')) return { output: logOutput, device: { platform: 'ios', id: 'booted' } }
57
- throw new Error('Unexpected args: ' + JSON.stringify(args))
58
- }
59
- const obs = new iOSObserve()
60
- const res = await obs.getLogs({ deviceId: 'booted' })
61
- assert(res.source === 'broad')
62
- assert(res.logCount === 1)
63
- } finally {
64
- (iosUtils as any).execCommand = orig
65
- }
66
- })
67
- })
@@ -1,129 +0,0 @@
1
- import { ToolsInteract } from '../../../src/interact/index.js'
2
- import { ToolsObserve } from '../../../src/observe/index.js'
3
-
4
- const origGet = (ToolsObserve as any).getUITreeHandler
5
- const origSwipe = (ToolsInteract as any).swipeHandler
6
-
7
- async function runTests() {
8
- // Use a stable logger to avoid test harness replacing console.log between calls
9
- console.log = (...args: any[]) => { try { process.stdout.write(args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n') } catch {} }
10
- console.log('Starting tests for scroll_to_element...')
11
-
12
- // Test 1: Element found immediately
13
- console.log('\nTest 1: Element found immediately')
14
- (ToolsObserve as any).getUITreeHandler = async () => ({
15
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
16
- screen: '',
17
- resolution: { width: 1080, height: 1920 },
18
- elements: [{
19
- text: 'Target',
20
- type: 'Button',
21
- contentDescription: null,
22
- clickable: true,
23
- enabled: true,
24
- visible: true,
25
- bounds: [0, 0, 100, 100],
26
- resourceId: null
27
- }]
28
- })
29
-
30
- const res1 = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: 'Target' }, direction: 'down', maxScrolls: 5, scrollAmount: 0.7 })
31
- console.log('Result:', res1.success === true ? 'PASS' : 'FAIL')
32
- console.log('scrollsPerformed:', (res1 as any).scrollsPerformed)
33
-
34
- // Test 2: Element found after scrolling
35
- console.log('\nTest 2: Element found after scrolling')
36
- let calls = 0
37
- (ToolsObserve as any).getUITreeHandler = async () => {
38
- calls++
39
- if (calls < 3) {
40
- return {
41
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
42
- screen: '',
43
- resolution: { width: 1080, height: 1920 },
44
- elements: []
45
- }
46
- }
47
- return {
48
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
49
- screen: '',
50
- resolution: { width: 1080, height: 1920 },
51
- elements: [{
52
- text: 'Target',
53
- type: 'Button',
54
- contentDescription: null,
55
- clickable: true,
56
- enabled: true,
57
- visible: true,
58
- bounds: [0, 0, 100, 100],
59
- resourceId: null
60
- }]
61
- }
62
- }
63
-
64
- // Stub swipe so it doesn't try to call adb/idb
65
- (ToolsInteract as any).swipeHandler = async () => ({ success: true })
66
-
67
- const res2 = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: 'Target' }, direction: 'down', maxScrolls: 5, scrollAmount: 0.7 })
68
- console.log('Result:', res2.success === true ? 'PASS' : 'FAIL')
69
- console.log('calls:', calls, calls >= 3 ? 'PASS' : 'FAIL')
70
-
71
- // Test 3: UI unchanged stops early
72
- console.log('\nTest 3: UI unchanged stops early')
73
- (ToolsObserve as any).getUITreeHandler = async () => ({
74
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
75
- screen: '',
76
- resolution: { width: 1080, height: 1920 },
77
- elements: []
78
- })
79
-
80
- (ToolsInteract as any).swipeHandler = async () => ({ success: true })
81
-
82
- const res3 = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: 'Missing' }, direction: 'down', maxScrolls: 5, scrollAmount: 0.7 })
83
- console.log('Result:', res3.success === false && (res3 as any).attempts === 1 ? 'PASS' : 'FAIL')
84
- console.log('Reason:', (res3 as any).reason || JSON.stringify(res3))
85
-
86
- // Test 4: Offscreen element scrolls into view
87
- console.log('\nTest 4: Offscreen element scrolls into view')
88
- const ai = new (await import('../../../src/interact/index.js')).AndroidInteract()
89
- const origObserveGet = ai['observe'].getUITree
90
- const origAiSwipe = ai.swipe
91
- let swiped = false
92
- let swipeCalled = 0
93
- ;(ai['observe'] as any).getUITree = async () => {
94
- if (!swiped) {
95
- return {
96
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
97
- screen: '',
98
- resolution: { width: 1080, height: 1920 },
99
- elements: [ { text: null, type: 'android.view.View', resourceId: null, contentDescription: null, bounds: [0,0,1080,200], visible: true } ]
100
- }
101
- }
102
- return {
103
- device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
104
- screen: '',
105
- resolution: { width: 1080, height: 1920 },
106
- elements: [{ text: 'OffscreenTarget', type: 'android.widget.Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [100,400,300,460], resourceId: null }]
107
- }
108
- }
109
- ;(ai as any).swipe = async () => { swipeCalled++; swiped = true; return { success: true } }
110
-
111
- const r4 = await ai.scrollToElement({ text: 'OffscreenTarget' }, 'down', 3, 0.7, 'mock')
112
- const ok4 = r4 && (r4 as any).success === true && (r4 as any).scrollsPerformed === 1 && swipeCalled === 1
113
- console.log('Result:', ok4 ? 'PASS' : 'FAIL')
114
- console.log(' success:', (r4 as any).success, 'scrollsPerformed:', (r4 as any).scrollsPerformed, 'swipeCalled:', swipeCalled)
115
-
116
- ;(ai['observe'] as any).getUITree = origObserveGet
117
- ;(ai as any).swipe = origAiSwipe
118
-
119
- // Restore
120
- (ToolsObserve as any).getUITreeHandler = origGet
121
- ;(ToolsInteract as any).swipeHandler = origSwipe
122
- }
123
-
124
- // Ensure console.log is a function (some test runners replace it)
125
- if (typeof console.log !== 'function') {
126
- console.log = (...args: any[]) => { try { process.stdout.write(args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n') } catch { /* swallow */ } }
127
- }
128
-
129
- runTests().catch(console.error)
@@ -1,2 +0,0 @@
1
- // wait_for_element tests removed — tool deprecated
2
- console.log('wait_for_element unit tests removed');
@@ -1,41 +0,0 @@
1
- import { ToolsInteract } from '../../../../src/interact/index.js'
2
- import * as Observe from '../../../../src/observe/index.js'
3
-
4
- async function run() {
5
- console.log('Unit: wait_for_ui edge cases')
6
-
7
- const origFind = (ToolsInteract as any).findElementHandler
8
- const origFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
9
-
10
- try {
11
- // 1) Immediate absence should pass for match='absent'
12
- (ToolsInteract as any).findElementHandler = async () => ({ found: false })
13
- const r1 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Nothing', timeoutMs: 2000, pollIntervalMs: 100, stability_ms: 200, match: 'absent', platform: 'android' })
14
- console.log('Immediate absent test:', r1 && (r1 as any).success ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r1 as any).poll_count, duration_ms: (r1 as any).duration_ms, stable_duration_ms: (r1 as any).stable_duration_ms, matchSource: (r1 as any).matchSource }, null, 2))
15
-
16
- // 2) Boundary stability: condition becomes true and stays exactly long enough
17
- // Use pollInterval 100ms and stability 300ms -> need ~3 consecutive trues
18
- let seq2 = [false, true, true, true]
19
- (ToolsInteract as any).findElementHandler = async () => ({ found: seq2.shift() ?? true })
20
- const r2 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Boundary', timeoutMs: 2000, pollIntervalMs: 100, stability_ms: 300, match: 'present', platform: 'android' })
21
- console.log('Boundary stability test:', r2 && (r2 as any).success ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r2 as any).poll_count, duration_ms: (r2 as any).duration_ms, stable_duration_ms: (r2 as any).stable_duration_ms, matchSource: (r2 as any).matchSource }, null, 2))
22
-
23
- // 3) Long flicker that never stabilizes should timeout/fail
24
- // Sequence toggles true/false repeatedly
25
- let seq3 = [false, true, false, true, false, true, false]
26
- (ToolsInteract as any).findElementHandler = async () => ({ found: seq3.shift() ?? false })
27
- const r3 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Flicker', timeoutMs: 1200, pollIntervalMs: 150, stability_ms: 400, match: 'present', platform: 'android' })
28
- console.log('Long flicker timeout test:', !(r3 && (r3 as any).success) ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r3 as any).poll_count, duration_ms: (r3 as any).duration_ms, stable_duration_ms: (r3 as any).stable_duration_ms, matchSource: (r3 as any).matchSource }, null, 2))
29
-
30
- // 4) Very short stability requirement should pass quickly
31
- (ToolsInteract as any).findElementHandler = async () => ({ found: true })
32
- const r4 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'ShortStable', timeoutMs: 2000, pollIntervalMs: 200, stability_ms: 50, match: 'present', platform: 'android' })
33
- console.log('Short stability test:', r4 && (r4 as any).success ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r4 as any).poll_count, duration_ms: (r4 as any).duration_ms, stable_duration_ms: (r4 as any).stable_duration_ms, matchSource: (r4 as any).matchSource }, null, 2))
34
-
35
- } finally {
36
- (ToolsInteract as any).findElementHandler = origFind
37
- (Observe as any).ToolsObserve.getScreenFingerprintHandler = origFp
38
- }
39
- }
40
-
41
- run().catch(e=>{ console.error(e); process.exit(1) })
@@ -1,30 +0,0 @@
1
- import { ToolsInteract } from '../../../../src/interact/index.js'
2
- import * as Observe from '../../../../src/observe/index.js'
3
-
4
- async function run() {
5
- console.log('Unit: wait_for_ui stability behavior')
6
-
7
- const origFind = (ToolsInteract as any).findElementHandler
8
- const origFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
9
-
10
- try {
11
- // Simulate UI flicker: present, absent, present, then stable
12
- const seq = [false, true, false, true, true, true]
13
- (ToolsInteract as any).findElementHandler = async () => ({ found: seq.shift() ?? true })
14
-
15
- const res = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'X', timeoutMs: 5000, pollIntervalMs: 100, stability_ms: 500, platform: 'android' })
16
- const ok = res && (res as any).success
17
- console.log('Flicker stability test:', ok ? 'PASS' : 'FAIL', JSON.stringify((res as any).telemetry || {}, null, 2))
18
-
19
- // Simulate immediate stable presence
20
- (ToolsInteract as any).findElementHandler = async () => ({ found: true })
21
- const res2 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Y', timeoutMs: 2000, pollIntervalMs: 100, stability_ms: 300, platform: 'android' })
22
- console.log('Immediate stable test:', res2 && (res2 as any).success ? 'PASS' : 'FAIL', JSON.stringify((res2 as any).telemetry || {}, null, 2))
23
-
24
- } finally {
25
- (ToolsInteract as any).findElementHandler = origFind
26
- (Observe as any).ToolsObserve.getScreenFingerprintHandler = origFp
27
- }
28
- }
29
-
30
- run().catch(e=>{ console.error(e); process.exit(1) })
@@ -1,25 +0,0 @@
1
- import assert from 'assert'
2
- import * as androidUtils from '../../src/utils/android/utils.js'
3
- import * as systemStatus from '../../src/system/index.js'
4
-
5
- const origEnsure = (androidUtils as any).ensureAdbAvailable
6
-
7
- function mockEnsure(returnVal: any) {
8
- (androidUtils as any).ensureAdbAvailable = () => returnVal
9
- }
10
-
11
- function restoreEnsure() {
12
- (androidUtils as any).ensureAdbAvailable = origEnsure
13
- }
14
-
15
- describe('adb version parsing', () => {
16
- afterEach(() => {
17
- restoreEnsure()
18
- })
19
-
20
- it('uses only the first line of multi-line adb --version output', async () => {
21
- mockEnsure({ adbCmd: 'adb', ok: true, version: 'Android Debug Bridge version 1.0.41\nRevision 8f3b7' })
22
- const res = await systemStatus.getSystemStatus()
23
- assert.strictEqual(res.adbVersion, 'Android Debug Bridge version 1.0.41')
24
- })
25
- })
@@ -1,52 +0,0 @@
1
- import assert from 'assert'
2
- import { ensureAdbAvailable } from '../../src/utils/android/utils.js'
3
- import { getXcrunCmd } from '../../src/utils/ios/utils.js'
4
-
5
- // We import the server handler module to access the internal get_system_status implementation.
6
- import * as server from '../../src/server.js'
7
-
8
- // Small helper to call the tool handler similarly to how the MCP transport would.
9
- async function callGetSystemStatus() {
10
- const req = { params: { name: 'get_system_status', arguments: {} } }
11
- // @ts-ignore - use the handler exported from server
12
- const handler = (server as any).defaultRequestHandler || (server as any).callToolHandler || (server as any).__callTool
13
- if (!handler) {
14
- // fallback: require the module and call the exported server instance's request handler
15
- // The server code registers the handler directly; we will emulate by requiring compiled code in dist if available.
16
- try {
17
- const dist = await import('../../dist/server.js')
18
- // Try to execute by sending a call via the server instance if exported
19
- if (dist && dist.server && typeof dist.server._handleCall === 'function') {
20
- return dist.server._handleCall(req)
21
- }
22
- } catch {
23
- // best effort only
24
- }
25
- throw new Error('Cannot locate server call handler for tests')
26
- }
27
-
28
- return handler(req)
29
- }
30
-
31
- describe('get_system_status tool (unit)', () => {
32
- it('returns structured result without throwing', async () => {
33
- const res = await callGetSystemStatus()
34
- // Handler returns { content: [{ type: 'text', text: JSON.stringify(...) }] }
35
- assert(res && res.content && Array.isArray(res.content))
36
- const textBlock = res.content.find((c: any) => c.type === 'text')
37
- assert(textBlock && textBlock.text)
38
- const payload = JSON.parse(textBlock.text)
39
- assert(typeof payload.success === 'boolean')
40
- assert(Array.isArray(payload.issues))
41
- }).timeout(5000)
42
-
43
- it('detects adb availability helper works', () => {
44
- const adb = ensureAdbAvailable()
45
- assert(adb && typeof adb.ok === 'boolean')
46
- })
47
-
48
- it('detects xcrun command helper exists', () => {
49
- const cmd = getXcrunCmd()
50
- assert(typeof cmd === 'string')
51
- })
52
- })
@@ -1,109 +0,0 @@
1
- import assert from 'assert'
2
- import child_process from 'child_process'
3
-
4
- import * as androidUtils from '../../src/utils/android/utils.js'
5
- import * as iosUtils from '../../src/utils/ios/utils.js'
6
- import * as systemStatus from '../../src/system/index.js'
7
-
8
- const origExecSync = child_process.execSync
9
- const origEnsure = (androidUtils as any).ensureAdbAvailable
10
- const origGetXcrun = (iosUtils as any).getXcrunCmd
11
-
12
- function mockExec(behaviour: (cmd: string) => string) {
13
- (child_process as any).execSync = (cmd: string) => {
14
- const s = typeof cmd === 'string' ? cmd : (Array.isArray(cmd) ? cmd.join(' ') : String(cmd))
15
- const out = behaviour(s)
16
- if (out instanceof Error) throw out
17
- return out
18
- }
19
- }
20
-
21
- function restoreExec() {
22
- (child_process as any).execSync = origExecSync
23
- }
24
-
25
- function mockEnsure(returnVal: any) {
26
- (androidUtils as any).ensureAdbAvailable = () => returnVal
27
- }
28
-
29
- function restoreEnsure() {
30
- (androidUtils as any).ensureAdbAvailable = origEnsure
31
- }
32
-
33
- function mockGetXcrun(val: string) {
34
- (iosUtils as any).getXcrunCmd = () => val
35
- }
36
-
37
- function restoreGetXcrun() {
38
- (iosUtils as any).getXcrunCmd = origGetXcrun
39
- }
40
-
41
- describe('system_status checks', () => {
42
- afterEach(() => {
43
- restoreExec(); restoreEnsure(); restoreGetXcrun()
44
- })
45
-
46
- it('reports healthy system when adb and xcrun present', async () => {
47
- mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
48
- mockGetXcrun('xcrun')
49
-
50
- mockExec((cmd) => {
51
- if (cmd.startsWith('adb devices')) return 'List of devices attached\nemulator-5554\tdevice'
52
- if (cmd.includes('adb logcat')) return 'I/Tag: ok'
53
- if (cmd.includes('adb shell pm path')) return 'package:/data/app/com.example-1/base.apk'
54
- if (cmd.startsWith('xcrun --version')) return 'xcrun version 123'
55
- if (cmd.includes('simctl list devices booted --json')) return JSON.stringify({ devices: {} })
56
- return ''
57
- })
58
-
59
- const res = await systemStatus.getSystemStatus()
60
- assert.strictEqual(res.success, true)
61
- assert.strictEqual(res.adbAvailable, true)
62
- assert.strictEqual(typeof res.adbVersion, 'string')
63
- })
64
-
65
- it('reports adb missing', async () => {
66
- mockEnsure({ adbCmd: 'adb', ok: false, error: 'not found' })
67
- mockGetXcrun('xcrun')
68
- mockExec((cmd) => {
69
- if (cmd.startsWith('xcrun --version')) return 'xcrun version'
70
- return ''
71
- })
72
-
73
- const res = await systemStatus.getSystemStatus()
74
- assert.strictEqual(res.success, false)
75
- assert(res.issues.some((i: string) => i.includes('ADB')))
76
- })
77
-
78
- it('detects unauthorized/offline devices', async () => {
79
- mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
80
- mockGetXcrun('xcrun')
81
- mockExec((cmd) => {
82
- if (cmd.startsWith('adb devices')) return 'List of devices attached\nserial1\tunauthorized\nserial2\toffline\n'
83
- if (cmd.startsWith('xcrun --version')) return 'xcrun version'
84
- return ''
85
- })
86
-
87
- const res = await systemStatus.getSystemStatus()
88
- assert.strictEqual(res.success, false)
89
- assert(res.issues.some((i: string) => i.includes('unauthorized')))
90
- assert(res.issues.some((i: string) => i.includes('offline')))
91
- })
92
-
93
- it('handles missing xcrun gracefully', async () => {
94
- mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
95
- mockGetXcrun('xcrun')
96
- mockExec((cmd) => {
97
- if (cmd.startsWith('adb devices')) return 'List of devices attached\nemulator-5554\tdevice'
98
- if (cmd.startsWith('xcrun --version')) throw new Error('not found')
99
- return ''
100
- })
101
-
102
- const res = await systemStatus.getSystemStatus()
103
- // Expect iOS check to be false and Android to be healthy
104
- assert.strictEqual(res.iosAvailable, false)
105
- assert.strictEqual(res.adbAvailable, true)
106
- // overall success may still be true (Android ok) but issues should include an xcrun-related message
107
- assert(res.issues.some((i: string) => i.toLowerCase().includes('xcrun') || i.toLowerCase().includes('ios')))
108
- })
109
- })
File without changes