import { test } from "node:test"; import assert from "node:assert/strict"; import type { ChannelBrief, ChannelStat, } from "../controller/channels"; import type { ChannelConfig } from "../lib/channelConfig"; import type { ChannelSnapshot } from "../controller/channelSnapshot"; import type { OperationSnapshotEntry } from "../lib/operations"; import type { SiteSettings } from "../lib/settings"; import type { Site } from "../lib/site"; import { normalizeBuckets } from "./pipeline/stageStatus"; import { buildChannelGroupSections, slugsInGroup, stationWorkFor, transcribeStationIds, type ChannelGroupSection, } from "./channelGroupSections"; // Run with the rest of the common suite (`pnpm --filter yt-dlp-transcript-common // test`), or alone from common/: // pnpm exec tsx --test views/channelGroupSections.test.ts // Snapshot shape as WRITTEN TO DISK — same trick as channelFlow.test.ts. function snapshotOf(patch: Partial = {}): ChannelSnapshot { return { generatedAt: "2026-08-01T00:00:00.000Z", totals: { videos: 100, transcribed: 40, downloaded: 60 }, buckets: normalizeBuckets(undefined), undownloadedIds: [], ...patch, }; } function entry(patch: Partial): OperationSnapshotEntry { return { ids: [], missing: 0, stale: 0, missingInput: 0, ...patch, } as OperationSnapshotEntry; } function channel( slug: string, config: Partial = {}, snapshot: ChannelSnapshot | null = snapshotOf(), ): { stat: ChannelStat; brief: ChannelBrief } { const merged: ChannelConfig = { handling: "transcribe", url: `https://example.test/${slug}`, ...config, }; return { stat: { slug, config: merged, playlistCount: 0, videoCount: 0, transcriptCount: 0, downloadCount: 0, }, brief: { slug, config: merged, snapshot }, }; } function siteOf(patch: Partial = {}): Site { return { siteId: "alpha", siteTitle: "Alpha", siteDescription: "", headerTitle: "Alpha", homeTagline: "", groups: [], defaultGroupId: "default", channels: [], ...patch, }; } // backfillLaneOperations / allOperations only read these two branches, so a // partial cast exercises the real registry predicates. function settingsOf(opts: { diarization?: boolean; attribution?: boolean; }): SiteSettings { return { diarization: { enabled: opts.diarization === true, segModel: "seg.onnx", embModel: "emb.onnx", }, attribution: { enabled: opts.attribution === true, textOnlyEnabled: opts.attribution === true, diarizedEnabled: opts.attribution === true, }, } as unknown as SiteSettings; } const LANE_ON = settingsOf({ diarization: true }); const LANE_OFF = settingsOf({}); function build( site: Site, channels: { stat: ChannelStat; brief: ChannelBrief }[], settings: SiteSettings = LANE_ON, ): ChannelGroupSection[] { return buildChannelGroupSections( site, channels.map((c) => c.stat), channels.map((c) => c.brief), settings, ); } const GROUPS = [ { id: "extended", name: "Extended Universe", selectedByDefault: false, order: 2 }, { id: "archives", name: "Archives", selectedByDefault: true, order: 1 }, ]; test("groups render in sortGroups order, and empty groups are omitted", () => { const a = channel("arch-a"); const b = channel("ext-b"); const sections = build( siteOf({ groups: [ ...GROUPS, { id: "guests", name: "Guest Appearances", selectedByDefault: true, order: 3 }, ], defaultGroupId: "archives", channels: [ { slug: "arch-a", groupId: "archives" }, { slug: "ext-b", groupId: "extended" }, ], }), [a, b], ); assert.deepEqual( sections.map((s) => s.group.id), ["archives", "extended"], ); assert.deepEqual(sections[0].channels.map((c) => c.slug), ["arch-a"]); }); test("an unknown groupId folds onto the site's default group", () => { const sections = build( siteOf({ groups: GROUPS, defaultGroupId: "archives", channels: [ { slug: "stray", groupId: "no-such-group" }, { slug: "ext-b", groupId: "extended" }, ], }), [channel("stray"), channel("ext-b")], ); assert.deepEqual( sections.map((s) => s.group.id), ["archives", "extended"], ); assert.deepEqual(sections[0].channels.map((c) => c.slug), ["stray"]); }); test("a site with no configured groups gets the one fallback section", () => { const sections = build( siteOf({ channels: [{ slug: "only" }] }), [channel("only")], ); assert.equal(sections.length, 1); assert.equal(sections[0].group.id, "default"); assert.equal(sections[0].group.name, "All channels"); }); test("within a group, membership order wins and absent order sorts last", () => { const sections = build( siteOf({ channels: [ { slug: "no-order" }, { slug: "second", order: 2 }, { slug: "first", order: 1 }, ], }), [channel("no-order"), channel("second"), channel("first")], ); assert.deepEqual(sections[0].channels.map((c) => c.slug), [ "first", "second", "no-order", ]); }); test("a channel with no snapshot lands in `unknown`, so the total is a floor", () => { const sections = build( siteOf({ channels: [{ slug: "reported" }, { slug: "silent" }] }), [ channel("reported", {}, snapshotOf({ undownloadedIds: ["v1", "v2"] })), channel("silent", {}, null), ], ); const { download } = sections[0]; assert.deepEqual(download.eligible.sort(), ["reported", "silent"]); assert.equal(download.total, 2); assert.deepEqual(download.unknown, ["silent"]); }); test("download excludes members-only/deleted/private ids", () => { const sections = build( siteOf({ channels: [{ slug: "c" }] }), [ channel( "c", {}, snapshotOf({ undownloadedIds: ["keep", "gone"], excludedFromDownload: { membersOnly: [], deleted: ["gone"], private: [], }, }), ), ], ); assert.equal(sections[0].download.total, 1); }); test("a youtube-handling channel IS transcribe-eligible: missing auto-subs is whisper work", () => { const sections = build( siteOf({ channels: [{ slug: "yt" }, { slug: "whisper" }] }), [ channel("yt", { handling: "youtube" }, snapshotOf({ undownloadedIds: ["a"], buckets: normalizeBuckets({ downloadedNoTranscript: ["x", "y"] }), })), channel("whisper", { handling: "transcribe" }, snapshotOf({ undownloadedIds: ["b"], buckets: normalizeBuckets({ downloadedNoTranscript: ["z"] }), })), ], ); const { download, transcribe } = sections[0]; assert.deepEqual(download.eligible.sort(), ["whisper", "yt"]); assert.equal(download.total, 2); // Handling does not decide it — the files do. The youtube channel's two // downloaded videos with no transcript at all are what its button queues. assert.deepEqual(transcribe.eligible.sort(), ["whisper", "yt"]); assert.equal(transcribe.total, 3); }); test("the transcribe figure counts downloaded auto-caption-only videos too", () => { const sections = build( siteOf({ channels: [{ slug: "yt" }] }), [ channel("yt", { handling: "youtube" }, snapshotOf({ buckets: normalizeBuckets({ downloadedAutoSubsOnly: ["a"] }), })), ], ); assert.deepEqual(sections[0].transcribe.eligible, ["yt"]); assert.equal(sections[0].transcribe.total, 1); }); test("a youtube channel with nothing to transcribe is eligible at 0 (skipped at click time)", () => { const sections = build( siteOf({ channels: [{ slug: "yt" }] }), [channel("yt", { handling: "youtube" })], ); assert.deepEqual(sections[0].transcribe.eligible, ["yt"]); assert.equal(sections[0].transcribe.total, 0); assert.deepEqual(sections[0].transcribe.unknown, []); }); test("transcribe drops excluded ids from BOTH buckets", () => { const snapshot = snapshotOf({ buckets: normalizeBuckets({ downloadedNoTranscript: ["keep-1", "gone-1"], downloadedAutoSubsOnly: ["keep-2", "gone-2"], }), excludedFromDownload: { membersOnly: ["gone-1"], deleted: [], private: ["gone-2"], }, }); const sections = build( siteOf({ channels: [{ slug: "yt" }] }), [channel("yt", { handling: "youtube" }, snapshot)], ); assert.equal(sections[0].transcribe.total, 2); assert.deepEqual(transcribeStationIds(snapshot), { missing: ["keep-1"], autoSubs: ["keep-2"], }); }); test("an excluded download is neither counted nor named for the batch (the review's walk)", () => { // x, y downloaded with no transcript; a downloaded with auto-captions only; // y went private after it was downloaded. The figure is 2 and the two id // lists the station queues BY ID are exactly [x] and [a] — never y. const snapshot = snapshotOf({ buckets: normalizeBuckets({ downloadedNoTranscript: ["x", "y"], downloadedAutoSubsOnly: ["a"], }), excludedFromDownload: { membersOnly: [], deleted: [], private: ["y"] }, }); const sections = build( siteOf({ channels: [{ slug: "yt" }] }), [channel("yt", { handling: "youtube" }, snapshot)], ); assert.equal(sections[0].transcribe.total, 2); assert.deepEqual(transcribeStationIds(snapshot), { missing: ["x"], autoSubs: ["a"], }); }); test("a social channel is not transcribe-eligible, and says why", () => { const { brief } = channel("posts", { sourceKind: "social", handling: "transcribe" }); assert.deepEqual(stationWorkFor("transcribe", brief, LANE_ON), { eligible: false, work: 0, reason: "social account", }); }); test("a social channel is eligible for sync only", () => { const sections = build( siteOf({ channels: [{ slug: "posts" }] }), [channel("posts", { sourceKind: "social", handling: "transcribe" })], ); const s = sections[0]; assert.deepEqual(s.sync.eligible, ["posts"]); assert.deepEqual(s.download.eligible, []); assert.deepEqual(s.transcribe.eligible, []); assert.deepEqual(s.digest.eligible, []); assert.deepEqual(s.speakers.eligible, []); }); test("sync skips a channel with no url", () => { const sections = build( siteOf({ channels: [{ slug: "ok" }, { slug: "nourl" }] }), [channel("ok"), channel("nourl", { url: undefined })], ); assert.deepEqual(sections[0].sync.eligible, ["ok"]); // Download needs a url too. assert.deepEqual(sections[0].download.eligible.sort(), ["ok"]); }); // The priority document answers the same question the flag does, for the `sync` // operation specifically — which is what makes the migration lossless: a channel // paused for sync alone is still drawn by every other station. test("sync skips a channel paused for the sync operation, and only sync does", () => { const settings = { ...LANE_ON, channelPriority: { focus: { kind: "none" as const }, channels: { pinned: { tier: "normal" as const, overrides: { sync: "paused" as const } }, off: { tier: "paused" as const }, }, }, } as SiteSettings; const sections = build( siteOf({ channels: [{ slug: "ok" }, { slug: "pinned" }, { slug: "off" }], }), [channel("ok"), channel("pinned"), channel("off")], settings, ); assert.deepEqual(sections[0].sync.eligible, ["ok"]); // A `sync` pin moves nothing else: both are still download candidates. assert.deepEqual(sections[0].download.eligible.sort(), ["off", "ok", "pinned"]); }); test("speakers reports `off`, not 0, when no operation on the backfill lane is enabled", () => { const withWork = channel( "c", {}, snapshotOf({ backfill: { diarization: entry({ missing: 7 }) }, }), ); const on = build(siteOf({ channels: [{ slug: "c" }] }), [withWork], LANE_ON); assert.equal(on[0].speakers.laneOff, undefined); assert.equal(on[0].speakers.total, 7); const off = build(siteOf({ channels: [{ slug: "c" }] }), [withWork], LANE_OFF); // The snapshot still records 7 — an off lane is not a finished one, and the // UI must be able to tell the difference. assert.equal(off[0].speakers.laneOff, true); assert.equal(off[0].speakers.total, 0); assert.deepEqual(off[0].speakers.eligible, []); }); test("digest and speakers totals are disjoint", () => { const sections = build( siteOf({ channels: [{ slug: "c" }] }), [ channel( "c", {}, snapshotOf({ backfill: { // The per-kind map carries EVERY catalog operation, digest included. digest: entry({ missing: 1200, stale: 4 }), diarization: entry({ missing: 80, partial: 8 }), }, }), ), ], ); assert.equal(sections[0].digest.total, 1204); assert.equal(sections[0].speakers.total, 88); }); test("digest counts reachable work from the registry entry", () => { const sections = build( siteOf({ channels: [{ slug: "some-work" }] }), [ channel( "some-work", {}, // The per-kind entry is the work list. `blocked` and `deferred` are work // the lane cannot do today and are deliberately not in the total. snapshotOf({ backfill: { digest: entry({ missing: 2, stale: 1, blocked: 4, deferred: 5, ids: ["a", "b", "c"], eligible: 3, }), }, }), ), ], ); assert.equal(sections[0].digest.total, 3); }); test("digest skips a channel whose report says it has no transcripts", () => { const sections = build( siteOf({ channels: [{ slug: "none" }, { slug: "some" }, { slug: "silent" }] }), [ channel("none", {}, snapshotOf({ totals: { videos: 10, transcribed: 0, downloaded: 10 }, })), channel("some", {}, snapshotOf({ totals: { videos: 10, transcribed: 5, downloaded: 10 }, backfill: { digest: entry({ missing: 5 }) }, })), // No report at all cannot claim "no transcripts" — it stays eligible. channel("silent", {}, null), ], ); const { digest } = sections[0]; assert.deepEqual(digest.eligible.sort(), ["silent", "some"]); assert.equal(digest.total, 5); assert.deepEqual(digest.unknown, ["silent"]); }); test("slugsInGroup buckets the same way the sections do", () => { const site = siteOf({ groups: GROUPS, defaultGroupId: "archives", channels: [ { slug: "a", groupId: "archives" }, { slug: "b", groupId: "extended" }, { slug: "c", groupId: "no-such-group" }, { slug: "d" }, ], }); assert.deepEqual(slugsInGroup(site, "archives"), ["a", "c", "d"]); assert.deepEqual(slugsInGroup(site, "extended"), ["b"]); assert.deepEqual(slugsInGroup(site, "guests"), []); }); test("slugsInGroup uses the fallback id when the site configures no groups", () => { const site = siteOf({ channels: [{ slug: "a" }, { slug: "b" }] }); assert.deepEqual(slugsInGroup(site, "default"), ["a", "b"]); assert.deepEqual(slugsInGroup(site, "archives"), []); });