rea/tests/boundary/filesystem/setupClientConfigCleanupOwn...

188 lines
6.5 KiB
TypeScript

import { mkdir, open as fsOpen, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
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/application/RegularFileRead.js";
import { configureDetectedClients } from "../../../src/application/SetupClients.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return { ...actual, open: vi.fn(actual.open) };
});
const openMock = vi.mocked(fsOpen);
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));
});
afterEach(() => openMock.mockReset());
describe("setup client configuration cleanup ownership", () => {
it("stops client configuration and retries the same alignment handle from host close", async () => {
const home = await createTestTempDirectory("rea-setup-config-close-");
const environment = {
HOME: home,
XDG_CONFIG_HOME: join(home, "xdg"),
};
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
environment,
);
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 });
const original = Buffer.from('{"servers": {"other": {}}}\n');
await writeFile(client.configPath, original);
const closeAttempts: object[] = [];
let allowClose = false;
openMock.mockImplementation(async (...args) => {
const handle = await actualFs.open(...args);
if (String(args[0]) !== client.configPath) return handle;
const nativeClose = handle.close.bind(handle);
vi.spyOn(handle, "close").mockImplementation(async () => {
closeAttempts.push(handle);
if (!allowClose) throw new Error("test holds the config handle open");
await nativeClose();
});
return handle;
});
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);
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(await readFile(client.configPath)).toEqual(original);
allowClose = true;
const close = host.close;
if (close === undefined) throw new Error("Setup host close is missing");
expect(await close()).toBeUndefined();
expect(closeAttempts).toHaveLength(3);
expect(closeAttempts[2]).toBe(closeAttempts[0]);
} finally {
allowClose = true;
await host.close?.();
}
});
it("waits for an admitted configuration read and rejects later admissions", async () => {
const home = await createTestTempDirectory("rea-setup-config-inflight-");
const environment = {
HOME: home,
XDG_CONFIG_HOME: join(home, "xdg"),
};
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
environment,
);
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 });
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;
let alignment: Promise<boolean> | undefined;
try {
alignment = host.clientNeedsConfigure(client, {}, ["rea", "mcp"]);
await readStarted;
const close = host.close;
if (close === undefined) throw new Error("Setup host close is missing");
const 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);
} finally {
unblockRead();
await alignment?.catch(() => undefined);
await host.close?.();
}
});
});