Files
rea/tests/process-global/setupConfigurationRollback.test.ts
T
42e1a17d6e fix(setup): preserve concurrent client configuration edits (#4855)
* fix(setup): preserve concurrent client configuration edits

* fix(setup): normalize configuration transaction queue paths

* fix(setup): report concurrent snapshot read conflicts

* test(setup): isolate Pi workflows from installed Hopper

* test: align inherited fixtures with current analysis contracts

* test(ida): assert search recovery instead of diagnostic prose

* fix(setup): share configuration queue keys across directory aliases

Resolve parent-directory aliases for regular and missing configuration
paths so a directory symlink and its real directory serialize setup
instead of racing the same file.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-23 05:06:13 +08:00

1367 lines
46 KiB
TypeScript

import { open, readFile, type FileHandle } from "node:fs/promises";
import { createHash } from "node:crypto";
import { join } from "node:path";
import { afterEach, expect, it, onTestFinished, vi } from "vitest";
const state = vi.hoisted(() => ({
readbackFailed: false,
failReadback: true,
rejectPublicationClose: false,
publicationCloseFailure: undefined as unknown,
publicationHandles: [] as FileHandle[],
beforePublicationOpen: undefined as
| ((path: string) => Promise<void>)
| undefined,
rejectPublicationMode: false,
rejectStageRemoval: false,
beforeFirstPublication: undefined as (() => Promise<void>) | undefined,
afterPublication: undefined as ((path: string) => Promise<void>) | undefined,
rejectRestore: false,
removeCopiedRecovery: false,
afterBackupCopy: undefined as
| ((source: string, destination: string) => Promise<void>)
| undefined,
initialCleanupFailure: undefined as unknown,
readbackCleanupFailure: undefined as unknown,
}));
afterEach(() => {
state.readbackFailed = false;
state.failReadback = true;
state.rejectPublicationClose = false;
state.publicationCloseFailure = undefined;
state.publicationHandles = [];
state.beforePublicationOpen = undefined;
state.rejectPublicationMode = false;
state.rejectStageRemoval = false;
state.afterPublication = undefined;
state.beforeFirstPublication = undefined;
state.rejectRestore = false;
state.removeCopiedRecovery = false;
state.afterBackupCopy = undefined;
state.initialCleanupFailure = undefined;
state.readbackCleanupFailure = undefined;
});
vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
return {
...original,
open: async (...arguments_: Parameters<typeof original.open>) => {
const writable =
typeof arguments_[1] === "number" && (arguments_[1] & 3) === 2;
if (writable && typeof arguments_[0] === "string")
await state.beforePublicationOpen?.(arguments_[0]);
const handle = await original.open(...arguments_);
if (typeof arguments_[1] === "number" && (arguments_[1] & 3) === 2) {
state.publicationHandles.push(handle);
const write = handle.write.bind(handle);
handle.write = ((...values: Parameters<typeof handle.write>) => {
if (state.rejectRestore && state.readbackFailed)
return Promise.reject(new Error("injected restore write failure"));
return write(...values);
}) as typeof handle.write;
const chmod = handle.chmod.bind(handle);
handle.chmod = async (mode) => {
if (state.rejectPublicationMode && mode === 0o400)
throw new Error("staging mode preparation denied");
await chmod(mode);
};
const close = handle.close.bind(handle);
handle.close = async () => {
if (state.rejectPublicationClose)
throw (
state.publicationCloseFailure ??
new Error("injected publication close failure")
);
await close();
};
}
return handle;
},
rmdir: async (...values: Parameters<typeof original.rmdir>) => {
if (
state.rejectStageRemoval &&
String(values[0]).includes(".rea-config-")
)
throw new Error("injected staging directory removal failure");
return original.rmdir(...values);
},
rename: async (...values: Parameters<typeof original.rename>) => {
const result = await original.rename(...values);
state.readbackFailed = true;
await state.afterPublication?.(String(values[1]));
return result;
},
link: async (...values: Parameters<typeof original.link>) => {
await state.beforeFirstPublication?.();
const result = await original.link(...values);
state.readbackFailed = true;
await state.afterPublication?.(String(values[1]));
return result;
},
copyFile: async (
...arguments_: Parameters<typeof original.copyFile>
): Promise<void> => {
await original.copyFile(...arguments_);
const source = arguments_[0];
const destination = arguments_[1];
if (typeof source === "string" && typeof destination === "string")
await state.afterBackupCopy?.(source, destination);
if (
state.removeCopiedRecovery &&
typeof destination === "string" &&
destination.endsWith(".recovery")
)
await original.rm(destination);
},
};
});
vi.mock("../../src/application/RegularFileRead.js", async (importOriginal) => {
const original =
await importOriginal<
typeof import("../../src/application/RegularFileRead.js")
>();
return {
...original,
readRegularFileText: (
...arguments_: Parameters<typeof original.readRegularFileText>
) =>
state.initialCleanupFailure !== undefined
? Promise.reject(state.initialCleanupFailure)
: state.readbackFailed && state.readbackCleanupFailure !== undefined
? Promise.reject(state.readbackCleanupFailure)
: state.readbackFailed && state.failReadback
? Promise.reject(new Error("injected readback failure"))
: original.readRegularFileText(...arguments_),
};
});
import { ClientConfigurationPublication } from "../../src/application/ClientConfigurationPublication.js";
import { configureClientConfiguration } from "../../src/application/SetupClientConfiguration.js";
import { RegularFileCleanupFailure } from "../../src/filesystem/OwnedRegularFile.js";
import { OwnedFileHandle } from "../../src/filesystem/OwnedFileHandle.js";
import type { DoctorHost } from "../../src/application/Doctor.js";
import { systemSetupHost } from "../../src/application/SetupHost.js";
import { systemUninstallHost } from "../../src/application/Uninstall.js";
import { ConfigurationError } from "../../src/domain/configurationErrors.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { createDeferred } from "../fixtures/binarySession.js";
const ownReadHandle = async (path: string) => {
const owner = new OwnedFileHandle(await open(path, "r"));
onTestFinished(() => owner.close());
return owner;
};
it("reports a failed config rollback and retains its backup path", async () => {
state.readbackFailed = false;
state.rejectRestore = false;
state.initialCleanupFailure = undefined;
state.readbackCleanupFailure = undefined;
const root = await createTestTempDirectory("rea-config-rollback-");
const path = join(root, "client.json");
const original = '{"keep":true}\n';
await import("node:fs/promises").then(({ writeFile }) =>
writeFile(path, original),
);
state.rejectRestore = true;
const result = await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
expect(result).toEqual({
status: "failed",
reason: "rollback",
backupPath: `${path}.rea.backup`,
failure: {
kind: "restore",
path,
detail: "injected restore write failure",
},
});
expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original);
expect(JSON.parse(await readFile(path, "utf8"))).toMatchObject({
mcpServers: { rea: { command: "rea", args: ["mcp"] } },
});
});
it("preserves the first backup and reports a verified backup of the current original", async () => {
state.readbackFailed = false;
state.rejectRestore = false;
state.initialCleanupFailure = undefined;
state.readbackCleanupFailure = undefined;
const root = await createTestTempDirectory("rea-config-stale-backup-");
const path = join(root, "client.json");
const firstOriginal = '{"keep":"first"}\n';
const currentOriginal = '{"keep":"current"}\n';
await import("node:fs/promises").then(async ({ writeFile }) => {
await writeFile(path, currentOriginal);
await writeFile(`${path}.rea.backup`, firstOriginal);
});
state.rejectRestore = true;
const result = await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
state.rejectRestore = false;
const recoveryPath = `${path}.rea.backup.${createHash("sha256")
.update(currentOriginal, "utf8")
.digest("hex")}.recovery`;
expect(result).toMatchObject({
status: "failed",
reason: "rollback",
backupPath: recoveryPath,
failure: {
kind: "restore",
path,
detail: "injected restore write failure",
},
});
expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(firstOriginal);
expect(await readFile(recoveryPath, "utf8")).toBe(currentOriginal);
});
it("does not write config when a recovery copy disappears before verification", async () => {
state.readbackFailed = false;
state.rejectRestore = false;
state.removeCopiedRecovery = false;
state.initialCleanupFailure = undefined;
state.readbackCleanupFailure = undefined;
const root = await createTestTempDirectory("rea-config-vanished-recovery-");
const path = join(root, "client.json");
const firstOriginal = '{"keep":"first"}\n';
const currentOriginal = '{"keep":"current"}\n';
await import("node:fs/promises").then(async ({ writeFile }) => {
await writeFile(path, currentOriginal);
await writeFile(`${path}.rea.backup`, firstOriginal);
});
state.removeCopiedRecovery = true;
const result = await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
state.removeCopiedRecovery = false;
expect(result).toEqual({ status: "failed", reason: "backup" });
expect(await readFile(path, "utf8")).toBe(currentOriginal);
expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(firstOriginal);
});
it("does not roll back or remove config when its initial read cleanup fails", async () => {
state.readbackFailed = false;
state.rejectRestore = false;
state.readbackCleanupFailure = undefined;
const root = await createTestTempDirectory("rea-config-initial-cleanup-");
const path = join(root, "client.json");
const original = '{"keep":true}\n';
await import("node:fs/promises").then(({ writeFile }) =>
writeFile(path, original),
);
const failure = new RegularFileCleanupFailure(
path,
await ownReadHandle(path),
{ kind: "completed", value: original },
new Error("injected initial close failure"),
);
state.initialCleanupFailure = failure;
await expect(
configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
),
).rejects.toBe(failure);
expect(await readFile(path, "utf8")).toBe(original);
});
it("retains first-run configuration for manual recovery without deleting by pathname", async () => {
state.readbackFailed = false;
state.rejectRestore = false;
state.initialCleanupFailure = undefined;
state.readbackCleanupFailure = undefined;
const root = await createTestTempDirectory("rea-config-first-run-rollback-");
const path = join(root, "client.json");
const result = await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
expect(result).toMatchObject({
status: "failed",
reason: "rollback",
failure: {
kind: "restore",
path,
detail:
"First-run configuration recovery requires manual removal; pathname deletion cannot prove ownership",
},
});
expect(result).not.toHaveProperty("backupPath");
expect(JSON.parse(await readFile(path, "utf8"))).toMatchObject({
mcpServers: { rea: { command: "rea", args: ["mcp"] } },
});
});
it("retains readback cleanup ownership when rollback also fails", async () => {
state.readbackFailed = false;
state.rejectRestore = false;
state.initialCleanupFailure = undefined;
const root = await createTestTempDirectory("rea-config-readback-cleanup-");
const path = join(root, "client.json");
const original = '{"keep":true}\n';
await import("node:fs/promises").then(({ writeFile }) =>
writeFile(path, original),
);
const owner = await ownReadHandle(path);
const cleanupFailure = new RegularFileCleanupFailure(
path,
owner,
{ kind: "completed", value: "" },
new Error("injected readback close failure"),
);
state.readbackCleanupFailure = cleanupFailure;
state.rejectRestore = true;
const host = systemSetupHost(
{ platform: "linux", homeDirectory: root } as DoctorHost,
{ HOME: root },
);
let caught: unknown;
try {
await host.configureClient(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
} catch (cause: unknown) {
caught = cause;
} finally {
await host.close?.();
}
if (!(caught instanceof ConfigurationError)) throw caught;
expect(caught.message).toContain("injected restore write failure");
expect(caught.message).toContain(`${path}.rea.backup`);
const cleanupOwnerError = caught.cause;
if (!(cleanupOwnerError instanceof RegularFileCleanupFailure))
throw new Error("Configuration failure lost its read owner");
expect(cleanupOwnerError).toMatchObject({
path,
outcome: { kind: "completed", value: "" },
cleanupCause: expect.any(AggregateError),
});
expect(cleanupOwnerError.owner).toBe(owner);
expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original);
});
it.each(["existing", "first-run"] as const)(
"retains a replacement symlink and its target when %s readback fails",
async (kind) => {
const root = await createTestTempDirectory("rea-config-replaced-rollback-");
const path = join(root, "client.json");
const replacement = join(root, "replacement.json");
const original = '{"keep":"original"}\n';
const foreign = '{"keep":"replacement"}\n';
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(replacement, foreign);
if (kind === "existing") await fs.writeFile(path, original);
state.afterPublication = async (publishedPath) => {
await fs.unlink(publishedPath);
await fs.symlink(replacement, publishedPath);
};
const result = await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
expect(await fs.readFile(replacement, "utf8")).toBe(foreign);
expect((await fs.lstat(path)).isSymbolicLink()).toBe(true);
expect(result).toMatchObject({ status: "failed", reason: "conflict" });
if (kind === "existing")
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
},
);
it("retains a replacement regular file when readback fails", async () => {
const root = await createTestTempDirectory("rea-config-replaced-file-");
const path = join(root, "client.json");
const replacement = join(root, "replacement.json");
const original = '{"keep":"original"}\n';
const foreign = '{"keep":"replacement"}\n';
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(path, original);
await fs.writeFile(replacement, foreign);
state.afterPublication = (publishedPath) =>
fs.rename(replacement, publishedPath);
const result = await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
);
expect(await fs.readFile(path, "utf8")).toBe(foreign);
expect(result).toMatchObject({ status: "failed", reason: "conflict" });
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
});
it("retains and retries both readback and publication descriptors after mixed close failures", async () => {
const root = await createTestTempDirectory("rea-config-two-close-owners-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(path, '{"keep":true}\n');
const readOwner = await ownReadHandle(path);
const closeRead = readOwner.handle.close.bind(readOwner.handle);
let rejectReadClose = true;
readOwner.handle.close = async () => {
if (rejectReadClose) throw new Error("injected readback close failure");
await closeRead();
};
state.readbackCleanupFailure = new RegularFileCleanupFailure(
path,
readOwner,
{ kind: "completed", value: "" },
new Error("injected readback close failure"),
);
state.rejectPublicationClose = true;
const host = systemSetupHost(
{ platform: "linux", homeDirectory: root } as DoctorHost,
{ HOME: root },
);
try {
await expect(
host.configureClient(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
),
).rejects.toBeInstanceOf(ConfigurationError);
expect(state.publicationHandles).toHaveLength(1);
expect(readOwner.handle.fd).toBeGreaterThanOrEqual(0);
expect(state.publicationHandles[0]!.fd).toBeGreaterThanOrEqual(0);
rejectReadClose = false;
state.rejectPublicationClose = false;
expect(await host.close?.()).toBeUndefined();
expect(readOwner.handle.fd).toBe(-1);
expect(state.publicationHandles[0]!.fd).toBe(-1);
} finally {
rejectReadClose = false;
state.rejectPublicationClose = false;
await host.close?.();
await readOwner.close();
for (const handle of state.publicationHandles)
if (handle.fd >= 0) await handle.close();
}
});
it("rejects a replaced publication even when the replacement passes registration readback", async () => {
const root = await createTestTempDirectory("rea-config-valid-replacement-");
const path = join(root, "client.json");
const replacement = join(root, "replacement.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original = '{"keep":true}\n';
await fs.writeFile(path, original);
let foreign = "";
state.failReadback = false;
state.afterPublication = async (publishedPath) => {
foreign = await fs.readFile(publishedPath, "utf8");
await fs.writeFile(replacement, foreign);
await fs.rename(replacement, publishedPath);
};
expect(
await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
),
).toMatchObject({ status: "failed", reason: "conflict" });
expect(await fs.readFile(path, "utf8")).toBe(foreign);
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
});
it("retains concurrent edits to the published inode rather than overwriting them", async () => {
const root = await createTestTempDirectory("rea-config-inode-edit-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original = '{"keep":true}\n';
const edited = '{"keep":"concurrent edit"}\n';
await fs.writeFile(path, original);
state.afterPublication = (publishedPath) =>
fs.writeFile(publishedPath, edited);
expect(
await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
),
).toMatchObject({ status: "failed", reason: "conflict" });
expect(await fs.readFile(path, "utf8")).toBe(edited);
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
});
it("does not adopt a descriptor named by an opaque publication close rejection", async () => {
const root = await createTestTempDirectory(
"rea-config-close-cause-ownership-",
);
const path = join(root, "client.json");
const unrelated = join(root, "unrelated.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(path, '{"keep":true}\n');
await fs.writeFile(unrelated, "unrelated");
const foreignOwner = await ownReadHandle(unrelated);
state.failReadback = false;
state.rejectPublicationClose = true;
state.publicationCloseFailure = new RegularFileCleanupFailure(
unrelated,
foreignOwner,
{ kind: "completed", value: "unrelated" },
new Error("opaque close rejection"),
);
const host = systemSetupHost(
{ platform: "linux", homeDirectory: root } as DoctorHost,
{ HOME: root },
);
try {
await expect(
host.configureClient(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
),
).rejects.toBeInstanceOf(ConfigurationError);
state.rejectPublicationClose = false;
expect(await host.close?.()).toBeUndefined();
expect(state.publicationHandles[0]!.fd).toBe(-1);
expect(foreignOwner.handle.fd).toBeGreaterThanOrEqual(0);
} finally {
state.rejectPublicationClose = false;
await host.close?.();
await foreignOwner.close();
for (const handle of state.publicationHandles)
if (handle.fd >= 0) await handle.close();
}
});
it.each(["symlink", "regular", "valid-readback"] as const)(
"uninstall retains a %s replacement instead of restoring through its name",
async (kind) => {
const root = await createTestTempDirectory(
"rea-uninstall-replaced-rollback-",
);
const path = join(root, "client.json");
const replacement = join(root, "replacement.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":"original"}\n';
const foreign = '{"keep":"replacement"}\n';
await fs.writeFile(path, original);
await fs.writeFile(replacement, foreign);
state.failReadback = kind !== "valid-readback";
state.afterPublication = async (publishedPath) => {
state.afterPublication = undefined;
if (kind === "symlink") {
await fs.unlink(publishedPath);
await fs.symlink(replacement, publishedPath);
} else await fs.rename(replacement, publishedPath);
};
const host = systemUninstallHost(root);
try {
const result = await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
});
expect(await fs.readFile(path, "utf8")).toBe(foreign);
if (kind === "symlink") {
expect((await fs.lstat(path)).isSymbolicLink()).toBe(true);
expect(await fs.readFile(replacement, "utf8")).toBe(foreign);
}
expect(result.status).toBe("failed");
expect(result.detail).not.toContain("The original was restored");
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
} finally {
await host.close?.();
}
},
);
it("uninstall retains both readback and publication owners until cleanup retry", async () => {
const root = await createTestTempDirectory("rea-uninstall-two-close-owners-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(
path,
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n',
);
const readOwner = await ownReadHandle(path);
const closeRead = readOwner.handle.close.bind(readOwner.handle);
let rejectReadClose = true;
readOwner.handle.close = async () => {
if (rejectReadClose) throw new Error("injected readback close failure");
await closeRead();
};
state.readbackCleanupFailure = new RegularFileCleanupFailure(
path,
readOwner,
{ kind: "completed", value: "" },
new Error("readback close failed"),
);
state.rejectPublicationClose = true;
const host = systemUninstallHost(root);
try {
const result = await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
});
expect(result.status).toBe("failed");
expect(state.publicationHandles).toHaveLength(1);
expect(readOwner.handle.fd).toBeGreaterThanOrEqual(0);
expect(state.publicationHandles[0]!.fd).toBeGreaterThanOrEqual(0);
rejectReadClose = false;
state.rejectPublicationClose = false;
expect(await host.close?.()).toBeUndefined();
expect(readOwner.handle.fd).toBe(-1);
expect(state.publicationHandles[0]!.fd).toBe(-1);
} finally {
rejectReadClose = false;
state.rejectPublicationClose = false;
await host.close?.();
await readOwner.close();
for (const handle of state.publicationHandles)
if (handle.fd >= 0) await handle.close();
}
});
it("uninstall retains the exact owner of a failed initial config read", async () => {
const root = await createTestTempDirectory(
"rea-uninstall-initial-read-owner-",
);
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original = '{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n';
await fs.writeFile(path, original);
const owner = await ownReadHandle(path);
const close = owner.handle.close.bind(owner.handle);
let rejectClose = true;
owner.handle.close = async () => {
if (rejectClose) throw new Error("initial close denied");
await close();
};
state.initialCleanupFailure = new RegularFileCleanupFailure(
path,
owner,
{ kind: "completed", value: original },
new Error("initial close denied"),
);
const host = systemUninstallHost(root);
try {
expect(
await host.inspectClient({
name: "cursor",
configPath: path,
format: "json",
}),
).toMatchObject({ status: "failed" });
expect(state.publicationHandles).toHaveLength(0);
expect(owner.handle.fd).toBeGreaterThanOrEqual(0);
rejectClose = false;
expect(await host.close?.()).toBeUndefined();
expect(owner.handle.fd).toBe(-1);
expect(await fs.readFile(path, "utf8")).toBe(original);
await expect(fs.readFile(`${path}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
rejectClose = false;
await host.close?.();
await owner.close();
}
});
it("uninstall close joins a publication already admitted by its host", async () => {
const root = await createTestTempDirectory(
"rea-uninstall-close-publication-",
);
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(
path,
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n',
);
const arrived = createDeferred<void>();
const release = createDeferred<void>();
state.afterPublication = async () => {
arrived.resolve();
await release.promise;
};
const host = systemUninstallHost(root);
const removal = host.removeClient({
name: "cursor",
configPath: path,
format: "json",
});
await arrived.promise;
let closed = false;
const closing = host.close?.().then((result) => {
closed = true;
return result;
});
try {
await new Promise<void>((resolve) => setImmediate(resolve));
expect(closed).toBe(false);
expect(state.publicationHandles[0]!.fd).toBeGreaterThanOrEqual(0);
} finally {
release.resolve();
}
expect((await removal).status).toBe("failed");
expect(await closing).toBeUndefined();
expect(state.publicationHandles[0]!.fd).toBe(-1);
await expect(
host.removeClient({ name: "cursor", configPath: path, format: "json" }),
).rejects.toThrow("closed");
});
it("cleanup retry never adopts an opaque cancellation reason as another owner", async () => {
const root = await createTestTempDirectory(
"rea-cleanup-cancellation-authority-",
);
const path = join(root, "owned.json");
const other = join(root, "other.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(path, "owned");
await fs.writeFile(other, "other");
const owned = await ownReadHandle(path);
const foreign = await ownReadHandle(other);
const opaque = new RegularFileCleanupFailure(
other,
foreign,
{ kind: "completed", value: "other" },
new Error("opaque reason"),
);
const { RegularFileOperationCancelled } =
await import("../../src/filesystem/RegularFile.js");
const { ArtifactResourceScope } =
await import("../../src/artifacts/ArtifactResourceScope.js");
const { retryRegularFileCleanupFailures } =
await import("../../src/application/RegularFileRead.js");
const resources = new ArtifactResourceScope();
const outcome = await retryRegularFileCleanupFailures(
new RegularFileCleanupFailure(
path,
owned,
{ kind: "failed", cause: new RegularFileOperationCancelled(opaque) },
new Error("owned close failed"),
),
resources,
);
expect(outcome.primary).toBe(opaque);
expect(outcome.failures).toHaveLength(1);
expect(owned.handle.fd).toBe(-1);
await resources.close();
expect(foreign.handle.fd).toBeGreaterThanOrEqual(0);
});
it("uninstall progresses when a peer leaves a denied-close descriptor pending", async () => {
const root = await createTestTempDirectory("rea-uninstall-peer-cleanup-");
const path = join(root, "client.json");
const other = join(root, "other.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(
path,
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n',
);
await fs.writeFile(other, "{}");
const owner = await ownReadHandle(other);
const close = owner.handle.close.bind(owner.handle);
let rejectClose = true;
owner.handle.close = async () => {
if (rejectClose) throw new Error("peer close denied");
await close();
};
const arrived = createDeferred<void>();
const release = createDeferred<void>();
state.failReadback = false;
state.afterPublication = async () => {
arrived.resolve();
await release.promise;
};
const host = systemUninstallHost(root);
const removal = host.removeClient({
name: "cursor",
configPath: path,
format: "json",
});
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
await arrived.promise;
state.initialCleanupFailure = new RegularFileCleanupFailure(
other,
owner,
{ kind: "completed", value: "{}" },
new Error("peer close denied"),
);
expect(
await host.inspectClient({
name: "other",
configPath: other,
format: "json",
}),
).toMatchObject({ status: "failed" });
state.initialCleanupFailure = undefined;
release.resolve();
const result = await Promise.race([
removal,
new Promise<never>((_resolve, reject) => {
timeout = setTimeout(
() =>
reject(
new Error("configuration admission deadlocked with file cleanup"),
),
5000,
);
}),
]);
expect(result.status).toBe("failed");
expect(owner.handle.fd).toBeGreaterThanOrEqual(0);
rejectClose = false;
expect(await host.close?.()).toBeUndefined();
expect(owner.handle.fd).toBe(-1);
expect(state.publicationHandles[0]!.fd).toBe(-1);
} finally {
if (timeout !== undefined) clearTimeout(timeout);
state.initialCleanupFailure = undefined;
rejectClose = false;
release.resolve();
await owner.close();
for (const handle of state.publicationHandles)
if (handle.fd >= 0) await handle.close();
}
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"exclusive staging refuses a preexisting entry without changing its permissions",
async () => {
const root = await createTestTempDirectory(
"rea-uninstall-staging-exclusive-",
);
const path = join(root, "client.json");
const foreign = join(root, "foreign.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n';
await fs.writeFile(path, original);
await fs.chmod(path, 0o400);
await fs.writeFile(foreign, "foreign");
await fs.chmod(foreign, 0o640);
let inserted: string | undefined;
state.beforePublicationOpen = async (stagedPath) => {
state.beforePublicationOpen = undefined;
inserted = stagedPath;
await fs.link(foreign, stagedPath);
};
const host = systemUninstallHost(root);
try {
expect(
(
await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
})
).status,
).toBe("failed");
expect(await fs.readFile(foreign, "utf8")).toBe("foreign");
expect((await fs.lstat(foreign)).mode & 0o777).toBe(0o640);
expect(await fs.readFile(path, "utf8")).toBe(original);
expect((await fs.lstat(path)).mode & 0o777).toBe(0o400);
expect(state.publicationHandles).toHaveLength(0);
expect(await host.close?.()).toContain("cleanup failed");
} finally {
if (inserted !== undefined) await fs.unlink(inserted);
await host.close?.();
}
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"uninstall retains the private stage after mode preparation and descriptor cleanup fail",
async () => {
const root = await createTestTempDirectory(
"rea-uninstall-staging-mode-retry-",
);
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n';
await fs.writeFile(path, original);
await fs.chmod(path, 0o400);
state.rejectPublicationMode = true;
state.rejectPublicationClose = true;
const host = systemUninstallHost(root);
try {
expect(
(
await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
})
).status,
).toBe("failed");
expect(state.publicationHandles).toHaveLength(1);
expect(state.publicationHandles[0]!.fd).toBeGreaterThanOrEqual(0);
expect(await host.close?.()).toContain("cleanup failed");
state.rejectPublicationMode = false;
state.rejectPublicationClose = false;
expect(await host.close?.()).toBeUndefined();
expect(state.publicationHandles[0]!.fd).toBe(-1);
expect(await fs.readFile(path, "utf8")).toBe(original);
expect((await fs.lstat(path)).mode & 0o777).toBe(0o400);
} finally {
state.rejectPublicationMode = false;
state.rejectPublicationClose = false;
await host.close?.();
for (const handle of state.publicationHandles)
if (handle.fd >= 0) await handle.close();
}
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"uninstall restores a read-only configuration through its retained writable descriptor",
async () => {
const root = await createTestTempDirectory(
"rea-uninstall-readonly-restore-",
);
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":true}\n';
await fs.writeFile(path, original);
await fs.chmod(path, 0o400);
const host = systemUninstallHost(root);
try {
const result = await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
});
expect(result.status).toBe("failed");
expect(result.detail).toContain("The original was restored");
expect(await fs.readFile(path, "utf8")).toBe(original);
expect((await fs.lstat(path)).mode & 0o777).toBe(0o400);
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
expect(state.publicationHandles[0]!.fd).toBe(-1);
} finally {
await host.close?.();
}
},
);
it.each(["setup", "uninstall"] as const)(
"%s refuses a configuration symlink inserted after backup without overwriting its target",
async (operation) => {
const root = await createTestTempDirectory(
"rea-config-prepublication-symlink-",
);
const path = join(root, "client.json");
const foreign = join(root, "foreign.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
operation === "setup"
? '{"keep":"original"}\n'
: '{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":"original"}\n';
const unrelated = '{"keep":"foreign"}\n';
await fs.writeFile(path, original);
await fs.writeFile(foreign, unrelated);
state.failReadback = false;
state.afterBackupCopy = async (source) => {
state.afterBackupCopy = undefined;
await fs.unlink(source);
await fs.symlink(foreign, source);
};
if (operation === "setup") {
expect(
(
await configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["rea", "mcp"],
)
).status,
).toBe("failed");
} else {
const host = systemUninstallHost(root);
try {
expect(
(
await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
})
).status,
).toBe("failed");
} finally {
await host.close?.();
}
}
expect(await fs.readFile(foreign, "utf8")).toBe(unrelated);
expect((await fs.lstat(path)).isSymbolicLink()).toBe(true);
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
},
);
const runConfigurationChange = async (
operation: "setup" | "uninstall",
root: string,
path: string,
) => {
if (operation === "setup")
return configureClientConfiguration(
{ name: "cursor", configPath: path, format: "json" },
{},
["new-rea", "mcp"],
);
const host = systemUninstallHost(root);
try {
return await host.removeClient({
name: "cursor",
configPath: path,
format: "json",
});
} finally {
await host.close?.();
}
};
it.each([
["setup", "edit"],
["uninstall", "edit"],
["setup", "delete"],
["uninstall", "delete"],
] as const)(
"%s preserves a configuration %s after backup",
async (operation, change) => {
const root = await createTestTempDirectory("rea-config-backup-change-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":true}\n';
const edited = '{"changed":"by client"}\n';
await fs.writeFile(path, original);
state.failReadback = false;
state.afterBackupCopy = async (source) => {
state.afterBackupCopy = undefined;
if (change === "edit") await fs.writeFile(source, edited);
else await fs.unlink(source);
};
expect((await runConfigurationChange(operation, root, path)).status).toBe(
"failed",
);
if (change === "edit") expect(await fs.readFile(path, "utf8")).toBe(edited);
else await expect(fs.lstat(path)).rejects.toMatchObject({ code: "ENOENT" });
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
expect(
(await fs.readdir(root)).filter((name) =>
name.startsWith(".rea-config-"),
),
).toEqual([]);
},
);
it.each(["setup", "uninstall"] as const)(
"%s refuses an input changed during staging",
async (operation) => {
const root = await createTestTempDirectory("rea-config-staging-change-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":true}\n';
const edited = '{"changed":"during staging"}\n';
await fs.writeFile(path, original);
state.failReadback = false;
state.beforePublicationOpen = async () => {
state.beforePublicationOpen = undefined;
await fs.writeFile(path, edited);
};
expect((await runConfigurationChange(operation, root, path)).status).toBe(
"failed",
);
expect(await fs.readFile(path, "utf8")).toBe(edited);
expect(await fs.readFile(`${path}.rea.backup`, "utf8")).toBe(original);
expect(
(await fs.readdir(root)).filter((name) =>
name.startsWith(".rea-config-"),
),
).toEqual([]);
},
);
it("first-run setup preserves a destination created during staging", async () => {
const root = await createTestTempDirectory("rea-config-first-conflict-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const edited = '{"created":"by client"}\n';
state.failReadback = false;
state.beforePublicationOpen = async () => {
state.beforePublicationOpen = undefined;
await fs.writeFile(path, edited);
};
expect((await runConfigurationChange("setup", root, path)).status).toBe(
"failed",
);
expect(await fs.readFile(path, "utf8")).toBe(edited);
expect(
(await fs.readdir(root)).filter((name) => name.startsWith(".rea-config-")),
).toEqual([]);
});
it.each(["setup", "uninstall"] as const)(
"%s retains staging directory cleanup for host retry",
async (operation) => {
const root = await createTestTempDirectory(
"rea-config-staging-directory-retry-",
);
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
await fs.writeFile(
path,
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":true}\n',
);
state.failReadback = false;
state.rejectStageRemoval = true;
const host =
operation === "setup"
? systemSetupHost(
{ platform: "linux", homeDirectory: root } as DoctorHost,
{ HOME: root },
)
: systemUninstallHost(root);
try {
const client = {
name: "cursor",
configPath: path,
format: "json" as const,
};
if (operation === "setup") {
await expect(
(host as ReturnType<typeof systemSetupHost>).configureClient(
client,
{},
["new-rea", "mcp"],
),
).rejects.toThrow();
} else {
expect(
(
await (host as ReturnType<typeof systemUninstallHost>).removeClient(
client,
)
).status,
).toBe("failed");
}
expect(await host.close?.()).toContain("cleanup");
expect(
(await fs.readdir(root)).filter((name) =>
name.startsWith(".rea-config-"),
),
).toHaveLength(1);
state.rejectStageRemoval = false;
expect(await host.close?.()).toBeUndefined();
expect(
(await fs.readdir(root)).filter((name) =>
name.startsWith(".rea-config-"),
),
).toEqual([]);
expect(state.publicationHandles[0]!.fd).toBe(-1);
} finally {
state.rejectStageRemoval = false;
await host.close?.();
}
},
);
it("first-run publication exclusively refuses a destination created after its final check", async () => {
const root = await createTestTempDirectory("rea-config-exclusive-link-");
const path = join(root, "client.json");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const edited = '{"created":"after final check"}\n';
state.beforeFirstPublication = async () => {
await fs.writeFile(path, edited);
};
await expect(
ClientConfigurationPublication.write(path, "{}", undefined),
).rejects.toMatchObject({ code: "EEXIST" });
expect(await fs.readFile(path, "utf8")).toBe(edited);
expect(
(await fs.readdir(root)).filter((name) => name.startsWith(".rea-config-")),
).toEqual([]);
});
it("publication keeps its selected canonical parent when a directory symlink is retargeted", async () => {
const root = await createTestTempDirectory("rea-config-parent-selection-");
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const selected = join(root, "selected");
const foreign = join(root, "foreign");
const alias = join(root, "alias");
await fs.mkdir(selected);
await fs.mkdir(foreign);
await fs.symlink(selected, alias, "dir");
const original = '{"original":true}\n';
const unrelated = '{"foreign":true}\n';
await fs.writeFile(join(selected, "client.json"), original);
await fs.writeFile(join(foreign, "client.json"), unrelated);
state.beforePublicationOpen = async () => {
state.beforePublicationOpen = undefined;
await fs.unlink(alias);
await fs.symlink(foreign, alias, "dir");
};
const publication = await ClientConfigurationPublication.write(
join(alias, "client.json"),
"{}",
original,
);
await publication.verify(() => publication.assertCurrent());
expect(await fs.readFile(join(selected, "client.json"), "utf8")).toBe("{}");
expect(await fs.readFile(join(foreign, "client.json"), "utf8")).toBe(
unrelated,
);
expect(
(await fs.readdir(selected)).filter((name) =>
name.startsWith(".rea-config-"),
),
).toEqual([]);
});
it.each(["setup", "uninstall"] as const)(
"%s reads back the selected canonical parent after its alias is retargeted",
async (operation) => {
const root = await createTestTempDirectory(
"rea-config-adapter-parent-selection-",
);
const fs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
const selected = join(root, "selected");
const foreign = join(root, "foreign");
const alias = join(root, "alias");
await fs.mkdir(selected);
await fs.mkdir(foreign);
await fs.symlink(selected, alias, "dir");
const original =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}},"keep":true}\n';
const unrelated = '{"foreign":true}\n';
await fs.writeFile(join(selected, "client.json"), original);
await fs.writeFile(join(foreign, "client.json"), unrelated);
state.failReadback = false;
state.beforePublicationOpen = async () => {
state.beforePublicationOpen = undefined;
await fs.unlink(alias);
await fs.symlink(foreign, alias, "dir");
};
expect(
(
await runConfigurationChange(
operation,
root,
join(alias, "client.json"),
)
).status,
).toBe(operation === "setup" ? "configured" : "removed");
expect(
JSON.parse(await fs.readFile(join(selected, "client.json"), "utf8")),
).toMatchObject({ keep: true });
expect(await fs.readFile(join(foreign, "client.json"), "utf8")).toBe(
unrelated,
);
expect(
await fs.readFile(join(selected, "client.json.rea.backup"), "utf8"),
).toBe(original);
},
);