import { test } from "node:test"; import assert from "node:assert/strict"; import { defaultBackfill, defaultDigest, defaultSiteSettings, sanitizeBackfill, sanitizeDigest, type SiteSettings, } from "./settings"; import { operationCatalog } from "./operations"; import { isGateHeld, pauseLaneFor, withGateHeld, type PauseLane, } from "./pauseGates"; // Run with: node_modules/.bin/tsx --test common/lib/pauseGates.test.ts // // What these pin is the thing the file exists to stop: a second opinion about // polarity. `backfill.enabled` was inverted and three readers used to spell // that inversion for themselves, so the round trip below is the contract, not // decoration. // // Slice 1.4 moved the gate onto `autoQueue[lane].held` and read the four // retired fields as a fallback; S0-pause deleted them once the live // settings.json carried every key. So there is ONE place a gate can be spelled, // and the two tests after the round trip pin the deletion from both ends: a // file that still spells a retired field cannot hold a lane with it, and no // sanitizer will carry that field back onto disk. const LANES: PauseLane[] = [ "transcription", "download", "digest", "backfill", ]; test("held round-trips through withGateHeld on every lane", () => { for (const lane of LANES) { const base = defaultSiteSettings(); const held = withGateHeld(base, lane, true); assert.equal(isGateHeld(held, lane), true, `${lane} should read held`); const released = withGateHeld(held, lane, false); assert.equal(isGateHeld(released, lane), false, `${lane} should read free`); // No lane's write reaches another lane's gate. for (const other of LANES) { if (other === lane) continue; assert.equal( isGateHeld(held, other), isGateHeld(base, other), `holding ${lane} moved ${other}`, ); } } }); test("a settings.json still spelling a retired pause field cannot hold a lane", () => { // THE DELETION, FROM THE READ SIDE. Until S0-pause an absent `held` asked // four fields — `transcriptionsPaused`, `downloadsPaused`, // `digest.digestsPaused` and the INVERTED `backfill.enabled`. They are gone // from SiteSettings, so a hand-edited or long-unwritten file that still // carries them is read past entirely. // // EVERY VALUE BELOW IS THE OPPOSITE OF THE ANSWER, which is what makes this a // test rather than a coincidence: under the old fallback these four said // held/held/held/free, and what comes out is the lane defaults — // free/free/free/held. A reader that still consulted them fails on all four. // // The precondition that made the deletion safe is recorded in // one-core-phase-2.md §S0-pause: the live settings.json carried all four // `held` keys before the fields went, so nothing paused came back running. const base = defaultSiteSettings(); const stale = { ...base, transcriptionsPaused: true, downloadsPaused: true, digest: { ...base.digest, digestsPaused: true }, backfill: { ...base.backfill, enabled: true }, } as unknown as SiteSettings; assert.deepEqual( LANES.map((lane) => isGateHeld(stale, lane)), [false, false, false, true], ); // The lane defaults are where those answers come from, not the stale object. assert.deepEqual( LANES.map((lane) => isGateHeld(base, lane)), [false, false, false, true], ); // And the key still answers on the same object: the retired fields are inert, // not consulted-and-outranked. assert.equal(isGateHeld(withGateHeld(stale, "digest", true), "digest"), true); assert.equal( isGateHeld(withGateHeld(stale, "backfill", false), "backfill"), false, ); }); test("no sanitizer names a retired pause field", () => { // THE PARTS `writeSettings` IS BUILT FROM. Its output is ONLY the known // operational fields — `sanitizeDigest`, `sanitizeBackfill` and an explicit // top-level literal — so a field no sanitizer names cannot survive a save. // // THIS IS NOT THE SAVE ITSELF, and the distinction matters: `writeSettings` // writes `getPaths().settingsFile`, which memoizes at module scope, so a // round trip needs an env seam this pure file deliberately does not have. // `lib/settingsWrite.test.ts` is that round trip — it saves an object // spelling all four retired fields into a temp dir and reads the file back — // and the e2e "a retired pause field holds nothing, and does not survive a // write" proves the same thing through a real editor saving a real file from // a page. What is asserted HERE is the pieces: nothing that composes the // output knows those names. const digest = sanitizeDigest({ ...defaultDigest(), digestsPaused: true, } as unknown) as Record; assert.equal("digestsPaused" in digest, false); const backfill = sanitizeBackfill({ ...defaultBackfill(), enabled: false, } as unknown) as Record; assert.equal("enabled" in backfill, false); const base = defaultSiteSettings() as unknown as Record; assert.equal("transcriptionsPaused" in base, false); assert.equal("downloadsPaused" in base, false); }); test("the backfill lane ships held, as its inverted field always made it", () => { // The one lane whose default gate is shut, and the reason is continuity: the // retired `backfill.enabled` defaulted FALSE and `isGateHeld` read it // inverted, so every settings.json that never named a gate has read this lane // as held since it existed. `defaultHeldFor` is that reading, kept. const base = defaultSiteSettings(); assert.equal(base.autoQueue.backfill.held, true); assert.equal(base.autoQueue.backfill.enabled, false, "and unarmed as well"); for (const lane of LANES) { if (lane === "backfill") continue; assert.equal(isGateHeld(base, lane), false, `${lane} ships free`); } }); test("holding a lane leaves the rest of its policy alone", () => { // The bug this forbids: a rebuilt literal instead of a spread would drop the // lane's other settings on a pause click. It used to guard the sweep's // persisted scope, then `backfill.concurrency`; the thing at risk is now the // lane's TREE, its order and its snooze, which is strictly more to lose. const base = defaultSiteSettings(); const authored: SiteSettings = { ...base, autoQueue: { ...base.autoQueue, backfill: { ...base.autoQueue.backfill, enabled: true, order: "newest", maxWorkers: 2, snoozeUntil: null, root: { id: "backfill-root", mode: "strict", weight: 1, maxWorkers: null, children: [ { id: "backfill-teamrcn", match: { type: "channel", value: "teamrcn" }, weight: 1, maxWorkers: null, }, ], }, }, }, backfill: { ...base.backfill, concurrency: 3, allowRedownload: true }, }; const held = withGateHeld(authored, "backfill", true); assert.equal(isGateHeld(held, "backfill"), true); assert.deepEqual( { ...held.autoQueue.backfill, held: undefined }, { ...authored.autoQueue.backfill, held: undefined }, ); // Nothing outside the lane's own policy moved — including the block that used // to carry this gate. assert.deepEqual(held.backfill, authored.backfill); assert.deepEqual( { ...held, autoQueue: null }, { ...authored, autoQueue: null }, ); }); test("isGateHeld answers for a partial settings object", () => { // laneGuards.test.ts casts a {digest, autoQueue}-only object to SiteSettings, // so a reader that reached for settings.backfill on the way past would throw. // `autoQueue` itself is read with `?.` for the same reason. const partial = { digest: { ...defaultDigest() }, autoQueue: { digest: { held: true } }, } as unknown as SiteSettings; assert.equal(isGateHeld(partial, "digest"), true); assert.equal(isGateHeld({} as SiteSettings, "digest"), false); }); test("pauseLaneFor answers for every catalog id", () => { const expected: Record = { download: "download", transcription: "transcription", diarization: "backfill", "attribution-diarized": "backfill", "attribution-text": "backfill", digest: "digest", // Catalogued, and deliberately gateless: the sync scheduler's own `enabled` // is its switch, and its queue key is neither sweep's. sync: null, // THE DOWNLOAD LANE'S, because the download runner is what dispatches it. // It used to be null, on the reasoning that a scan fetches no media and the // operator runs it precisely to decide what a paused lane should fetch when // it resumes. That reasoning is intact and now belongs to the RUN BUTTON, // which checks the platform cooldown and no gate at all: pausing downloads // stops the lane from auto-dispatching a scan, and stops nothing the // operator does by hand. "metadata-scan": "download", }; const ids = operationCatalog().map((o) => o.id); assert.equal(ids.length, 8); for (const id of ids) { assert.ok(id in expected, `catalog gained ${id} with no expected lane`); assert.equal(pauseLaneFor(id), expected[id], `pauseLaneFor(${id})`); } }); test("an id the catalog does not know has no lane", () => { assert.equal(pauseLaneFor("not-an-operation"), null); }); // RELEASE 18: the publish lane is a PIPELINE lane, not a LANES entry, and its // gate is `settings.publish.held` — the one gate not on an `autoQueue` policy. test("the publish gate round-trips through settings.publish.held, and touches no other gate", () => { const base = defaultSiteSettings(); assert.equal(isGateHeld(base, "publish"), false, "the publish lane ships free"); const held = withGateHeld(base, "publish", true); assert.equal(held.publish.held, true); assert.equal(isGateHeld(held, "publish"), true); // The rest of the block is kept (a spread, never a rebuilt literal). assert.deepEqual({ ...held.publish, held: false }, base.publish); assert.equal("publish" in held.autoQueue, false, "no autoQueue.publish is invented"); for (const lane of LANES) { assert.equal(isGateHeld(held, lane), isGateHeld(base, lane), `holding publish moved ${lane}`); assert.equal(isGateHeld(withGateHeld(base, lane, true), "publish"), false, `holding ${lane} held publish`); } assert.equal(isGateHeld(withGateHeld(held, "publish", false), "publish"), false); // A partial object (laneGuards.test casts one) answers free, never throws. assert.equal(isGateHeld({} as SiteSettings, "publish"), false); });