Files
reverse-engineering-anything/tests/boundary/cli/mcpAddOptionValidation.test.ts
T
morluto 69630c0362 fix(cli)!: own MCP client registration boundaries (#10102)
* 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
2026-11-11 09:05:21 -05:00

245 lines
8.3 KiB
TypeScript

import {
access,
mkdir,
readFile,
readdir,
stat,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { registrationEnvironment } from "../../support/cli/registrationEnvironment.js";
cliTest("mcp add rejects valued options without a value", async ({ cli }) => {
const root = await createTestTempDirectory("rea-mcp-add-value-");
const home = join(root, "home");
const settingsPath = (selectedHome: string) =>
join(selectedHome, ".config", "amp", "settings.json");
const run = async (arguments_: readonly string[], selectedHome = home) =>
cli.run({
arguments: [...arguments_, "--json"],
cwd: root,
environment: await registrationEnvironment(selectedHome),
timeoutMs: 20_000,
});
for (const [flag, field] of [
["--agent", "agent"],
["--command", "command"],
["-c", "command"],
["-c=", "command"],
["--agent=", "agent"],
] as const) {
const result = await run(["mcp", "add", flag]);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: expect.arrayContaining([
expect.objectContaining({
code: "invalid_value",
missing: false,
path: field,
}),
]),
});
await expect(access(settingsPath(home))).rejects.toMatchObject({
code: "ENOENT",
});
}
// Incur would swallow the following flag as the option value; REA rejects it.
const swallowed = await run(["mcp", "add", "--command", "--agent", "amp"]);
expect(swallowed.exitCode, swallowed.stderr).toBe(1);
expect(swallowed.json).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: expect.arrayContaining([
expect.objectContaining({
code: "invalid_value",
missing: false,
path: "command",
}),
]),
});
await expect(access(settingsPath(home))).rejects.toMatchObject({
code: "ENOENT",
});
const existingHome = join(root, "existing-home");
const existingSettingsPath = settingsPath(existingHome);
await mkdir(join(existingHome, ".config", "amp"), { recursive: true });
const existingSettings = '{"existing":true}\n';
await writeFile(existingSettingsPath, existingSettings);
const rejectedWithExistingSettings = await run(
["mcp", "add", "--agent"],
existingHome,
);
expect(
rejectedWithExistingSettings.exitCode,
rejectedWithExistingSettings.stderr,
).toBe(1);
expect(rejectedWithExistingSettings.json).toMatchObject({
code: "VALIDATION_ERROR",
});
expect(await readFile(existingSettingsPath, "utf8")).toBe(existingSettings);
// A registration with an explicit agent still writes the Amp registration.
const registered = await run(["mcp", "add", "--agent", "amp"]);
expect(registered.exitCode, registered.stderr).toBe(0);
const settings = JSON.parse(await readFile(settingsPath(home), "utf8"));
expect(settings["amp.mcpServers"].rea).toMatchObject({
command: expect.any(String),
});
});
cliTest(
"mcp add rejects an argument that is neither an option nor a value",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-mcp-add-argument-");
const home = join(root, "home");
const settingsPath = join(home, ".config", "amp", "settings.json");
const environment = await registrationEnvironment(home);
const run = (arguments_: readonly string[]) =>
cli.run({
arguments: [...arguments_, "--json"],
cwd: root,
environment,
timeoutMs: 20_000,
});
// Incur skips a stray word, so the registration used to succeed for a request
// REA cannot honor and wrote the settings file anyway.
for (const arguments_ of [
["mcp", "add", "extra", "--agent", "amp"],
["mcp", "add", "--agent", "amp", "extra"],
["mcp", "add", "--agent=amp", "extra"],
["mcp", "add", "--bogus", "--agent", "amp"],
] as const) {
const result = await run(arguments_);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
code: "VALIDATION_ERROR",
message: expect.stringMatching(/Unexpected argument: (extra|--bogus)/),
fieldErrors: expect.arrayContaining([
expect.objectContaining({
code: "invalid_value",
missing: false,
path: expect.stringMatching(/arguments|command/),
}),
]),
});
await expect(access(settingsPath)).rejects.toMatchObject({
code: "ENOENT",
});
}
const original =
'{"amp.mcpServers":{"rea":{"command":"old","env":{"KEEP":"1"}}},"theme":"dark"}\n';
await mkdir(join(home, ".config", "amp"), { recursive: true });
await writeFile(settingsPath, original);
const before = await stat(settingsPath);
const rejected = await run(["mcp", "add", "extra", "--agent=amp"]);
expect(rejected.exitCode, rejected.stderr).toBe(1);
expect(await readFile(settingsPath, "utf8")).toBe(original);
expect((await stat(settingsPath)).mtimeMs).toBe(before.mtimeMs);
expect(await readdir(join(home, ".config", "amp"))).toEqual([
"settings.json",
]);
// Verify the actual stored command, rather than an already-existing file.
for (const options of [
["--agent", "amp", "--command", "node --version"],
["--agent=amp", "--command=node --version"],
["--agent=amp", "-c=node --version"],
]) {
const registered = await run(["mcp", "add", ...options]);
expect(registered.exitCode, registered.stderr).toBe(0);
expect(JSON.parse(await readFile(settingsPath, "utf8"))).toEqual({
"amp.mcpServers": {
rea: { command: "node", args: ["--version"], env: { KEEP: "1" } },
},
theme: "dark",
});
}
expect(await readFile(`${settingsPath}.rea.backup`, "utf8")).toBe(original);
},
);
cliTest(
"mcp add names the option the reported error is about",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-mcp-add-field-");
const home = join(root, "home");
const environment = await registrationEnvironment(home);
const run = (arguments_: readonly string[]) =>
cli.run({
arguments: [...arguments_, "--json"],
cwd: root,
environment,
timeoutMs: 20_000,
});
// A stray word alongside a missing value: the message describes the missing
// flag, so `path` must name that option rather than the stray argument.
for (const [arguments_, field] of [
[["mcp", "add", "extra", "--command"], "command"],
[["mcp", "add", "extra", "-c"], "command"],
[["mcp", "add", "extra", "--agent"], "agent"],
] as const) {
const result = await run(arguments_);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: [
expect.objectContaining({ code: "invalid_value", path: field }),
],
});
expect(result.json).toMatchObject({
message: expect.stringContaining("Missing value for flag"),
});
}
},
);
cliTest(
"packaged --no-global refuses a global-only member before writing any selected client",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-mcp-project-scope-");
const project = join(home, "project");
await mkdir(project);
const environment = await registrationEnvironment(home);
const result = await cli.run({
arguments: [
"mcp",
"add",
"--agent",
"cursor",
"--agent",
"goose",
"--no-global",
"--json",
],
cwd: project,
environment,
timeoutMs: 20_000,
});
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
code: "VALIDATION_ERROR",
message: expect.stringContaining("project-scoped"),
});
const goose =
process.platform === "win32"
? join(home, "appdata", "Block", "goose", "config", "config.yaml")
: join(home, ".config", "goose", "config.yaml");
for (const path of [
join(home, ".cursor", "mcp.json"),
join(project, ".cursor", "mcp.json"),
goose,
])
await expect(access(path)).rejects.toMatchObject({ code: "ENOENT" });
},
);