import { mkdir, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, resetData, resolvePath, } from "./helpers"; async function setQueueViaCustom( page: import("@playwright/test").Page, actionLabel: string, value: string, ) { await page .getByLabel(`queue for ${actionLabel}`, { exact: true }) .selectOption({ label: "Custom…" }); await page .getByLabel(`custom queue for ${actionLabel}`, { exact: true }) .fill(value); await page .getByRole("button", { name: `save custom queue for ${actionLabel}` }) .click(); } test("default queue is platform:", async ({ page }) => { await resetData("slow-pipeline-channel"); await generateReport(page, "slow-channel"); await page.goto(channelStage("slow-channel", "playlist")); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await page.goto("/jobs"); const firstRow = page.getByRole("row").nth(1); // 0 is header await expect(firstRow).toContainText("platform:youtube"); }); test("Sync is refused while its platform is in a rate-limit cooldown", async ({ page, }) => { await resetData("slow-pipeline-channel"); // Seed a live download cooldown for youtube (the slow-channel platform) — as // if auto-download or a prior sync had just hit a 429. A manual Sync must // refuse rather than re-storm the source. const until = Date.now() + 5 * 60_000; const stateFile = resolvePath("test-transcripts/.auto-queue/state.json"); await mkdir(resolvePath("test-transcripts/.auto-queue"), { recursive: true }); await writeFile( stateFile, JSON.stringify({ transcription: { runtime: {}, picks: [], platformBackoff: {} }, download: { runtime: {}, picks: [], platformBackoff: { youtube: { until, fails: 1 } }, }, }), ); await generateReport(page, "slow-channel"); await page.goto(channelStage("slow-channel", "playlist")); await page.getByRole("button", { name: "Sync" }).click(); // The refusal is a neutral notice (info), naming the platform + cooldown. await expect(page.getByLabel("Sync notice")).toContainText( /rate-limit cooldown/i, { timeout: 10_000 }, ); // No job was created (refused before enqueue) — the /jobs table has no // slow-channel row. await page.goto("/jobs"); await expect( page.getByRole("row").filter({ hasText: "slow-channel" }), ).toHaveCount(0); }); test("unknown platform infers its queue from the URL domain", async ({ page, }) => { await resetData("empty"); await generateReport(page, "new"); await page.goto("/channels/new"); await page.getByLabel(/^name/i).fill("Vimeo Channel"); await page.getByLabel(/^slug/i).fill("vimeo-test"); await page.getByLabel(/^url/i).fill("https://player.vimeo.com/video/12345"); await page.getByRole("button", { name: /create channel/i }).click(); await page.waitForURL("**/channels/vimeo-test", { timeout: 10_000 }); // Creation lands on the overview; Sync's queue control is on the Playlist // stage. await page.goto(channelStage("vimeo-test", "playlist")); // No known platform → queue keyed by the registrable domain (subdomain // stripped), not the catch-all platform:unknown. await expect( page.getByLabel("queue for Sync", { exact: true }), ).toHaveValue("platform:vimeo.com"); }); test("queues a second job in the same queue, runs sequentially", async ({ page, }) => { await resetData("two-slow-channels"); await generateReport(page, "slow-a"); await page.goto(channelStage("slow-a", "playlist")); await setQueueViaCustom(page, "Sync", "qShared"); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await generateReport(page, "slow-b"); await page.goto(channelStage("slow-b", "playlist")); await page .getByLabel("queue for Sync", { exact: true }) .selectOption({ label: "qShared" }); await page.getByRole("button", { name: "Sync" }).click(); // Second job should display the queued banner. await expect(page.getByLabel("Sync queue status")).toContainText("qShared", { timeout: 10_000, }); await page.goto("/jobs"); // Two body rows, both in queue qShared. Most recent first. const row0 = page.getByRole("row").nth(1); const row1 = page.getByRole("row").nth(2); await expect(row0).toContainText("qShared"); await expect(row1).toContainText("qShared"); // slow-a (queued first) currently running. slow-b (queued second) queued. const slowARow = page.getByRole("row").filter({ hasText: "slow-a" }).first(); const slowBRow = page.getByRole("row").filter({ hasText: "slow-b" }).first(); await expect(slowARow.getByText("running", { exact: true })).toBeVisible(); await expect(slowBRow.getByText("queued", { exact: true })).toBeVisible(); // Cancel the running job; the queued one advances to running. await slowARow.getByRole("button", { name: /^Cancel$/ }).click(); await expect(slowBRow.getByText("running", { exact: true })).toBeVisible({ timeout: 10_000, }); }); test("runs jobs in different queues in parallel", async ({ page }) => { await resetData("two-slow-channels"); await generateReport(page, "slow-a"); await page.goto(channelStage("slow-a", "playlist")); await setQueueViaCustom(page, "Sync", "qA"); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await generateReport(page, "slow-b"); await page.goto(channelStage("slow-b", "playlist")); await setQueueViaCustom(page, "Sync", "qB"); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await page.goto("/jobs"); const slowARow = page.getByRole("row").filter({ hasText: "slow-a" }).first(); const slowBRow = page.getByRole("row").filter({ hasText: "slow-b" }).first(); await expect(slowARow.getByText("running", { exact: true })).toBeVisible(); await expect(slowBRow.getByText("running", { exact: true })).toBeVisible(); }); test("cancels a queued job without disturbing the one running ahead of it", async ({ page, }) => { await resetData("two-slow-channels"); await generateReport(page, "slow-a"); await page.goto(channelStage("slow-a", "playlist")); await setQueueViaCustom(page, "Sync", "qShared"); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await generateReport(page, "slow-b"); await page.goto(channelStage("slow-b", "playlist")); await page .getByLabel("queue for Sync", { exact: true }) .selectOption({ label: "qShared" }); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync queue status")).toBeVisible({ timeout: 10_000, }); await page.goto("/jobs"); const slowBRow = page.getByRole("row").filter({ hasText: "slow-b" }).first(); await expect(slowBRow.getByText("queued", { exact: true })).toBeVisible(); await slowBRow.getByRole("button", { name: /^Cancel$/ }).click(); await expect( slowBRow.getByText("cancelled", { exact: true }), ).toBeVisible({ timeout: 10_000 }); // slow-a (in front) keeps running. const slowARow = page.getByRole("row").filter({ hasText: "slow-a" }).first(); await expect(slowARow.getByText("running", { exact: true })).toBeVisible(); }); test("empty queue runs immediately even when another job is queued in the same name", async ({ page, }) => { await resetData("two-slow-channels"); // Park slow-a's Sync into qShared and start it. await generateReport(page, "slow-a"); await page.goto(channelStage("slow-a", "playlist")); await setQueueViaCustom(page, "Sync", "qShared"); await page.getByRole("button", { name: "Sync" }).click(); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); // slow-b sets queue to immediate (the default empty option) and starts; // it should bypass qShared and run right away. await generateReport(page, "slow-b"); await page.goto(channelStage("slow-b", "playlist")); await page .getByLabel("queue for Sync", { exact: true }) .selectOption({ label: "(immediate)" }); await page.getByRole("button", { name: "Sync" }).click(); // No "queued" banner: it goes straight to running and produces output. await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await page.goto("/jobs"); const slowARow = page.getByRole("row").filter({ hasText: "slow-a" }).first(); const slowBRow = page.getByRole("row").filter({ hasText: "slow-b" }).first(); await expect(slowARow.getByText("running", { exact: true })).toBeVisible(); await expect(slowBRow.getByText("running", { exact: true })).toBeVisible(); await expect(slowBRow).toContainText("(immediate)"); });