/*
Unit tests for fs atomic covering its documented public contract and regression-sensitive edge cases.
- Do not cover the workspace-write-boundary lint here — see workspace-write-boundary.test.ts in site-kernel-checks.
- RFC-1311: Compass header sweep — declares the fs suite's contract for test-file scaffold coverage.
*/
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { test, expect } from "vitest";
import { writeFileAtomic, __setRenameImplForTests } from "../fs-atomic.ts";
async function makeTmpDir(): Promise {
return fs.mkdtemp(path.join(os.tmpdir(), "fs-atomic-"));
}
test("writeFileAtomic: content lands intact", async () => {
const dir = await makeTmpDir();
try {
const target = path.join(dir, "out.json");
const content = JSON.stringify({ hello: "world", n: 42 }, null, 2);
await writeFileAtomic(target, content);
const read = await fs.readFile(target, "utf8");
expect(read).toBe(content);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test("writeFileAtomic: no leftover temp files after a successful write", async () => {
const dir = await makeTmpDir();
try {
const target = path.join(dir, "out.json");
await writeFileAtomic(target, "content");
const entries = await fs.readdir(dir);
expect(entries).toEqual(["out.json"]);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test("writeFileAtomic: a reader polling the target during 200 sequential writes never observes a partial file", async () => {
const dir = await makeTmpDir();
try {
const target = path.join(dir, "shared.json");
const totalWrites = 200;
let stop = false;
let observedPartial = false;
let observedCount = 0;
// Each write produces a distinct, fully-valid JSON payload of a known shape.
function payloadFor(i: number): string {
return JSON.stringify({ seq: i, marker: "X".repeat(32) }) + "\n";
}
const writer = (async () => {
for (let i = 0; i < totalWrites; i++) {
// A polling reader sharing the event loop can still collide with the
// rename often enough to exceed the production default's 5-retry
// budget on Windows. Widen the retry budget for this synthetic
// worst-case poll loop only; the production default stays 5 (RFC-0258).
await writeFileAtomic(target, payloadFor(i), { retries: 80 });
}
stop = true;
})();
const reader = (async () => {
while (!stop) {
let raw: string | undefined;
try {
raw = await fs.readFile(target, "utf8");
} catch {
// Target may not exist yet at the very start — not a partial-read failure.
await new Promise((resolve) => setImmediate(resolve));
continue;
}
observedCount += 1;
try {
const parsed = JSON.parse(raw) as { seq: number; marker: string };
if (parsed.marker !== "X".repeat(32)) {
observedPartial = true;
}
} catch {
observedPartial = true;
}
// Yield once per read so this is a fast, realistic poller rather than
// a worst-case tight spin that starves the writer's rename retries.
await new Promise((resolve) => setImmediate(resolve));
}
})();
await Promise.all([writer, reader]);
expect(observedPartial).toBe(false);
expect(observedCount > 0).toBeTruthy();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test("writeFileAtomic: temp file is removed when the write ultimately fails", async () => {
const dir = await makeTmpDir();
try {
// Target directory does not exist -> the temp-file write itself fails,
// and no temp file should remain in `dir`.
const target = path.join(dir, "missing-subdir", "out.json");
await expect(() => writeFileAtomic(target, "content")).rejects.toThrow();
const entries = await fs.readdir(dir);
expect(entries).toEqual([]);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test("writeFileAtomic: simulated EPERM on first rename attempt is retried and succeeds", async () => {
const dir = await makeTmpDir();
try {
const target = path.join(dir, "out.json");
let calls = 0;
__setRenameImplForTests(async (...args: Parameters) => {
calls += 1;
if (calls === 1) {
const error = new Error("simulated EPERM") as NodeJS.ErrnoException;
error.code = "EPERM";
throw error;
}
return fs.rename(...args);
});
await writeFileAtomic(target, "content", { retries: 5 });
expect(calls >= 2).toBeTruthy();
const read = await fs.readFile(target, "utf8");
expect(read).toBe("content");
} finally {
__setRenameImplForTests(undefined);
await fs.rm(dir, { recursive: true, force: true });
}
});