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(); 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( "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((resolve) => { beginRead = resolve; }); let unblockRead!: () => void; const readGate = new Promise((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 | 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?.(); } }); });