server/atomic.test.ts
Rama main · réplica de lectura del repositorio original.
import { execFileSync } from "node:child_process";
import {
chmodSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Spied, not replaced: every call still reaches the real filesystem, and the
// tests can count the fsyncs and reads a save costs. The shared test setup
// has already loaded atomic.ts (through config.ts) against the real module,
// so load a fresh copy that sees the spies.
vi.mock("node:fs", { spy: true });
vi.resetModules();
const spied = await import("node:fs");
const { renameWithRetry, writeFileAtomic, writeFileAtomicIfChanged } = await import("./atomic.ts");
describe("writeFileAtomic", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "omb-atomic-"));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
it("writes the file", () => {
const p = join(dir, "x.json");
writeFileAtomic(p, '{"a":1}');
expect(readFileSync(p, "utf8")).toBe('{"a":1}');
});
it("replaces existing contents in full", () => {
const p = join(dir, "x.json");
writeFileAtomic(p, "old-and-longer");
writeFileAtomic(p, "new");
expect(readFileSync(p, "utf8")).toBe("new");
});
it("leaves no temp files behind", () => {
const p = join(dir, "x.json");
writeFileAtomic(p, "a");
writeFileAtomic(p, "b");
expect(readdirSync(dir)).toEqual(["x.json"]);
});
it("preserves unicode across the write", () => {
const p = join(dir, "u.json");
const s = JSON.stringify({ msg: "café — 日本語 — 🚀" });
writeFileAtomic(p, s);
expect(readFileSync(p, "utf8")).toBe(s);
expect(existsSync(p)).toBe(true);
});
it.skipIf(process.platform === "win32")("applies the requested mode when replacing a file", () => {
const p = join(dir, "secret.json");
writeFileAtomic(p, "old");
chmodSync(p, 0o644);
writeFileAtomic(p, "new", { mode: 0o600 });
expect(statSync(p).mode & 0o777).toBe(0o600);
});
it("cleans up the temporary file when replacement fails", () => {
const p = join(dir, "target");
mkdirSync(p);
expect(() => writeFileAtomic(p, "cannot replace a directory")).toThrow();
expect(readdirSync(dir)).toEqual(["target"]);
});
});
// fsync is F_FULLFSYNC on macOS (~4 ms of blocked event loop), and several per-turn saves rewrite identical bytes.
describe("writeFileAtomic durability", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "omb-atomic-"));
vi.mocked(spied.fsyncSync).mockClear();
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
it("fsyncs by default", () => {
writeFileAtomic(join(dir, "x.json"), "a");
expect(spied.fsyncSync).toHaveBeenCalledTimes(1);
});
it("still replaces atomically without the fsync when durable is false", () => {
const p = join(dir, "x.json");
writeFileAtomic(p, "old");
vi.mocked(spied.fsyncSync).mockClear();
writeFileAtomic(p, "new", { durable: false });
expect(spied.fsyncSync).not.toHaveBeenCalled();
expect(readFileSync(p, "utf8")).toBe("new");
expect(readdirSync(dir)).toEqual(["x.json"]);
});
});
describe("writeFileAtomicIfChanged", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "omb-atomic-"));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
const clearSpies = () => {
vi.mocked(spied.fsyncSync).mockClear();
vi.mocked(spied.readFileSync).mockClear();
};
it("leaves the file alone when it already holds exactly these bytes", () => {
const p = join(dir, "x.json");
const data = JSON.stringify({ msg: "café — 日本語" });
writeFileAtomic(p, data, { mode: 0o600 });
const before = statSync(p);
clearSpies();
expect(writeFileAtomicIfChanged(p, data, { mode: 0o600 })).toBe(false);
const after = statSync(p);
expect(after.ino).toBe(before.ino);
expect(after.mtimeMs).toBe(before.mtimeMs);
expect(spied.fsyncSync).not.toHaveBeenCalled();
expect(readdirSync(dir)).toEqual(["x.json"]);
});
it("writes and fsyncs when the bytes differ", () => {
const p = join(dir, "x.json");
writeFileAtomic(p, '{"unread":true}');
clearSpies();
// Same length, different bytes: only the read can tell.
expect(writeFileAtomicIfChanged(p, '{"unread":fals}')).toBe(true);
expect(readFileSync(p, "utf8")).toBe('{"unread":fals}');
expect(spied.fsyncSync).toHaveBeenCalledTimes(1);
});
it("does not read the old contents when the size already differs", () => {
const p = join(dir, "x.json");
writeFileAtomic(p, "short");
clearSpies();
expect(writeFileAtomicIfChanged(p, "a longer replacement")).toBe(true);
expect(spied.readFileSync).not.toHaveBeenCalled();
expect(readFileSync(p, "utf8")).toBe("a longer replacement");
});
it("writes a file that does not exist yet", () => {
const p = join(dir, "new.json");
expect(writeFileAtomicIfChanged(p, "fresh", { mode: 0o600 })).toBe(true);
expect(readFileSync(p, "utf8")).toBe("fresh");
});
it("passes durable through to the write", () => {
const p = join(dir, "x.json");
clearSpies();
writeFileAtomicIfChanged(p, "token", { durable: false });
expect(spied.fsyncSync).not.toHaveBeenCalled();
expect(readFileSync(p, "utf8")).toBe("token");
});
it.skipIf(process.platform === "win32")("rewrites identical bytes whose permissions are broader than requested", () => {
const p = join(dir, "secret.json");
writeFileAtomic(p, "same");
chmodSync(p, 0o644);
expect(writeFileAtomicIfChanged(p, "same", { mode: 0o600 })).toBe(true);
expect(statSync(p).mode & 0o777).toBe(0o600);
});
it.skipIf(process.platform === "win32")("replaces a symlink holding identical bytes instead of following it", () => {
const target = join(dir, "elsewhere.json");
const p = join(dir, "x.json");
writeFileSync(target, "same");
symlinkSync(target, p);
expect(writeFileAtomicIfChanged(p, "same")).toBe(true);
expect(lstatSync(p).isSymbolicLink()).toBe(false);
expect(readFileSync(p, "utf8")).toBe("same");
expect(readFileSync(target, "utf8")).toBe("same");
});
it.skipIf(process.platform === "win32")("does not hang on a FIFO planted at the path", () => {
const p = join(dir, "x.json");
execFileSync("mkfifo", [p]);
expect(writeFileAtomicIfChanged(p, "data")).toBe(true);
expect(lstatSync(p).isFile()).toBe(true);
expect(readFileSync(p, "utf8")).toBe("data");
});
it("still fails like writeFileAtomic when the path cannot be replaced", () => {
const p = join(dir, "target");
mkdirSync(p);
expect(() => writeFileAtomicIfChanged(p, "cannot replace a directory")).toThrow();
expect(readdirSync(dir)).toEqual(["target"]);
});
});
// Windows refuses a rename onto a path while a virus scanner or the indexer holds a handle for a few milliseconds.
describe("renameWithRetry", () => {
function failing(times: number, code: string) {
let calls = 0;
return {
get calls() {
return calls;
},
rename: () => {
calls += 1;
if (calls <= times) throw Object.assign(new Error(`${code}: simulated`), { code });
},
};
}
it("survives a transient EPERM instead of losing the write", () => {
const stub = failing(3, "EPERM");
expect(() => renameWithRetry("a.tmp", "a", stub.rename)).not.toThrow();
expect(stub.calls).toBe(4);
});
it("retries EACCES and EBUSY the same way", () => {
for (const code of ["EACCES", "EBUSY"]) {
const stub = failing(1, code);
expect(() => renameWithRetry("a.tmp", "a", stub.rename)).not.toThrow();
expect(stub.calls).toBe(2);
}
});
it("gives up rather than retrying forever", () => {
const stub = failing(Number.MAX_SAFE_INTEGER, "EPERM");
expect(() => renameWithRetry("a.tmp", "a", stub.rename)).toThrow(/EPERM/);
expect(stub.calls).toBe(6); // first attempt plus five backoffs
});
it("does not retry an error that will never clear", () => {
// Busy-waiting on a missing directory or a cross-device rename would hide
// a real bug behind a delay and still fail.
for (const code of ["ENOENT", "EXDEV", "EISDIR"]) {
const stub = failing(Number.MAX_SAFE_INTEGER, code);
expect(() => renameWithRetry("a.tmp", "a", stub.rename)).toThrow(new RegExp(code));
expect(stub.calls).toBe(1);
}
});
});