/** * TASK-0102 §3/§7: temp-file lifecycle — the object "never stays on disk > * scan duration + 5 min cleanup cron". The cleanup reaps files older than * FILES_TEMP_MAX_AGE_MS (crash backstop) and leaves fresh (in-use) files * alone; removeTempFile is finally-safe (missing file = no throw). */ import { mkdtemp, writeFile, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { cleanupTempFiles, createTempFile, removeTempFile, tempDir } from "../temp.js"; async function freshDir(): Promise { const d = await mkdtemp(path.join(tmpdir(), "factory-files-test-")); await mkdir(d, { recursive: true }); return d; } describe("files temp-file lifecycle (TASK-0102 §3/§7)", () => { it("createTempFile writes into the pipeline temp dir", async () => { const dir = await freshDir(); const p = await createTempFile({ FILES_TEMP_DIR: dir }, 0); expect(path.dirname(p)).toBe(dir); expect(p).toMatch(/file-/); await removeTempFile(p); }); it("removeTempFile is idempotent (missing file does not throw)", async () => { await expect(removeTempFile(undefined)).resolves.toBeUndefined(); await expect(removeTempFile("/nonexistent/none-1234")).resolves.toBeUndefined(); }); it("cleanup reaps files older than max age, leaves fresh files", async () => { const dir = await freshDir(); const oldFile = path.join(dir, "file-old"); const freshFile = path.join(dir, "file-fresh"); await writeFile(oldFile, "old payload"); await writeFile(freshFile, "fresh payload"); // Backdate the "old" file to 11 minutes ago (max age = 10 min). const past = new Date(Date.now() - 11 * 60_000); await import("node:fs/promises").then((m) => Promise.all([m.utimes(oldFile, past, past), m.utimes(dir, past, past)]).catch(() => {}) ); const res = await cleanupTempFiles({ FILES_TEMP_DIR: dir, FILES_TEMP_MAX_AGE_MS: 10 * 60_000 }); expect(res.deleted).toBe(1); expect(res.bytesFreed).toBe("old payload".length); const { stat } = await import("node:fs/promises"); await expect(stat(freshFile)).resolves.toMatchObject({ size: "fresh payload".length }); }); it("cleanup on a missing dir is a no-op (0 deleted)", async () => { const res = await cleanupTempFiles({ FILES_TEMP_DIR: path.join(tmpdir(), "factory-files-test-none-999"), FILES_TEMP_MAX_AGE_MS: 600_000 }); expect(res).toEqual({ deleted: 0, bytesFreed: 0 }); }); it("tempDir falls back to os.tmpdir()/factory-files when unset", () => { expect(tempDir({ FILES_TEMP_DIR: "" })).toBe(path.join(tmpdir(), "factory-files")); expect(tempDir({ FILES_TEMP_DIR: "/custom" })).toBe("/custom"); }); });