mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
283 lines
11 KiB
TypeScript
283 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.execPath, launcher, "mcp"],
|
|
},
|
|
});
|
|
},
|
|
);
|