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.
117 lines
4.2 KiB
TypeScript
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]);
|
|
},
|
|
);
|
|
}
|