import { Cli } from "incur"; import { access, writeFile } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, it, onTestFinished, vi } from "vitest"; import { SqliteDatabaseService } from "../../../src/application/sqlite/SqliteDatabaseService.js"; import { registerSqliteCommands } from "../../../src/cli/sqliteCommands.js"; import { createSqliteDatabaseService } from "../../../src/composition/sqlite.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { silentLogger } from "../../../src/logger.js"; import { SqliteDatabaseProvider } from "../../../src/sqlite/SqliteDatabaseProvider.js"; import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; import { createSqliteDatabaseFixture } from "../../fixtures/sqlite/database.js"; import { createTestWorkspace, removeTestWorkspace, } from "../../support/workspace/workspaceFixture.js"; const descriptorCleanup = vi.hoisted(() => ({ path: "", denied: false, invalidate: false, attempts: 0, handles: [] as FileHandle[], readFailure: undefined as Error | undefined, onOpen: undefined as (() => void) | undefined, })); vi.mock("../../../src/composition/sqlite.js", () => ({ createSqliteDatabaseService: vi.fn(), })); vi.mock("node:fs/promises", async (importOriginal) => { const filesystem = await importOriginal(); return { ...filesystem, open: async (...args: Parameters) => { const handle = await filesystem.open(...args); if (args[0] === descriptorCleanup.path) { descriptorCleanup.handles.push(handle); const close = handle.close.bind(handle); handle.close = async () => { // FileHandle.close also asks attached streams to close the invalidated handle. if (handle.fd < 0) { await close(); return; } descriptorCleanup.attempts += 1; if (descriptorCleanup.denied) { if (descriptorCleanup.invalidate) await close(); throw new Error("injected SQLite descriptor close failure"); } await close(); }; if (descriptorCleanup.readFailure !== undefined) vi.spyOn(handle, "stat").mockRejectedValue( descriptorCleanup.readFailure, ); descriptorCleanup.onOpen?.(); } return handle; }, }; }); const fixture = async ( member: "database" | "WAL" | "journal" | "reply", invalidate = false, ) => { const workspace = await createTestWorkspace("rea-sqlite-descriptor-cleanup-"); const database = createSqliteDatabaseFixture(workspace.root, true); const path = member === "database" ? database.path : member === "WAL" ? database.walPath : member === "journal" ? `${database.path}-journal` : ""; if (member === "journal") await writeFile(path, Buffer.alloc(28)); const roots: PrivateRuntimeRoot[] = []; const provider = new SqliteDatabaseProvider( process.env, (spawn) => { if (member !== "reply") throw new Error("SQLite worker deliberately unavailable"); return spawnOwnedProviderProcess({ ...spawn, arguments: spawn.arguments.map((argument, index) => index === 1 ? fileURLToPath( new URL( "../../../dist/sqlite/SqliteDatabaseWorker.js", import.meta.url, ), ) : argument, ), }); }, async () => { const root = await PrivateRuntimeRoot.create({ parent: workspace.root }); if (member === "reply") descriptorCleanup.path = join(root.path, "reply.json"); roots.push(root); return root; }, ); descriptorCleanup.path = path; descriptorCleanup.denied = true; descriptorCleanup.invalidate = invalidate; descriptorCleanup.attempts = 0; descriptorCleanup.handles = []; descriptorCleanup.readFailure = undefined; descriptorCleanup.onOpen = undefined; onTestFinished(async () => { descriptorCleanup.denied = false; try { await provider.close(); } catch (cause) { if (!invalidate) throw cause; } for (const handle of descriptorCleanup.handles) await handle.close(); descriptorCleanup.path = ""; database.close(); await removeTestWorkspace(workspace.root); }); return { provider, roots, path, database }; }; const serveInspection = async ( provider: SqliteDatabaseProvider, path: string, ) => { const previousExitCode = process.exitCode; onTestFinished(() => { process.exitCode = previousExitCode; }); vi.mocked(createSqliteDatabaseService).mockReturnValue( new SqliteDatabaseService(provider), ); const cli = Cli.create("rea-sqlite-cleanup-test", { sync: false }); registerSqliteCommands(cli, silentLogger, {}); let stdout = ""; await cli.serve(["inspect-sqlite-database", path, "--json"], { env: {}, stdout: (text) => { stdout += text; }, exit: () => {}, }); return { output: JSON.parse(stdout) as unknown, exitCode: process.exitCode }; }; it.each(["database", "WAL", "journal"] as const)( "retains a failed %s descriptor and retries it at shutdown", async (member) => { const selected = await fixture(member); const first = await selected.provider.inspect({ path: selected.database.path, }); expect(first).toMatchObject({ ok: false, error: { cleanupIncomplete: true, cleanupResources: [expect.stringContaining(selected.path)], }, }); const handle = descriptorCleanup.handles[0]; if (handle === undefined) throw new Error("Expected owned source descriptor"); expect(handle.fd).toBeGreaterThanOrEqual(0); const blocked = await selected.provider.inspect({ path: selected.database.path, }); expect(blocked).toMatchObject({ ok: false, error: { cleanupIncomplete: true }, }); expect(selected.roots).toHaveLength(1); await expect(selected.provider.close()).rejects.toMatchObject({ cleanupIncomplete: true, }); expect(descriptorCleanup.attempts).toBe(3); descriptorCleanup.denied = false; await selected.provider.close(); expect(descriptorCleanup.attempts).toBe(4); expect(handle.fd).toBe(-1); for (const root of selected.roots) await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" }); }, ); it.each(["EACCES", "EPERM"])( "retains the original %s inspection diagnostics through CLI shutdown", async (code) => { const selected = await fixture("database"); descriptorCleanup.readFailure = Object.assign( new Error("injected source read denial"), { code }, ); const result = await serveInspection( selected.provider, selected.database.path, ); expect(result).toMatchObject({ exitCode: 1, output: { code: "cleanup_incomplete", details: { resources: [expect.stringContaining(selected.path)], diagnostics: { primary_error: { details: { diagnostics: { previous_error: { code: "access_denied", details: { path: selected.path, system_code: code }, }, }, }, }, cleanup_error: { code: "cleanup_incomplete" }, }, }, }, }); expect(descriptorCleanup.attempts).toBe(2); }, ); it("retains cancellation diagnostics and the interrupt exit code through CLI shutdown", async () => { const selected = await fixture("database"); descriptorCleanup.onOpen = () => { const interrupt = process.listeners("SIGINT").at(-1); if (interrupt === undefined) throw new Error("Missing CLI interrupt handler"); interrupt("SIGINT"); }; const result = await serveInspection( selected.provider, selected.database.path, ); expect(result).toMatchObject({ exitCode: 130, output: { code: "cleanup_incomplete", details: { resources: [expect.stringContaining(selected.path)], diagnostics: { primary_error: { details: { diagnostics: { previous_error: { code: "cancelled" } }, }, }, cleanup_error: { code: "cleanup_incomplete" }, }, }, }, }); expect(descriptorCleanup.attempts).toBeGreaterThanOrEqual(2); }); it("retains reply-read cancellation and retries the failed reply descriptor close", async () => { const selected = await fixture("reply"); const controller = new AbortController(); descriptorCleanup.onOpen = () => controller.abort(new Error("requested cancellation during reply read")); const result = await selected.provider.inspect( { path: selected.database.path }, { signal: controller.signal }, ); if (result.ok) throw new Error("Expected cancelled reply read"); expect(projectAnalysisError(result.error)).toMatchObject({ code: "cleanup_incomplete", details: { execution_failure: "cancelled", resources: [expect.stringContaining(descriptorCleanup.path)], captured_output: { stdout: "", stderr: expect.any(String), truncated: false, }, }, }); const handle = descriptorCleanup.handles[0]; if (handle === undefined) throw new Error("Expected owned reply descriptor"); expect(handle.fd).toBeGreaterThanOrEqual(0); await expect(selected.provider.close()).rejects.toMatchObject({ cleanupIncomplete: true, }); descriptorCleanup.denied = false; await selected.provider.close(); expect(handle.fd).toBe(-1); for (const root of selected.roots) await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" }); }); it("retries source cleanup before admitting another snapshot", async () => { const selected = await fixture("database"); await selected.provider.inspect({ path: selected.database.path }); const first = descriptorCleanup.handles[0]; if (first === undefined) throw new Error("Expected source descriptor"); descriptorCleanup.denied = false; const resumed = await selected.provider.inspect({ path: selected.database.path, }); expect(selected.roots).toHaveLength(2); expect(first.fd).toBe(-1); if (resumed.ok) throw new Error("The worker is deliberately unavailable"); expect(resumed.error.cleanupIncomplete).toBe(false); await selected.provider.close(); }); it("keeps an invalidated descriptor close failure unconfirmed across retries", async () => { const selected = await fixture("journal", true); const first = await selected.provider.inspect({ path: selected.database.path, }); expect(first).toMatchObject({ ok: false, error: { cleanupIncomplete: true }, }); expect(descriptorCleanup.handles[0]?.fd).toBe(-1); descriptorCleanup.denied = false; await expect(selected.provider.close()).rejects.toMatchObject({ cleanupIncomplete: true, }); await expect(selected.provider.close()).rejects.toMatchObject({ cleanupIncomplete: true, }); expect(descriptorCleanup.attempts).toBe(1); }); it.each(["EACCES", "EPERM"])( "preserves %s read diagnostics alongside a failed descriptor close", async (code) => { const selected = await fixture("database"); descriptorCleanup.readFailure = Object.assign( new Error("injected source read denial"), { code }, ); const result = await selected.provider.inspect({ path: selected.database.path, }); expect(result).toMatchObject({ ok: false, error: { cleanupIncomplete: true, diagnostics: { previous_error: { code: "access_denied" } }, }, }); }, ); it.each(["database", "WAL", "journal"] as const)( "preserves caller cancellation alongside a failed %s descriptor close", async (member) => { const selected = await fixture(member); const controller = new AbortController(); descriptorCleanup.onOpen = () => controller.abort(new Error("requested caller cancellation")); const result = await selected.provider.inspect( { path: selected.database.path }, { signal: controller.signal }, ); expect(result).toMatchObject({ ok: false, error: { cleanupIncomplete: true, diagnostics: { previous_error: { code: "cancelled" } }, }, }); }, );