mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
JSON.parse, JSONC parse, and native Claude Code keep the last duplicate object member, and parseClientConfiguration therefore validates the last one. The shared setup/uninstall serializer instead edited text through jsonc-parser's modify and findNodeAtLocation, which select the first matching member, so setup and uninstall on configurations with repeated server tables or rea members wrote into a shadowed member and failed readback. Locate every JSON text edit by the effective (last) member in one place, replacing the first-member modify path. Removing an effective member that shadows an earlier duplicate would activate that duplicate, whatever its ownership, so the serializer now returns a tagged shadowed_member failure and setup and uninstall refuse before writing a backup or the configuration. Fixes #4952 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { join } 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;
|
|
|
|
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",
|
|
});
|
|
},
|
|
);
|