mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(adb): preserve empty properties and verbatim version names
This commit is contained in:
@@ -169,7 +169,7 @@ export const parseGetpropOutput = (
|
||||
}
|
||||
const properties = DEVICE_PROPERTY_WHITELIST.flatMap((name) => {
|
||||
const value = values.get(name);
|
||||
return value === undefined || value === ""
|
||||
return value === undefined
|
||||
? []
|
||||
: [{ name, value } satisfies AdbDeviceProperty];
|
||||
});
|
||||
|
||||
@@ -18,6 +18,16 @@ const packageDump = (installerLine: string | undefined): string =>
|
||||
].join("\n");
|
||||
|
||||
describe("parseDumpsysPackageOutput", () => {
|
||||
it.each(["", " My Build ", " ", "null"])(
|
||||
"preserves the printed version name %j",
|
||||
(versionName) => {
|
||||
const result = parseDumpsysPackageOutput(
|
||||
` pkg=Package{abcdef com.example}\n versionName=${versionName}\n`,
|
||||
);
|
||||
expect(result.versionName).toBe(versionName);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a reported installer package name", () => {
|
||||
expect(
|
||||
parseDumpsysPackageOutput(
|
||||
|
||||
@@ -335,13 +335,16 @@ export const parseDumpsysPackageOutput = (
|
||||
const match = DETAIL_LINE.exec(line);
|
||||
if (match === null) continue;
|
||||
const key = match[1]!;
|
||||
const value = match[2]!.trim();
|
||||
// versionName is an arbitrary manifest string, printed verbatim by AOSP.
|
||||
// Only the fields with their own structured grammar are normalized below.
|
||||
const rawValue = match[2]!;
|
||||
const value = rawValue.trim();
|
||||
if (key === "pkg" && parsedPackageAvailable === null) {
|
||||
if (value === "null") parsedPackageAvailable = false;
|
||||
else if (value.startsWith("Package{") && value.endsWith("}"))
|
||||
parsedPackageAvailable = true;
|
||||
} else if (key === "versionName" && details.versionName === null)
|
||||
details.versionName = value === "" ? null : value;
|
||||
details.versionName = rawValue;
|
||||
else if (key === "versionCode" && details.versionCode === null) {
|
||||
const parsed = /^(-?\d+)/u.exec(value);
|
||||
if (parsed !== null) details.versionCode = parsed[1]!;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ADB_TOOL_CONTRACTS = [
|
||||
name: "inspect_adb_device",
|
||||
...toolContractMetadata("inspect_adb_device"),
|
||||
description:
|
||||
"Inspect one device's build identity through a fixed `getprop` whitelist: Android and SDK versions, security patch, fingerprint, product identity, CPU ABIs, and debuggable flag, plus the observed emulator indicator ro.kernel.qemu. Read-only with respect to the device.",
|
||||
"Inspect one device's build identity through a fixed `getprop` whitelist: Android and SDK versions, security patch, fingerprint, product identity, CPU ABIs, and debuggable flag, plus the observed emulator indicator ro.kernel.qemu. Present properties retain empty values; omitted keys were not reported. Read-only with respect to the device.",
|
||||
kind: "adb-provider",
|
||||
inputSchema: adbInputSchemas.inspect_adb_device,
|
||||
outputSchema: evidenceResultOf(adbResultSchemas.inspect_adb_device),
|
||||
|
||||
@@ -527,7 +527,7 @@ export const adbResultSchemas = {
|
||||
client,
|
||||
serial: z.string().min(1),
|
||||
properties: z.array(
|
||||
z.strictObject({ name: z.string().min(1), value: z.string().min(1) }),
|
||||
z.strictObject({ name: z.string().min(1), value: z.string() }),
|
||||
),
|
||||
property_count: z.int().nonnegative(),
|
||||
emulator_observed: z.boolean().nullable(),
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { chmod } from "node:fs/promises";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AdbProvider } from "../../../src/adb/AdbProvider.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { execFileOutput } from "../../../src/process/ExecFileOutput.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import {
|
||||
createTestWorkspace,
|
||||
removeTestWorkspace,
|
||||
} from "../../support/workspace/workspaceFixture.js";
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
title: "empty, whitespace and zero device properties",
|
||||
tool: "inspect_adb_device",
|
||||
argv: ["inspect-adb-device", "device", "--json"],
|
||||
input: { serial: "device" },
|
||||
expected: {
|
||||
serial: "device",
|
||||
property_count: 4,
|
||||
emulator_observed: false,
|
||||
properties: [
|
||||
{ name: "ro.build.version.security_patch", value: "" },
|
||||
{ name: "ro.debuggable", value: "0" },
|
||||
{ name: "ro.kernel.qemu", value: "0" },
|
||||
{ name: "ro.product.model", value: " " },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "version name whitespace",
|
||||
tool: "inspect_adb_package",
|
||||
argv: ["inspect-adb-package", "device", "com.example", "--json"],
|
||||
input: { serial: "device", package: "com.example" },
|
||||
expected: {
|
||||
serial: "device",
|
||||
package_name: "com.example",
|
||||
version_name: " My Build ",
|
||||
version_code: "0",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
it.skipIf(process.platform === "win32").each(["cli", "mcp"] as const)(
|
||||
`preserves ${scenario.title} through %s`,
|
||||
async (adapter) => {
|
||||
const workspace = await createTestWorkspace("rea-adb-properties-");
|
||||
onTestFinished(() => removeTestWorkspace(workspace.root));
|
||||
const binary = await workspace.write(
|
||||
"adb",
|
||||
`#!/bin/sh
|
||||
if [ "$1" = version ]; then
|
||||
printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\nInstalled as %s\\n' "$0"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = -s ] && [ "$3" = shell ] && [ "$4" = getprop ]; then
|
||||
printf '[ro.build.version.security_patch]: []\\n[ro.product.model]: [ ]\\n[ro.debuggable]: [0]\\n[ro.kernel.qemu]: [0]\\n[sys.unlisted]: []\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = -s ] && [ "$3" = shell ] && [ "$4" = dumpsys ]; then
|
||||
printf ' Package [com.example] (abcdef):\\n pkg=Package{abcdef com.example}\\n versionName= My Build \\n versionCode=0 minSdk=23 targetSdk=34\\n'
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
`,
|
||||
);
|
||||
await chmod(binary, 0o755);
|
||||
let evidence;
|
||||
if (adapter === "cli") {
|
||||
const output = await execFileOutput(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", ...scenario.argv],
|
||||
{
|
||||
env: { ...process.env, REA_ADB_PATH: binary },
|
||||
timeout: 20_000,
|
||||
stopSignal: "SIGTERM",
|
||||
maxBuffer: 1024 * 1024,
|
||||
},
|
||||
);
|
||||
evidence = parseEvidence(JSON.parse(output.stdout));
|
||||
} else {
|
||||
const server = createServer(
|
||||
{
|
||||
kind: "session",
|
||||
session: createTestBinarySession(() => {
|
||||
throw new Error(
|
||||
"Device inspection must not open a binary provider",
|
||||
);
|
||||
}),
|
||||
},
|
||||
{
|
||||
adbDeviceAnalysis: new AdbProvider({
|
||||
environment: { REA_ADB_PATH: binary },
|
||||
}),
|
||||
},
|
||||
);
|
||||
onTestFinished(() => server.close());
|
||||
const client = new Client({ name: "adb-properties", version: "1" });
|
||||
onTestFinished(() => client.close());
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const { tools } = await client.listTools();
|
||||
const advertised = tools.find(({ name }) => name === scenario.tool);
|
||||
const outputSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(advertised?.outputSchema);
|
||||
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
||||
expect(ajv.validateSchema(outputSchema)).toBe(true);
|
||||
const validate = ajv.compile(outputSchema);
|
||||
const reply = await client.callTool({
|
||||
name: scenario.tool,
|
||||
arguments: scenario.input,
|
||||
});
|
||||
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
||||
expect(
|
||||
validate(reply.structuredContent),
|
||||
JSON.stringify(validate.errors),
|
||||
).toBe(true);
|
||||
evidence = parseEvidence(reply.structuredContent);
|
||||
}
|
||||
expect(evidence.normalized_result).toMatchObject(scenario.expected);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user