import { mkdir, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, readJson, resetData, resolvePath, } from "./helpers"; import { baseUrl } from "./baseUrl"; // The transcribe fixture's channel slug. const SLUG = "test-transcribe"; // Persist a video's source container into the saved-video store: write the // container under test-transcripts/saved-videos/// and a pointer // sidecar back in the data dir. Mirrors persistSourceVideo's on-disk result. async function seedSavedVideo( videoId: string, bytes: number, sha256?: string, ): Promise { const storeDir = resolvePath( `test-transcripts/saved-videos/${SLUG}/${videoId}`, ); await mkdir(storeDir, { recursive: true }); await writeFile(`${storeDir}/source-media.mp4`, "x".repeat(bytes)); const pointer = { storedAt: "2026-06-01T00:00:00.000Z", dir: storeDir, file: "source-media.mp4", bytes, keepReason: "keep-latest", ...(sha256 ? { sha256 } : {}), }; await writeFile( resolvePath(`test-transcripts/channels/${SLUG}/data/${videoId}/saved-video.json`), JSON.stringify(pointer, null, 2) + "\n", ); await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); } test("Saved Videos page lists persisted source videos per channel", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedSavedVideo("vidA", 2048, "a".repeat(64)); await page.goto("/saved-videos"); await expect( page.getByRole("heading", { name: "Saved videos", level: 1 }), ).toBeVisible(); // Summary cards: one saved video, one channel, one with checksum. const summary = page.getByLabel("store summary"); await expect(summary).toContainText("Saved videos"); // Per-channel table row links to the channel and reports the saved count. const byChannel = page.getByLabel("per-channel saved videos"); await expect(byChannel.getByRole("link", { name: SLUG })).toBeVisible(); await expect(byChannel).toContainText("1/1"); // checksummed }); test("Saved Videos page saves the backup configuration", async ({ page }) => { await resetData("one-transcribe-channel-with-audio"); await page.goto("/saved-videos"); const dest = page.getByLabel("backup destination"); await dest.fill("/tmp/ttb-e2e-backup-dest"); await page.getByLabel("scheduled backup enabled").check(); await page.getByRole("button", { name: "Save backup config" }).click(); await expect(page.getByLabel("backup config saved")).toBeVisible(); // The setting persisted to test-settings.json. const settings = await readJson<{ savedVideoBackup?: { dest?: string; enabled?: boolean }; }>("test-settings.json"); expect(settings.savedVideoBackup?.dest).toBe("/tmp/ttb-e2e-backup-dest"); expect(settings.savedVideoBackup?.enabled).toBe(true); // The run/verify buttons become enabled once a dest is configured. await expect( page.getByRole("button", { name: "Back up now" }), ).toBeEnabled(); }); test("channel Cleanup stage shows the retention & persistence section", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedSavedVideo("vidA", 1024); await generateReport(page, SLUG); await page.goto(channelStage(SLUG, "cleanup")); const section = page.getByLabel("retention and persistence section"); await expect(section).toBeVisible(); await expect(section).toContainText("Saved source videos"); await expect( section.getByRole("button", { name: "Check kept videos" }), ).toBeVisible(); await expect( section.getByRole("button", { name: "Persist kept now" }), ).toBeVisible(); }); test("video page shows persisted source status and an unpersist control", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedSavedVideo("vidA", 4096); await page.goto(`/channels/${SLUG}/videos/vidA`); await expect( page.getByLabel("unpersist source video vidA"), ).toBeVisible(); }); // The persist run writes saved-video.json and StreamActionLog then calls // router.refresh() ITSELF, which re-renders this section with the pointer now on // disk. The section used to early-return the persisted view at that point, // unmounting the log — and an unmounted StreamActionLog renders no //
 at all, so the operator's log does not go stale, it
// disappears. The assertion order is the point: read the log only AFTER the
// persisted view is on screen, so the refresh has provably landed. See
// plans/FACTS.md, "A run log lives in the panel's React state".
test("the persist run's log survives the refresh that lands the pointer", async ({
  page,
}) => {
  // Three waits in sequence (the run, the pointer landing, the log) — without
  // this the config's 30 s budget expires first and the failure reads as a test
  // timeout instead of naming the assertion that actually failed.
  test.setTimeout(120_000);
  await resetData("one-transcribe-channel-with-audio");
  // redownloadToArchiveAction resolves the source URL from metadata.info.json
  // or the playlist, and this fixture ships neither.
  await writeFile(
    resolvePath(`test-transcripts/channels/${SLUG}/playlist`),
    "https://www.youtube.com/watch?v=vidA\n",
  );
  await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {});

  await page.goto(`/channels/${SLUG}/videos/vidA`);
  // Nothing is persisted yet, so the card starts closed.
  await page.getByLabel("Source video stage summary").click();
  // exact: the persisted view's "unpersist source video vidA" button matches
  // this name as a substring once the run has landed the pointer.
  const run = page.getByRole("button", {
    name: "Persist source video",
    exact: true,
  });
  await expect(run).toBeEnabled();
  await run.click();

  const log = page.getByLabel("Persist source video for vidA output");
  await expect(log).toContainText("Re-downloading vidA to archive", {
    timeout: 15_000,
  });

  // The state the run produces: the pointer is on disk, so the panel draws the
  // persisted view. That is the proof the refresh landed.
  await expect(page.getByLabel("unpersist source video vidA")).toBeVisible({
    timeout: 15_000,
  });
  // And the log is STILL on screen, carrying the run's closing line.
  await expect(log).toContainText("Persisted source video to");
  // Kept for its log, not for a second run: the container is in the store now.
  await expect(run).toBeDisabled();
});