import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { Paths } from "../lib/paths"; import { emptyAutoQueueState, readAutoQueueState, writeAutoQueueState, } from "./autoQueueState"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test common/jobs/autoQueueState.test.ts // Only autoQueueStateFile is touched by these functions, so a minimal Paths is fine. async function withPaths( fn: (paths: Paths) => Promise, ): Promise { const dir = await mkdtemp(path.join(tmpdir(), "ttb-aqstate-")); const paths = { autoQueueStateFile: path.join(dir, ".auto-queue", "state.json"), } as Paths; try { await fn(paths); } finally { await rm(dir, { recursive: true, force: true }); } } test("platformBackoff round-trips through write/read", async () => { await withPaths(async (paths) => { const state = emptyAutoQueueState(); state.download.platformBackoff = { odysee: { until: 123456, fails: 3 }, youtube: { until: 999, fails: 1 }, }; await writeAutoQueueState(paths, state); const back = await readAutoQueueState(paths); assert.deepEqual(back.download.platformBackoff, { odysee: { until: 123456, fails: 3 }, youtube: { until: 999, fails: 1 }, }); assert.deepEqual(back.transcription.platformBackoff, {}); }); }); test("missing file yields empty state with empty platformBackoff", async () => { await withPaths(async (paths) => { const back = await readAutoQueueState(paths); assert.deepEqual(back.download.platformBackoff, {}); assert.deepEqual(back.transcription.platformBackoff, {}); }); }); test("corrupt platformBackoff field coerces to empty without throwing", async () => { await withPaths(async (paths) => { // Write a valid state first so the .auto-queue dir exists, then overwrite // the file with a hand-corrupted shape. await writeAutoQueueState(paths, emptyAutoQueueState()); await writeFile( paths.autoQueueStateFile, JSON.stringify({ download: { platformBackoff: { odysee: { until: "nope" }, junk: 5 } }, }), ); const back = await readAutoQueueState(paths); assert.deepEqual(back.download.platformBackoff, {}); }); }); test("every lane is keyed, and a state file written before a lane existed coerces to empty", async () => { await withPaths(async (paths) => { // Exactly what `.auto-queue/state.json` holds in production today: the two // runner lanes and nothing else. // Write a valid state first so the .auto-queue dir exists. await writeAutoQueueState(paths, emptyAutoQueueState()); await writeFile( paths.autoQueueStateFile, JSON.stringify({ transcription: { runtime: { currentWeights: { "node-1": 3 } }, picks: [], platformBackoff: {}, }, download: { runtime: { currentWeights: {} }, picks: [], platformBackoff: {} }, }), ); const back = await readAutoQueueState(paths); assert.deepEqual(Object.keys(back).sort(), [ "backfill", "digest", "download", "transcription", ]); // The lanes it does not name are EMPTY, not undefined: the status poll // reads state[lane].picks for every lane the console draws. assert.deepEqual(back.digest, { runtime: { currentWeights: {} }, picks: [], platformBackoff: {}, videoDeferrals: {}, platformPace: {}, platformHolds: {}, subtitleDeferrals: {}, }); assert.deepEqual(back.backfill.picks, []); // And the lanes it does name are untouched. assert.deepEqual(back.transcription.runtime.currentWeights, { "node-1": 3 }); }); }); test("a lane missing from an in-memory state object still writes", async () => { await withPaths(async (paths) => { // Defensive, like every coercion in this module: a hand-built state (or one // deserialized from an older build) must not make the persist throw and // lose the fairness memory of the lanes that ARE there. const partial = { transcription: emptyAutoQueueState().transcription } as ReturnType< typeof emptyAutoQueueState >; await writeAutoQueueState(paths, partial); const back = await readAutoQueueState(paths); assert.deepEqual(Object.keys(back).sort(), [ "backfill", "digest", "download", "transcription", ]); }); }); test("videoDeferrals round-trips through write/read; other lanes stay {}", async () => { await withPaths(async (paths) => { const state = emptyAutoQueueState(); state.download.videoDeferrals = { a1: { until: 123456, channelSlug: "alpha" }, }; await writeAutoQueueState(paths, state); const back = await readAutoQueueState(paths); assert.deepEqual(back.download.videoDeferrals, { a1: { until: 123456, channelSlug: "alpha" }, }); assert.deepEqual(back.transcription.videoDeferrals, {}); }); }); test("a state file written before videoDeferrals existed coerces it to {}; corrupt entries drop", async () => { await withPaths(async (paths) => { await writeAutoQueueState(paths, emptyAutoQueueState()); await writeFile( paths.autoQueueStateFile, JSON.stringify({ download: { runtime: { currentWeights: {} }, picks: [], platformBackoff: { youtube: { until: 5, fails: 2 } }, }, transcription: { videoDeferrals: { x: { until: "nope" }, y: 7 } }, }), ); const back = await readAutoQueueState(paths); assert.deepEqual(back.download.videoDeferrals, {}); assert.deepEqual(back.download.platformBackoff, { youtube: { until: 5, fails: 2 }, }); assert.deepEqual(back.transcription.videoDeferrals, {}); }); }); test("the pacing memory round-trips; a file written before it coerces to {} (release 17, RL)", async () => { await withPaths(async (paths) => { const state = emptyAutoQueueState(); state.download.platformPace = { youtube: { sleepRequestsSeconds: 4, baseSeconds: 1, cleanUnits: 2, steppedAt: 7 }, }; state.download.platformHolds = { youtube: { since: 10, probeAt: 20, rateLimited: false } }; state.download.subtitleDeferrals = { v1: { count: 2, lastAt: 5, until: 6, channelSlug: "alpha" }, }; await writeAutoQueueState(paths, state); const back = await readAutoQueueState(paths); assert.deepEqual(back.download.platformPace, state.download.platformPace); assert.deepEqual(back.download.platformHolds, state.download.platformHolds); assert.deepEqual(back.download.subtitleDeferrals, state.download.subtitleDeferrals); assert.deepEqual(back.transcription.platformPace, {}); await writeFile( paths.autoQueueStateFile, JSON.stringify({ download: { platformPace: { youtube: { sleepRequestsSeconds: "x" }, rumble: { sleepRequestsSeconds: 2 } }, platformHolds: { youtube: { since: 1 } }, subtitleDeferrals: { v1: { count: 0, lastAt: 1, until: 2, channelSlug: "a" } }, }, }), ); const old = await readAutoQueueState(paths); assert.deepEqual(old.download.platformPace, { rumble: { sleepRequestsSeconds: 2, baseSeconds: 0, cleanUnits: 0, steppedAt: 0 }, }); assert.deepEqual(old.download.platformHolds, {}); assert.deepEqual(old.download.subtitleDeferrals, {}); }); }); // --- write safety ------------------------------------------------------------ test("overlapping writes do not collide on the tmp file", async () => { await withPaths(async (paths) => { // persist() is fire-and-forget and two lanes' runners can persist within // milliseconds of each other. With one pid-named tmp the second write // truncated the first's temp and one rename found it gone (ENOENT). await Promise.all( Array.from({ length: 12 }, (_, i) => { const state = emptyAutoQueueState(); state.download.platformBackoff = { youtube: { until: i, fails: i } }; return writeAutoQueueState(paths, state); }), ); // Every write landed and nothing threw; the last ISSUED is on disk (the // writes are chained per path) and no temp is left behind. const back = await readAutoQueueState(paths); assert.deepEqual(back.download.platformBackoff, { youtube: { until: 11, fails: 11 }, }); assert.deepEqual(await readdir(path.dirname(paths.autoQueueStateFile)), [ "state.json", ]); }); });