import { execFile } from "node:child_process"; import { mkdir, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; import { expect, it, vi } from "vitest"; import { AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { ArtifactFileCleanupFailure } from "../../artifacts/ArtifactFileOperation.js"; import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js"; import { openRegularFile, RegularFileAdmissionFailure, } from "../../filesystem/RegularFile.js"; import { readWithoutFifoWriter } from "../../../tests/fixtures/fifoInput.js"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; import { readRecoveryFile, snapshotRecoveryInputs } from "./RecoveryFiles.js"; vi.mock("../../filesystem/RegularFile.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, openRegularFile: vi.fn(actual.openRegularFile) }; }); // Fault injection for the snapshot write; other calls use the real filesystem. const snapshotFault = vi.hoisted(() => ({ mkdir: undefined as Error | undefined, writeFile: undefined as Error | undefined, })); vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, mkdir: async (...args: Parameters) => { if (snapshotFault.mkdir !== undefined) throw snapshotFault.mkdir; return actual.mkdir(...args); }, writeFile: async (...args: Parameters) => { if (snapshotFault.writeFile !== undefined) throw snapshotFault.writeFile; return actual.writeFile(...args); }, }; }); const filesystemError = (code: string): NodeJS.ErrnoException => Object.assign(new Error(`${code}: injected snapshot failure`), { code }); const snapshotWithFault = async ( fault: Partial, ): Promise => { const root = await createTestTempDirectory("rea-recovery-fault-"); const selected = join(root, "bundle.js"); await writeFile(selected, "module.exports = 1;\n"); const snapshots = join(root, "snapshots"); await mkdir(snapshots); Object.assign(snapshotFault, fault); try { return await snapshotRecoveryInputs([selected], snapshots).catch( (cause: unknown) => cause, ); } finally { snapshotFault.mkdir = undefined; snapshotFault.writeFile = undefined; } }; it.skipIf(process.platform === "win32")( "rejects a named pipe without waiting for a writer", async () => { const root = await createTestTempDirectory("rea-recovery-fifo-"); const regular = join(root, "regular.js"); await writeFile(regular, "module.exports = 1;\n"); await expect(readRecoveryFile(regular, 1024)).resolves.toHaveLength(20); const fifoPath = join(root, "sample.js"); await promisify(execFile)("mkfifo", [fifoPath]); const outcome = await readWithoutFifoWriter(fifoPath, () => readRecoveryFile(fifoPath, 1024).catch((cause: unknown) => cause), ); if (outcome.state !== "completed") throw new Error("Recovery input read waited for a FIFO writer"); expect(outcome.result).toBeInstanceOf(AnalysisOutputError); }, ); it("keeps a failed admission close owned when cancellation is also requested", async () => { const owner = new OwnedFileHandle({ fd: -1 } as never); const failure = new RegularFileAdmissionFailure( "/selected.js", new Error("cancelled during admission"), new Error("close failed"), owner, ); const controller = new AbortController(); vi.mocked(openRegularFile).mockImplementationOnce(async () => { controller.abort(); throw failure; }); const observed = await readRecoveryFile( "/selected.js", 1024, controller.signal, ).catch((cause: unknown) => cause); expect(observed).toBeInstanceOf(ArtifactFileCleanupFailure); if (!(observed instanceof ArtifactFileCleanupFailure)) return; expect(observed.fileCleanupFailure.owner).toBe(owner); expect(observed.fileCleanupFailure.outcome).toEqual({ kind: "failed", cause: failure.cause, }); expect(observed.fileCleanupFailure.cleanupCause).toBe(failure.cleanupCause); }); it("reports one file selected under two letter cases as an input error", async (context) => { const root = await createTestTempDirectory("rea-recovery-case-"); const lower = join(root, "bundle.js"); const upper = join(root, "BUNDLE.js"); await writeFile(lower, "module.exports = 1;\n"); // Only a case-insensitive filesystem, such as the default Windows and macOS // ones, resolves both spellings to the selected file. if ( !(await stat(upper).then( () => true, () => false, )) ) context.skip(); const snapshots = join(root, "snapshots"); await mkdir(snapshots); const failure = await snapshotRecoveryInputs([lower, upper], snapshots).catch( (cause: unknown) => cause, ); expect(failure).toBeInstanceOf(AnalysisInputError); expect((failure as AnalysisInputError).issues).toEqual([ expect.objectContaining({ path: ["additional_paths"], message: expect.stringContaining(upper), }), ]); }); it("reports one file selected twice as an input error", async () => { const root = await createTestTempDirectory("rea-recovery-twice-"); const selected = join(root, "bundle.js"); await writeFile(selected, "module.exports = 1;\n"); const snapshots = join(root, "snapshots"); await mkdir(snapshots); const failure = await snapshotRecoveryInputs( [selected, selected], snapshots, ).catch((cause: unknown) => cause); expect(failure).toBeInstanceOf(AnalysisInputError); expect((failure as AnalysisInputError).issues).toEqual([ expect.objectContaining({ path: ["additional_paths"], message: expect.stringContaining("collides with another selected input"), }), ]); // The filesystem's own report stays attached for diagnosis. expect((failure as AnalysisInputError).cause).toMatchObject({ code: "EEXIST", }); }); it("reports a file and directory name clash as an input error", async () => { const clash = filesystemError("ENOTDIR"); const failure = await snapshotWithFault({ mkdir: clash }); expect(failure).toBeInstanceOf(AnalysisInputError); expect((failure as AnalysisInputError).issues).toEqual([ expect.objectContaining({ path: ["additional_paths"] }), ]); expect((failure as AnalysisInputError).cause).toBe(clash); }); it("keeps an unrelated snapshot write failure unchanged", async () => { const fullDisk = filesystemError("ENOSPC"); await expect(snapshotWithFault({ writeFile: fullDisk })).resolves.toBe( fullDisk, ); });