import { test } from "node:test"; import assert from "node:assert/strict"; import { defaultSiteSettings, type SiteSettings } from "../lib/settings"; import { withGateHeld } from "../lib/pauseGates"; import type { JobRecord, JobTask } from "../jobs/registry"; import type { WorkerSummary } from "../jobs/workerPool"; import type { RegistryReader, PoolReader } from "./inputs"; import { buildWorkersPayload, type WorkersInputs } from "./workers"; // Run with: node_modules/.bin/tsx --test common/views/workers.test.ts // // The point of the move is that this file exists at all: the payload used to // need a worker pool, a job registry, a settings read and a synchronous disk // read to say anything, so nothing pinned it. Every input below is a literal. function worker(over: Partial = {}): WorkerSummary { return { id: "w1", name: "Worker 1", kind: "local", priority: 0, busy: false, state: "enabled", degraded: false, enabled: true, ...over, }; } function task(over: Partial = {}): JobTask { return { id: "v1", label: "A video", kind: "transcribe", startedAt: 1_000, ...over, }; } function job(over: Partial = {}): JobRecord { return { id: "j1", kind: "transcribe", queueKey: "transcribe", status: "running", queuedAt: 0, logPath: "/dev/null", ...over, }; } function inputs(over: { jobs?: JobRecord[]; workers?: WorkerSummary[]; paused?: boolean; canStopPartial?: (id: string) => boolean; settings?: SiteSettings; workerDefaults?: WorkersInputs["workerDefaults"]; }): WorkersInputs { const jobs = over.jobs ?? []; const registry: RegistryReader = { list: () => jobs, get: (id: string) => jobs.find((j) => j.id === id), }; const pool: PoolReader = { summary: () => over.workers ?? [], isPaused: () => over.paused ?? false, canStopPartial: over.canStopPartial ?? (() => false), }; return { registry, pool, settings: over.settings ?? defaultSiteSettings(), workerDefaults: over.workerDefaults ?? null, }; } test("tasks are grouped by workerId, and only running transcribe tasks count", () => { const payload = buildWorkersPayload( inputs({ workers: [worker({ id: "a" }), worker({ id: "b", name: "Worker 2" })], jobs: [ job({ id: "j1", channelSlug: "chan", tasks: [ task({ id: "v1", workerId: "a" }), task({ id: "v2", workerId: "b" }), // Same worker as the first: both land in a's list. task({ id: "v3", workerId: "a", fraction: 0.5 }), // Not a transcription — a download task carries no worker. task({ id: "v4", kind: "download", workerId: "a" }), // A transcribe task with no worker yet (queued behind a slot). task({ id: "v5" }), ], }), // A queued job's tasks are not in flight; a done job's are stale. job({ id: "j2", status: "queued", tasks: [task({ id: "v6", workerId: "b" })], }), job({ id: "j3", status: "done", tasks: [task({ id: "v7", workerId: "b" })], }), ], }), ); const byId = new Map(payload.workers.map((w) => [w.id, w])); assert.deepEqual( byId.get("a")?.tasks.map((t) => t.id), ["v1", "v3"], ); assert.deepEqual( byId.get("b")?.tasks.map((t) => t.id), ["v2"], ); // The channel comes off the JOB, not the task — that is the only reason the // fold walks jobs rather than a flat task list. assert.equal(byId.get("a")?.tasks[0].channelSlug, "chan"); assert.equal(byId.get("a")?.tasks[1].fraction, 0.5); }); test("a worker with nothing running gets an empty list, not undefined", () => { const payload = buildWorkersPayload(inputs({ workers: [worker()] })); assert.deepEqual(payload.workers[0].tasks, []); }); test("canStopPartial is asked per worker", () => { const asked: string[] = []; const payload = buildWorkersPayload( inputs({ workers: [worker({ id: "a" }), worker({ id: "b" })], canStopPartial: (id) => { asked.push(id); return id === "b"; }, }), ); assert.deepEqual(asked, ["a", "b"]); assert.equal(payload.workers[0].canStopPartial, false); assert.equal(payload.workers[1].canStopPartial, true); }); test("defaultEnabledIds is null until a default has been saved", () => { // Null and [] are different answers: null is "no default has ever been // saved" (the button says Set as default), [] is "the saved default is // nothing enabled". assert.equal( buildWorkersPayload(inputs({ workerDefaults: null })).defaultEnabledIds, null, ); assert.deepEqual( buildWorkersPayload(inputs({ workerDefaults: { enabledWorkerIds: [] } })) .defaultEnabledIds, [], ); assert.deepEqual( buildWorkersPayload( inputs({ workerDefaults: { enabledWorkerIds: ["a", "b"] } }), ).defaultEnabledIds, ["a", "b"], ); }); test("paused is the live pool; downloadsPaused is the persisted download gate", () => { const free = buildWorkersPayload(inputs({ paused: true })); assert.equal(free.paused, true); assert.equal(free.downloadsPaused, false); const held = buildWorkersPayload( inputs({ paused: false, settings: withGateHeld(defaultSiteSettings(), "download", true), }), ); assert.equal(held.paused, false); assert.equal(held.downloadsPaused, true); // The two gates are independent, and holding the TRANSCRIPTION lane must not // read as downloads paused. const other = buildWorkersPayload( inputs({ settings: withGateHeld(defaultSiteSettings(), "transcription", true), }), ); assert.equal(other.downloadsPaused, false); });