import { chmod, copyFile, lstat, readFile, realpath, rm, writeFile, } from "node:fs/promises"; import { constants as fsConstants } from "node:fs"; import { createHash } from "node:crypto"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { parse as parseToml } from "smol-toml"; import { configureClientConfiguration, inspectClientConfiguration, } from "../../../src/application/SetupClientConfiguration.js"; import { systemUninstallHost, type UninstallFileSystem, } from "../../../src/application/Uninstall.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { ClientConfigurationPublication } from "../../../src/application/ClientConfigurationPublication.js"; const failedVerificationFileSystem = ( failure: "verification" | "recovery", ): UninstallFileSystem => { return { readText: (path) => readFile(path, "utf8"), copy: (source, destination) => copyFile(source, destination, fsConstants.COPYFILE_EXCL), publishConfiguration: async (path, contents, original) => { const publication = await ClientConfigurationPublication.write( path, contents, original, ); publication.assertCurrent = async () => { throw new Error("verification failed"); }; if (failure === "recovery") publication.restore = async () => { throw new Error("restore failed"); }; return publication; }, stat: (path) => lstat(path), realpath: (path) => realpath(path), remove: (path) => rm(path, { recursive: true }), }; }; describe("uninstall backups after edits made since setup", () => { const editedSinceSetup = async (prefix: string) => { const root = await createTestTempDirectory(prefix); const configPath = join(root, "config.json"); const client = { name: "cursor", format: "json", configPath } as const; const beforeSetup = '{"keep":"before setup"}\n'; await writeFile(configPath, beforeSetup); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toMatchObject({ status: "configured" }); const configured = JSON.parse(await readFile(configPath, "utf8")) as Record< string, unknown >; const beforeUninstall = `${JSON.stringify({ ...configured, keep: "edited after setup" }, null, 2)}\n`; await writeFile(configPath, beforeUninstall); const recoveryPath = `${configPath}.rea.backup.${createHash("sha256") .update(beforeUninstall, "utf8") .digest("hex")}.recovery`; return { root, configPath, client, beforeSetup, beforeUninstall, recoveryPath, }; }; it("preserves the pre-uninstall bytes when removal succeeds", async () => { const { root, configPath, client, beforeSetup, beforeUninstall, recoveryPath, } = await editedSinceSetup("rea-uninstall-current-backup-"); expect((await systemUninstallHost(root).removeClient(client)).status).toBe( "removed", ); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe( beforeSetup, ); expect(await readFile(recoveryPath, "utf8")).toBe(beforeUninstall); }); it.each([ [ "verification", (path: string) => `Configuration could not be updated. The original was restored and its backup at ${path} was retained. Repair the configuration or restore that backup, then rerun uninstall.`, ], [ "recovery", (path: string) => `Configuration could not be updated or restored. Restore the backup at ${path} manually, then rerun uninstall.`, ], ] as const)( "names the backup holding the pre-uninstall bytes when %s fails", async (failure, message) => { const { root, configPath, client, beforeSetup, beforeUninstall, recoveryPath, } = await editedSinceSetup("rea-uninstall-current-backup-failure-"); expect( await systemUninstallHost( root, failedVerificationFileSystem(failure), ).removeClient(client), ).toEqual({ name: "cursor", status: "failed", detail: message(recoveryPath), }); if (failure === "verification") expect(await readFile(configPath, "utf8")).toBe(beforeUninstall); else expect(JSON.parse(await readFile(configPath, "utf8"))).toEqual({ keep: "edited after setup", mcpServers: {}, }); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe( beforeSetup, ); expect(await readFile(recoveryPath, "utf8")).toBe(beforeUninstall); }, ); it("blocks removal when the content-addressed backup holds other data", async () => { const { root, configPath, client, beforeUninstall, recoveryPath } = await editedSinceSetup("rea-uninstall-current-backup-occupied-"); await writeFile(recoveryPath, "occupied by other data"); const result = await systemUninstallHost(root).removeClient(client); expect(result.status).toBe("failed"); expect(result.detail).toContain(recoveryPath); expect(await readFile(configPath, "utf8")).toBe(beforeUninstall); expect(await readFile(recoveryPath, "utf8")).toBe("occupied by other data"); }); it.each([ ["missing", async () => undefined], [ "wrong", (_source: string, destination: string) => writeFile(destination, "not the current configuration", { flag: "wx", }), ], ] as const)( "blocks removal when the reported copy leaves %s backup bytes", async (_label, copy) => { const { root, configPath, client, beforeUninstall } = await editedSinceSetup("rea-uninstall-current-backup-unverified-"); let writes = 0; const result = await systemUninstallHost(root, { ...failedVerificationFileSystem("verification"), copy, publishConfiguration: async () => { writes += 1; throw new Error("unexpected publication before verified backup"); }, }).removeClient(client); expect(result.status).toBe("failed"); expect(result.detail).toMatch( /^Configuration could not be backed up, so no change was made\. /, ); expect(writes).toBe(0); expect(await readFile(configPath, "utf8")).toBe(beforeUninstall); }, ); }); it("accepts a literal UTF-8 replacement character without changing its value", async () => { const root = await createTestTempDirectory("rea-client-config-utf8-"); const configPath = join(root, "config.json"); const original = Buffer.concat([ Buffer.from('{"label":"'), Buffer.from("\uFFFD"), Buffer.from('"}'), ]); await writeFile(configPath, original); const result = await configureClientConfiguration( { name: "cursor", format: "json", configPath }, {}, ["rea", "mcp"], ); expect(result.status).toBe("configured"); expect(await readFile(`${configPath}.rea.backup`)).toEqual(original); expect(JSON.parse(await readFile(configPath, "utf8")).label).toBe("\uFFFD"); }); it("previews and creates a current recovery backup while preserving the first backup", async () => { const root = await createTestTempDirectory("rea-client-current-recovery-"); const configPath = join(root, "config.json"); const firstOriginal = '{"keep":"first"}\n'; const currentOriginal = '{"keep":"current"}\n'; const client = { name: "cursor", format: "json", configPath } as const; await writeFile(configPath, currentOriginal); await writeFile(`${configPath}.rea.backup`, firstOriginal); const recoveryPath = `${configPath}.rea.backup.${createHash("sha256") .update(currentOriginal, "utf8") .digest("hex")}.recovery`; expect(await inspectClientConfiguration(client, {}, ["rea", "mcp"])).toEqual({ status: "update", backupPath: recoveryPath, }); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toEqual({ status: "configured", backupPath: recoveryPath }); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe( firstOriginal, ); expect(await readFile(recoveryPath, "utf8")).toBe(currentOriginal); }); it("blocks configuration when the planned content-addressed backup is occupied", async () => { const root = await createTestTempDirectory("rea-client-recovery-conflict-"); const configPath = join(root, "config.json"); const firstOriginal = '{"keep":"first"}\n'; const currentOriginal = '{"keep":"current"}\n'; const client = { name: "cursor", format: "json", configPath } as const; await writeFile(configPath, currentOriginal); await writeFile(`${configPath}.rea.backup`, firstOriginal); const recoveryPath = `${configPath}.rea.backup.${createHash("sha256") .update(currentOriginal, "utf8") .digest("hex")}.recovery`; await writeFile(recoveryPath, "occupied by other data"); expect( await inspectClientConfiguration(client, {}, ["rea", "mcp"]), ).toMatchObject({ status: "invalid", remediation: expect.stringContaining(recoveryPath), }); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toEqual({ status: "failed", reason: "backup" }); expect(await readFile(configPath, "utf8")).toBe(currentOriginal); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe( firstOriginal, ); expect(await readFile(recoveryPath, "utf8")).toBe("occupied by other data"); }); describe("client configuration write failures", () => { it.skipIf(process.platform === "win32" || process.getuid?.() === 0).each([ ["json", configureClientConfiguration], ["toml", configureClientConfiguration], ] as const)( "classifies a denied %s write as a write failure", async (format, configure) => { const root = await createTestTempDirectory("rea-client-unwritable-"); await chmod(root, 0o500); try { expect( await configure({ name: format === "toml" ? "codex" : "cursor", format, configPath: join(root, `config.${format}`), }), ).toEqual({ status: "failed", reason: "write" }); } finally { await chmod(root, 0o700); } }, ); }); describe("TOML client configuration comparison", () => { it("replaces a registration containing a non-finite TOML float and preserves unrelated settings", async () => { const value = "-inf"; const root = await createTestTempDirectory("rea-client-toml-"); const configPath = join(root, "config.toml"); const original = `unrelated = ${value}\n[mcp_servers.rea]\ncommand = "old"\nstartup_timeout_sec = ${value}\n`; await writeFile(configPath, original); const client = { name: "codex", format: "toml", configPath } as const; expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toEqual({ status: "configured", backupPath: `${configPath}.rea.backup`, }); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); expect(await readFile(configPath, "utf8")).toContain( `unrelated = ${value}`, ); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toEqual({ status: "unchanged", }); }); }); const unrelatedToml = [ "# Keep this explanation.", 'notify = ["a", "b"]', "literal = 'C:\\demo\\path'", 'disabled_mcp_servers = ["rea", "other"] # Not a Codex registration.', "", ].join("\n"); const unrelatedServer = [ "# Keep the other server.", '[mcp_servers."rea.env"]', "command = 'other'", "", ].join("\n"); const unrelatedValues = (text: string) => { const document = parseToml(text); const servers = document.mcp_servers; if (typeof servers === "object" && servers !== null) { Reflect.deleteProperty(servers, "rea"); if (Object.keys(servers).length === 0) delete document.mcp_servers; } return document; }; describe("Codex configuration text preservation", () => { it.each([ { name: "new registration", original: unrelatedToml, preserved: [unrelatedToml], }, { name: "quoted tables and nested environment", original: `${unrelatedToml}[mcp_servers."rea"]\ncommand = "old"\n[mcp_servers.rea.env]\nOLD = "value"\n${unrelatedServer}`, preserved: [unrelatedToml, unrelatedServer], }, { name: "dotted registration keys", original: `${unrelatedToml}mcp_servers.rea.command = "old"\n${unrelatedServer}`, preserved: [unrelatedToml, unrelatedServer], }, { name: "inline server table", original: `${unrelatedToml}mcp_servers = { rea = { command = "old" }, other = { command = 'other' } } # Shared table.\n`, preserved: [ unrelatedToml, "other = { command = 'other' }", "# Shared table.", ], }, { name: "BOM, CRLF, and multiline text resembling a registration", original: `\uFEFF${unrelatedToml}banner = '''\n[mcp_servers.rea]\n# This is string content.\n'''\n`.replaceAll( "\n", "\r\n", ), preserved: [ unrelatedToml.replaceAll("\n", "\r\n"), "banner = '''\r\n[mcp_servers.rea]\r\n# This is string content.\r\n'''", ], }, { name: "comments without settings", original: "# Keep this explanation.\n# And this trailing comment.\n\n", preserved: ["# Keep this explanation.", "# And this trailing comment."], }, ])( "preserves unrelated text through $name setup and uninstall", async ({ original, preserved }) => { const root = await createTestTempDirectory("rea-codex-preserve-"); const configPath = join(root, "config.toml"); const client = { name: "codex", format: "toml", configPath } as const; await writeFile(configPath, original); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toMatchObject({ status: "configured" }); const configured = await readFile(configPath, "utf8"); for (const text of preserved) expect(configured).toContain(text); expect(unrelatedValues(configured)).toEqual(unrelatedValues(original)); expect(configured.startsWith("\uFEFF")).toBe( original.startsWith("\uFEFF"), ); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toEqual({ status: "unchanged" }); expect(await readFile(configPath, "utf8")).toBe(configured); expect( (await systemUninstallHost(root).removeClient(client)).status, ).toBe("removed"); const removed = await readFile(configPath, "utf8"); for (const text of preserved) expect(removed).toContain(text); expect(unrelatedValues(removed)).toEqual(unrelatedValues(original)); expect(parseToml(removed)).not.toHaveProperty("mcp_servers.rea"); expect(removed.startsWith("\uFEFF")).toBe(original.startsWith("\uFEFF")); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); }, ); it("retains the first backup after uninstall and a later setup", async () => { const root = await createTestTempDirectory("rea-codex-backup-"); const configPath = join(root, "config.toml"); const client = { name: "codex", format: "toml", configPath } as const; const original = 'notify = ["a", "b"]\n'; await writeFile(configPath, original); await configureClientConfiguration(client, {}, ["rea", "mcp"]); expect((await systemUninstallHost(root).removeClient(client)).status).toBe( "removed", ); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); await configureClientConfiguration(client, {}, ["rea", "mcp"]); expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); }); it("refuses malformed Codex TOML unchanged without creating a backup", async () => { const root = await createTestTempDirectory("rea-codex-malformed-"); const configPath = join(root, "config.toml"); const client = { name: "codex", format: "toml", configPath } as const; const original = "# Keep this explanation.\n[mcp_servers.rea]\ncommand = [broken\n"; await writeFile(configPath, original); expect( await configureClientConfiguration(client, {}, ["rea", "mcp"]), ).toEqual({ status: "failed", reason: "readback" }); expect((await systemUninstallHost(root).removeClient(client)).status).toBe( "failed", ); expect(await readFile(configPath, "utf8")).toBe(original); await expect( readFile(`${configPath}.rea.backup`, "utf8"), ).rejects.toMatchObject({ code: "ENOENT" }); }); }); it .skipIf(process.platform === "win32" || process.getuid?.() === 0) .each([0o400, 0o640])( "uninstall preserves configuration mode %s while retaining recovery authority", async (mode) => { const root = await createTestTempDirectory("rea-uninstall-file-mode-"); const configPath = join(root, "config.json"); await writeFile( configPath, '{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":true}\n', ); await chmod(configPath, mode); const host = systemUninstallHost(root); try { expect( ( await host.removeClient({ name: "cursor", format: "json", configPath, }) ).status, ).toBe("removed"); expect((await lstat(configPath)).mode & 0o777).toBe(mode); expect(JSON.parse(await readFile(configPath, "utf8"))).toEqual({ mcpServers: {}, keep: true, }); } finally { await host.close?.(); } }, );