Files
reverse-engineering-anything/tests/boundary/filesystem/setupClientConfigCleanupOwnership.test.ts
T
morluto 60149282fc test: strengthen integration oracles and verification (#8295)
* test: strengthen integration oracles and verification

* test: bound lifecycle gates and strengthen workflow checks

* test: verify complete paged and extracted results

* test: preserve bytecode and cancellation evidence

* test: strengthen retained evidence and fixture cleanup

* test: require cross-projection bridge identities

* test: retain fixture owners through cleanup failures

* test: verify graph examples and project selectors

* test: tighten provider and lifecycle regressions

* test: consolidate hex coverage and bound cutter cleanup

* test: bound subprocess output waits and strengthen graph checks

* test: check mcp recovery and preserve cleanup owners

* test: verify structured mcp argument failures

* test: preserve artifact cleanup and gdb limit evidence

* test: preserve evidence identities and bound provider gates

* test: retain browser and comparison cleanup owners

* test: bound startup and strengthen fixture oracles

* test: bound lifecycle gates and verify recovered evidence

* test: verify public outcomes and lifecycle ownership

* test: assert precise evidence and release lifecycle gates

* test: verify selected facts and bound cleanup waits

* test: check retained evidence and decoded records

* test: verify final reports and retain cleanup ownership

* test: bound reviewed startup gates and retain cleanup

* test: verify PE errors and preserve MCP cleanup

* test: preserve MCP fixture cleanup ownership

* test: release provider gates before late startup
2026-11-01 00:15:28 +08:00

1023 lines
36 KiB
TypeScript

import {
lstat as fsLstat,
mkdir,
open as fsOpen,
readFile,
realpath as fsRealpath,
rename,
writeFile,
type FileHandle,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
readClientRegistrationInspections,
readClientRegistrationStatuses,
} from "../../../src/application/ClientRegistrationStatus.js";
import {
existingMaintenanceScope,
planIntegrationMaintenance,
} from "../../../src/application/UpdateMaintenance.js";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import { ok } from "../../../src/domain/result.js";
import type { DoctorHost } from "../../../src/application/Doctor.js";
import { ConfigurationError } from "../../../src/domain/configurationErrors.js";
import {
filterClientsNeedingConfigure,
systemSetupHost,
} from "../../../src/application/SetupHost.js";
import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js";
import { configureDetectedClients } from "../../../src/application/SetupClients.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
import {
systemUninstallHost,
runUninstall,
} from "../../../src/application/Uninstall.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
open: vi.fn(actual.open),
lstat: vi.fn(actual.lstat),
realpath: vi.fn(actual.realpath),
};
});
const openMock = vi.mocked(fsOpen);
const lstatMock = vi.mocked(fsLstat);
const realpathMock = vi.mocked(fsRealpath);
let actualFs: typeof import("node:fs/promises");
beforeEach(async () => {
actualFs =
await vi.importActual<typeof import("node:fs/promises")>(
"node:fs/promises",
);
openMock.mockImplementation((...args) => actualFs.open(...args));
lstatMock.mockImplementation((...args) => actualFs.lstat(...args));
realpathMock.mockImplementation((...args) => actualFs.realpath(...args));
});
afterEach(() => {
openMock.mockReset();
lstatMock.mockReset();
realpathMock.mockReset();
});
const holdReadHandle = (
path: string,
reason: string,
initiallyHeld = true,
maxFailures = Number.POSITIVE_INFINITY,
) => {
const closeAttempts: FileHandle[] = [];
let released = false;
let held = initiallyHeld;
let remainingFailures = maxFailures;
openMock.mockImplementation(async (...args) => {
const handle = await actualFs.open(...args);
if (String(args[0]) !== path) return handle;
const nativeClose = handle.close.bind(handle);
vi.spyOn(handle, "close").mockImplementation(async () => {
closeAttempts.push(handle);
if (held && !released && remainingFailures > 0) {
remainingFailures -= 1;
throw new Error(reason);
}
await nativeClose();
});
return handle;
});
return {
closeAttempts,
hold: () => {
held = true;
},
release: () => {
released = true;
},
};
};
/** Linux setup host with its VS Code client configuration directory prepared. */
const preparedVsCodeSetupHost = async (prefix: string) => {
const home = await createTestTempDirectory(prefix);
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home, XDG_CONFIG_HOME: join(home, "xdg") },
);
const client = (await host.supportedClients()).find(
({ name }) => name === "vscode",
);
if (client === undefined) throw new Error("VS Code setup client missing");
await mkdir(dirname(client.configPath), { recursive: true });
return { home, host, client };
};
/** Release the held read handle and verify host cleanup retries that same owner. */
const expectCloseReclaimsHandle = async (
host: { close?: () => Promise<unknown> },
closeAttempts: readonly object[],
release: () => void,
) => {
const attemptsBeforeClose = closeAttempts.length;
release();
expect(await host.close?.()).toBeUndefined();
expect(closeAttempts).toHaveLength(attemptsBeforeClose + 1);
expect(closeAttempts.at(-1)).toBe(closeAttempts[0]);
};
/** Give up on the assertion path: release the handle and let cleanup close it. */
const cleanupHeldHandle = async (
host: { close?: () => Promise<unknown> },
release: () => void,
) => {
release();
await host.close?.();
};
it("retains invalid UTF-8 diagnostics and the uninstall read owner when close fails", async () => {
const home = await createTestTempDirectory("rea-uninstall-utf8-close-");
const path = join(home, "client.json");
const original = Buffer.from([0xff]);
await writeFile(path, original);
const { closeAttempts, release } = holdReadHandle(
path,
"held uninstall handle",
);
const host = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
expect(
await host.removeClient({
name: "cursor",
format: "json",
configPath: path,
}),
).toMatchObject({
status: "failed",
detail: expect.stringContaining("not valid UTF-8"),
});
expect(Buffer.compare(await readFile(path), original)).toBe(0);
expect(closeAttempts).toHaveLength(3);
expect(closeAttempts.every((handle) => handle === closeAttempts[0])).toBe(
true,
);
await expectCloseReclaimsHandle(host, closeAttempts, release);
} finally {
await cleanupHeldHandle(host, release);
}
});
it("stops uninstall and retains the backup read owner when its close fails", async () => {
const home = await createTestTempDirectory("rea-uninstall-backup-close-");
const path = join(home, "client.json");
const client = { name: "cursor", format: "json", configPath: path } as const;
await writeFile(path, '{"keep":"before setup"}\n');
expect(
await configureClientConfiguration(client, {}, ["rea", "mcp"]),
).toMatchObject({ status: "configured" });
const configured = await readFile(path, "utf8");
const { closeAttempts, release } = holdReadHandle(
`${path}.rea.backup`,
"held backup handle",
);
const host = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
expect(await host.removeClient(client)).toEqual({
name: "cursor",
status: "failed",
detail:
"Configuration could not be backed up, so no change was made. Check file permissions, then rerun uninstall.",
});
expect(await readFile(path, "utf8")).toBe(configured);
expect(closeAttempts).toHaveLength(2);
await expectCloseReclaimsHandle(host, closeAttempts, release);
} finally {
await cleanupHeldHandle(host, release);
}
});
it("keeps shared-ownership proof when the ownership read completes but its close fails", async () => {
// FreeBuff without a profile marker leaves its shared storage applicability-
// unverified. The completed ownership read is the proof that later read
// failures are blocking; discarding it softens every failure to retained,
// so purge runs while the active shared registration remains.
const home = await createTestTempDirectory("rea-freebuff-ownership-close-");
const setupHost = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home },
);
const client = (await setupHost.supportedClients()).find(
({ name }) => name === "freebuff",
);
if (client === undefined) throw new Error("freebuff client missing");
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(
client.configPath,
JSON.stringify({ mcpServers: { other: { command: "other" } } }),
);
expect(
await configureClientConfiguration(client, {}, ["rea", "mcp"]),
).toMatchObject({ status: "configured" });
const state = join(home, ".rea", "state.json");
await mkdir(dirname(state), { recursive: true });
await writeFile(state, "{}\n");
const { release } = holdReadHandle(
client.configPath,
"held ownership read handle",
);
const host = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
const result = await runUninstall(true, host);
expect(result.status).toBe("failed");
expect(result.items).toContainEqual(
expect.objectContaining({ name: "freebuff", status: "failed" }),
);
expect(result.items).toContainEqual(
expect.objectContaining({
name: "uninstall",
status: "skipped",
detail: expect.stringContaining(
"No REA registration, skill, or data was removed",
),
}),
);
// Data must not be purged while the shared registration is still active.
await expect(readFile(state, "utf8")).resolves.toBe("{}\n");
} finally {
await cleanupHeldHandle(host, release);
}
});
it("keeps shared-ownership proof when removal refuses a shadowed duplicate", async () => {
// The effective row is REA-owned while an earlier duplicate is foreign, so
// removal refuses the edit. That refusal must not erase ownership: without
// the positive row fact, recurring close failures soften to retained and
// purge runs while the owned row remains active.
const home = await createTestTempDirectory("rea-freebuff-shadowed-close-");
const setupHost = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home },
);
const client = (await setupHost.supportedClients()).find(
({ name }) => name === "freebuff",
);
if (client === undefined) throw new Error("freebuff client missing");
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(
client.configPath,
'{"mcpServers":{"rea":{"command":"foreign-server","args":[]},"rea":{"command":"rea","args":["mcp"]}}}',
);
const state = join(home, ".rea", "state.json");
await mkdir(dirname(state), { recursive: true });
await writeFile(state, "{}\n");
const { release } = holdReadHandle(
client.configPath,
"held ownership read handle",
);
const host = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
const result = await runUninstall(true, host);
expect(result.status).toBe("failed");
expect(result.items).toContainEqual(
expect.objectContaining({ name: "freebuff", status: "failed" }),
);
expect(result.items).toContainEqual(
expect.objectContaining({
name: "uninstall",
status: "skipped",
detail: expect.stringContaining(
"No REA registration, skill, or data was removed",
),
}),
);
await expect(readFile(state, "utf8")).resolves.toBe("{}\n");
} finally {
await cleanupHeldHandle(host, release);
}
});
it("stops removal when a completed reread leaves its handle unreleased", async () => {
// Greptile P1: preflight and reread both complete with a foreign-only row,
// but the reread handle cannot be closed even by the cleanup retry. REA
// still owns that open handle, so removal must stop before purge: serving
// the completed text would let removeAll delete managed data while the
// incomplete cleanup is only reported later, by close().
const home = await createTestTempDirectory("rea-freebuff-reread-leak-");
const setupHost = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home },
);
const client = (await setupHost.supportedClients()).find(
({ name }) => name === "freebuff",
);
if (client === undefined) throw new Error("freebuff client missing");
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(
client.configPath,
'{"mcpServers":{"rea":{"command":"foreign-server","args":[]}}}',
);
const state = join(home, ".rea", "state.json");
await mkdir(dirname(state), { recursive: true });
await writeFile(state, "{}\n");
const { hold, release } = holdReadHandle(
client.configPath,
"held removal reread handle",
false,
);
const base = systemUninstallHost(home, undefined, { HOME: home }, "linux");
const host = {
...base,
inspectSkill: async () => {
hold();
return base.inspectSkill?.();
},
};
try {
const result = await runUninstall(true, host);
expect(result.status).toBe("failed");
expect(result.items).toContainEqual(
expect.objectContaining({
name: "freebuff",
status: "failed",
detail: expect.stringContaining("its resources could not be released"),
}),
);
expect(result.items).toContainEqual(
expect.objectContaining({
name: "uninstall",
status: "skipped",
detail: expect.stringContaining("Uninstall stopped at this failure"),
}),
);
// Managed data must survive an unreleased read handle.
await expect(readFile(state, "utf8")).resolves.toBe("{}\n");
} finally {
await cleanupHeldHandle(host, release);
}
});
it("keeps the shared registration fail-closed when ZCode's probe is unresolved", async () => {
// A denied profile probe leaves ZCode's applicability unresolved, never
// absent: the shared file stays fail-closed so its row is inspected and
// removed instead of leaving an installed ZCode wired to purged data.
// Definitive absence still leaves the file to FreeBuff, ZCode's owner.
const home = await createTestTempDirectory("rea-zcode-fallback-unresolved-");
const fallback = join(home, ".agents", "mcp.json");
const profile = join(home, ".zcode", "cli");
lstatMock.mockImplementation((path) =>
String(path) === profile
? Promise.reject(
Object.assign(new Error("EACCES: permission denied, lstat"), {
code: "EACCES",
}),
)
: actualFs.lstat(path),
);
const unresolved = systemUninstallHost(
home,
undefined,
{ HOME: home },
"linux",
);
try {
const rows = (await unresolved.clients()).filter(
(candidate) => candidate.configPath === fallback,
);
// One physical file, one owner: FreeBuff edits the shared file once and
// ZCode reports that same outcome instead of racing it to "absent".
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ name: "freebuff", sharedWith: ["zcode"] });
// Fail-closed: the unresolved reader carries no unverified softening, so a
// failed read blocks removal instead of retaining around the purge.
expect(rows[0]?.applicability).toBeUndefined();
} finally {
await unresolved.close?.();
}
lstatMock.mockImplementation((...args) => actualFs.lstat(...args));
const absent = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
const rows = (await absent.clients()).filter(
(candidate) => candidate.configPath === fallback,
);
expect(rows).toHaveLength(1);
// Definitive absence keeps ZCode out of another product's primary storage.
expect(rows[0]?.sharedWith).toBeUndefined();
} finally {
await absent.close?.();
}
});
it("removes a shared registration once and reports that outcome to both clients", async () => {
// ZCode and FreeBuff both load ~/.agents/mcp.json. Queuing it per client
// removed the row as ZCode and then reported FreeBuff as "absent", losing
// the outcome for the client that owns the file, and it read and edited one
// shared resource twice.
const home = await createTestTempDirectory("rea-shared-removal-once-");
const shared = join(home, ".agents", "mcp.json");
await mkdir(join(home, ".zcode", "cli"), { recursive: true });
await mkdir(join(home, ".config", "freebuff-desktop"), { recursive: true });
expect(
await configureClientConfiguration(
{ name: "freebuff", format: "json", configPath: shared },
{},
["rea", "mcp"],
),
).toMatchObject({ status: "configured" });
const host = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
const result = await runUninstall(false, host);
expect(result.status).toBe("complete");
expect(result.items).toContainEqual(
expect.objectContaining({
name: "freebuff",
status: "removed",
configuration_path: shared,
}),
);
// The fallback reader reports the owner's removal, never "absent".
const zcode = result.items.find(({ name }) => name === "zcode");
expect(zcode?.actions).toContainEqual(
expect.objectContaining({
status: "removed",
configuration_path: shared,
}),
);
expect(zcode?.actions).not.toContainEqual(
expect.objectContaining({
status: "skipped",
configuration_path: shared,
}),
);
expect(JSON.parse(await readFile(shared, "utf8"))).toEqual({
mcpServers: {},
});
} finally {
await host.close?.();
}
});
it("keeps one row per client when OpenCode is pointed at FreeBuff's file", async () => {
// OPENCODE_CONFIG can make OpenCode and FreeBuff edit one file with different
// tables (`mcp.rea` and `mcpServers.rea`). Sharing one decision by filename
// would remove only OpenCode's row and report it for FreeBuff too, leaving
// FreeBuff registered while uninstall reports complete.
const home = await createTestTempDirectory("rea-shared-dialects-");
const shared = join(home, ".agents", "mcp.json");
await mkdir(dirname(shared), { recursive: true });
await mkdir(join(home, ".config", "freebuff-desktop"), { recursive: true });
expect(
await configureClientConfiguration(
{ name: "freebuff", format: "json", configPath: shared },
{},
["rea", "mcp"],
),
).toMatchObject({ status: "configured" });
const host = systemUninstallHost(
home,
undefined,
{ HOME: home, OPENCODE_CONFIG: shared },
"linux",
);
try {
const rows = (await host.clients()).filter(
(candidate) => candidate.configPath === shared,
);
expect(rows.map(({ name }) => name).sort()).toEqual([
"freebuff",
"opencode",
]);
expect(await runUninstall(false, host)).toMatchObject({
status: "complete",
});
expect(JSON.parse(await readFile(shared, "utf8"))).toEqual({
mcpServers: {},
});
} finally {
await host.close?.();
}
});
it.each([true, false])(
"projects only the shared table's outcome to ZCode (shared row owned: %s)",
async (sharedOwned) => {
const home = await createTestTempDirectory("rea-shared-table-outcome-");
const shared = join(home, ".agents", "mcp.json");
const zcodePath = join(home, ".zcode", "cli", "config.json");
await mkdir(dirname(shared), { recursive: true });
await mkdir(dirname(zcodePath), { recursive: true });
await writeFile(zcodePath, '{"mcp":{"servers":{}}}');
await writeFile(
shared,
JSON.stringify({
mcp: {
rea: {
type: "local",
command: [sharedOwned ? "foreign-server" : "rea", "mcp"],
},
},
mcpServers: {
rea: {
command: sharedOwned ? "rea" : "foreign-server",
args: ["mcp"],
},
},
}),
);
const host = systemUninstallHost(
home,
undefined,
{ HOME: home, OPENCODE_CONFIG: shared },
"linux",
);
try {
const result = await runUninstall(false, host);
expect(result.status).toBe("complete");
const zcode = result.items.find(({ name }) => name === "zcode");
expect(zcode?.status).toBe(sharedOwned ? "removed" : "retained");
expect(
zcode?.actions?.filter(
({ configuration_path }) => configuration_path === shared,
),
).toEqual([
expect.objectContaining({
status: sharedOwned ? "removed" : "retained",
}),
]);
const remaining = JSON.parse(await readFile(shared, "utf8"));
expect(remaining.mcp).toEqual(
sharedOwned
? { rea: { type: "local", command: ["foreign-server", "mcp"] } }
: {},
);
expect(remaining.mcpServers).toEqual(
sharedOwned
? {}
: { rea: { command: "foreign-server", args: ["mcp"] } },
);
} finally {
await host.close?.();
}
},
);
it("does not treat a dangling FreeBuff profile link as installed", async () => {
// Setup and doctor follow ~/.config/freebuff-desktop and reject a link whose
// target is gone. Discovery must agree: counting the leftover link as an
// installed product turns an unrelated malformed shared file into a blocking
// failure that stops every other REA removal.
const home = await createTestTempDirectory("rea-freebuff-dangling-profile-");
const shared = join(home, ".agents", "mcp.json");
const profile = join(home, ".config", "freebuff-desktop");
const cursor = join(home, ".cursor", "mcp.json");
await mkdir(dirname(shared), { recursive: true });
await writeFile(shared, "{ not json");
await mkdir(dirname(cursor), { recursive: true });
await writeFile(cursor, '{"keep":"before setup"}\n');
expect(
await configureClientConfiguration(
{ name: "cursor", format: "json", configPath: cursor },
{},
["rea", "mcp"],
),
).toMatchObject({ status: "configured" });
lstatMock.mockImplementation((path) =>
String(path) === profile
? Promise.resolve({
isFile: () => false,
isDirectory: () => false,
isSymbolicLink: () => true,
} as unknown as Awaited<ReturnType<typeof fsLstat>>)
: actualFs.lstat(path),
);
realpathMock.mockImplementation((path) =>
String(path) === profile
? Promise.reject(
Object.assign(
new Error("ENOENT: no such file or directory, realpath"),
{ code: "ENOENT" },
),
)
: actualFs.realpath(path),
);
const host = systemUninstallHost(home, undefined, { HOME: home }, "linux");
try {
const result = await runUninstall(false, host);
expect(result.status).toBe("complete");
// The unrelated registration is removed instead of blocked.
expect(result.items).toContainEqual(
expect.objectContaining({ name: "cursor", status: "removed" }),
);
expect(JSON.parse(await readFile(cursor, "utf8"))).not.toHaveProperty(
"mcpServers.rea",
);
expect(result.items).toContainEqual(
expect.objectContaining({ name: "freebuff", status: "retained" }),
);
} finally {
await host.close?.();
}
});
it("serves a completed removal reread when its close retry releases the handle", async () => {
// Preflight saw no owned row; another writer adds one before removal. The
// reread's first close fails but its cleanup retry releases the handle:
// the completed text is the ownership evidence that drives removal of the
// concurrently added row instead of softening to retained and leaving it
// active while data is purged.
const home = await createTestTempDirectory("rea-freebuff-reread-close-");
const setupHost = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home },
);
const client = (await setupHost.supportedClients()).find(
({ name }) => name === "freebuff",
);
if (client === undefined) throw new Error("freebuff client missing");
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(
client.configPath,
'{"mcpServers":{"rea":{"command":"foreign-server","args":[]}}}',
);
const cacheOwned = join(home, ".rea", "cache", "owned");
await mkdir(dirname(cacheOwned), { recursive: true });
await writeFile(cacheOwned, "owned\n");
const { hold, release } = holdReadHandle(
client.configPath,
"held removal reread handle",
false,
1,
);
const base = systemUninstallHost(home, undefined, { HOME: home }, "linux");
const host = {
...base,
inspectSkill: async () => {
// Another writer registers REA between preflight and removal.
await writeFile(
client.configPath,
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}',
);
hold();
return base.inspectSkill?.();
},
};
try {
const result = await runUninstall(true, host);
expect(result.status).toBe("complete");
expect(result.items).toContainEqual(
expect.objectContaining({ name: "freebuff", status: "removed" }),
);
expect(await readFile(client.configPath, "utf8")).not.toContain("rea");
await expect(readFile(cacheOwned)).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
await cleanupHeldHandle(host, release);
}
});
describe("ZCode fallback cleanup ownership", () => {
it.each(["preflight", "configure"])(
"retains a failed ZCode fallback close during %s until host cleanup",
async (operation) => {
const home = await createTestTempDirectory("rea-zcode-fallback-close-");
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home },
);
const client = (await host.supportedClients()).find(
({ name }) => name === "zcode",
);
if (client?.zcodeFallbackPath === undefined)
throw new Error("ZCode fallback path missing");
const fallback = client.zcodeFallbackPath;
await mkdir(dirname(fallback), { recursive: true });
const original = '{"mcpServers":{"other":{"command":"other"}}}\n';
await writeFile(fallback, original);
const { closeAttempts, release } = holdReadHandle(
fallback,
"held fallback handle",
);
try {
const action =
operation === "preflight"
? host.inspectClientConfiguration?.(client, {}, ["rea", "mcp"])
: host.configureClient(client, {}, ["rea", "mcp"]);
if (action === undefined) throw new Error("setup preflight missing");
await expect(action).rejects.toMatchObject({
_tag: "ConfigurationError",
cause: expect.any(RegularFileCleanupFailure),
cleanup: { resources: [fallback] },
});
expect(closeAttempts).toHaveLength(2);
expect(closeAttempts[0]).toBe(closeAttempts[1]);
expect(await readFile(fallback, "utf8")).toBe(original);
await expect(readFile(client.configPath)).rejects.toMatchObject({
code: "ENOENT",
});
await expectCloseReclaimsHandle(host, closeAttempts, release);
} finally {
await cleanupHeldHandle(host, release);
}
},
);
});
describe("setup client configuration cleanup ownership", () => {
it.each([
["valid UTF-8", Buffer.from('{"servers": {"other": {}}}\n')],
["malformed UTF-8", Buffer.from([0xff])],
] as const)(
"stops configuration and retains the %s read owner until host close",
async (encoding, original) => {
const { host, client } = await preparedVsCodeSetupHost(
"rea-setup-config-close-",
);
await writeFile(client.configPath, original);
const { closeAttempts, release } = holdReadHandle(
client.configPath,
"test holds the config handle open",
);
const configure = vi.spyOn(host, "configureClient");
try {
const failure = await filterClientsNeedingConfigure(
host,
[client],
{},
["rea", "mcp"],
)
.then((detectedClients) =>
configureDetectedClients({
host,
detectedClients,
providerEnvironment: {},
command: ["rea", "mcp"],
clients: {},
appliedActions: [],
}),
)
.then(
() => undefined,
(cause: unknown) => cause,
);
expect(failure).toBeInstanceOf(ConfigurationError);
if (!(failure instanceof ConfigurationError))
throw new Error(
"Expected the configuration failure to retain cleanup authority",
);
if (encoding === "malformed UTF-8")
expect(failure).toMatchObject({
cause: {
outcome: { kind: "failed", cause: expect.any(TypeError) },
},
});
expect(failure).toMatchObject({
_tag: "ConfigurationError",
cause: expect.any(RegularFileCleanupFailure),
cleanup: {
resources: [client.configPath],
reason: expect.stringContaining(
"test holds the config handle open",
),
},
});
expect(configure).not.toHaveBeenCalled();
expect(closeAttempts).toHaveLength(2);
expect(closeAttempts[0]).toBe(closeAttempts[1]);
expect(
Buffer.compare(await readFile(client.configPath), original),
).toBe(0);
await expectCloseReclaimsHandle(host, closeAttempts, release);
expect(failure.cleanupIncomplete).toBe(false);
expect(failure.cleanupResources).toEqual([]);
} finally {
await cleanupHeldHandle(host, release);
}
},
);
it("waits for an admitted configuration read and rejects later admissions", async () => {
const { host, client } = await preparedVsCodeSetupHost(
"rea-setup-config-inflight-",
);
await writeFile(client.configPath, '{"servers": {}}\n');
let beginRead!: () => void;
const readStarted = new Promise<void>((resolve) => {
beginRead = resolve;
});
let unblockRead!: () => void;
const readGate = new Promise<void>((resolve) => {
unblockRead = resolve;
});
let heldRead = false;
openMock.mockImplementation(async (...args) => {
const handle = await actualFs.open(...args);
if (String(args[0]) !== client.configPath || heldRead) return handle;
heldRead = true;
const nativeRead = handle.read.bind(handle);
vi.spyOn(handle, "read").mockImplementation(async (...readArgs) => {
beginRead();
await readGate;
return nativeRead(...readArgs);
});
return handle;
});
let hostCloseFinished = false;
const alignment = host.clientNeedsConfigure(client, {}, ["rea", "mcp"]);
let closeResult: ReturnType<NonNullable<typeof host.close>> | undefined;
await withVerifierCleanup(async () => {
await awaitAcknowledgement(readStarted, alignment, "configuration read");
const close = host.close;
if (close === undefined) throw new Error("Setup host close is missing");
closeResult = close().then((result) => {
hostCloseFinished = true;
return result;
});
const lateAdmission = await host
.clientNeedsConfigure(client, {}, ["rea", "mcp"])
.then(
() => undefined,
(cause: unknown) => cause,
);
expect(lateAdmission).toBeInstanceOf(ConfigurationError);
expect(lateAdmission).toMatchObject({
message: expect.stringContaining("Artifact resource scope is closed"),
});
expect(hostCloseFinished).toBe(false);
unblockRead();
await expect(alignment).resolves.toBe(true);
expect(await closeResult).toBeUndefined();
expect(hostCloseFinished).toBe(true);
}, [
unblockRead,
async () => {
await alignment;
},
async () => {
await closeResult;
},
async () => {
await host.close?.();
},
]);
});
});
const unscopedRegistrationReaders: readonly {
readonly name: string;
readonly read: (
home: string,
environment: NodeJS.ProcessEnv,
execute: Parameters<typeof planIntegrationMaintenance>[3],
) => Promise<unknown>;
}[] = [
{
name: "inspections",
read: (home, environment) =>
readClientRegistrationInspections(home, "/current/rea", { environment }),
},
{
name: "statuses",
read: (home, environment) =>
readClientRegistrationStatuses(home, "/current/rea", { environment }),
},
{
name: "maintenance scope",
read: (home, environment) =>
existingMaintenanceScope(home, "/current/rea", environment),
},
{
name: "maintenance plan",
read: (home, environment, execute) =>
planIntegrationMaintenance(home, "/current/rea", environment, execute),
},
];
it.each(
unscopedRegistrationReaders.flatMap((reader) =>
["configuration", "policy"].flatMap((source) =>
["completed", "failed"].map((readOutcome) => ({
...reader,
source,
readOutcome,
})),
),
),
)(
"$name transfers the exact $source owner after a $readOutcome read without a supplied scope",
async ({ read, source, readOutcome }) => {
const home = await createTestTempDirectory(
"rea-registration-unscoped-close-",
);
const config = join(
home,
source === "policy" ? ".gemini/settings.json" : ".cursor/mcp.json",
);
await mkdir(dirname(config), { recursive: true });
const registration =
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}\n';
await writeFile(config, registration);
const heldPath = source === "policy" ? join(home, "system.json") : config;
const heldText = source === "policy" ? "{}\n" : registration;
await writeFile(
heldPath,
readOutcome === "failed" ? Buffer.from([0xff]) : heldText,
);
const environment = {
HOME: home,
GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, "system.json"),
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "absent-defaults.json"),
};
const held = holdReadHandle(
heldPath,
"held standalone registration descriptor",
);
const execute = vi.fn(async () =>
ok('{"status":"planned","plannedActions":[]}'),
);
try {
const outcome = await read(home, environment, execute).then(
(value) => ({ kind: "returned" as const, value }),
(cause: unknown) => ({ kind: "threw" as const, cause }),
);
expect(outcome).toMatchObject({
kind: "threw",
cause: expect.any(RegularFileCleanupFailure),
});
if (
outcome.kind !== "threw" ||
!(outcome.cause instanceof RegularFileCleanupFailure)
)
throw new Error("Read lost its failed cleanup owner");
const failure = outcome.cause;
expect(failure.path).toBe(heldPath);
if (readOutcome === "completed")
expect(failure.outcome).toEqual({ kind: "completed", value: heldText });
else
expect(failure.outcome).toMatchObject({
kind: "failed",
cause: { _tag: "InvalidUtf8FileError", path: heldPath },
});
expect(failure.owner.handle).toBe(held.closeAttempts[0]);
await expect(failure.owner.close()).rejects.toThrow("held standalone");
await rename(heldPath, `${heldPath}.retired`);
await writeFile(heldPath, "replacement");
held.release();
await failure.owner.close();
expect(failure.owner.handle.fd).toBe(-1);
expect(new Set(held.closeAttempts).size).toBe(1);
expect(await readFile(heldPath, "utf8")).toBe("replacement");
expect(execute).not.toHaveBeenCalled();
} finally {
held.release();
for (const handle of new Set(held.closeAttempts)) await handle.close();
}
},
);
it("keeps a supplied registration scope responsible for diagnostics and cleanup retry", async () => {
const home = await createTestTempDirectory("rea-registration-scoped-close-");
const config = join(home, ".cursor/mcp.json");
await mkdir(dirname(config), { recursive: true });
await writeFile(
config,
'{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}',
);
const held = holdReadHandle(config, "held scoped registration descriptor");
const resources = new ArtifactResourceScope();
try {
expect(
(
await readClientRegistrationStatuses(home, "/current/rea", {
environment: { HOME: home },
resources,
})
).find((status) => status.client === "cursor"),
).toMatchObject({ state: "invalid", command: [] });
expect(resources.hasPending).toBe(true);
await expect(resources.close()).rejects.toThrow();
held.release();
await resources.close();
expect(resources.hasPending).toBe(false);
expect(new Set(held.closeAttempts).size).toBe(1);
expect(held.closeAttempts[0]!.fd).toBe(-1);
} finally {
held.release();
await resources.close();
}
});