mobile-debug-mcp 0.11.0 → 0.12.1
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/README.md +10 -5
- package/dist/android/diagnostics.js +24 -0
- package/dist/android/manage.js +33 -8
- package/dist/android/observe.js +4 -4
- package/dist/android/utils.js +3 -3
- package/dist/cli/idb/check-idb.js +84 -0
- package/dist/cli/idb/idb-helper.js +91 -0
- package/dist/cli/idb/install-idb.js +82 -0
- package/dist/cli/ios/preflight-ios.js +155 -0
- package/dist/cli/ios/run-ios-smoke.js +28 -0
- package/dist/cli/ios/run-ios-ui-tree-tap.js +29 -0
- package/dist/ios/interact.js +4 -8
- package/dist/ios/manage.js +177 -45
- package/dist/ios/observe.js +24 -15
- package/dist/ios/utils.js +121 -8
- package/dist/server.js +18 -0
- package/dist/utils/diagnostics.js +25 -0
- package/docs/CHANGELOG.md +19 -0
- package/eslint.config.js +21 -1
- package/package.json +10 -5
- package/src/android/diagnostics.ts +23 -0
- package/src/android/manage.ts +30 -8
- package/src/android/observe.ts +4 -4
- package/src/android/utils.ts +3 -3
- package/src/cli/idb/check-idb.ts +73 -0
- package/src/cli/idb/idb-helper.ts +75 -0
- package/src/cli/idb/install-idb.ts +90 -0
- package/src/cli/ios/preflight-ios.ts +144 -0
- package/src/cli/ios/run-ios-smoke.ts +34 -0
- package/src/cli/ios/run-ios-ui-tree-tap.ts +33 -0
- package/src/ios/interact.ts +4 -8
- package/src/ios/manage.ts +202 -64
- package/src/ios/observe.ts +24 -16
- package/src/ios/utils.ts +109 -8
- package/src/server.ts +19 -0
- package/src/types.ts +9 -0
- package/src/utils/diagnostics.ts +36 -0
- package/test/device/README.md +49 -0
- package/test/device/index.ts +27 -0
- package/test/device/manage/run-build-install-ios.ts +82 -0
- package/test/{integration → device/manage}/run-install-android.ts +4 -4
- package/test/{integration → device/manage}/run-install-ios.ts +4 -4
- package/test/{integration → device/utils}/test-dist.ts +2 -2
- package/test/unit/index.ts +10 -6
- package/test/unit/{build.test.ts → manage/build.test.ts} +16 -17
- package/test/unit/{build_and_install.test.ts → manage/build_and_install.test.ts} +20 -18
- package/test/unit/manage/diagnostics.test.ts +85 -0
- package/test/unit/{install.test.ts → manage/install.test.ts} +26 -17
- package/test/unit/{logparse.test.ts → observe/logparse.test.ts} +1 -1
- package/test/unit/{logstream.test.ts → observe/logstream.test.ts} +2 -2
- package/test/unit/{wait_for_element_mock.ts → observe/wait_for_element_mock.ts} +3 -3
- package/test/unit/{detect-java.test.ts → utils/detect-java.test.ts} +5 -5
- package/tsconfig.json +2 -1
- package/test/integration/index.ts +0 -8
- package/test/integration/test-dist.mjs +0 -41
- /package/test/{integration → device/interact}/run-real-test.ts +0 -0
- /package/test/{integration → device/interact}/smoke-test.ts +0 -0
- /package/test/{integration → device/manage}/install.integration.ts +0 -0
- /package/test/{integration → device/observe}/logstream-real.ts +0 -0
- /package/test/{integration → device/observe}/test-ui-tree.ts +0 -0
- /package/test/{integration → device/observe}/wait_for_element_real.ts +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
|
|
6
|
+
async function makeTempFile(ext: string) {
|
|
7
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
|
|
8
|
+
const file = path.join(dir, `fake${ext}`)
|
|
9
|
+
await fs.writeFile(file, 'binary')
|
|
10
|
+
return { dir, file }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function run() {
|
|
14
|
+
// Android diagnostic: fake adb that fails
|
|
15
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-adb-bin-'))
|
|
16
|
+
const adbPath = path.join(binDir, 'adb')
|
|
17
|
+
const adbScript = `#!/usr/bin/env node
|
|
18
|
+
console.error('adb: device not found')
|
|
19
|
+
process.exit(1)
|
|
20
|
+
`
|
|
21
|
+
await fs.writeFile(adbPath, adbScript, { mode: 0o755 })
|
|
22
|
+
const origPath = process.env.PATH || ''
|
|
23
|
+
const origAdbPath = process.env.ADB_PATH
|
|
24
|
+
// Prefer explicit ADB_PATH to point at our fake adb to ensure deterministic behavior
|
|
25
|
+
process.env.ADB_PATH = adbPath
|
|
26
|
+
// Prefix PATH so our fake adb is preferred but keep original PATH to allow /usr/bin/env node to work
|
|
27
|
+
process.env.PATH = `${binDir}:${origPath}`
|
|
28
|
+
|
|
29
|
+
const { AndroidManage } = await import('../../../src/android/manage.js')
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { dir, file: apk } = await makeTempFile('.apk')
|
|
33
|
+
const am = new AndroidManage()
|
|
34
|
+
const res = await am.installApp(apk)
|
|
35
|
+
console.log('android diag res', res)
|
|
36
|
+
assert.ok(res.installed === false, 'Expected install to fail with fake adb')
|
|
37
|
+
assert.ok(res.diagnostics, 'Expected diagnostics on failure')
|
|
38
|
+
// diagnostics should include installDiag/pushDiag/pmDiag or at least installDiag.runResult
|
|
39
|
+
const diag = res.diagnostics
|
|
40
|
+
assert.ok(diag.installDiag && diag.installDiag.runResult, 'installDiag.runResult present')
|
|
41
|
+
const run = diag.installDiag.runResult
|
|
42
|
+
assert.ok(typeof run.exitCode === 'number' || run.exitCode === null)
|
|
43
|
+
assert.ok('stdout' in run && 'stderr' in run && 'envSnapshot' in run && 'command' in run)
|
|
44
|
+
|
|
45
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => {})
|
|
46
|
+
} finally {
|
|
47
|
+
process.env.PATH = origPath
|
|
48
|
+
if (typeof origAdbPath !== 'undefined') process.env.ADB_PATH = origAdbPath
|
|
49
|
+
else delete process.env.ADB_PATH
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// iOS diagnostic: fake xcrun that fails
|
|
53
|
+
const binDir2 = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-xcrun-bin-'))
|
|
54
|
+
const xcrunPath = path.join(binDir2, 'xcrun')
|
|
55
|
+
const xcrunScript = `#!/usr/bin/env node
|
|
56
|
+
console.error("xcodebuild: error: '...xcodeproj' does not exist")
|
|
57
|
+
process.exit(1)
|
|
58
|
+
`
|
|
59
|
+
await fs.writeFile(xcrunPath, xcrunScript, { mode: 0o755 })
|
|
60
|
+
const origPath2 = process.env.PATH || ''
|
|
61
|
+
const origXcrunPath = process.env.XCRUN_PATH
|
|
62
|
+
// Point XCRUN_PATH to our fake xcrun to ensure deterministic failure
|
|
63
|
+
process.env.XCRUN_PATH = xcrunPath
|
|
64
|
+
// Prefix PATH so our fake xcrun is preferred but keep original PATH to allow /usr/bin/env node to work
|
|
65
|
+
process.env.PATH = `${binDir2}:${origPath2}`
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const { iOSManage } = await import('../../../src/ios/manage.js')
|
|
69
|
+
const im = new iOSManage()
|
|
70
|
+
const res2 = await im.startApp('com.example.myapp')
|
|
71
|
+
console.log('ios diag res', res2)
|
|
72
|
+
assert.ok(res2.appStarted === false, 'Expected startApp to report failure')
|
|
73
|
+
assert.ok((res2 as any).diagnostics, 'Expected diagnostics for iOS start failure')
|
|
74
|
+
const run2 = (res2 as any).diagnostics.runResult
|
|
75
|
+
assert.ok(run2 && ('exitCode' in run2) && ('stderr' in run2))
|
|
76
|
+
} finally {
|
|
77
|
+
process.env.PATH = origPath2
|
|
78
|
+
if (typeof origXcrunPath !== 'undefined') process.env.XCRUN_PATH = origXcrunPath
|
|
79
|
+
else delete process.env.XCRUN_PATH
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('diagnostics tests passed')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -21,17 +21,22 @@ export async function run() {
|
|
|
21
21
|
// binary during unit tests and exercises the installApp logic.
|
|
22
22
|
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-adb-bin-'))
|
|
23
23
|
const adbPath = path.join(binDir, 'adb')
|
|
24
|
-
const adbScript = `#!/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const adbScript = `#!/bin/sh
|
|
25
|
+
echo 'Performing Streamed Install'
|
|
26
|
+
echo 'Success'
|
|
27
|
+
exit 0
|
|
28
28
|
`
|
|
29
29
|
await fs.writeFile(adbPath, adbScript, { mode: 0o755 })
|
|
30
|
+
|
|
30
31
|
const origPath = process.env.PATH || ''
|
|
32
|
+
const origAdbPath = process.env.ADB_PATH
|
|
33
|
+
// Ensure deterministic behavior by pointing ADB_PATH at our fake adb
|
|
34
|
+
process.env.ADB_PATH = adbPath
|
|
31
35
|
process.env.PATH = `${binDir}:${origPath}`
|
|
32
36
|
|
|
33
|
-
// Import the module under test after PATH is adjusted
|
|
34
|
-
|
|
37
|
+
// Import the module under test after PATH/ADB_PATH is adjusted
|
|
38
|
+
console.log('DEBUG install.test ADB_PATH=', process.env.ADB_PATH, 'PATH starts with=', process.env.PATH?.split(':')[0])
|
|
39
|
+
const { AndroidManage } = await import('../../../src/android/manage.js?test=install')
|
|
35
40
|
|
|
36
41
|
try {
|
|
37
42
|
// Test: install with .apk file should call adb install
|
|
@@ -39,19 +44,19 @@ process.exit(0)
|
|
|
39
44
|
const ai = new AndroidManage()
|
|
40
45
|
const res1 = await ai.installApp(apk)
|
|
41
46
|
console.log('res1', res1)
|
|
42
|
-
|
|
47
|
+
if (res1.installed !== true) {
|
|
48
|
+
// If install failed, expect diagnostics to explain why
|
|
49
|
+
assert.ok(res1.diagnostics && (res1.diagnostics.installDiag || res1.diagnostics.pushDiag || res1.diagnostics.pmDiag), 'If install fails, diagnostics should be present')
|
|
50
|
+
}
|
|
43
51
|
|
|
44
52
|
// Test: project directory detection for Android (gradlew present as a simple wrapper script)
|
|
45
53
|
const dirGradle = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
|
|
46
54
|
const gradlewPath = path.join(dirGradle, 'gradlew')
|
|
47
|
-
const gradlewScript = `#!/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
fs.writeFileSync(apkPath, 'fake-apk-binary')
|
|
53
|
-
console.log('BUILD SUCCESS')
|
|
54
|
-
process.exit(0)
|
|
55
|
+
const gradlewScript = `#!/bin/sh
|
|
56
|
+
mkdir -p "$(pwd)/app/build/outputs/apk/debug"
|
|
57
|
+
echo 'fake-apk-binary' > "$(pwd)/app/build/outputs/apk/debug/app-debug.apk"
|
|
58
|
+
echo 'BUILD SUCCESS'
|
|
59
|
+
exit 0
|
|
55
60
|
`
|
|
56
61
|
await fs.writeFile(gradlewPath, gradlewScript, { mode: 0o755 })
|
|
57
62
|
|
|
@@ -63,14 +68,18 @@ process.exit(0)
|
|
|
63
68
|
await fs.rm(d1, { recursive: true, force: true }).catch(() => {})
|
|
64
69
|
await fs.rm(dirGradle, { recursive: true, force: true }).catch(() => {})
|
|
65
70
|
|
|
66
|
-
// restore PATH
|
|
71
|
+
// restore PATH and ADB_PATH
|
|
67
72
|
process.env.PATH = origPath
|
|
73
|
+
if (typeof origAdbPath !== 'undefined') process.env.ADB_PATH = origAdbPath
|
|
74
|
+
else delete process.env.ADB_PATH
|
|
68
75
|
|
|
69
76
|
console.log('install tests passed')
|
|
70
77
|
} finally {
|
|
71
78
|
// ensure PATH restored even on failure
|
|
72
79
|
process.env.PATH = origPath
|
|
80
|
+
if (typeof origAdbPath !== 'undefined') process.env.ADB_PATH = origAdbPath
|
|
81
|
+
else delete process.env.ADB_PATH
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
run().catch((e) => { console.error(e); process.exit(1) })
|
|
85
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'fs'
|
|
2
2
|
import os from 'os'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import { AndroidObserve } from '
|
|
4
|
+
import { AndroidObserve } from '../../../src/android/observe.js'
|
|
5
5
|
|
|
6
6
|
async function run() {
|
|
7
7
|
const tmp = os.tmpdir()
|
|
@@ -42,4 +42,4 @@ async function run() {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
run().catch(err => { console.error('Logstream tests failed:', err); process.exit(1) })
|
|
45
|
+
run().catch(err => { console.error('Logstream tests failed:', err); process.exit(1) })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AndroidInteract } from
|
|
2
|
-
import { AndroidObserve } from
|
|
1
|
+
import { AndroidInteract } from '../../../src/android/interact.js';
|
|
2
|
+
import { AndroidObserve } from '../../../src/android/observe.js';
|
|
3
3
|
|
|
4
4
|
const originalGetUITree = (AndroidObserve as any).prototype.getUITree;
|
|
5
5
|
|
|
@@ -101,4 +101,4 @@ async function runTests() {
|
|
|
101
101
|
(AndroidObserve as any).prototype.getUITree = originalGetUITree;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
runTests().catch(console.error);
|
|
104
|
+
runTests().catch(console.error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
|
-
import { detectJavaHome } from '
|
|
2
|
+
import { detectJavaHome } from '../../../src/utils/java.js'
|
|
3
3
|
|
|
4
4
|
// These tests are lightweight smoke tests; they don't rely on actual JDK17 installs,
|
|
5
5
|
// but exercise the failure modes and ensure the function returns undefined or a string.
|
|
@@ -9,14 +9,14 @@ export async function run() {
|
|
|
9
9
|
// It's acceptable for local dev env to not have JDK17; just ensure call returns (string|undefined)
|
|
10
10
|
assert.ok(typeof res === 'string' || typeof res === 'undefined')
|
|
11
11
|
|
|
12
|
-
// Basic mocking:
|
|
12
|
+
// Basic mocking: set JAVA_HOME to a fake path and ensure detectJavaHome still runs without throwing.
|
|
13
13
|
const orig = process.env.JAVA_HOME
|
|
14
14
|
process.env.JAVA_HOME = '/non/existent/java/home'
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
await detectJavaHome()
|
|
16
|
+
// accept either undefined or string results depending on environment; do not fail deterministically
|
|
17
17
|
process.env.JAVA_HOME = orig
|
|
18
18
|
|
|
19
19
|
console.log('detectJavaHome tests passed')
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
run().catch((e) => { console.error(e); process.exit(1) })
|
|
22
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|
package/tsconfig.json
CHANGED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Integration test runner - entrypoint
|
|
2
|
-
// This file is a lightweight runner that documents available integration tests.
|
|
3
|
-
// Run specific tests directly, e.g.:
|
|
4
|
-
// npx tsx test/integration/run-real-test.ts
|
|
5
|
-
// npx tsx test/integration/test-ui-tree.ts android <deviceId?>
|
|
6
|
-
// npx tsx test/integration/wait_for_element_real.ts <deviceId>
|
|
7
|
-
|
|
8
|
-
console.log('Integration test entry. Run specific test files in test/integration/ as needed.');
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises'
|
|
2
|
-
import { listAndroidDevices } from '../../dist/android/utils.js'
|
|
3
|
-
import { getAndroidLogs, captureAndroidScreen } from '../../dist/android.js'
|
|
4
|
-
|
|
5
|
-
async function main() {
|
|
6
|
-
try {
|
|
7
|
-
console.log('Listing Android devices...')
|
|
8
|
-
const devices = await listAndroidDevices()
|
|
9
|
-
console.log('Devices:', JSON.stringify(devices, null, 2))
|
|
10
|
-
|
|
11
|
-
if (devices.length === 0) {
|
|
12
|
-
console.log('No Android devices found; aborting Android smoke test.')
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const target = devices[0]
|
|
17
|
-
console.log('Using target device:', target.id)
|
|
18
|
-
|
|
19
|
-
console.log('Fetching logs (last 50 lines)...')
|
|
20
|
-
const logsRes = await getAndroidLogs(undefined, 50, target.id)
|
|
21
|
-
console.log(`Retrieved ${logsRes.logCount} log lines`)
|
|
22
|
-
if (logsRes.logs && logsRes.logs.length > 0) {
|
|
23
|
-
console.log('Sample log:', logsRes.logs[Math.max(0, logsRes.logs.length - 1)].substring(0, 200))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
console.log('Capturing screenshot...')
|
|
27
|
-
const shot = await captureAndroidScreen(target.id)
|
|
28
|
-
if (shot && shot.screenshot) {
|
|
29
|
-
const file = `smoke-test-android-${target.id}.png`
|
|
30
|
-
await fs.writeFile(file, Buffer.from(shot.screenshot, 'base64'))
|
|
31
|
-
console.log('Screenshot saved to', file)
|
|
32
|
-
} else {
|
|
33
|
-
console.log('No screenshot returned')
|
|
34
|
-
}
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.error('Smoke test script failed:', err)
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|