import { test } from "node:test"; import assert from "node:assert/strict"; import { evaluateTranscribingVideo, evaluateTranscriptionActivity, workerContendsForGpu, } from "./digestYield"; import { defaultDigest, sanitizeDigest } from "../lib/settings"; import type { JobTask } from "../jobs/registry"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test common/controller/digestYield.test.ts // This box, exactly: one worker on the engine's default device plus two pinned to // CPU, all `kind: "local"` and all enabled. Reproducing it here is the point — the // bug was invisible to a single-worker mental model. const GPU = { kind: "local" as const, device: undefined }; const CPU_A = { kind: "local" as const, device: "cpu" }; const CPU_B = { kind: "local" as const, device: "cpu" }; const CUDA = { kind: "local" as const, device: "cuda:0" }; const REMOTE = { kind: "remote" as const, device: undefined }; function busy(w: T) { return { ...w, busy: true }; } function idle(w: T) { return { ...w, busy: false }; } test("only an explicit cpu device is non-contending", () => { // The fix: explicit "cpu" competes for zero GPU shaders. assert.equal(workerContendsForGpu(CPU_A, false), false); // Trimmed and case-folded: settings.json is hand-editable. assert.equal( workerContendsForGpu({ ...CPU_A, device: " CPU " }, false), false, ); // A device we did not set is the engine binary's OWN default, which may be the // GPU. The unknown case must fail SAFE, i.e. still contend. assert.equal(workerContendsForGpu(GPU, false), true); assert.equal( workerContendsForGpu({ kind: "local", device: "" }, false), true, ); assert.equal(workerContendsForGpu(CUDA, false), true); // A remote worker runs on another machine and competes for nothing here. assert.equal(workerContendsForGpu(REMOTE, false), false); // Opting in restores the old blanket behaviour for every local worker. assert.equal(workerContendsForGpu(CPU_A, true), true); assert.equal(workerContendsForGpu(REMOTE, true), false); }); // THE CLAIM OF THE FIX, both directions. Two CPU-pinned transcriptions running // while the GPU worker sits idle must NOT stop the digest lane under the default, // and flipping the setting must restore exactly today's behaviour. test("busy CPU workers do not stall the digest lane by default", () => { const workers = [idle(GPU), busy(CPU_A), busy(CPU_B)]; // A real transcription batch IS running while those CPU workers work — that is // what made the naive queued-job signal keep the bug alive. const activity = evaluateTranscriptionActivity({ workers, transcriptionJobRunning: true, yieldToCpuWorkers: false, }); assert.deepEqual(activity, { busy: false, reason: null }); const optedIn = evaluateTranscriptionActivity({ workers, transcriptionJobRunning: true, yieldToCpuWorkers: true, }); assert.deepEqual(optedIn, { busy: true, reason: "local-worker" }); }); test("a busy GPU worker still stops the digest lane", () => { for (const yieldToCpuWorkers of [false, true]) { assert.deepEqual( evaluateTranscriptionActivity({ workers: [busy(GPU), idle(CPU_A)], transcriptionJobRunning: true, yieldToCpuWorkers, }), { busy: true, reason: "local-worker" }, `default-device worker must contend (yieldToCpuWorkers=${yieldToCpuWorkers})`, ); assert.deepEqual( evaluateTranscriptionActivity({ workers: [busy(CUDA), busy(CPU_A)], transcriptionJobRunning: true, yieldToCpuWorkers, }), { busy: true, reason: "local-worker" }, `explicit cuda worker must contend (yieldToCpuWorkers=${yieldToCpuWorkers})`, ); } }); // The gap signal is why this file has two signals at all: between worker // acquisitions (audio extraction, model load) the pool shows nothing busy while // the card may well be loading a model. Narrowing the CPU case must not cost it. test("a running job with no busy worker still covers the acquisition gap", () => { assert.deepEqual( evaluateTranscriptionActivity({ workers: [idle(GPU), idle(CPU_A), idle(CPU_B)], transcriptionJobRunning: true, yieldToCpuWorkers: false, }), { busy: true, reason: "queued-job" }, ); // …and an idle box yields to nothing. assert.deepEqual( evaluateTranscriptionActivity({ workers: [idle(GPU)], transcriptionJobRunning: false, yieldToCpuWorkers: false, }), { busy: false, reason: null }, ); }); // A busy REMOTE worker is not local activity, so the gap signal is still the one // that speaks — the remote's own box arbitrates its own card. test("a busy remote worker does not suppress the gap signal", () => { assert.deepEqual( evaluateTranscriptionActivity({ workers: [busy(REMOTE), idle(GPU)], transcriptionJobRunning: true, yieldToCpuWorkers: false, }), { busy: true, reason: "queued-job" }, ); }); // Absence must fall to OFF, and it must not disturb the master switch's own // default. The two fields use opposite idioms on purpose. test("yieldToCpuWorkers defaults off; yieldToTranscription defaults on", () => { assert.equal(defaultDigest().yieldToCpuWorkers, false); assert.equal(defaultDigest().yieldToTranscription, true); // A settings file written before either field existed. const legacy = sanitizeDigest({}); assert.equal(legacy.yieldToCpuWorkers, false); assert.equal(legacy.yieldToTranscription, true); // Explicit values survive, in both directions, for both fields. assert.equal( sanitizeDigest({ yieldToCpuWorkers: true }).yieldToCpuWorkers, true, ); assert.equal( sanitizeDigest({ yieldToCpuWorkers: false }).yieldToCpuWorkers, false, ); assert.equal( sanitizeDigest({ yieldToTranscription: false }).yieldToTranscription, false, ); // Garbage is not truthy-coerced into opting in. for (const v of ["true", 1, {}, [], null]) { assert.equal( sanitizeDigest({ yieldToCpuWorkers: v }).yieldToCpuWorkers, false, `${JSON.stringify(v)} must not enable yieldToCpuWorkers`, ); } }); // --------------------------------------------------------------------------- // evaluateTranscribingVideo — the narrower question: is a transcription running // on THIS video? The backfill's cleanup asks it before unlinking a re-acquired // audio file, so a false positive leaks a file and a false negative kills a run. // A registry task as the registry actually stores it (label/startedAt are set by // addTask); only `kind` and `id` are consulted. function task(kind: JobTask["kind"], id: string): JobTask { return { id, label: id, kind, startedAt: 0 }; } const TRANSCRIBE_TASK = task("transcribe", "vid1"); const DOWNLOAD_TASK = task("download", "vid1"); test("a running job with a transcribe task on this video counts", () => { assert.equal( evaluateTranscribingVideo( [{ status: "running", tasks: [TRANSCRIBE_TASK] }], "vid1", ), true, ); }); test("a DOWNLOAD task on the same video does not count", () => { // Same id, different lane: a download holds no engine open on the audio, and // treating it as one would keep media the backfill should have removed. assert.equal( evaluateTranscribingVideo( [{ status: "running", tasks: [DOWNLOAD_TASK] }], "vid1", ), false, ); }); test("a finished job does not count, whatever its tasks say", () => { // Tasks are cleared when a job ends, but a record read mid-transition must not // be trusted on `tasks` alone. assert.equal( evaluateTranscribingVideo( [{ status: "done", tasks: [TRANSCRIBE_TASK] }], "vid1", ), false, ); }); test("another video's transcription does not count", () => { assert.equal( evaluateTranscribingVideo( [{ status: "running", tasks: [task("transcribe", "vid2")] }], "vid1", ), false, ); }); test("no jobs, or a running job with no tasks, is false", () => { assert.equal(evaluateTranscribingVideo([], "vid1"), false); assert.equal( evaluateTranscribingVideo([{ status: "running", tasks: undefined }], "vid1"), false, ); });