import { stat } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { buildIndex, channelStage, generateReport, pathExists, readJson, resetData, resolvePath, writeSite, } from "./helpers"; const CHANNEL = "availability-test"; const FIXTURE = "availability-baseline"; const VIDEOS = [ { id: "vidpublic1", expected: "public" }, { id: "vidpublic2", expected: "public" }, { id: "viddeleted1", expected: "deleted" }, { id: "vidprivate1", expected: "private" }, { id: "vidmembers1", expected: "members_only" }, { id: "vidneedsauth1", expected: "needs_auth" }, ]; type AvailabilityFile = { checkedAt: string; availability: string; webpageUrl?: string; error?: string; history?: Array<{ availability: string; observedAt: string; source: string }>; }; function availabilityPath(id: string): string { return `test-transcripts/channels/${CHANNEL}/data/${id}/availability.json`; } async function readAvailability(id: string): Promise { return readJson(availabilityPath(id)); } test("resume checks classify each fixture video into its expected bucket", async ({ page, }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); const log = page.getByLabel("Resume availability checks output"); await expect(log).toContainText("attempted=6", { timeout: 20_000 }); await expect(log).toContainText("Availability check done"); for (const v of VIDEOS) { expect(await pathExists(availabilityPath(v.id))).toBe(true); const record = await readAvailability(v.id); expect(record.availability).toBe(v.expected); expect(typeof record.checkedAt).toBe("string"); // Each first-time check appends exactly one history entry (source "check"). expect(record.history?.length).toBe(1); expect(record.history?.[0]).toMatchObject({ availability: v.expected, source: "check", }); } }); test("resume is idempotent: a second run skips all videos", async ({ page }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); const log = page.getByLabel("Resume availability checks output"); await expect(log).toContainText("attempted=6", { timeout: 20_000 }); await page.getByRole("button", { name: "Resume checks" }).click(); await expect(log).toContainText("attempted=0 checked=0 skipped=6", { timeout: 20_000, }); }); test("resume log breaks down already-checked vs unchecked counts", async ({ page, }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); const log = page.getByLabel("Resume availability checks output"); // First run: nothing checked yet. await expect(log).toContainText("0 already checked, 6 unchecked", { timeout: 20_000, }); await expect(log).toContainText("concurrency=1"); await expect(log).toContainText("attempted=6", { timeout: 20_000 }); // Second run: all six have sidecars from the first run. await page.getByRole("button", { name: "Resume checks" }).click(); await expect(log).toContainText("6 already checked, 0 unchecked", { timeout: 20_000, }); }); test("recheck non-deleted skips deleted but advances other checkedAt", async ({ page, }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); await expect( page.getByLabel("Resume availability checks output"), ).toContainText("attempted=6", { timeout: 20_000 }); const deletedBefore = await readAvailability("viddeleted1"); const publicBefore = await readAvailability("vidpublic1"); // Wait long enough for ISO timestamps to differ. await page.waitForTimeout(20); await page.getByRole("button", { name: "Recheck non-deleted" }).click(); await expect( page.getByLabel("Recheck non-deleted availability output"), ).toContainText("attempted=5 checked=5 skipped=1", { timeout: 20_000 }); const deletedAfter = await readAvailability("viddeleted1"); const publicAfter = await readAvailability("vidpublic1"); expect(deletedAfter.checkedAt).toBe(deletedBefore.checkedAt); expect(deletedAfter.availability).toBe("deleted"); expect(publicAfter.checkedAt).not.toBe(publicBefore.checkedAt); expect(publicAfter.availability).toBe("public"); }); test("recheck all re-probes every video including deleted", async ({ page }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); await expect( page.getByLabel("Resume availability checks output"), ).toContainText("attempted=6", { timeout: 20_000 }); const before = new Map(); for (const v of VIDEOS) { const r = await readAvailability(v.id); before.set(v.id, r.checkedAt); } await page.waitForTimeout(20); await page.getByRole("button", { name: "Recheck all" }).click(); await expect( page.getByLabel("Recheck all availability output"), ).toContainText("attempted=6 checked=6 skipped=0", { timeout: 20_000 }); for (const v of VIDEOS) { const after = await readAvailability(v.id); expect(after.checkedAt).not.toBe(before.get(v.id)); expect(after.availability).toBe(v.expected); } }); test("build index marks deleted videos in the export summaries", async ({ page, }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); await expect( page.getByLabel("Resume availability checks output"), ).toContainText("attempted=6", { timeout: 20_000 }); // Add a transcript for the public video so it appears in the index. // (buildIndex requires a transcript.en.vtt or transcript.json.) const { writeFile } = await import("node:fs/promises"); await writeFile( resolvePath( `test-transcripts/channels/${CHANNEL}/data/vidpublic1/transcript.en.vtt`, ), "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nhello\n", ); await writeFile( resolvePath( `test-transcripts/channels/${CHANNEL}/data/viddeleted1/transcript.en.vtt`, ), "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nhello deleted\n", ); await writeSite("testsite", { channels: [{ slug: CHANNEL, groupId: "default" }], }); await buildIndex(page); const pageJson = await readJson< Array<{ id: string; isDeleted: boolean }> >("test-transcripts/.export-index/sites/testsite/summaries/page-0000.json"); const byId = new Map(pageJson.map((s) => [s.id, s])); expect(byId.get("vidpublic1")?.isDeleted).toBe(false); expect(byId.get("viddeleted1")?.isDeleted).toBe(true); }); // Ensure the on-disk file matches the in-memory test data shape so future // schema changes break this test rather than silently breaking the search UI. test("availability.json shape matches the type contract", async ({ page }) => { await resetData(FIXTURE); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "diagnostics")); await page.getByRole("button", { name: "Resume checks" }).click(); await expect( page.getByLabel("Resume availability checks output"), ).toContainText("attempted=6", { timeout: 20_000 }); const record = await readAvailability("vidpublic1"); expect(record.availability).toBe("public"); expect(record.webpageUrl).toBe("https://www.youtube.com/watch?v=vidpublic1"); expect(record.error).toBeUndefined(); // The sidecar should be tiny. const stats = await stat(resolvePath(availabilityPath("vidpublic1"))); expect(stats.size).toBeLessThan(500); });