mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 09:52:27 +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
245 lines
8.3 KiB
TypeScript
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" });
|
|
},
|
|
);
|