import { mkdir, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { baseUrl } from "./baseUrl"; import { channelStage, resetData, resolvePath, writeChannelConfig, writeSettings, } from "./helpers"; // THE CHANNEL LINE. // // The hero of the channel page: stations are stages, the gap between two // stations carries the shortfall. Three of the four cases below are invariants // this codebase has paid for once already — // // * a pre-`eligible` snapshot must render "—", never "0%". A third of the // snapshots on disk are months old and predate the denominator; a 0 there // reads as "nothing digested" on a fully digested channel. // * a lane that is switched OFF is neutral, not "ok" and not amber. An empty // work list because a feature is off is not the same as being finished. // * `deferred` / `blocked` / `missingInput` are OFF the line and must never be // added to a gap's reachable count. // // — and the fourth (the promoted bottleneck) is the whole reason the line is // drawn as a line. const SLUG = "line-chan"; const ROOT = `test-transcripts/channels/${SLUG}`; type Entry = Record; // Settings with the diarization backfill on or off. The models are /dev/null // placeholders — nothing here runs the lane, it only has to be ENABLED so that // backfillLaneOperations reports it. function laneSettings(enabled: boolean) { return { adminTitle: "Test Admin", maxTranscriptPageBytes: 8388608, sleepBetweenDownloadsSeconds: 0, minFreeDiskGB: 0, verifyAvailabilityBeforeClean: false, syncScheduler: { fullSweepIntervalMinutes: 0 }, diarization: { enabled, inlineAfterTranscribe: false, threshold: 0.5, threads: 1, python: "python3", segModel: "/dev/null", embModel: "/dev/null", concurrency: 1, }, backfill: { concurrency: 1, allowRedownload: false, }, // The lane's gate followed its `enabled` flag when that flag was the // inverted `backfill.enabled`; S0-pause deleted the field, so the same // thing is said on the lane. autoQueue: { backfill: { held: !enabled } }, }; } async function seed(snapshot: Record) { await resetData(); await writeChannelConfig(SLUG, { handling: "transcribe", platform: "youtube", name: "Line Channel", lastSyncedAt: new Date().toISOString(), }); await mkdir(resolvePath(`${ROOT}/data`), { recursive: true }); // The playlist file is the first station's denominator — one small read, and // the only thing on this page that is not the snapshot. await writeFile( resolvePath(`${ROOT}/playlist`), Array.from({ length: 125 }, (_, i) => `https://youtu.be/v${i}`).join("\n") + "\n", ); await writeFile( resolvePath(`${ROOT}/snapshot.json`), JSON.stringify( { generatedAt: new Date().toISOString(), totals: { videos: 100, transcribed: 40, downloaded: 60 }, buckets: {}, undownloadedIds: [], ...snapshot, }, null, 2, ), ); await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); } // A per-kind entry as an OLD build wrote it: no `eligible`, no `deferred`, no // `blocked`. This is the shape most snapshots on disk are still in. function legacyEntry(patch: Entry = {}): Entry { return { ids: [], missing: 0, stale: 0, missingInput: 0, ...patch }; } test("stations show their counts and coverage against the right denominators", async ({ page, }) => { await seed({}); await page.goto(`/channels/${SLUG}`); const line = page.getByLabel("channel line"); await expect(line).toBeVisible(); // Playlist: 100 known of the 125 the playlist file names. await expect(line.getByLabel("Playlist through")).toHaveText("100"); await expect(line.getByLabel("Playlist coverage", { exact: true })).toContainText("80%"); // Download: 60 of the 100 known. await expect(line.getByLabel("Download through")).toHaveText("60"); await expect(line.getByLabel("Download coverage", { exact: true })).toContainText("60%"); // Transcribe's denominator is what was DOWNLOADED, not what is known: an // undownloaded video is not eligible for transcription. await expect(line.getByLabel("Transcribe through")).toHaveText("40"); await expect(line.getByLabel("Transcribe coverage", { exact: true })).toContainText("67%"); }); test("a pre-`eligible` snapshot renders an em dash, not 0%", async ({ page, }) => { await seed({ backfill: { digest: legacyEntry({ missing: 5 }), diarization: legacyEntry({ missing: 3 }), }, }); await page.goto(`/channels/${SLUG}`); const line = page.getByLabel("channel line"); await expect(line.getByLabel("Digest through")).toHaveText("—"); await expect(line.getByLabel("Digest coverage", { exact: true })).toHaveText("—"); // NOT "Backfill": the station is named after the operations it holds, not // after the queue key they happen to share. This fixture runs on the default // settings, where no speaker operation is enabled — so the honest label is // the derived generic one, and it cannot claim "Speakers". await expect(line.getByLabel("Derived data through")).toHaveText("—"); // The negative assertion is the point: a 0% here is the bug. await expect(line.getByLabel("Digest coverage", { exact: true })).not.toContainText("0%"); }); test("the largest gap is promoted and the rest recede", async ({ page }) => { await seed({ undownloadedIds: ["a", "b"], backfill: { digest: { ids: [], missing: 1675, stale: 0, partial: 0, missingInput: 0, deferred: 0, blocked: 0, eligible: 1797, }, }, }); await page.goto(`/channels/${SLUG}`); const line = page.getByLabel("channel line"); // 1,675 to digest dwarfs the 2 to download, so the digest gap is the promoted // one — exactly one gap carries the marker. await expect(line.locator("[data-bottleneck='true']")).toHaveCount(1); await expect(line.locator("[data-bottleneck='true']")).toHaveAttribute( "data-gap", "digest", ); await expect(line.getByLabel("to digest shortfall")).toContainText("1,675"); await expect(line.getByLabel("to download shortfall")).toContainText("2"); }); test("deferred and blocked hang off the line, never inside the shortfall", async ({ page, }) => { await seed({ backfill: { digest: { ids: ["a", "b", "c", "d"], missing: 2, stale: 1, partial: 1, missingInput: 900, deferred: 40, blocked: 1631, eligible: 3000, }, }, }); await page.goto(`/channels/${SLUG}`); const line = page.getByLabel("channel line"); // missing + stale + partial. NOT + 900 + 40 + 1631. await expect(line.getByLabel("to digest shortfall")).toContainText("4"); await expect(line.getByLabel("to digest shortfall")).not.toContainText("2,575"); // The excluded populations are present — on the other axis. await expect( line.getByLabel("waiting on a transcript siding"), ).toContainText("1,631"); }); test("a lane that is switched off reads neutral, not finished", async ({ page, }) => { const laneWork = { backfill: { diarization: { ids: ["a", "b"], missing: 7, stale: 0, partial: 0, missingInput: 0, deferred: 0, blocked: 0, eligible: 50, }, }, }; // writeSettings AFTER seed: seed() calls resetData(), which restores the // default settings file. // Lane ON: the recorded work is real work, on the line. await seed(laneWork); await writeSettings(laneSettings(true)); await page.goto(`/channels/${SLUG}`); await expect( page.getByLabel("channel line").getByLabel("to speakers shortfall"), ).toContainText("7"); await expect( page.getByLabel("channel line").getByLabel("Speakers through"), ).toHaveText("43"); // Lane OFF, same snapshot: nothing is offered, and coverage is unknown rather // than complete. The station must not go green on a feature nobody enabled. await seed(laneWork); await writeSettings(laneSettings(false)); await page.goto(`/channels/${SLUG}`); const line = page.getByLabel("channel line"); // With nothing enabled the label falls back to the derived generic name — // operationsGroupLabel over an empty set. It cannot claim "Speakers" when no // speaker operation is on. await expect(line.getByLabel("Derived data through")).toHaveText("—"); await expect(line.getByLabel("to speakers shortfall")).toContainText("0"); // …and the stage says why rather than claiming completion. await page.goto(channelStage(SLUG, "speakers")); await expect(page.getByLabel("Derived data stage summary")).toContainText( "Nothing here is enabled", ); }); test("the next action is the furthest upstream gap, and vanishes when clean", async ({ page, }) => { await seed({ undownloadedIds: ["a", "b"] }); await page.goto(`/channels/${SLUG}`); const action = page.getByLabel("next action"); await expect(action).toBeVisible(); await expect(action.getByRole("button")).toHaveText(/Download missing/); // A channel with nothing outstanding offers nothing. await seed({ totals: { videos: 10, transcribed: 10, downloaded: 10 } }); await writeFile( resolvePath(`${ROOT}/playlist`), Array.from({ length: 10 }, (_, i) => `https://youtu.be/v${i}`).join("\n") + "\n", ); await page.goto(`/channels/${SLUG}`); await expect(page.getByLabel("next action")).toHaveCount(0); }); test("the attention strip says 'all clear' rather than disappearing", async ({ page, }) => { await seed({ totals: { videos: 10, transcribed: 10, downloaded: 10 } }); await page.goto(`/channels/${SLUG}`); const strip = page.getByLabel("attention strip"); await expect(strip).toBeVisible(); await expect(strip).toHaveAttribute("data-attention", "clear"); await seed({ buckets: { needsCookies: ["a", "b", "c", "d"] } }); await page.goto(`/channels/${SLUG}`); const flagged = page.getByLabel("attention strip"); await expect(flagged).toHaveAttribute("data-attention", "flagged"); await expect(flagged.getByLabel("need cookies anomaly")).toContainText("4"); });