Files
rea/tests/acceptance/setup/claudeCodeStrictJsonSetup.test.ts
Kaoru0822andClaude Opus 5.5 18aa9af2b2 fix(setup): admit only strict JSON for Claude Code configuration (#4944)
Claude Code shared the generic `json` client format, which the shared
configuration reader admits through JSONC parsing with comments and
trailing commas enabled. Native Claude Code reads `.claude.json` with a
strict JSON parser (after a UTF-8 BOM), so setup reported `ready`, wrote
a registration into a file the client rejects as corrupt, and doctor
reported it `aligned`.

Give Claude Code its own `claude_code` configuration format and parse it
at the owning boundary with strict JSON.parse after removing a leading
BOM. Setup, doctor, and uninstall all consume that parser, so comment or
trailing-comma documents are now refused as malformed before any backup
or write, while plain and BOM-prefixed JSON still configure. Clients that
natively accept JSONC keep the `json` format and its behavior.

Fixes #4940

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:57:28 -07:00

129 lines
4.0 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { 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";
const peer = '"peer": { "command": "/usr/bin/true", "args": [] }';
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 = [
"setup",
"--client",
"claude_code",
"--skill=false",
"--json",
] as const;
const doctorArguments = [
"doctor",
"--client",
"claude_code",
"--skill=false",
"--json",
] as const;
// Native Claude Code rejects these documents with its strict JSON reader.
for (const [syntax, original] of [
[
"a comment",
`{\n // preserve this unrelated setting\n "mcpServers": {\n ${peer}\n }\n}\n`,
],
["a trailing comma", `{\n "mcpServers": {\n ${peer},\n }\n}\n`],
] as const)
cliTest(
`Claude Code setup, doctor, and uninstall refuse JSON with ${syntax}`,
async ({ cli }) => {
const home = await createTestTempDirectory("rea-claude-strict-json-");
const config = join(home, ".claude.json");
await writeFile(config, original);
const run = runIn(cli, home);
for (const mode of ["--dry-run", "--yes"]) {
const setup = await run([...setupArguments, mode]);
expect(setup.exitCode, setup.stdout).toBe(1);
expect(setup.json).toMatchObject({
status: "needs_human",
plannedActions: [],
appliedActions: [],
remediation: expect.stringContaining("malformed"),
});
}
const doctor = await run(doctorArguments);
expect(doctor.exitCode).toBe(1);
expect(doctor.json).toMatchObject({
healthy: false,
identity: {
registrations: [
expect.objectContaining({
client: "claude_code",
state: "invalid",
}),
],
},
});
const removed = await run(["uninstall", "--json"]);
expect(removed.exitCode).toBe(1);
expect(removed.json).toMatchObject({
status: "failed",
items: expect.arrayContaining([
expect.objectContaining({ name: "claude_code", status: "failed" }),
]),
});
expect(await readFile(config, "utf8")).toBe(original);
await expect(readFile(`${config}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
for (const prefix of ["", ""])
cliTest(
`Claude Code setup configures strict JSON with prefix ${JSON.stringify(prefix)}`,
async ({ cli }) => {
const home = await createTestTempDirectory("rea-claude-strict-json-");
const config = join(home, ".claude.json");
const original = `${prefix}{\n "mcpServers": {\n ${peer}\n }\n}\n`;
await writeFile(config, original);
const run = runIn(cli, home);
const setup = await run([...setupArguments, "--yes"]);
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.startsWith(`${prefix}{`)).toBe(true);
expect(JSON.parse(configured.slice(prefix.length))).toMatchObject({
mcpServers: {
peer: { command: "/usr/bin/true", args: [] },
rea: { command: expect.any(String) },
},
});
const doctor = await run(doctorArguments);
expect(doctor.json).toMatchObject({
identity: {
registrations: [
expect.objectContaining({
client: "claude_code",
state: "aligned",
}),
],
},
});
},
);