mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
* fix(cli)!: own MCP client registration boundaries * fix(cli): bind registration to native connection facts * test(cli): isolate registration failure and fixture controls * test(cli): distinguish native syntax and launcher alignment * fix(cli): preserve registration selection and cancellation * fix(cli): honor Goose and Zed native settings paths * fix(cli): honor registration output controls * fix(cli): reject disabled Junie global locations
285 lines
9.4 KiB
TypeScript
285 lines
9.4 KiB
TypeScript
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { Parser, z } from "incur";
|
|
import { decode } from "@toon-format/toon";
|
|
|
|
import { mcpRegistrationReportSchema } from "../../../src/contracts/mcpRegistration.js";
|
|
import { splitIncurBuiltinArguments } from "../../../src/cliBuiltinArguments.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import { registrationEnvironment } from "../../support/cli/registrationEnvironment.js";
|
|
|
|
const fixture = async (initial: string) => {
|
|
const home = await createTestTempDirectory("rea-default-amp-");
|
|
const directory = join(home, ".config", "amp");
|
|
const settings = join(directory, "settings.json");
|
|
await mkdir(directory, { recursive: true });
|
|
await writeFile(settings, initial);
|
|
return {
|
|
home,
|
|
directory,
|
|
settings,
|
|
environment: await registrationEnvironment(home),
|
|
};
|
|
};
|
|
const original = `${JSON.stringify({
|
|
"amp.mcpServers": {
|
|
rea: { command: "old", env: { KEEP_ME: "1" } },
|
|
peer: { command: "peer" },
|
|
},
|
|
theme: "dark",
|
|
})}\n`;
|
|
const report = (stdout: string, json: boolean) =>
|
|
mcpRegistrationReportSchema.parse(json ? JSON.parse(stdout) : decode(stdout));
|
|
|
|
cliTest(
|
|
"packaged default registration preserves Amp settings and repeats without another backup",
|
|
async ({ cli }) => {
|
|
for (const json of [false, true]) {
|
|
const f = await fixture(original);
|
|
const run = () =>
|
|
cli.run({
|
|
arguments: [
|
|
"mcp",
|
|
"add",
|
|
"--command",
|
|
"fixture-server --mcp",
|
|
...(json ? ["--json"] : []),
|
|
],
|
|
cwd: f.home,
|
|
environment: f.environment,
|
|
timeoutMs: 20_000,
|
|
});
|
|
const first = await run();
|
|
expect(first.exitCode, first.stderr).toBe(0);
|
|
const outcome = report(first.stdout, json).clients.find(
|
|
(client) => client.client === "amp",
|
|
);
|
|
expect(outcome?.status).toBe("configured");
|
|
if (outcome?.status !== "configured" || outcome.backup_path === undefined)
|
|
throw Error("Missing Amp backup receipt.");
|
|
expect(await readFile(outcome.backup_path, "utf8")).toBe(original);
|
|
if (process.platform !== "win32")
|
|
expect((await stat(outcome.backup_path)).mode & 0o777).toBe(0o600);
|
|
expect(JSON.parse(await readFile(f.settings, "utf8"))).toEqual({
|
|
"amp.mcpServers": {
|
|
rea: {
|
|
command: "fixture-server",
|
|
args: ["--mcp"],
|
|
env: { KEEP_ME: "1" },
|
|
},
|
|
peer: { command: "peer" },
|
|
},
|
|
theme: "dark",
|
|
});
|
|
const before = await stat(f.settings);
|
|
const repeated = await run();
|
|
expect(repeated.exitCode, repeated.stderr).toBe(0);
|
|
expect(
|
|
report(repeated.stdout, json).clients.find(
|
|
(client) => client.client === "amp",
|
|
),
|
|
).toMatchObject({ status: "unchanged" });
|
|
expect((await stat(f.settings)).ino).toBe(before.ino);
|
|
expect(repeated.stderr).not.toContain("backup retained");
|
|
expect(await readdir(f.directory)).toEqual([
|
|
"settings.json",
|
|
"settings.json.rea.backup",
|
|
]);
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"packaged default registration reports an Amp failure without claiming its old entry changed",
|
|
async ({ cli }) => {
|
|
for (const initial of [
|
|
"broken JSON\n",
|
|
"null\n",
|
|
'{"amp.mcpServers":[]}\n',
|
|
original,
|
|
]) {
|
|
const f = await fixture(initial);
|
|
if (initial === original) await mkdir(`${f.settings}.rea.backup`);
|
|
for (const json of [false, true]) {
|
|
const result = await cli.run({
|
|
arguments: ["mcp", "add", ...(json ? ["--json"] : [])],
|
|
cwd: f.home,
|
|
environment: f.environment,
|
|
timeoutMs: 20_000,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
const document = report(result.stdout, json);
|
|
expect(document.ok).toBe(false);
|
|
expect(
|
|
document.clients.find((client) => client.client === "amp"),
|
|
).toMatchObject({
|
|
status: "failed",
|
|
error: { code: expect.any(String), message: expect.any(String) },
|
|
});
|
|
expect(await readFile(f.settings, "utf8")).toBe(initial);
|
|
expect(await readdir(f.directory)).toEqual(
|
|
initial === original
|
|
? ["settings.json", "settings.json.rea.backup"]
|
|
: ["settings.json"],
|
|
);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"unselected malformed Amp settings do not block named clients or discovery",
|
|
async ({ cli }) => {
|
|
const f = await fixture("broken JSON\n");
|
|
for (const arguments_ of [
|
|
["mcp", "add", "--agent=cursor", "--agent", "claude-code", "--json"],
|
|
["mcp", "add", "--llms"],
|
|
["mcp", "add", "--llms-full"],
|
|
]) {
|
|
const result = await cli.run({
|
|
arguments: arguments_,
|
|
cwd: f.home,
|
|
environment: f.environment,
|
|
timeoutMs: 20_000,
|
|
});
|
|
expect(result.exitCode, result.stdout).toBe(0);
|
|
expect(await readFile(f.settings, "utf8")).toBe("broken JSON\n");
|
|
expect(await readdir(f.directory)).toEqual(["settings.json"]);
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"timing globals retain registration and owned-document failure semantics",
|
|
async ({ cli }) => {
|
|
for (const arguments_ of [
|
|
["--no-timings", "mcp", "add"],
|
|
["--timings=false", "mcp", "add", "--json"],
|
|
["mcp", "--timings=true", "add"],
|
|
["mcp", "add", "--agent", "--no-timings", "amp", "--json"],
|
|
["mcp", "add", "--agent", "--timings=false", "amp"],
|
|
]) {
|
|
const f = await fixture(original);
|
|
const run = () =>
|
|
cli.run({
|
|
arguments: arguments_,
|
|
cwd: f.home,
|
|
environment: f.environment,
|
|
timeoutMs: 20_000,
|
|
});
|
|
const registered = await run();
|
|
expect(registered.exitCode, registered.stdout).toBe(0);
|
|
expect(
|
|
JSON.parse(await readFile(f.settings, "utf8"))["amp.mcpServers"].rea
|
|
.env,
|
|
).toEqual({ KEEP_ME: "1" });
|
|
expect(await readFile(`${f.settings}.rea.backup`, "utf8")).toBe(original);
|
|
await writeFile(f.settings, "broken JSON\n");
|
|
const rejected = await run();
|
|
expect(rejected.exitCode).toBe(1);
|
|
expect(
|
|
report(rejected.stdout, arguments_.includes("--json")).clients.find(
|
|
(client) => client.client === "amp",
|
|
),
|
|
).toMatchObject({ status: "failed" });
|
|
expect(await readFile(f.settings, "utf8")).toBe("broken JSON\n");
|
|
expect(await readFile(`${f.settings}.rea.backup`, "utf8")).toBe(original);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("timing splitting matches Incur after output values and before the terminator", () => {
|
|
for (const flag of [
|
|
"--timings",
|
|
"--no-timings",
|
|
"--timings=false",
|
|
"--timings=true",
|
|
"--timings=other",
|
|
]) {
|
|
for (const command of [
|
|
[flag, "mcp", "add"],
|
|
["mcp", "add", "--agent", flag, "amp"],
|
|
["mcp", "add", "--command", flag],
|
|
["mcp", "add", "--", flag],
|
|
]) {
|
|
const expected = Parser.parseGlobals(
|
|
command,
|
|
z.object({ timings: z.boolean().default(false) }),
|
|
undefined,
|
|
{ validate: false },
|
|
).rest;
|
|
expect(splitIncurBuiltinArguments(command).command).toEqual(expected);
|
|
expect(
|
|
splitIncurBuiltinArguments(["--filter-output", flag, ...command])
|
|
.command,
|
|
).toEqual(expected);
|
|
}
|
|
}
|
|
});
|
|
|
|
cliTest(
|
|
"equals-form flag-shaped command values remain launcher data",
|
|
async ({ cli }) => {
|
|
const f = await fixture(original);
|
|
for (const command of ["--help", "--llms"]) {
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"mcp",
|
|
"add",
|
|
"--agent",
|
|
"amp",
|
|
`--command=${command}`,
|
|
"--json",
|
|
],
|
|
cwd: f.home,
|
|
environment: f.environment,
|
|
timeoutMs: 20_000,
|
|
});
|
|
expect(result.exitCode, result.stdout).toBe(0);
|
|
expect(report(result.stdout, true)).toMatchObject({
|
|
command,
|
|
args: [],
|
|
ok: true,
|
|
});
|
|
expect(
|
|
JSON.parse(await readFile(f.settings, "utf8"))["amp.mcpServers"].rea,
|
|
).toMatchObject({ command, args: [], env: { KEEP_ME: "1" } });
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"invalid registration preserves settings and explicit sensitive-value handling",
|
|
async ({ cli }) => {
|
|
const f = await fixture(original);
|
|
const before = await stat(f.settings);
|
|
for (const options of [
|
|
["--bogus"],
|
|
["--agents", "claude-code"],
|
|
["--unknown=declared-literal", "--sensitive-value", "declared-literal"],
|
|
["--unknown=API_SECRET_ordinary-evidence"],
|
|
]) {
|
|
const result = await cli.run({
|
|
arguments: ["mcp", "add", ...options, "--json"],
|
|
cwd: f.home,
|
|
environment: f.environment,
|
|
timeoutMs: 20_000,
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "VALIDATION_ERROR",
|
|
fieldErrors: [expect.objectContaining({ path: "arguments" })],
|
|
});
|
|
if (options.includes("--sensitive-value")) {
|
|
expect(result.stdout + result.stderr).not.toContain("declared-literal");
|
|
expect(result.stdout).toContain("[REDACTED]");
|
|
} else expect(result.stdout).toContain(options[0]);
|
|
expect(await readFile(f.settings, "utf8")).toBe(original);
|
|
expect((await stat(f.settings)).mtimeMs).toBe(before.mtimeMs);
|
|
expect(await readdir(f.directory)).toEqual(["settings.json"]);
|
|
}
|
|
},
|
|
);
|