Files
rea/tests/boundary/cli/cliBuiltinArguments.test.ts
T
2SinceJuly e9ea9d6f50 fix(cli): validate MCP registration before side effects (#9057)
Fixes #7375. Fixes #7608.

Share argument consumption and agent facts between admission and scope checks; reject invalid registrations before settings, backups or runner startup. Preserve missing-value priority, explicit redaction, discovery and valid registration controls.

Validated 33 focused tests (one platform skip), current-main RED and independent compiled-CLI before/after workflows; check:fast passes.

Full technical CI 42297438912 passes, including all eight shards and required aggregates. Greptile reviews this head at 5/5; no completed Codex review is available and the bot reports exhausted quota. The original author readiness declarations remain verbatim; no new human-review attestation is added.

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
2026-11-06 21:56:35 +00:00

246 lines
6.4 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
classifyMcpAddDiscovery,
isProjectScopedAmpRegistration,
} from "../../../src/cliBuiltinArguments.js";
describe("mcp add discovery classification", () => {
it("bypasses help and listing flags", () => {
for (const flag of [
"--help",
"-h",
"--llms",
"--llms-full",
"--mcp",
"--update",
"--incur-update-check",
]) {
expect(classifyMcpAddDiscovery(new Set([flag]))).toBe("bypass");
}
});
it("answers version and schema inside the mcp add special-casing", () => {
expect(classifyMcpAddDiscovery(new Set(["--version"]))).toBe("version");
expect(classifyMcpAddDiscovery(new Set(["--schema"]))).toBe("schema");
});
it("keeps routing precedence when several discovery flags combine", () => {
expect(classifyMcpAddDiscovery(new Set(["--help", "--version"]))).toBe(
"bypass",
);
expect(classifyMcpAddDiscovery(new Set(["--version", "--schema"]))).toBe(
"version",
);
});
it("returns undefined for real registrations", () => {
expect(classifyMcpAddDiscovery(new Set(["--json"]))).toBeUndefined();
expect(classifyMcpAddDiscovery(new Set())).toBeUndefined();
});
});
describe("project-scoped Amp registration guard", () => {
it("flags explicit Amp targets under --no-global", () => {
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent",
"amp",
"--command",
"node project-rea.mjs",
]),
).toBe(true);
});
it("flags the default Amp registration when no agent is given", () => {
expect(isProjectScopedAmpRegistration(["mcp", "add", "--no-global"])).toBe(
true,
);
});
it("ignores output flags Incur strips before dispatch", () => {
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent",
"amp",
"--command",
"node project-rea.mjs",
"--json",
]),
).toBe(true);
});
it("passes global Amp registration through", () => {
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--agent",
"amp",
"--command",
"node project-rea.mjs",
]),
).toBe(false);
});
it("passes project-scoped non-Amp agents through", () => {
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent",
"claude-code",
]),
).toBe(false);
});
it("passes discovery requests through, matching mcp add discovery", () => {
for (const discovery of [
"--help",
"-h",
"--llms",
"--llms-full",
"--version",
"--schema",
]) {
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent",
"amp",
discovery,
]),
).toBe(false);
}
expect(
isProjectScopedAmpRegistration(["mcp", "add", "--no-global", "--help"]),
).toBe(false);
});
it("reads equals-form agents the same way dispatch does", () => {
// --agent=claude-code is a non-Amp registration, not the default Amp.
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent=claude-code",
]),
).toBe(false);
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent=amp",
]),
).toBe(true);
// A later equals-form Amp must not slip past an earlier spaced agent.
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent",
"claude-code",
"--agent=amp",
]),
).toBe(true);
// A --command value is never read as an agent, even in equals form.
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--command=--agent",
"amp",
]),
).toBe(true);
});
it("passes other commands through", () => {
expect(isProjectScopedAmpRegistration(["mcp", "doctor"])).toBe(false);
expect(isProjectScopedAmpRegistration(["doctor"])).toBe(false);
expect(
isProjectScopedAmpRegistration([
"mcp",
"add",
"--no-global",
"--agent",
"Amp",
]),
).toBe(false);
});
});
describe("project-scoped Amp registration launcher", () => {
it("refuses explicit Amp and default-agent requests without writing settings", async () => {
const home = await createTestTempDirectory("rea-amp-launcher-home-");
const workingDirectory = await createTestTempDirectory(
"rea-amp-launcher-cwd-",
);
const ampDirectory = join(home, ".config", "amp");
await mkdir(ampDirectory, { recursive: true });
const settingsPath = join(ampDirectory, "settings.json");
const sentinel = JSON.stringify({ preexisting: true });
await writeFile(settingsPath, sentinel);
for (const args of [
["mcp", "add", "--no-global", "--agent", "amp", "--json"],
["mcp", "add", "--no-global", "--json"],
["mcp", "add", "--no-global", "--agent=amp", "--json"],
[
"mcp",
"add",
"--no-global",
"--agent",
"claude-code",
"--agent=amp",
"--json",
],
]) {
const result = spawnSync(
process.execPath,
[resolve("scripts/rea.mjs"), ...args],
{
cwd: workingDirectory,
env: {
PATH: process.env.PATH ?? "",
HOME: home,
REA_LOG_LEVEL: "silent",
},
encoding: "utf8",
timeout: 60_000,
},
);
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status).toBe(1);
expect(JSON.parse(result.stdout)).toEqual({
ok: false,
error: {
code: "AMP_PROJECT_SCOPE_UNSUPPORTED",
message: expect.any(String),
},
});
expect(result.stderr).toBe("");
expect(await readFile(settingsPath, "utf8")).toBe(sentinel);
expect(await readdir(workingDirectory)).toEqual([]);
}
});
});