import { test, expect } from "@playwright/test"; import { channelStage, generateReport, readJson, resetData } from "./helpers"; // THE SOFT BLOCK. Measured on a real channel: after ~100 good records YouTube // answered "Video unavailable" for 545 CONSECUTIVE videos, all of which fetched // fine when probed individually minutes later. Believing that would write 545 // `deleted` errors, each suppressing a re-scan for a day — the scan would teach // itself that most of the channel is gone. // // The fixture is the same shape, small: one genuinely members-only video, two // readable ones, then a run of soft-blocked ids. const CHANNEL = "test-softblock"; const ROOT = `test-transcripts/channels/${CHANNEL}`; type MetadataScan = { entries: Record; errors: Record; lastRun: { scanned: number; errors: number; stopped?: string; message?: string } | null; }; test("a run of \"Video unavailable\" stops the scan and records none of it", async ({ page, }) => { test.setTimeout(180_000); await resetData("metadata-scan-softblock"); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "playlist")); await page.getByRole("button", { name: "Scan metadata" }).click(); const log = page.getByLabel("Scan metadata output"); await expect(log).toContainText("STOPPED", { timeout: 60_000 }); await expect(log).toContainText('consecutive "Video unavailable" errors'); await expect(log).toContainText("A per-platform cooldown has been recorded"); const scan = await readJson(`${ROOT}/metadata-scan.json`); expect(scan.lastRun?.stopped).toBe("rate_limit"); // The two readable videos are kept — a stopped run keeps what it learned. expect(Object.keys(scan.entries).sort()).toEqual([ "guestvid0001", "guestvid0002", ]); // NOT ONE soft-blocked id is recorded. That is the whole point: recording them // would mark 20 public videos deleted and suppress re-reading them for a day. for (const id of Object.keys(scan.errors)) { expect(id).not.toContain("softblock"); } // The members-only video is a real, permanent failure and is NOT thrown away // with the streak — it is not part of it, and it never will be fetchable. expect(scan.errors.membersonly01?.class).toBe("members_only"); // The cooldown is real, not just a log line: the next scan refuses to start. // A refusal is a NOTICE, not an error and not log output — the action returns // `info: true` and StreamActionLog renders it as a status beside the button. await page.getByRole("button", { name: "Scan metadata" }).click(); await expect(page.getByLabel("Scan metadata notice")).toContainText( "rate-limit cooldown", { timeout: 30_000 }, ); });