import { test } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import { mkdtemp, readFile, readdir, writeFile } from "node:fs/promises"; import { createRequire, syncBuiltinESMExports } from "node:module"; import os from "node:os"; import path from "node:path"; import { copyFileAtomic, jsonFileState, jsonText, pendingJsonWrites, readJsonFile, readJsonFileSync, tmpPathFor, writeJsonAtomic, writeFileAtomic, writeJsonAtomicSync, } from "./jsonFile-server"; const require = createRequire(import.meta.url); async function scratch(): Promise { return mkdtemp(path.join(os.tmpdir(), "jsonfile-")); } test("readJsonFile: ok, absent, unparseable, unreadable — and the sync twin agrees", async () => { const dir = await scratch(); const good = path.join(dir, "good.json"); const bad = path.join(dir, "bad.json"); await writeFile(good, '{"a":1}'); await writeFile(bad, '{"a":'); const cases: Array<[string, unknown]> = [ [good, { ok: true, value: { a: 1 } }], [path.join(dir, "missing.json"), { ok: false, reason: "absent" }], [path.join(dir, "no-such-dir", "x.json"), { ok: false, reason: "absent" }], [bad, { ok: false, reason: "unparseable" }], // A directory is readable as a path but not as a file: EISDIR. [dir, { ok: false, reason: "unreadable" }], ]; for (const [file, want] of cases) { assert.deepEqual(await readJsonFile(file), want, file); assert.deepEqual(readJsonFileSync(file), want, file); } }); test("jsonText: indent 2 vs 0, newline on by default", () => { const v = { a: [1, 2] }; assert.equal(jsonText(v), '{\n "a": [\n 1,\n 2\n ]\n}\n'); assert.equal(jsonText(v, { indent: 0 }), '{"a":[1,2]}\n'); assert.equal(jsonText(v, { indent: 0, newline: false }), '{"a":[1,2]}'); assert.equal(jsonText(v, { newline: false }), '{\n "a": [\n 1,\n 2\n ]\n}'); }); test("writeJsonAtomic: the bytes on disk are jsonText's, per option", async () => { const dir = await scratch(); for (const opts of [{}, { indent: 0 as const }, { indent: 0 as const, newline: false }]) { const file = path.join(dir, `f-${JSON.stringify(opts)}.json`); await writeJsonAtomic(file, { x: "y" }, opts); assert.equal(await readFile(file, "utf8"), jsonText({ x: "y" }, opts)); } }); test("temp names are unique per write, not per process", () => { const a = tmpPathFor("/x/config.json"); const b = tmpPathFor("/x/config.json"); assert.notEqual(a, b); assert.ok(a.startsWith(`/x/config.json.tmp-${process.pid}-`)); assert.match(a, /\.tmp-\d+-\d+-[0-9a-f]{8}$/); }); test("a second copy of the module shares the one chain on globalThis", async () => { // Next can load this module twice in one server; simulate the second copy // with a fresh import (a distinct module URL) and check both see one state. const copy = (await import(`./jsonFile-server.ts?copy=${Date.now()}`)) as typeof import("./jsonFile-server"); assert.notEqual(copy.writeJsonAtomic, writeJsonAtomic, "a genuinely separate module instance"); assert.equal(copy.jsonFileState(), jsonFileState()); const dir = await scratch(); const file = path.join(dir, "shared.json"); const writes = []; for (let i = 0; i < 20; i++) { const w = i % 2 === 0 ? writeJsonAtomic : copy.writeJsonAtomic; writes.push(w(file, { i })); } // While in flight, both copies see the same chain entry. assert.equal(copy.pendingJsonWrites(), pendingJsonWrites()); await Promise.all(writes); assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { i: 19 }); assert.deepEqual(await readdir(dir), ["shared.json"]); }); test("two same-path writers serialise: the last ISSUED lands, no temp is left, no write fails", async () => { const dir = await scratch(); const file = path.join(dir, "config.json"); const writes = []; for (let i = 0; i < 50; i++) writes.push(writeJsonAtomic(file, { i })); await Promise.all(writes); assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { i: 49 }); assert.deepEqual(await readdir(dir), ["config.json"]); assert.equal(pendingJsonWrites(), 0); }); test("the value is serialised at the call, not when the chain reaches it", async () => { const dir = await scratch(); const file = path.join(dir, "v.json"); const v = { n: 1 }; const first = writeJsonAtomic(file, { hold: true }); const second = writeJsonAtomic(file, v); v.n = 2; await Promise.all([first, second]); assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { n: 1 }); }); test("a failed write does not block the next one on the same path, and cleans its temp", async () => { const dir = await scratch(); const file = path.join(dir, "sub", "x.json"); // No parent dir and no mkdir: this one fails. await assert.rejects(writeJsonAtomic(file, { a: 1 })); await writeJsonAtomic(file, { a: 2 }, { mkdir: true }); assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { a: 2 }); assert.deepEqual(await readdir(path.dirname(file)), ["x.json"]); }); test("writeJsonAtomicSync: same bytes, mkdir, no temp left", async () => { const dir = await scratch(); const file = path.join(dir, "a", "b.json"); writeJsonAtomicSync(file, { k: 1 }, { newline: false, mkdir: true }); assert.equal(fs.readFileSync(file, "utf8"), '{\n "k": 1\n}'); assert.deepEqual(await readdir(path.dirname(file)), ["b.json"]); }); test("writeFileAtomic: exact bytes for a string and a Buffer, mkdir, no temp left", async () => { const dir = await scratch(); const txt = path.join(dir, "d", "playlist"); await writeFileAtomic(txt, "a\nb\n", { mkdir: true }); assert.equal(await readFile(txt, "utf8"), "a\nb\n"); const bin = path.join(dir, "d", "blob"); const bytes = Buffer.from([0, 255, 10, 13, 128]); await writeFileAtomic(bin, bytes); assert.deepEqual(await readFile(bin), bytes); // An empty string is a real write (clearFailedTranscriptions relies on it). await writeFileAtomic(txt, ""); assert.equal(await readFile(txt, "utf8"), ""); assert.deepEqual((await readdir(path.join(dir, "d"))).sort(), ["blob", "playlist"]); }); test("writeFileAtomic: mode is on the file from creation (0o600 cookie jar)", async () => { const dir = await scratch(); const file = path.join(dir, "cookies.txt"); await writeFileAtomic(file, "secret\n", { mode: 0o600 }); assert.equal(fs.statSync(file).mode & 0o777, 0o600); // The temp itself carries the mode BEFORE the rename: intercept the rename // (the module calls fs/promises' `rename`, so patch it and sync the builtin // ESM exports) and stat its source — the temp — at that moment. const fsp = require("node:fs/promises") as typeof import("node:fs/promises"); const realRename = fsp.rename; const seen: number[] = []; fsp.rename = (async (from: fs.PathLike, to: fs.PathLike) => { seen.push(fs.statSync(from).mode & 0o777); return realRename(from, to); }) as typeof fsp.rename; syncBuiltinESMExports(); try { await writeFileAtomic(file, "second\n", { mode: 0o600 }); } finally { fsp.rename = realRename; syncBuiltinESMExports(); } assert.deepEqual(seen, [0o600], "the rename saw exactly one temp, already 0o600"); assert.equal(await readFile(file, "utf8"), "second\n"); }); test("writeFileAtomic and writeJsonAtomic share one chain on one path", async () => { const dir = await scratch(); const file = path.join(dir, "shared.json"); const writes: Promise[] = []; for (let i = 0; i < 40; i++) { writes.push( i % 2 === 0 ? writeJsonAtomic(file, { i }) : writeFileAtomic(file, `{"i":${i}}`), ); } assert.equal(pendingJsonWrites(), 1, "one chain entry for the path, not two"); await Promise.all(writes); assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { i: 39 }); assert.deepEqual(await readdir(dir), ["shared.json"]); }); test("copyFileAtomic: dest gets src's bytes, src stays, no temp left", async () => { const dir = await scratch(); const src = path.join(dir, "src.bin"); await writeFile(src, Buffer.from([1, 2, 3])); const dest = path.join(dir, "out", "dest.bin"); await copyFileAtomic(src, dest, { mkdir: true }); assert.deepEqual(await readFile(dest), Buffer.from([1, 2, 3])); assert.deepEqual(await readdir(path.join(dir, "out")), ["dest.bin"]); await assert.rejects(copyFileAtomic(path.join(dir, "nope"), dest)); assert.deepEqual(await readdir(path.join(dir, "out")), ["dest.bin"]); }); test("the remark-empty transcript is the literal it replaced (videoActions)", () => { // editor/app/channels/[slug]/videos/[id]/videoActions.ts wrote this literal // by hand before slice W; it now writes the object with { indent: 0 }. assert.equal(jsonText({ transcription: [] }, { indent: 0 }), '{"transcription":[]}\n'); });