Files
rea/tests/acceptance/setup/duplicateJsonMemberSetup.test.ts
22ddd82014 fix(uninstall): preflight repeated registration ownership
Read shadowed registrations from the effective JSONC syntax tree before
any client is changed. Share the same typed ownership decision with the
reread before removal, including foreign OpenCode V1 fallbacks. Remove
all admitted duplicates using one member/separator planner and grant
serializer authority only for exact admitted paths.

Setup may remove its explicitly superseded OpenCode V1 path. Preserve
inactive repeated parents, unrelated entries, comments and source backups.
Use the pinned JSONC scanner instead of handwritten string decoding.

Validation: final 86 focused cases and 9 setup acceptance cases passed;
104 additional filesystem consumer cases passed. Static/architecture,
docs and discovery checks passed. A broader 186-case run had one OMP
timeout; its isolated 9-case rerun passed, with timeout cause unverified.
Controlled compiled CLI comparisons verify removal/refusal and backups;
real developer-client execution remains unverified.

Adapted from #4977 at 4cd22d4ec2.
Closes #4976

Co-authored-by: Nikhil Bhagat <knewboy.nykhil@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 06:32:14 +08:00

286 lines
11 KiB
TypeScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
// JSON.parse, JSONC parse, and native Claude Code read the last duplicate.
const shadowParent = '"mcpServers": {"shadow": {"command": "/usr/bin/false"}},';
const shadowLeaf = '"rea": {"command": "/usr/bin/false"},';
const owned =
'"rea": {"command": "npx", "args": ["-y", "rea-agents@6.6.0", "mcp"]}';
const runIn = (cli: TestCli, home: string) => (arguments_: readonly string[]) =>
cli.run({
arguments: arguments_,
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
CLAUDE_CONFIG_DIR: home,
XDG_CONFIG_HOME: join(home, ".config"),
},
});
const setupArguments = (client: string) =>
["setup", "--client", client, "--skill=false", "--json", "--yes"] as const;
const openCodeOwned =
'{"type":"local","command":["npx","-y","rea-agents@6.6.0","mcp"]}';
const openCodeForeign = '{"type":"local","command":["/usr/bin/false"]}';
const writeConfiguration = async (path: string, text: string) => {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, text);
};
for (const [layout, original] of [
[
"a repeated server table",
`{\n ${shadowParent}\n "mcpServers": {"peer": {"command": "/usr/bin/true"}}\n}\n`,
],
[
"a repeated rea member",
`{\n "mcpServers": {${shadowLeaf} "rea": {"command": "/usr/bin/true"}}\n}\n`,
],
] as const)
cliTest(
`Claude Code setup edits the effective member of ${layout}`,
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-duplicate-");
const config = join(home, ".claude.json");
await writeFile(config, original);
const run = runIn(cli, home);
const setup = await run(setupArguments("claude_code"));
expect(setup.exitCode, setup.stdout).toBe(0);
expect(setup.json).toMatchObject({
status: "ready",
clients: { claude_code: { status: "configured" } },
});
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
const configured = await readFile(config, "utf8");
expect(configured).toContain(
layout === "a repeated server table" ? shadowParent : shadowLeaf,
);
expect(JSON.parse(configured)).toMatchObject({
mcpServers: { rea: { command: expect.not.stringMatching(/^\/usr/) } },
});
const repeated = await run(setupArguments("claude_code"));
expect(repeated.exitCode, repeated.stdout).toBe(0);
expect(repeated.json).toMatchObject({
status: "ready",
plannedActions: [],
});
expect(await readFile(config, "utf8")).toBe(configured);
},
);
cliTest(
"JSONC setup keeps comments and the shadowed server table",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-jsonc-duplicate-");
await mkdir(join(home, ".cursor"));
const config = join(home, ".cursor", "mcp.json");
const head = `{\n // shadowed table\n ${shadowParent}\n /* effective table */\n`;
await writeFile(
config,
`${head} "mcpServers": {\n "peer": {"command": "/usr/bin/true"} // peer\n }\n}\n`,
);
const setup = await runIn(cli, home)(setupArguments("cursor"));
expect(setup.exitCode, setup.stdout).toBe(0);
expect(setup.json).toMatchObject({
clients: { cursor: { status: "configured" } },
});
const configured = await readFile(config, "utf8");
expect(configured.startsWith(head)).toBe(true);
expect(configured).toContain("// peer");
expect(configured.indexOf('"rea"')).toBeGreaterThan(head.length);
},
);
cliTest(
"uninstall removes an owned registration from the effective server table",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-duplicate-");
const config = join(home, ".claude.json");
const original = `{\n ${shadowParent}\n "mcpServers": {"peer": {"command": "/usr/bin/true"}, ${owned}}\n}\n`;
await writeFile(config, original);
const removed = await runIn(cli, home)(["uninstall", "--json"]);
expect(removed.exitCode, removed.stdout).toBe(0);
expect(removed.json).toMatchObject({
items: expect.arrayContaining([
expect.objectContaining({ name: "claude_code", status: "removed" }),
]),
});
const remaining = await readFile(config, "utf8");
expect(remaining.startsWith(`{\n ${shadowParent}\n`)).toBe(true);
expect(JSON.parse(remaining)).toEqual({
mcpServers: { peer: { command: "/usr/bin/true" } },
});
},
);
cliTest(
"uninstall refuses to activate a shadowed duplicate registration",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-duplicate-");
const config = join(home, ".claude.json");
const original = `{\n "mcpServers": {${shadowLeaf} ${owned}}\n}\n`;
await writeFile(config, original);
const removed = await runIn(cli, home)(["uninstall", "--json"]);
expect(removed.exitCode, removed.stdout).toBe(1);
expect(removed.json).toMatchObject({
status: "failed",
items: expect.arrayContaining([
expect.objectContaining({
name: "claude_code",
status: "failed",
detail: expect.stringContaining("activate an earlier duplicate"),
}),
]),
});
expect(await readFile(config, "utf8")).toBe(original);
await expect(readFile(`${config}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
cliTest(
"uninstall removes repeated owned entries and preserves inactive parents, peers, and its original backup",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-owned-repeats-");
const config = join(home, ".claude.json");
const inactive = '{"inactivePeer":{"command":"/usr/bin/false"}}';
const active = `{"rea":{"command":"npx","args":["-y","rea-agents@6.6.0","mcp"]},"peer":{"command":"/usr/bin/true"},"rea":{"command":"npx","args":["-y","rea-agents@6.5.0","mcp"]}}`;
const original = `{"mcpServers":${inactive},"mcpServers":${active}}\n`;
await writeFile(config, original);
const run = runIn(cli, home);
const removed = await run(["uninstall", "--json"]);
expect(removed.exitCode, removed.stdout).toBe(0);
expect(removed.json).toMatchObject({
status: "complete",
items: expect.arrayContaining([
expect.objectContaining({ name: "claude_code", status: "removed" }),
]),
});
const afterRemoval = await readFile(config, "utf8");
expect(JSON.parse(afterRemoval)).toEqual({
mcpServers: { peer: { command: "/usr/bin/true" } },
});
expect(afterRemoval).toContain(`"mcpServers":${inactive}`);
expect(afterRemoval).toContain('"peer":{"command":"/usr/bin/true"}');
expect(afterRemoval).not.toContain("rea-agents@");
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
const repeated = await run(["uninstall", "--json"]);
expect(repeated.exitCode, repeated.stdout).toBe(0);
expect(await readFile(config, "utf8")).toBe(afterRemoval);
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
},
);
cliTest(
"preflights a later Cursor null duplicate before changing Claude or creating backups",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-repeat-preflight-");
const claude = join(home, ".claude.json");
const claudeOriginal = `{"mcpServers":{${owned}}}\n`;
await writeFile(claude, claudeOriginal);
const cursor = join(home, ".cursor", "mcp.json");
const cursorOriginal = `{"mcpServers":{"rea":null,${owned}}}\n`;
await writeConfiguration(cursor, cursorOriginal);
const removed = await runIn(cli, home)(["uninstall", "--json"]);
expect(removed.exitCode, removed.stdout).toBe(1);
expect(removed.json).toMatchObject({
status: "failed",
items: expect.arrayContaining([
expect.objectContaining({
name: "cursor",
status: "failed",
detail: expect.stringContaining("earlier duplicate"),
}),
expect.objectContaining({ name: "uninstall", status: "skipped" }),
]),
});
expect(await readFile(claude, "utf8")).toBe(claudeOriginal);
expect(await readFile(cursor, "utf8")).toBe(cursorOriginal);
for (const path of [claude, cursor])
await expect(readFile(`${path}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
cliTest(
"preflights a foreign OpenCode V1 fallback before changing Claude or creating backups",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-opencode-fallback-");
const claude = join(home, ".claude.json");
const claudeOriginal = `{"mcpServers":{${owned}}}\n`;
await writeFile(claude, claudeOriginal);
const openCode = join(home, ".config", "opencode", "opencode.json");
const openCodeOriginal = `{"mcp":{"servers":{"rea":${openCodeOwned}},"rea":${openCodeForeign}}}\n`;
await writeConfiguration(openCode, openCodeOriginal);
const removed = await runIn(cli, home)(["uninstall", "--json"]);
expect(removed.exitCode, removed.stdout).toBe(1);
expect(removed.json).toMatchObject({
status: "failed",
items: expect.arrayContaining([
expect.objectContaining({
name: "opencode",
status: "failed",
detail: expect.stringContaining("would activate it"),
}),
expect.objectContaining({ name: "uninstall", status: "skipped" }),
]),
});
expect(await readFile(claude, "utf8")).toBe(claudeOriginal);
expect(await readFile(openCode, "utf8")).toBe(openCodeOriginal);
for (const path of [claude, openCode])
await expect(readFile(`${path}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
cliTest(
"OpenCode setup replaces every repeated V1 entry while preserving the V2 peer",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-json-opencode-migration-");
const config = join(home, ".config", "opencode", "opencode.json");
const original = `{"mcp":{"servers":{"peer":{"type":"local","command":["/usr/bin/true"]}},"rea":${openCodeForeign},"rea":${openCodeForeign}}}\n`;
await writeConfiguration(config, original);
const setup = await runIn(cli, home)(setupArguments("opencode"));
expect(setup.exitCode, setup.stdout).toBe(0);
expect(setup.json).toMatchObject({
clients: { opencode: { status: "configured" } },
});
const { mcp } = JSON.parse(await readFile(config, "utf8")) as {
mcp: Record<string, unknown> & { servers: Record<string, unknown> };
};
expect(Object.hasOwn(mcp, "rea")).toBe(false);
const launcher = resolve("scripts/rea.mjs");
expect(mcp.servers).toMatchObject({
peer: { command: ["/usr/bin/true"] },
rea: {
type: "local",
command:
process.platform === "win32"
? [process.execPath, launcher, "mcp"]
: [launcher, "mcp"],
},
});
},
);