Files
rea/tests/boundary/cli/adbUninstallScope.test.ts
morluto be0b2d138c fix(adb): scope package uninstall requests to user zero
Send shell pm uninstall --user 0 through the shared quoted command boundary. Align tool guidance and Evidence limitations with the selected request scope while preserving device refusal diagnostics.

Validation: two provider controls failed before the fix and pass afterward; three public CLI/MCP scenarios capture exact arguments and equivalent success facts; nineteen command/SDK contract checks, check:fast and docs:check pass. No real device, multi-user data preservation, vendor/APEX behavior or performance measurement. Refs #4673.
2026-10-17 13:30:29 +08:00

117 lines
4.2 KiB
TypeScript

import { readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { expect, onTestFinished } from "vitest";
import { AdbProvider } from "../../../src/adb/AdbProvider.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
for (const { verdict, exit, succeeds } of [
{ verdict: "Success", exit: 0, succeeds: true },
{ verdict: "Failure [not installed for 0]", exit: 0, succeeds: false },
{ verdict: "Failure [not installed for 0]", exit: 1, succeeds: false },
]) {
cliTest.skipIf(process.platform === "win32")(
`selects user 0 through CLI and MCP for ${verdict}, exit ${String(exit)}`,
async ({ cli }) => {
const root = await createTestTempDirectory("rea-adb-uninstall-scope-");
const binary = join(root, "fake-adb.mjs");
await writeFile(
binary,
`#!/usr/bin/env node
import { appendFileSync } from 'node:fs';
const args = process.argv.slice(2);
appendFileSync(process.argv[1] + '.calls', JSON.stringify(args) + '\\n');
if (args.length === 1 && args[0] === 'version') {
console.log('Android Debug Bridge version 1.0.41\\nVersion 34.0.5-fixture');
} else {
console.log(${JSON.stringify(verdict)});
process.exitCode = ${String(exit)};
}
`,
{ mode: 0o700 },
);
const environment = {
REA_ADB_PATH: binary,
HOME: root,
TMPDIR: root,
PATH: `${dirname(process.execPath)}:/usr/bin:/bin`,
};
const input = { serial: "emulator-5554", package: "com.example.app" };
const output = await cli.run({
arguments: [
"uninstall-adb-package",
input.serial,
input.package,
"--json",
],
environment,
});
expect(output.exitCode).toBe(succeeds ? 0 : 1);
expect(output.stderr).toBe("");
const harness = await createApplicationMcpHarness({
serverOptions: { adbDeviceAnalysis: new AdbProvider({ environment }) },
});
onTestFinished(() => harness.close());
const schemas = await advertisedMcpToolSchemas(
harness.client,
"uninstall_adb_package",
);
const response = await harness.client.callTool({
name: schemas.name,
arguments: input,
});
if (succeeds) {
const evidence = parseEvidence(output.json);
expect(evidence.normalized_result).toMatchObject({
serial: input.serial,
package_name: input.package,
});
expect(evidence.limitations).toContain(
"Uninstall explicitly requests user 0 without data retention (-k); device enforcement is not independently verified",
);
expect(response.isError).not.toBe(true);
expect(
schemas.ajv.validate(
schemas.outputSchema,
response.structuredContent,
),
).toBe(true);
const mcpEvidence = parseEvidence(response.structuredContent);
expect(mcpEvidence.parameters).toEqual(evidence.parameters);
expect(mcpEvidence.normalized_result).toEqual(
evidence.normalized_result,
);
expect(mcpEvidence.limitations).toEqual(evidence.limitations);
} else {
expect(output.json).toMatchObject({
code: "unsupported_target",
details: { reason: "not installed for 0" },
});
expect(parseMcpToolError(response).error).toMatchObject({
code: "unsupported_target",
details: { reason: "not installed for 0" },
});
}
const calls = (await readFile(binary + ".calls", "utf8"))
.trim()
.split("\n")
.map((line) => JSON.parse(line));
const scoped = [
"-s",
input.serial,
"shell",
"pm",
"uninstall",
"--user",
"0",
input.package,
];
expect(calls).toEqual([["version"], scoped, ["version"], scoped]);
},
);
}