// WHAT A SAVE PUTS ON DISK. // // Run with: node_modules/.bin/tsx --test common/lib/settingsWrite.test.ts // // Its own file because it needs a SETTINGS SEAM: `writeSettings` writes // `getPaths().settingsFile`, and `getPaths()` memoizes its first answer at // module scope — so SETTINGS_FILE has to be set before anything can import the // module under test. Same arrangement as controller/videoOperations.test.ts, // and the reason the module is imported dynamically below. // // THE CLAIM IT PINS is the write half of S0-pause: the four retired pause // fields (`transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused`, // the inverted `backfill.enabled`) cannot survive a save, because // `writeSettings` builds its output from ONLY the known operational fields and // none of them is one any more. A read-side test can say the fields are // ignored; only this can say they are GONE, which is what stops a stale file // resurrecting a pause an operator has lifted. import { mkdtempSync, readFileSync } from "node:fs"; import { rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test, after } from "node:test"; import assert from "node:assert/strict"; const ROOT = mkdtempSync(path.join(os.tmpdir(), "settings-write-")); process.env.TRANSCRIPTS_DIR = ROOT; process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); // Type-only, so it is erased and cannot execute the module before the env above // is set; the VALUES come through the dynamic import below. import type { SiteSettings } from "./settings"; const { defaultSiteSettings, writeSettings } = await import("./settings"); const { isGateHeld } = await import("./pauseGates"); after(() => rm(ROOT, { recursive: true, force: true })); type Stale = Record; test("a save drops every retired pause field and keeps the lane's key", async () => { // The pre-1.4 spelling, all four, each saying the OPPOSITE of the lane // defaults: held/held/held/free against free/free/free/held. So a save that // carried any of them through would be visible in both directions. const base = defaultSiteSettings(); const stale = { ...base, transcriptionsPaused: true, downloadsPaused: true, digest: { ...base.digest, digestsPaused: true }, backfill: { ...base.backfill, enabled: true }, } as unknown as SiteSettings; await writeSettings(stale); const onDisk = JSON.parse( readFileSync(process.env.SETTINGS_FILE!, "utf8"), ) as Stale; assert.equal("transcriptionsPaused" in onDisk, false); assert.equal("downloadsPaused" in onDisk, false); assert.equal("digestsPaused" in (onDisk.digest as Stale), false); assert.equal("enabled" in (onDisk.backfill as Stale), false); // And what DID land is the gate, one key per lane, at the lane defaults — // so the next reader gets the same answer this object gave. const autoQueue = onDisk.autoQueue as Record; assert.deepEqual( ["transcription", "download", "digest", "backfill"].map( (lane) => autoQueue[lane]?.held, ), [false, false, false, true], ); }); test("a held lane survives its own round trip", async () => { // The other direction, because "the field is dropped" would also be true of a // save that dropped the gate with it. const base = defaultSiteSettings(); const held: SiteSettings = { ...base, autoQueue: { ...base.autoQueue, digest: { ...base.autoQueue.digest, held: true }, backfill: { ...base.autoQueue.backfill, held: false }, }, }; await writeSettings(held); const onDisk = JSON.parse( readFileSync(process.env.SETTINGS_FILE!, "utf8"), ) as Stale; const back = { ...base, autoQueue: onDisk.autoQueue } as SiteSettings; assert.equal(isGateHeld(back, "digest"), true); assert.equal(isGateHeld(back, "backfill"), false); }); // The retired `buildPipeline.mode` (release 11, follow-up O6c): a file on disk // that still says it loads without error, and the next save drops it while // keeping the three real fields. test("a settings.json with the retired buildPipeline.mode loads, and a save drops the key", async () => { const { writeFileSync } = await import("node:fs"); const { getSettings } = await import("./settings"); writeFileSync( process.env.SETTINGS_FILE!, JSON.stringify({ buildPipeline: { mode: "basic", maxParallelBuilds: 3, dockerImage: "kept-image", dockerfile: "Dockerfile.build" }, }), ); const loaded = getSettings(); assert.equal("mode" in loaded.buildPipeline, false); assert.equal(loaded.buildPipeline.maxParallelBuilds, 3); assert.equal(loaded.buildPipeline.dockerImage, "kept-image"); await writeSettings(loaded); const onDisk = JSON.parse(readFileSync(process.env.SETTINGS_FILE!, "utf8")) as { buildPipeline: Record; }; assert.equal("mode" in onDisk.buildPipeline, false); assert.deepEqual(onDisk.buildPipeline, { maxParallelBuilds: 3, dockerImage: "kept-image", dockerfile: "Dockerfile.build", }); });