import { mkdir, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { resetData, resolvePath } from "./helpers"; // The /jobs screen reads `.log` files in TRANSCRIPTS_DIR/.jobs and enriches any // not in the in-memory registry from a `.meta.json` sidecar (see // common/jobs/jobMeta.ts + listJobs.ts). resetData() wipes the dir and resets // the registry, so writing a log + sidecar pair gives a deterministic // "archived" row — exercising both the metadata-read path and giving rows with // known kinds/statuses for the client-side filter bar. const JOBS_DIR = "test-transcripts/.jobs"; type SeedJob = { id: string; kind: string; status: "done" | "failed" | "cancelled"; channelSlug?: string; videoId?: string; queueKey: string; queuedAt: number; startedAt?: number; endedAt?: number; exitCode?: number; // Written by the boot pass (common/jobs/bootQueuedJobs.ts) on a job a // restart left queued. cancelReason?: string; }; async function seedJob(job: SeedJob): Promise { const dir = resolvePath(JOBS_DIR); await mkdir(dir, { recursive: true }); // listAllJobs enumerates `.log` files; the sidecar enriches each one. await writeFile(resolvePath(`${JOBS_DIR}/${job.id}.log`), `log for ${job.id}\n`); await writeFile( resolvePath(`${JOBS_DIR}/${job.id}.meta.json`), JSON.stringify(job), ); } function rowById(page: import("@playwright/test").Page, id: string) { return page.getByRole("row").filter({ hasText: id }); } test("archived jobs keep their metadata from the sidecar", async ({ page }) => { await resetData(); await seedJob({ id: "meta-sync-1", kind: "sync", status: "done", channelSlug: "alpha", queueKey: "alpha", queuedAt: 1000, startedAt: 1000, endedAt: 3000, exitCode: 0, }); await page.goto("/jobs"); const row = rowById(page, "meta-sync-1"); // Kind, status and the real duration all come from the sidecar — without it // an archived row would show no kind and a 0ms (mtime-based) duration. await expect(row).toContainText("sync"); await expect(row).toContainText("done"); await expect(row).toContainText("2.0s"); await expect(row.getByRole("link", { name: "alpha" })).toHaveAttribute( "href", "/channels/alpha", ); }); // Release 10 (L2): a job the boot pass closed says WHY, on its row and on its // page — the reason used to be only in the meta and the log's last line. test("a job the boot pass cancelled shows its reason on /jobs and on its page", async ({ page, }) => { await resetData(); const reason = "server restarted; the scheduler re-derives syncs"; await seedJob({ id: "boot-cancelled-1", kind: "sync", status: "cancelled", channelSlug: "alpha", queueKey: "platform:youtube", queuedAt: 1000, endedAt: 2000, cancelReason: reason, }); // A cancel with no recorded reason (a person pressed Cancel) draws none. await seedJob({ id: "plain-cancelled-1", kind: "sync", status: "cancelled", channelSlug: "alpha", queueKey: "platform:youtube", queuedAt: 1000, startedAt: 1000, endedAt: 2000, }); await page.goto("/jobs"); const row = rowById(page, "boot-cancelled-1"); await expect(row).toContainText("cancelled"); await expect(row.getByTestId("cancel-reason")).toHaveText(reason); await expect( rowById(page, "plain-cancelled-1").getByTestId("cancel-reason"), ).toHaveCount(0); await page.goto("/jobs/boot-cancelled-1"); const cell = page.getByTestId("cancel-reason"); await expect(cell).toContainText("Cancelled because"); await expect(cell).toContainText(reason); await page.goto("/jobs/plain-cancelled-1"); await expect(page.getByText("Exit code")).toBeVisible(); await expect(page.getByTestId("cancel-reason")).toHaveCount(0); }); test("filters hide refresh-report by default, persist, and reset", async ({ page, }) => { await resetData(); await seedJob({ id: "rr-1", kind: "refresh-report", status: "done", channelSlug: "alpha", queueKey: "alpha", queuedAt: 3000, startedAt: 3000, endedAt: 3500, exitCode: 0, }); await seedJob({ id: "sync-1", kind: "sync", status: "done", channelSlug: "beta", queueKey: "beta", queuedAt: 2000, startedAt: 2000, endedAt: 2400, exitCode: 0, }); await seedJob({ id: "wh-1", kind: "whisper-video", status: "failed", channelSlug: "beta", videoId: "v1", queueKey: "beta", queuedAt: 1000, startedAt: 1000, endedAt: 1800, exitCode: 1, }); await page.goto("/jobs"); // Default: refresh-report hidden, the rest shown, summary reflects 1 hidden. await expect(rowById(page, "rr-1")).toHaveCount(0); await expect(rowById(page, "sync-1")).toBeVisible(); await expect(rowById(page, "wh-1")).toBeVisible(); await expect(page.getByText(/1 hidden/)).toBeVisible(); // Reveal refresh-report via its (struck-through) kind chip. await page.getByRole("button", { name: "refresh-report" }).click(); await expect(rowById(page, "rr-1")).toBeVisible(); // Choice persists across a reload (localStorage). await page.reload(); await expect(rowById(page, "rr-1")).toBeVisible(); // Status filter: hiding "failed" drops the whisper-video row. await page.getByRole("button", { name: "failed" }).click(); await expect(rowById(page, "wh-1")).toHaveCount(0); await expect(rowById(page, "sync-1")).toBeVisible(); // Search narrows to a channel: only the alpha (refresh-report) job matches. const search = page.getByLabel("Search jobs"); await search.fill("alpha"); await expect(rowById(page, "rr-1")).toBeVisible(); await expect(rowById(page, "sync-1")).toHaveCount(0); // Reset restores defaults: refresh-report hidden again, failed shown, no query. await page.getByRole("button", { name: "Reset filters" }).click(); await expect(rowById(page, "rr-1")).toHaveCount(0); await expect(rowById(page, "wh-1")).toBeVisible(); await expect(search).toHaveValue(""); });