From ab44487ff86d064e924fd2c44b02f5a5430f57f3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:49:46 +0800 Subject: [PATCH] fix(adb): preserve process names and pin fixture interpreters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consume Toybox’s single NAME separator without stripping source whitespace. Keep MCP subprocess fixtures independent of ambient PATH and validate calls through the existing named-schema helper. Validation: focused ADB provider/parser and MCP tests passed; no real Android device was used. --- src/adb/AdbProvider.test.ts | 18 ++++++++++++++++++ src/adb/AdbShellOutput.ts | 15 ++++++++------- tests/boundary/mcp/adbStartMcp.test.ts | 25 +++++++++++++------------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/adb/AdbProvider.test.ts b/src/adb/AdbProvider.test.ts index 7542dc8357..1bccde00bc 100644 --- a/src/adb/AdbProvider.test.ts +++ b/src/adb/AdbProvider.test.ts @@ -1505,6 +1505,24 @@ exit 1`, } }); +it("preserves process names printed by Toybox without treating their whitespace as column padding", async () => { + // Toybox's final NAME column is left justified without padding; only the + // single separator after S belongs to the table (toys/posix/ps.c show_ps). + const name = " native tool "; + const { binary } = await writeAdbStub( + shellStub({ + ps: `USER PID PPID VSZ RSS WCHAN ADDR S NAME\nroot 1234 567 123456 7890 0 0 S ${name}\r\n`, + }), + ); + const result = await evidenceResult(providerOver(binary), { + operation: "list_adb_processes", + input: { serial: "emulator-5554" }, + }); + expect(result.processes).toEqual([ + { user: "root", pid: 1234, ppid: 567, rss: 7890, state: "S", name }, + ]); +}); + it("lists processes, features, and services through fixed commands", async () => { const { binary } = await writeAdbStub( shellStub({ diff --git a/src/adb/AdbShellOutput.ts b/src/adb/AdbShellOutput.ts index d0112a2ca5..ed35d975a9 100644 --- a/src/adb/AdbShellOutput.ts +++ b/src/adb/AdbShellOutput.ts @@ -27,8 +27,10 @@ export interface AdbProcessEntry { readonly name: string; } +// Toybox emits one separator before the unpadded final NAME column. +// Remaining whitespace belongs to argv[0], not the table layout. const PROCESS_LINE = - /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/u; + /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+) (.+)$/u; const safeInteger = (text: string): number | null => { const value = Number(text); @@ -46,22 +48,21 @@ export const parseProcessListOutput = ( const processes: AdbProcessEntry[] = []; const unparsed = collectUnparsedLines(); let columns: string | null = null; - for (const rawLine of stdout.split(/\r?\n/u)) { - const line = rawLine.trimEnd(); - if (line === "") continue; + for (const line of stdout.split(/\r?\n/u)) { + if (line.trim() === "") continue; if (/^USER\s+PID\s+PPID\b/u.test(line)) { columns = line.trim(); continue; } const match = PROCESS_LINE.exec(line); if (match === null) { - unparsed.add(rawLine); + unparsed.add(line); continue; } const pid = safeInteger(match[2]!); const ppid = safeInteger(match[3]!); const rss = safeInteger(match[5]!); - if (pid === null || ppid === null || rss === null) unparsed.add(rawLine); + if (pid === null || ppid === null || rss === null) unparsed.add(line); if (pid === null) continue; processes.push({ user: match[1]!, @@ -69,7 +70,7 @@ export const parseProcessListOutput = ( ppid, rss, state: match[8]!, - name: match[9]!.trim(), + name: match[9]!, }); } return { columns, processes, unparsedLines: unparsed.facts() }; diff --git a/tests/boundary/mcp/adbStartMcp.test.ts b/tests/boundary/mcp/adbStartMcp.test.ts index 2f735ded6f..063e33726f 100644 --- a/tests/boundary/mcp/adbStartMcp.test.ts +++ b/tests/boundary/mcp/adbStartMcp.test.ts @@ -1,3 +1,4 @@ +import { artifactMcpCall } from "../../../scripts/lib/artifact-e2e.mjs"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -28,7 +29,7 @@ it.skipIf(process.platform === "win32")( await chmod(localPath, 0o755); await writeFile( adbPath, - `#!/usr/bin/env node + `#!${process.execPath} const fs = require("node:fs"); const args = process.argv.slice(2); if (args[0] === "version") { process.stdout.write("Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\n"); } @@ -63,7 +64,7 @@ else if (args[0] === "-s" && args[2] === "shell" && args[3] === "test") { const client = new Client({ name: "adb-push-boundary", version: "0.0.0" }); onTestFinished(() => client.close()); await client.connect(clientTransport); - const response = await client.callTool({ + const response = await artifactMcpCall(client, { name: "push_adb_file", arguments: { serial: "emulator-5554", @@ -104,7 +105,7 @@ const createFakeAdb = async () => { const stderrFile = join(directory, "stderr"); const callsFile = join(directory, "calls"); const exitCodeFile = join(directory, "exit-code"); - const fakeAdbSource = `#!/usr/bin/env node + const fakeAdbSource = `#!${process.execPath} const fs = require("node:fs"); const [stdoutPath, stderrPath, callsPath, exitCodePath] = ${JSON.stringify([stdoutFile, stderrFile, callsFile, exitCodeFile])}; const args = process.argv.slice(2); @@ -158,7 +159,7 @@ const assertAdvertisedActivityExtras = async ( ).toBe(true); // Canonical validation rejects malformed extras before starting ADB. const callsBeforeInvalidExtra = await calls(); - const invalidExtra = await client.callTool({ + const invalidExtra = await artifactMcpCall(client, { name: "start_adb_activity", arguments: { ...input, @@ -220,7 +221,7 @@ it { key: "scale", type: "float" as const, value: "1e-3" }, ], }; - const success = await client.callTool({ + const success = await artifactMcpCall(client, { name: operation, arguments: input, }); @@ -241,7 +242,7 @@ it stdout = `${starting}Status: timeout\nActivity: com.example/.Main\nWaitTime: 10004\nComplete\n`; await fakeAdb.setReport(stdout, stderr); - const timeout = await client.callTool({ + const timeout = await artifactMcpCall(client, { name: operation, arguments: input, }); @@ -272,7 +273,7 @@ it stdout = `${starting}Status: ok\nActivity: com.example/.Main\nComplete\n`; stderr = "Exception: Activity class does not exist\n"; await fakeAdb.setReport(stdout, stderr); - const hardError = await client.callTool({ + const hardError = await artifactMcpCall(client, { name: operation, arguments: input, }); @@ -362,7 +363,7 @@ it.skipIf(process.platform === "win32")( validateFormats: false, }).compile(advertised.inputSchema); expect(validateInput(input)).toBe(true); - const refused = await client.callTool({ + const refused = await artifactMcpCall(client, { name: "start_adb_activity", arguments: input, }); @@ -393,7 +394,7 @@ it.skipIf(process.platform === "win32")( "Starting: Intent { act=android.intent.action.MAIN cmp=com.example/.Outer$InnerActivity }\n"; const transportStderr = "error: device offline\n"; await fakeAdb.setReport(transportStdout, transportStderr, 1); - const transport = await client.callTool({ + const transport = await artifactMcpCall(client, { name: "start_adb_activity", arguments: input, }); @@ -411,7 +412,7 @@ it.skipIf(process.platform === "win32")( "Starting: Intent { act=android.intent.action.MAIN cmp=com.example/.Outer$InnerActivity }\n"; const incompleteStderr = "transport disconnected by peer\n"; await fakeAdb.setReport(incompleteStdout, incompleteStderr, 1); - const incomplete = await client.callTool({ + const incomplete = await artifactMcpCall(client, { name: "start_adb_activity", arguments: input, }); @@ -435,7 +436,7 @@ it.skipIf(process.platform === "win32")( const stderrOnly = "Error: Activity class {com.example/com.example.Outer$InnerActivity} does not exist.\n"; await fakeAdb.setReport("", stderrOnly, 1); - const stderrOnlyFailure = await client.callTool({ + const stderrOnlyFailure = await artifactMcpCall(client, { name: "start_adb_activity", arguments: input, }); @@ -461,7 +462,7 @@ it.skipIf(process.platform === "win32")( "Error: " + "x".repeat(70 * 1024); await fakeAdb.setReport(oversizedReport, "", 1); - const truncated = await client.callTool({ + const truncated = await artifactMcpCall(client, { name: "start_adb_activity", arguments: input, });