import { test } from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; import { mkdtemp, rm, statfs, writeFile } from "node:fs/promises"; import { diskGate, evaluateDiskGate, getFreeBytes, isDiskGateLatched, resetDiskGate, } from "./diskSpace"; import type { Paths } from "./paths"; import type { SiteSettings } from "./settings"; // The gate's whole job is the pair (floor, hysteresis). evaluateDiskGate is the // pure core precisely so both can be pinned without a filesystem: the flapping // bug these tests exist to prevent only shows up as a SEQUENCE of decisions, and // a test that could only take one measurement could never see it. const GB = 1024 ** 3; const gate = (freeGB: number, latched: boolean, min = 5, margin = 2) => evaluateDiskGate({ freeBytes: freeGB * GB, minFreeDiskGB: min, resumeMarginGB: margin, latched, }); test("above the floor, an open gate stays open", () => { const g = gate(10, false); assert.equal(g.ok, true); assert.equal(g.reason, "ok"); assert.equal(g.latched, false); assert.equal(g.message, ""); }); test("below the floor closes the gate and latches it", () => { const g = gate(3, false); assert.equal(g.ok, false); assert.equal(g.reason, "below-floor"); assert.equal(g.latched, true); assert.match(g.message, /below the/); }); test("a latched gate does NOT reopen at the floor — this is the flap", () => { // 6 GB free is above the 5 GB floor, so the un-latched rule would say go. The // latched rule holds out for 7 GB. Without this, the first resumed download // drops free space back under 5 and the pipeline oscillates forever. const open = gate(6, false); assert.equal(open.ok, true, "an open gate is happy at 6 GB"); const held = gate(6, true); assert.equal(held.ok, false); assert.equal(held.reason, "below-resume-margin"); assert.equal(held.latched, true, "stays latched"); assert.match(held.message, /waiting for/); }); test("a latched gate reopens once the resume mark is cleared", () => { const g = gate(7, true); assert.equal(g.ok, true); assert.equal(g.reason, "ok"); assert.equal(g.latched, false, "the latch clears itself"); }); test("the full stop-hold-resume sequence does not flap", () => { let latched = false; const decisions: boolean[] = []; // Free space drops under the floor, recovers slowly past it, then past the // resume mark. The gate must open exactly once, at the end. for (const freeGB of [4, 4.5, 5, 5.5, 6, 6.9, 7]) { const g = gate(freeGB, latched); latched = g.latched; decisions.push(g.ok); } assert.deepEqual(decisions, [ false, false, false, false, false, false, true, ]); }); test("a zero margin resumes at the floor (hysteresis opted out)", () => { const g = gate(5, true, 5, 0); assert.equal(g.ok, true); assert.equal(g.resumeBytes, 5 * GB); }); test("minFreeDiskGB 0 disables the gate and clears any latch", () => { const g = gate(0, true, 0, 2); assert.equal(g.enabled, false); assert.equal(g.ok, true); assert.equal(g.latched, false); }); test("exactly at the floor is enough for an open gate", () => { // The floor is a minimum to HAVE, not to exceed — checkDiskSpace has always // used >=, and the gate must not silently tighten it. assert.equal(gate(5, false).ok, true); }); test("a statfs failure fails open", () => { // getFreeBytes reports Infinity when statfs throws. A measurement glitch must // never be the thing that stops a multi-week pipeline. const g = evaluateDiskGate({ freeBytes: Number.POSITIVE_INFINITY, minFreeDiskGB: 5, resumeMarginGB: 2, latched: true, }); assert.equal(g.ok, true); assert.equal(g.latched, false); }); // --- THE LATCH IS PER VOLUME ---------------------------------------------- // // There is no longer one disk. A channel whose media has been relocated writes // its downloads to another drive through the data/ symlink // (common/lib/channelMedia.ts), so one shared boolean would have let a full SSD // pause work landing on the platter and a healthy platter reopen the gate the // SSD had closed — in both directions, and reading as the manual pause while it // did it. // // These take a real measurement, which is what makes them worth having next to // the pure cases above. BOTH DIRS ARE REAL AND EXIST: a path that does not exist // is no longer a stand-in for "a volume with room", because getFreeBytes now // measures a missing dir's nearest existing ANCESTOR (see below — that fix is // what makes the per-channel gate work at all). So "full" is a real path under // an absurd floor and "roomy" is a real path under a floor of about a kilobyte. // No filesystem is written to. const FULL = "/"; // a real path, measured, and always under the absurd floor const ROOMY = os.tmpdir(); // a real path, measured, and always over a 1 KB floor const settingsWithFloor = (minFreeDiskGB: number) => ({ minFreeDiskGB, resumeMarginGB: 1 }) as SiteSettings; const somePaths = { transcriptsDir: FULL } as Paths; // A floor of ~1 KB: enabled (a floor of 0 disables the gate entirely) and // cleared by any filesystem with room on it. const TINY_FLOOR = 1e-6; test("a latch on one volume does not hold another", async () => { resetDiskGate(); // An absurd floor no real filesystem clears. const full = await diskGate(somePaths, settingsWithFloor(1e9), { dir: FULL }); assert.equal(full.ok, false); assert.equal(isDiskGateLatched(FULL), true); assert.equal(isDiskGateLatched(ROOMY), false); // The other volume is unaffected — and, crucially, asking about it does not // clear the first one's latch. With one shared boolean it would have. const roomy = await diskGate(somePaths, settingsWithFloor(TINY_FLOOR), { dir: ROOMY, }); assert.equal(roomy.ok, true); assert.equal(isDiskGateLatched(FULL), true); assert.equal(isDiskGateLatched(ROOMY), false); resetDiskGate(); }); // --- A DIR THAT DOES NOT EXIST YET IS MEASURED ON ITS PARENT'S VOLUME ------- // // The six per-channel gate callers pass `channels//data`, which does not // exist until the channel's first download creates it. Fail-open on ENOENT // answered Infinity there, so the gate waved through exactly the download it // was installed to stop. It measures the nearest existing ancestor instead. test("getFreeBytes measures a not-yet-existing dir on its nearest existing ancestor", async () => { const root = os.tmpdir(); const missing = path.join(root, "ttb-no-such-dir", "slug", "data"); const measured = await getFreeBytes(missing); const onRoot = await statfs(root); assert.equal(Number.isFinite(measured), true, "must not fail open"); // Same volume, so the same figure — modulo whatever the machine wrote // between the two calls, which is why this is a band and not an equality. const expected = onRoot.bsize * onRoot.bavail; assert.ok( Math.abs(measured - expected) < expected * 0.05 + 1024 ** 3, `expected ~${expected}, got ${measured}`, ); }); test("a non-ENOENT failure still fails open", async () => { // A path UNDER A FILE is ENOTDIR, not ENOENT — nonsense rather than // not-there-yet — so the walk does not run and the original contract holds: // a measurement glitch never blocks a download. This is the half of the old // fail-open that survives, and it is deliberately still Infinity. const dir = await mkdtemp(path.join(os.tmpdir(), "ttb-disk-")); try { const file = path.join(dir, "a-file"); await writeFile(file, "x"); assert.equal(await getFreeBytes(path.join(file, "nope")), Infinity); } finally { await rm(dir, { recursive: true, force: true }); } }); test("the gate latches for a data dir that does not exist yet", async () => { resetDiskGate(); const notYet = path.join(os.tmpdir(), "ttb-unborn-channel", "data"); const status = await diskGate(somePaths, settingsWithFloor(1e9), { dir: notYet, }); // Measured on tmpdir's volume, which no absurd floor clears. Before the // ancestor walk this was Infinity and `ok: true`. assert.equal(status.ok, false); assert.equal(isDiskGateLatched(notYet), true); resetDiskGate(); }); test("the hysteresis is still per volume: a latched dir is held to the higher bar", async () => { resetDiskGate(); await diskGate(somePaths, settingsWithFloor(1e9), { dir: FULL }); assert.equal(isDiskGateLatched(FULL), true); // A floor of 0 disables the gate entirely, which clears every volume: the // operator switching the floor off means nothing is held anywhere. const off = await diskGate(somePaths, settingsWithFloor(0), { dir: FULL }); assert.equal(off.ok, true); assert.equal(off.enabled, false); assert.equal(isDiskGateLatched(), false); }); test("observe reads a volume's latch without writing it", async () => { resetDiskGate(); const observed = await diskGate(somePaths, settingsWithFloor(1e9), { dir: FULL, mode: "observe", }); assert.equal(observed.ok, false); // Nothing was recorded: a dashboard poll is not the thing that latches. assert.equal(isDiskGateLatched(FULL), false); resetDiskGate(); }); test("resetDiskGate drops one volume or all of them", async () => { resetDiskGate(); await diskGate(somePaths, settingsWithFloor(1e9), { dir: FULL }); assert.equal(isDiskGateLatched(), true); resetDiskGate(ROOMY); assert.equal(isDiskGateLatched(FULL), true); resetDiskGate(FULL); assert.equal(isDiskGateLatched(FULL), false); assert.equal(isDiskGateLatched(), false); }); test("with no dir the gate measures paths.transcriptsDir, as it always has", async () => { resetDiskGate(); await diskGate(somePaths, settingsWithFloor(1e9)); assert.equal(isDiskGateLatched(somePaths.transcriptsDir), true); resetDiskGate(); });