import { mkdir, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { pathExists, resetData, resolvePath, generateReport } from "./helpers"; // yt-dlp names Twitch VOD dirs `v` (its extractor id) while the app keys // videos by the canonical URL id ``, so the audio/metadata land in // data/v/ and the app-written download-outcome.json lands in data//. // generateChannelSnapshot reconciles those dirs back to the canonical layout. // // This used to be triggered by merely OPENING the channel page, which ran a // full channel analysis inside a GET (multi-minute on a large channel). The // page now shows a "no report yet" state and asks, so the test asks too — the // subject here was always reconcileVideoDirs, not what happened to invoke it. const CHANNEL = "twitch-reconcile"; const ROOT = `test-transcripts/channels/${CHANNEL}`; async function write(rel: string, contents: string): Promise { const full = resolvePath(`${ROOT}/${rel}`); await mkdir(full.slice(0, full.lastIndexOf("/")), { recursive: true }); await writeFile(full, contents); } async function seedChannel(): Promise { await write( "config.json", JSON.stringify({ handling: "transcribe", name: "Twitch Reconcile", url: "https://www.twitch.tv/reconcile/videos", platform: "twitch", }), ); // Case 1 — merge: bytes in v123, sidecar already in canonical 123. await write( "data/v123/metadata.info.json", JSON.stringify({ id: "v123", webpage_url: "https://www.twitch.tv/videos/123" }), ); await write("data/v123/audio.mp3", "fake audio 123"); await write( "data/123/download-outcome.json", JSON.stringify({ videoId: "123", status: "ok" }), ); // Case 2 — rename: bytes in v456, no canonical dir yet. await write( "data/v456/metadata.info.json", JSON.stringify({ id: "v456", webpage_url: "https://www.twitch.tv/videos/456" }), ); await write("data/v456/audio.mp3", "fake audio 456"); // Case 3 — orphan with no metadata: can't determine canonical id, left alone. await write( "data/orphan999/download-outcome.json", JSON.stringify({ videoId: "orphan999", status: "failed" }), ); } test("channel snapshot reconciles Twitch v dirs into canonical dirs", async ({ page, }) => { await resetData(null); await seedChannel(); // Deliberately NOT pre-generated: this test is about what generating does. // The page renders normally with an empty report and a banner offering to // build one — which is what runs reconcileVideoDirs. await page.goto(`/channels/${CHANNEL}`); await expect( page.getByRole("heading", { name: "Twitch Reconcile" }), ).toBeVisible({ timeout: 15_000 }); await expect(page.getByTestId("no-report-banner")).toBeVisible(); await generateReport(page, CHANNEL); // The reconcile is the observable effect; poll for it rather than racing the // button's own status text. await expect .poll(() => pathExists(`${ROOT}/data/123/audio.mp3`), { timeout: 15_000 }) .toBe(true); // Case 1: v123 merged into 123 — all three files now in 123, v123 gone. expect(await pathExists(`${ROOT}/data/123/audio.mp3`)).toBe(true); expect(await pathExists(`${ROOT}/data/123/metadata.info.json`)).toBe(true); expect(await pathExists(`${ROOT}/data/123/download-outcome.json`)).toBe(true); expect(await pathExists(`${ROOT}/data/v123`)).toBe(false); // Case 2: v456 renamed to 456. expect(await pathExists(`${ROOT}/data/456/audio.mp3`)).toBe(true); expect(await pathExists(`${ROOT}/data/456/metadata.info.json`)).toBe(true); expect(await pathExists(`${ROOT}/data/v456`)).toBe(false); // Case 3: metadata-less orphan left untouched. expect( await pathExists(`${ROOT}/data/orphan999/download-outcome.json`), ).toBe(true); });