mobile-debug-mcp 0.10.0 → 0.12.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 (67) hide show
  1. package/README.md +20 -5
  2. package/dist/android/diagnostics.js +24 -0
  3. package/dist/android/interact.js +1 -145
  4. package/dist/android/manage.js +162 -0
  5. package/dist/android/observe.js +133 -88
  6. package/dist/android/run.js +187 -0
  7. package/dist/android/utils.js +137 -147
  8. package/dist/ios/interact.js +4 -175
  9. package/dist/ios/manage.js +169 -0
  10. package/dist/ios/observe.js +129 -13
  11. package/dist/ios/run.js +200 -0
  12. package/dist/ios/utils.js +138 -124
  13. package/dist/server.js +45 -17
  14. package/dist/tools/interact.js +21 -71
  15. package/dist/tools/manage.js +180 -0
  16. package/dist/tools/observe.js +23 -69
  17. package/dist/tools/run.js +180 -0
  18. package/dist/utils/diagnostics.js +25 -0
  19. package/docs/CHANGELOG.md +14 -0
  20. package/eslint.config.js +22 -1
  21. package/package.json +8 -5
  22. package/scripts/check-idb.js +83 -0
  23. package/scripts/check-idb.ts +73 -0
  24. package/scripts/idb-helper.ts +76 -0
  25. package/scripts/install-idb.js +88 -0
  26. package/scripts/install-idb.ts +90 -0
  27. package/scripts/run-ios-smoke.ts +34 -0
  28. package/scripts/run-ios-ui-tree-tap.ts +33 -0
  29. package/src/android/diagnostics.ts +23 -0
  30. package/src/android/interact.ts +2 -155
  31. package/src/android/manage.ts +157 -0
  32. package/src/android/observe.ts +129 -97
  33. package/src/android/utils.ts +147 -149
  34. package/src/ios/interact.ts +5 -181
  35. package/src/ios/manage.ts +164 -0
  36. package/src/ios/observe.ts +130 -14
  37. package/src/ios/utils.ts +127 -128
  38. package/src/server.ts +47 -17
  39. package/src/tools/interact.ts +23 -62
  40. package/src/tools/manage.ts +171 -0
  41. package/src/tools/observe.ts +24 -74
  42. package/src/types.ts +9 -0
  43. package/src/utils/diagnostics.ts +36 -0
  44. package/test/device/README.md +49 -0
  45. package/test/device/index.ts +27 -0
  46. package/test/device/manage/run-build-install-ios.ts +82 -0
  47. package/test/{integration → device/manage}/run-install-android.ts +4 -4
  48. package/test/{integration → device/manage}/run-install-ios.ts +4 -4
  49. package/test/{integration → device/observe}/logstream-real.ts +5 -4
  50. package/test/{integration → device/utils}/test-dist.ts +2 -2
  51. package/test/unit/index.ts +10 -6
  52. package/test/unit/manage/build.test.ts +83 -0
  53. package/test/unit/manage/build_and_install.test.ts +134 -0
  54. package/test/unit/manage/diagnostics.test.ts +85 -0
  55. package/test/unit/{install.test.ts → manage/install.test.ts} +27 -18
  56. package/test/unit/{logparse.test.ts → observe/logparse.test.ts} +1 -1
  57. package/test/unit/{logstream.test.ts → observe/logstream.test.ts} +9 -10
  58. package/test/unit/{wait_for_element_mock.ts → observe/wait_for_element_mock.ts} +3 -3
  59. package/test/unit/{detect-java.test.ts → utils/detect-java.test.ts} +5 -5
  60. package/tsconfig.json +2 -1
  61. package/test/integration/index.ts +0 -8
  62. package/test/integration/test-dist.mjs +0 -41
  63. /package/test/{integration → device/interact}/run-real-test.ts +0 -0
  64. /package/test/{integration → device/interact}/smoke-test.ts +0 -0
  65. /package/test/{integration → device/manage}/install.integration.ts +0 -0
  66. /package/test/{integration → device/observe}/test-ui-tree.ts +0 -0
  67. /package/test/{integration → device/observe}/wait_for_element_real.ts +0 -0
@@ -0,0 +1,49 @@
1
+ Device-dependent integration tests
2
+
3
+ Overview
4
+
5
+ This folder contains integration tests that require a simulator or a real device (Android/iOS). These tests exercise real device flows (install, start, UI-tree, logs, interaction) and are intentionally gated from the default CI to avoid failures on runners without devices.
6
+
7
+ Prerequisites
8
+
9
+ - Build the project: npm run build
10
+ - Android: adb available (set ADB_PATH if custom) and a connected device or emulator
11
+ - iOS: idb (fb-idb) and idb_companion installed and available. Prefer setting MCP_IDB_PATH or IDB_PATH, or add `idb` to PATH. For simulator tests ensure a simulator is booted.
12
+
13
+ Environment variables
14
+
15
+ - RUN_DEVICE_TESTS=true — enable device tests when running the integration runner
16
+ - ADB_PATH — custom path to adb (optional)
17
+ - MCP_IDB_PATH / IDB_PATH — path to idb CLI (optional)
18
+ - DEVICE_ID — when a test requires a device id
19
+ - APP_ID — when a test requires an app package/bundle id (used by some scripts)
20
+
21
+ How to run
22
+
23
+ - Run all non-device integration tests (default):
24
+ npm run test:integration
25
+
26
+ - Run device tests (all):
27
+ RUN_DEVICE_TESTS=true npm run test:integration
28
+
29
+ - Run a single device test (example: iOS UI tree):
30
+ npx tsx test/device/observe/test-ui-tree.ts ios booted
31
+
32
+ - Run install integration for a project:
33
+ npx tsx test/device/manage/install.integration.ts /path/to/project [deviceId]
34
+
35
+ iOS notes
36
+
37
+ - If using idb_companion, prefer starting it bound to a UDID to reduce flakiness:
38
+ idb_companion --udid <UDID> &
39
+ - Boot a simulator if needed:
40
+ xcrun simctl boot <UDID>
41
+
42
+ Android notes
43
+
44
+ - Ensure an emulator or device is connected (adb devices)
45
+ - Some tests read DEVICE_ID or accept it as an argument
46
+
47
+ CI recommendation
48
+
49
+ Keep these tests gated behind RUN_DEVICE_TESTS and run them only on macOS runners (for iOS) or self-hosted runners with attached devices.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Device tests runner
3
+ // This runner loads minimal utilities, and only imports the device tests when
4
+ // RUN_DEVICE_TESTS=true to avoid running them in CI by default.
5
+
6
+ import './utils/test-dist';
7
+
8
+ (async () => {
9
+ if (process.env.RUN_DEVICE_TESTS === 'true') {
10
+ console.log('RUN_DEVICE_TESTS=true: running device integration tests');
11
+ await Promise.all([
12
+ import('./manage/install.integration'),
13
+ import('./manage/run-install-android'),
14
+ import('./manage/run-install-ios'),
15
+ import('./observe/logstream-real'),
16
+ import('./observe/test-ui-tree'),
17
+ import('./observe/wait_for_element_real'),
18
+ import('./interact/run-real-test'),
19
+ import('./interact/smoke-test')
20
+ ]);
21
+ console.log('Device integration imports complete');
22
+ } else {
23
+ console.log('Skipping device-dependent integration tests. Set RUN_DEVICE_TESTS=true to enable them.');
24
+ }
25
+ })();
26
+
27
+ console.log('Device tests runner ready');
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+
6
+ async function findAppInDerived(derivedPath: string): Promise<string | null> {
7
+ const products = path.join(derivedPath, 'Build', 'Products')
8
+ try {
9
+ const entries = await fs.promises.readdir(products, { withFileTypes: true })
10
+ for (const e of entries) {
11
+ if (e.isDirectory() && e.name.includes('Debug-iphonesimulator')) {
12
+ const full = path.join(products, e.name)
13
+ const apps = await fs.promises.readdir(full, { withFileTypes: true })
14
+ for (const a of apps) {
15
+ if (a.isDirectory() && a.name.endsWith('.app')) return path.join(full, a.name)
16
+ }
17
+ }
18
+ }
19
+ } catch {
20
+ return null
21
+ }
22
+ return null
23
+ }
24
+
25
+ function spawnStream(cmd: string, args: string[], opts: any = {}): Promise<number> {
26
+ return new Promise((resolve, reject) => {
27
+ const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], ...opts })
28
+ child.stdout?.on('data', d => process.stdout.write(d))
29
+ child.stderr?.on('data', d => process.stderr.write(d))
30
+ child.on('close', code => resolve(code ?? 0))
31
+ child.on('error', err => reject(err))
32
+ })
33
+ }
34
+
35
+ async function main() {
36
+ const [, , projectPath, deviceId = 'booted'] = process.argv
37
+ if (!projectPath) {
38
+ console.error('Usage: tsx test/integration/manage/run-build-install-ios.ts <project-dir> [deviceId]')
39
+ process.exit(1)
40
+ }
41
+
42
+ const derived = `/tmp/derived_ios_integration_${Date.now()}`
43
+ // detect workspace or project
44
+ const files = await fs.promises.readdir(projectPath).catch(() => [])
45
+ const workspace = files.find(f => f.endsWith('.xcworkspace'))
46
+ const proj = files.find(f => f.endsWith('.xcodeproj'))
47
+ if (!workspace && !proj) {
48
+ console.error('No Xcode project/workspace found in', projectPath)
49
+ process.exit(2)
50
+ }
51
+
52
+ const buildArgs = workspace
53
+ ? ['-workspace', path.join(projectPath, workspace), '-scheme', workspace.replace(/\.xcworkspace$/, ''), '-configuration', 'Debug', '-sdk', 'iphonesimulator', '-derivedDataPath', derived, 'build']
54
+ : ['-project', path.join(projectPath, proj!), '-scheme', proj!.replace(/\.xcodeproj$/, ''), '-configuration', 'Debug', '-sdk', 'iphonesimulator', '-derivedDataPath', derived, 'build']
55
+
56
+ console.error('Building with xcodebuild... derivedDataPath=', derived)
57
+ const code = await spawnStream('xcodebuild', buildArgs, { cwd: projectPath })
58
+ if (code !== 0) {
59
+ console.error('xcodebuild failed with code', code)
60
+ process.exit(3)
61
+ }
62
+
63
+ const app = await findAppInDerived(derived)
64
+ if (!app) {
65
+ console.error('Could not find built .app in derived data')
66
+ process.exit(4)
67
+ }
68
+
69
+ console.error('Built app at', app)
70
+
71
+ console.error('Installing via simctl...')
72
+ const installCode = await spawnStream('xcrun', ['simctl', 'install', deviceId, app])
73
+ if (installCode !== 0) {
74
+ console.error('simctl install failed with code', installCode)
75
+ process.exit(5)
76
+ }
77
+
78
+ console.log(JSON.stringify({ success: true, app, deviceId }))
79
+ process.exit(0)
80
+ }
81
+
82
+ main().catch(e => { console.error('Unexpected error', e); process.exit(10) })
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { AndroidInteract } from '../../dist/android/interact.js'
2
+ import { AndroidManage } from '../../../dist/android/manage.js'
3
3
 
4
4
  async function main() {
5
5
  const [, , appPath, deviceId] = process.argv
@@ -8,11 +8,11 @@ async function main() {
8
8
  process.exit(1)
9
9
  }
10
10
 
11
- const inter = new AndroidInteract()
11
+ const mgr = new AndroidManage()
12
12
  try {
13
- const res = await inter.installApp(appPath, deviceId)
13
+ const res = await mgr.installApp(appPath, deviceId)
14
14
  console.log(JSON.stringify(res, null, 2))
15
- } catch {
15
+ } catch (err:any) {
16
16
  console.error('Install failed:', err instanceof Error ? err.message : String(err))
17
17
  process.exit(2)
18
18
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { iOSInteract } from '../../dist/ios/interact.js'
2
+ import { iOSManage } from '../../../dist/ios/manage.js'
3
3
 
4
4
  async function main() {
5
5
  const [, , appPath, deviceId] = process.argv
@@ -8,11 +8,11 @@ async function main() {
8
8
  process.exit(1)
9
9
  }
10
10
 
11
- const inter = new iOSInteract()
11
+ const mgr = new iOSManage()
12
12
  try {
13
- const res = await inter.installApp(appPath, deviceId || 'booted')
13
+ const res = await mgr.installApp(appPath, deviceId || 'booted')
14
14
  console.log(JSON.stringify(res, null, 2))
15
- } catch {
15
+ } catch (err:any) {
16
16
  console.error('Install failed:', err instanceof Error ? err.message : String(err))
17
17
  process.exit(2)
18
18
  }
@@ -1,4 +1,4 @@
1
- import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from '../../src/android/utils.js'
1
+ import { AndroidObserve } from '../../src/android/observe.js'
2
2
 
3
3
  async function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
4
4
 
@@ -6,7 +6,8 @@ async function main() {
6
6
  const packageName = process.argv[2] || 'com.android.systemui'
7
7
  const sessionId = 'real-logstream'
8
8
  console.log('Starting log stream for', packageName)
9
- const start = await startAndroidLogStream(packageName, 'error', undefined, sessionId)
9
+ const obs = new AndroidObserve()
10
+ const start = await obs.startLogStream(packageName, 'error', undefined, sessionId)
10
11
  console.log('start result:', start)
11
12
  if (!start.success) {
12
13
  console.error('Failed to start log stream:', start.error)
@@ -16,7 +17,7 @@ async function main() {
16
17
  try {
17
18
  for (let i=0;i<10;i++) {
18
19
  console.log('\nPolling logs (iteration', i+1, ')')
19
- const { entries, crash_summary } = await readLogStreamLines(sessionId, 50)
20
+ const { entries, crash_summary } = await obs.readLogStream(sessionId, 50)
20
21
  console.log(`Entries: ${entries.length}`)
21
22
  if (entries.length > 0) console.log('Latest:', entries[entries.length-1])
22
23
  console.log('Crash summary:', crash_summary)
@@ -28,7 +29,7 @@ async function main() {
28
29
  }
29
30
  } finally {
30
31
  console.log('Stopping log stream')
31
- await stopAndroidLogStream(sessionId)
32
+ await obs.stopLogStream(sessionId)
32
33
  }
33
34
  }
34
35
 
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises'
2
- import { listAndroidDevices } from '../../dist/android/utils.js'
3
- import { getAndroidLogs, captureAndroidScreen } from '../../dist/android.js'
2
+ import { listAndroidDevices } from '../../../dist/android/utils.js'
3
+ import { getAndroidLogs, captureAndroidScreen } from '../../../dist/android.js'
4
4
 
5
5
  async function main() {
6
6
  try {
@@ -1,7 +1,11 @@
1
- // Unit test runner - imports mocked/unit tests
2
- import './wait_for_element_mock';
3
- import './logstream.test';
4
- import './logparse.test';
5
- import './install.test';
1
+ // Aggregator entrypoint for unit tests
2
+ import './utils/detect-java.test.ts'
3
+ import './observe/logparse.test.ts'
4
+ import './observe/logstream.test.ts'
5
+ import './observe/wait_for_element_mock.ts'
6
+ import './manage/install.test.ts'
7
+ import './manage/build.test.ts'
8
+ import './manage/build_and_install.test.ts'
9
+ import './manage/diagnostics.test.ts'
6
10
 
7
- console.log('Unit tests loaded. Run with: npx tsx test/unit/index.ts');
11
+ console.log('Unit tests loaded.')
@@ -0,0 +1,83 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+
6
+ // Test build_app handler by creating fake gradlew and xcodebuild behaviours
7
+
8
+ async function makeTempProject(platform: 'android' | 'ios') {
9
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), `mcp-build-${platform}-`))
10
+ if (platform === 'android') {
11
+ const gradlew = path.join(dir, 'gradlew')
12
+ const script = `#!/bin/sh
13
+ mkdir -p "$(pwd)/app/build/outputs/apk/debug"
14
+ echo 'fake-apk' > "$(pwd)/app/build/outputs/apk/debug/app-debug.apk"
15
+ echo 'BUILD SUCCESS'
16
+ exit 0
17
+ `
18
+ await fs.writeFile(gradlew, script, { mode: 0o755 })
19
+ } else {
20
+ // create minimal Xcode workspace structure
21
+ const ws = path.join(dir, 'Example.xcworkspace')
22
+ await fs.writeFile(ws, '')
23
+ const script = `#!/bin/sh
24
+ mkdir -p "$(pwd)/Build/Products/Debug-iphonesimulator/Example.app"
25
+ echo '<plist/>' > "$(pwd)/Build/Products/Debug-iphonesimulator/Example.app/Info.plist"
26
+ echo 'BUILD SUCCESS'
27
+ exit 0
28
+ `
29
+ const xbuild = path.join(dir, 'xcodebuild')
30
+ await fs.writeFile(xbuild, script, { mode: 0o755 })
31
+ }
32
+ return dir
33
+ }
34
+
35
+ export async function run() {
36
+ const androidProject = await makeTempProject('android')
37
+ const iosProject = await makeTempProject('ios')
38
+
39
+ // Create a fake xcodebuild in PATH for the iOS build step
40
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-xcode-bin-'))
41
+ const xcodeScript = `#!/usr/bin/env node
42
+ const fs = require('fs')
43
+ const path = require('path')
44
+ // emulate xcodebuild by creating an Example.app inside Build/Products/Debug-iphonesimulator
45
+ const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
46
+ fs.mkdirSync(app, { recursive: true })
47
+ fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
48
+ console.log('xcodebuild simulated')
49
+ process.exit(0)
50
+ `
51
+ const xcodePath = path.join(binDir, 'xcodebuild')
52
+ await fs.writeFile(xcodePath, xcodeScript, { mode: 0o755 })
53
+
54
+ const origPath = process.env.PATH || ''
55
+ const origXcode = process.env.XCODEBUILD_PATH
56
+ process.env.PATH = `${binDir}:${origPath}`
57
+ // Prefer explicit XCODEBUILD_PATH to ensure deterministic behavior
58
+ process.env.XCODEBUILD_PATH = xcodePath
59
+
60
+ const { ToolsManage } = await import('../../../src/tools/manage.js')
61
+
62
+ try {
63
+ const ares = await ToolsManage.buildAppHandler({ platform: 'android', projectPath: androidProject })
64
+ console.log('android build', ares)
65
+ assert.ok((ares as any).artifactPath && (ares as any).artifactPath.endsWith('.apk'))
66
+
67
+ const ires = await ToolsManage.buildAppHandler({ platform: 'ios', projectPath: iosProject })
68
+ console.log('ios build', ires)
69
+ assert.ok((ires as any).artifactPath && (ires as any).artifactPath.endsWith('.app'))
70
+
71
+ console.log('build tests passed')
72
+ } finally {
73
+ // cleanup
74
+ await fs.rm(androidProject, { recursive: true, force: true }).catch(() => {})
75
+ await fs.rm(iosProject, { recursive: true, force: true }).catch(() => {})
76
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
77
+ process.env.PATH = origPath
78
+ if (typeof origXcode !== 'undefined') process.env.XCODEBUILD_PATH = origXcode
79
+ else delete process.env.XCODEBUILD_PATH
80
+ }
81
+ }
82
+
83
+ run().catch(e => { console.error(e); process.exit(1) })
@@ -0,0 +1,134 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+
6
+ async function makeAndroidProject() {
7
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-build-ai-android-'))
8
+ const gradlew = path.join(dir, 'gradlew')
9
+ const script = `#!/bin/sh
10
+ mkdir -p "$(pwd)/app/build/outputs/apk/debug"
11
+ echo 'fake-apk' > "$(pwd)/app/build/outputs/apk/debug/app-debug.apk"
12
+ echo 'BUILD SUCCESS'
13
+ exit 0
14
+ `
15
+ await fs.writeFile(gradlew, script, { mode: 0o755 })
16
+ return dir
17
+ }
18
+
19
+ async function makeIOSProject() {
20
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-build-ai-ios-'))
21
+ const ws = path.join(dir, 'Example.xcworkspace')
22
+ await fs.writeFile(ws, '')
23
+ return dir
24
+ }
25
+
26
+ export async function run() {
27
+ const androidProject = await makeAndroidProject()
28
+ const iosProject = await makeIOSProject()
29
+
30
+ // Create fake bin dir with adb and simctl
31
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-bin-'))
32
+ const adbPath = path.join(binDir, 'adb')
33
+ const adbScript = `#!/usr/bin/env node
34
+ const args = process.argv.slice(2)
35
+ if (args.includes('devices')) {
36
+ console.log('List of devices attached')
37
+ console.log('emulator-5554\tdevice product:sdk_gphone64_arm64')
38
+ process.exit(0)
39
+ }
40
+ if (args.includes('install')) {
41
+ console.log('Performing Streamed Install')
42
+ console.log('Success')
43
+ process.exit(0)
44
+ }
45
+ if (args.includes('shell')) {
46
+ const idx = args.indexOf('shell')
47
+ const shellArgs = args.slice(idx+1)
48
+ if (shellArgs[0] === 'getprop') {
49
+ console.log('')
50
+ process.exit(0)
51
+ }
52
+ console.log('Success')
53
+ process.exit(0)
54
+ }
55
+ console.log('OK')
56
+ process.exit(0)
57
+ `
58
+ await fs.writeFile(adbPath, adbScript, { mode: 0o755 })
59
+
60
+ const simctlPath = path.join(binDir, 'simctl')
61
+ const simctlScript = `#!/usr/bin/env node
62
+ const args = process.argv.slice(2)
63
+ if (args.includes('install')) {
64
+ console.log('simctl install simulated')
65
+ process.exit(0)
66
+ }
67
+ // Respond to 'list devices --json'
68
+ if (args.includes('list') && args.includes('devices') && args.includes('--json')) {
69
+ const out = { devices: { 'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [ { udid: 'booted', name: 'iPhone 14', state: 'Booted' } ] } }
70
+ console.log(JSON.stringify(out))
71
+ process.exit(0)
72
+ }
73
+ console.log('simctl ok')
74
+ process.exit(0)
75
+ `
76
+ await fs.writeFile(simctlPath, simctlScript, { mode: 0o755 })
77
+
78
+ // fake xcodebuild for iOS build step
79
+ const xcodeScript = `#!/usr/bin/env node
80
+ const fs = require('fs')
81
+ const path = require('path')
82
+ const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
83
+ fs.mkdirSync(app, { recursive: true })
84
+ fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
85
+ console.log('xcodebuild simulated')
86
+ process.exit(0)
87
+ `
88
+ const xcodePath = path.join(binDir, 'xcodebuild')
89
+ await fs.writeFile(xcodePath, xcodeScript, { mode: 0o755 })
90
+
91
+ const origPath = process.env.PATH || ''
92
+ const origXcrun = process.env.XCRUN_PATH
93
+ process.env.PATH = `${binDir}:${origPath}`
94
+ process.env.XCRUN_PATH = simctlPath
95
+
96
+ const { ToolsManage } = await import('../../../src/tools/manage.js')
97
+
98
+ try {
99
+ // Android build_and_install
100
+ const ares = await ToolsManage.buildAndInstallHandler({ platform: 'android', projectPath: androidProject, deviceId: 'emulator-5554' })
101
+ console.log('android ndjson:\n', ares.ndjson)
102
+ console.log('android result:', ares.result)
103
+ if (ares.result.success !== true) {
104
+ assert.ok(ares.result.error || ares.result.diagnostics, 'If build_and_install fails, expect error or diagnostics')
105
+ } else {
106
+ assert.ok(ares.result.artifactPath && ares.result.artifactPath.endsWith('.apk'))
107
+ assert.ok(ares.ndjson.includes('"type":"build"'))
108
+ assert.ok(ares.ndjson.includes('"type":"install"'))
109
+ }
110
+
111
+ // iOS build_and_install
112
+ const ires = await ToolsManage.buildAndInstallHandler({ platform: 'ios', projectPath: iosProject, deviceId: 'booted' })
113
+ console.log('ios ndjson:\n', ires.ndjson)
114
+ console.log('ios result:', ires.result)
115
+ if (ires.result.success !== true) {
116
+ assert.ok(ires.result.error || ires.result.diagnostics, 'If build_and_install fails for iOS, expect error or diagnostics')
117
+ } else {
118
+ assert.ok(ires.result.artifactPath && ires.result.artifactPath.endsWith('.app'))
119
+ assert.ok(ires.ndjson.includes('"type":"build"'))
120
+ assert.ok(ires.ndjson.includes('"type":"install"'))
121
+ }
122
+
123
+ console.log('build_and_install tests passed')
124
+ } finally {
125
+ process.env.PATH = origPath
126
+ if (origXcrun === undefined) delete process.env.XCRUN_PATH
127
+ else process.env.XCRUN_PATH = origXcrun
128
+ await fs.rm(androidProject, { recursive: true, force: true }).catch(() => {})
129
+ await fs.rm(iosProject, { recursive: true, force: true }).catch(() => {})
130
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
131
+ }
132
+ }
133
+
134
+ run().catch(e => { console.error(e); process.exit(1) })
@@ -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,37 +21,42 @@ 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 = `#!/usr/bin/env node
25
- console.log('Performing Streamed Install')
26
- console.log('Success')
27
- process.exit(0)
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
- const { AndroidInteract } = await import('../../src/android/interact.js')
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
38
43
  const { dir: d1, file: apk } = await makeTempFile('.apk')
39
- const ai = new AndroidInteract()
44
+ const ai = new AndroidManage()
40
45
  const res1 = await ai.installApp(apk)
41
46
  console.log('res1', res1)
42
- assert.ok(res1.installed === true, 'APK install should succeed')
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 = `#!/usr/bin/env node
48
- const fs = require('fs')
49
- const path = require('path')
50
- const apkPath = path.join(process.cwd(), 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk')
51
- fs.mkdirSync(path.dirname(apkPath), { recursive: true })
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,4 +1,4 @@
1
- import { parseLogLine } from '../../src/android/utils.js'
1
+ import { parseLogLine } from '../../../src/android/utils.js'
2
2
 
3
3
  function assert(cond: boolean, msg?: string) { if (!cond) throw new Error(msg || 'Assertion failed') }
4
4