import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, readJson, resetData, resolvePath, } from "./helpers"; // A DIAGNOSTICS RETRY KEEPS ITS LOG WHEN ITS BUCKET EMPTIES (release 13 slice // W1) — the Diagnostics twin of cookies-mode.spec's "defer" test (release 11 // slice O3), which closed the same hole on the Download stage. // // A retry empties its own bucket, and the run refreshes the page after every // video it fetches (recordTaskDone → the snapshot regen → the pulse → // AutoRefresh), then once more when it ends. Both Diagnostics grids used to // filter an emptied bucket's card out — `populated` for the channel-health // cards, `listed` for the availability cards — and the card took the // RetryBucketControl, its StreamActionLog and the run's log with it. Each test // below waits for the refreshed, EMPTY list, which renders only inside a card // that survived the refresh: on the old code that wait is what fails, every // time, rather than the log check racing the refresh. const CHANNEL = "availability-test"; const FIXTURE = "availability-baseline"; const ROOT = `test-transcripts/channels/${CHANNEL}`; type Snapshot = { buckets?: { noMetadata?: string[] }; availability?: { byStatus?: Record }; }; async function writePlaylist(ids: string[]): Promise { // retry-bucket reads the channel playlist to learn each id's URL. await writeFile( resolvePath(`${ROOT}/playlist`), ids.map((id) => `https://www.youtube.com/watch?v=${id}\n`).join(""), ); } async function writeAvailability(id: string, availability: string) { const path = resolvePath(`${ROOT}/data/${id}/availability.json`); await mkdir(dirname(path), { recursive: true }); await writeFile( path, JSON.stringify({ checkedAt: new Date().toISOString(), availability, webpageUrl: `https://www.youtube.com/watch?v=${id}`, }), ); } async function waitForSnapshot( read: (snap: Snapshot) => number | undefined, size: number, ): Promise { await expect .poll( async () => { const snap = await readJson(`${ROOT}/snapshot.json`).catch( () => null, ); return snap ? (read(snap) ?? -1) : -1; }, { timeout: 15_000 }, ) .toBe(size); } test("channel health: a Retry that empties Missing metadata keeps its card and its log", async ({ page, }) => { test.setTimeout(90_000); await resetData(FIXTURE); // A video directory with nothing in it: no metadata.info.json, so it is in // the retryable "Missing metadata.info.json" bucket. The retry's download // writes the metadata, which is what empties the bucket. const ID = "vidnometa1"; await mkdir(resolvePath(`${ROOT}/data/${ID}`), { recursive: true }); await writePlaylist([ID]); await generateReport(page, CHANNEL); await waitForSnapshot((s) => s.buckets?.noMetadata?.length, 1); await page.goto(channelStage(CHANNEL, "diagnostics")); const card = page.getByLabel("retry missing metadata bucket"); await card.getByRole("button", { name: /^Retry \(1\)$/ }).click(); const log = page.getByLabel("Retry missing metadata output"); await expect(log).toContainText("Retry allowlist: kept 1 of 1", { timeout: 30_000, }); await expect(log).toContainText("Managed download complete", { timeout: 30_000, }); // The bucket empties once the snapshot regenerates… await waitForSnapshot((s) => s.buckets?.noMetadata?.length, 0); // …and the page refreshes onto the EMPTY list, inside the card that must // have survived that refresh, with the run's log still in it. await expect(page.getByLabel("missing metadata empty")).toBeVisible({ timeout: 20_000, }); await expect( page.getByRole("heading", { name: "Missing metadata.info.json (0)" }), ).toBeVisible(); await expect(log).toContainText("Managed download complete"); await expect(card.getByRole("button", { name: /^Retry \(0\)$/ })).toBeDisabled(); // "All clear." is what the grid used to fall back to once its only card went. await expect(page.getByLabel("channel health empty")).toHaveCount(0); // A fresh page has nothing to show for an empty bucket. await page.reload(); await expect(page.getByLabel("channel health empty")).toBeVisible(); await expect(page.getByLabel("retry missing metadata bucket")).toHaveCount(0); }); test("availability: a Needs auth card whose bucket empties during its Retry keeps its log", async ({ page, }) => { test.setTimeout(90_000); await resetData(FIXTURE); const ID = "vidneedsauth1"; await writeAvailability(ID, "needs_auth"); await writePlaylist([ID]); await generateReport(page, CHANNEL); await waitForSnapshot((s) => s.availability?.byStatus?.needs_auth?.length, 1); await page.goto(channelStage(CHANNEL, "diagnostics")); const card = page.getByLabel("retry needs auth bucket"); await expect(card.getByRole("button", { name: /^Retry \(1\)$/ })).toBeVisible(); // A retry never rewrites availability.json — a download records its // availability history only, with the top level untouched — so what empties // an availability bucket is a PROBE. Land one now, as a check running beside // the retry would: the page still shows the snapshot it rendered from, and // the regeneration the retry's own download triggers is the first to read it. await writeAvailability(ID, "public"); await card.getByRole("button", { name: /^Retry \(1\)$/ }).click(); const log = page.getByLabel("Retry needs auth output"); await expect(log).toContainText("Managed download complete", { timeout: 30_000, }); await waitForSnapshot((s) => s.availability?.byStatus?.needs_auth?.length, 0); await expect(page.getByLabel("availability needs_auth empty")).toBeVisible({ timeout: 20_000, }); await expect(page.getByRole("heading", { name: "Needs auth (0)" })).toBeVisible(); await expect(log).toContainText("Managed download complete"); await expect(card.getByRole("button", { name: /^Retry \(0\)$/ })).toBeDisabled(); await page.reload(); await expect(page.getByLabel("availability public count")).toBeVisible(); await expect(page.getByLabel("retry needs auth bucket")).toHaveCount(0); });