import { createHash } from "node:crypto"; import { mkdir, open as fsOpen, writeFile, type FileHandle, } from "node:fs/promises"; import { dirname, join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js"; import { runDoctorOwned, type DoctorHost, } from "../../../src/application/Doctor.js"; import { readSkillFile, skillDestinations, } from "../../../src/application/SetupSkill.js"; import { RegularFileCleanupFailure } from "../../../src/application/RegularFileRead.js"; import { systemSetupHost } from "../../../src/application/SetupHost.js"; import { runUninstall, systemUninstallHost, } from "../../../src/application/Uninstall.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()); const failFirstCloses = (path: string) => { const attempts: FileHandle[] = []; let allowClose = false; 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 () => { attempts.push(handle); if (!allowClose) throw new Error("test holds the skill handle open"); await nativeClose(); }); return handle; }); return { attempts, release: () => { allowClose = true; }, }; }; describe("skill regular-file cleanup ownership", () => { it.each(["first", "recovery", "verification"] as const)( "retains a failed %s backup read until setup host disposal", async (stage) => { const home = await createTestTempDirectory( "rea-setup-skill-backup-close-", ); const [destination] = skillDestinations(home, [], {}, "linux"); if (destination === undefined) throw new Error("Shared skill root missing"); const manifest = join(destination.path, "SKILL.md"); const original = "stale managed skill\n"; const firstBackup = `${manifest}.rea.backup`; const backup = stage === "recovery" ? `${firstBackup}.${createHash("sha256").update(original).digest("hex")}.recovery` : firstBackup; await mkdir(dirname(manifest), { recursive: true }); await writeFile(manifest, original); if (stage === "recovery") await writeFile(firstBackup, "historical skill\n"); if (stage !== "verification") await writeFile(backup, original); const close = failFirstCloses(backup); const host = systemSetupHost( { platform: "linux", homeDirectory: home } as DoctorHost, { HOME: home }, ); try { const result = await host.installSkill([]); expect(result).toMatchObject({ ok: false, error: { stage: "backup" } }); expect(close.attempts.length).toBeGreaterThan(0); expect(new Set(close.attempts).size).toBe(1); const opensBeforeAdmission = openMock.mock.calls.length; expect((await host.installSkill([])).ok).toBe(false); expect(openMock).toHaveBeenCalledTimes(opensBeforeAdmission); expect(new Set(close.attempts).size).toBe(1); expect(await actualFs.readFile(manifest, "utf8")).toBe(original); expect(await host.close?.()).toContain(backup); const attemptsBeforeClose = close.attempts.length; close.release(); expect(await host.close?.()).toBeUndefined(); expect(close.attempts.length).toBeGreaterThan(attemptsBeforeClose); expect(close.attempts[0]?.fd).toBe(-1); } finally { close.release(); await host.close?.(); // Reclaim the real test descriptor even when the retention regression fails. for (const handle of new Set(close.attempts)) await handle.close(); } }, ); it("retains a failed setup skill read until setup host disposal", async () => { const home = await createTestTempDirectory("rea-setup-skill-close-"); const [destination] = skillDestinations(home, [], {}, "linux"); if (destination === undefined) throw new Error("Shared skill root missing"); const manifest = join(destination.path, "SKILL.md"); await mkdir(dirname(manifest), { recursive: true }); await writeFile(manifest, "stale managed skill\n"); const close = failFirstCloses(manifest); const host = systemSetupHost( { platform: "linux", homeDirectory: home } as DoctorHost, { HOME: home }, ); try { const failure = await host.skillNeedsInstall([]).then( () => undefined, (cause: unknown) => cause, ); expect(failure).toBeInstanceOf(RegularFileCleanupFailure); expect(failure).toMatchObject({ path: manifest, outcome: { kind: "completed" }, cleanupCause: expect.objectContaining({ message: "test holds the skill handle open", }), }); expect(close.attempts.length).toBeGreaterThan(0); expect(new Set(close.attempts).size).toBe(1); const attemptsBeforeClose = close.attempts.length; close.release(); expect(await host.close?.()).toBeUndefined(); expect(close.attempts.length).toBeGreaterThan(attemptsBeforeClose); expect(new Set(close.attempts).size).toBe(1); expect(close.attempts[0]?.fd).toBe(-1); } finally { close.release(); await host.close?.(); } }); it("waits for an admitted skill read before its resource scope closes", async () => { const home = await createTestTempDirectory("rea-setup-skill-inflight-"); const [destination] = skillDestinations(home, []); if (destination === undefined) throw new Error("Shared skill root missing"); const manifest = join(destination.path, "SKILL.md"); await mkdir(dirname(manifest), { recursive: true }); await writeFile(manifest, "stale managed skill\n"); const resources = new ArtifactResourceScope(); 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]) !== manifest || 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; }); try { const reading = readSkillFile(manifest, resources); await readStarted; let closed = false; const closing = resources.close().then(() => { closed = true; }); await Promise.resolve(); expect(closed).toBe(false); unblockRead(); expect(await reading).toBe("stale managed skill\n"); await closing; expect(closed).toBe(true); } finally { unblockRead(); await resources.close(); } }); }); describe("skill cleanup diagnostics", () => { it("preserves read and host cleanup failures when doctor identity cannot complete", async () => { const home = await createTestTempDirectory("rea-doctor-skill-close-"); const [destination] = skillDestinations(home, []); if (destination === undefined) throw new Error("Shared skill root missing"); const manifest = join(destination.path, "SKILL.md"); await mkdir(dirname(manifest), { recursive: true }); await writeFile(manifest, "managed skill fixture\n"); const close = failFirstCloses(manifest); const resources = new ArtifactResourceScope(); const host = createDoctorHostFixture({ installedSkillIdentity: async () => { await readSkillFile(manifest, resources); return { version: null, toolCount: null, canonical: true }; }, close: async () => { try { await resources.close(); return undefined; } catch (cause: unknown) { return cause instanceof Error ? cause.message : String(cause); } }, }); try { const failure = await runDoctorOwned(undefined, host, { skill: true, }).then( () => undefined, (cause: unknown) => cause, ); expect(failure).toBeInstanceOf(AggregateError); if (!(failure instanceof AggregateError)) throw new Error("Doctor cleanup should preserve both failures"); expect(failure.errors[0]).toBeInstanceOf(RegularFileCleanupFailure); expect(failure.errors[0]).toMatchObject({ path: manifest, outcome: { kind: "completed" }, }); expect(failure.errors[1]).toEqual( expect.objectContaining({ message: expect.stringContaining(manifest), }), ); expect(close.attempts.length).toBeGreaterThan(0); expect(new Set(close.attempts).size).toBe(1); const attemptsBeforeClose = close.attempts.length; close.release(); expect(await host.close?.()).toBeUndefined(); expect(close.attempts.length).toBeGreaterThan(attemptsBeforeClose); expect(new Set(close.attempts).size).toBe(1); expect(close.attempts[0]?.fd).toBe(-1); } finally { close.release(); await host.close?.(); } }); it("returns uninstall changes with a failed cleanup item for a retained handle", async () => { const home = await createTestTempDirectory("rea-uninstall-skill-close-"); const [destination] = skillDestinations(home, []); if (destination === undefined) throw new Error("Shared skill root missing"); const manifest = join(destination.path, "SKILL.md"); await mkdir(dirname(manifest), { recursive: true }); await writeFile( manifest, `---\nname: ${PRODUCT_IDENTITY.skillName}\n---\nmanaged fixture\n`, ); const close = failFirstCloses(manifest); const host = systemUninstallHost(home, undefined, { HOME: home }); try { const result = await runUninstall(false, host); expect(result.status).toBe("failed"); expect(result.items).toContainEqual( expect.objectContaining({ name: "uninstall_cleanup", status: "failed", detail: expect.stringContaining(manifest), }), ); expect(close.attempts.length).toBeGreaterThan(0); expect(new Set(close.attempts).size).toBe(1); const attemptsBeforeClose = close.attempts.length; close.release(); expect(await host.close?.()).toBeUndefined(); expect(close.attempts.length).toBeGreaterThan(attemptsBeforeClose); expect(new Set(close.attempts).size).toBe(1); expect(close.attempts[0]?.fd).toBe(-1); } finally { close.release(); await host.close?.(); } }); });