import { test, expect } from "@playwright/test"; import type { Page } from "@playwright/test"; import { generateReport, resetData, writeChannelConfig, writeDigestVideo, writeSettings, writeSite, } from "./helpers"; // THE FOCUS BANNER AND THE GENERATED TREE, on a lane console. // // One document — `settings.channelPriority` — changes three things about // /operations/ at once, and this spec is what says so: // // 1. THE BANNER. A focus is a statement nothing else on the page can make: // the ladder can be full and the count in the thousands while most of it is // held behind a group at the top. "Focus: (N channels) · // pending in this lane · waiting behind it · M channels held". // 2. THE LADDER IS THE COMPILED TREE. The status payload used to ship the // STORED policy, and with a model set the lane does not dispatch from it — // so the rungs are `prio-focus-*` / `prio-normal-*` / `prio-all`, keyed by // the compiled ids, which is what makes the counts land on them. // 3. THE EDITOR IS READ-ONLY. Editing a rung could not change dispatch (the // compiler wins), so the controls that would rewrite the tree are disabled // and the ones that would add or remove a node are gone. // // And clearing the document puts all three back exactly as they were, which is // the half that matters most: every corpus has no priorities set until one does. // // THE SEEDED SETTINGS CARRY THE COMPILED ROOTS TOO, because that is what lands // on disk — S3's one writer persists `channelPriority` and the four // `autoQueue[lane].root` trees in a single save. Seeding the model alone would // be a state no writer produces. const FOCUSED = "focus-chan"; const OTHER = "other-chan"; const SITE = "focusite"; const SITE_TITLE = "Focus Site"; const SLOW = 120_000; type Leaf = { id: string; match: { type: string; value?: string }; weight: number; maxWorkers: number | null; }; type Group = { id: string; mode: string; weight: number; maxWorkers: number | null; children: (Group | Leaf)[]; }; // `compileLaneRoot`'s output, spelled out rather than imported: the ids and the // group order ARE the contract this page renders, so writing them here asserts // them a second time instead of re-deriving them from the code under test. function channelLeaf(tier: string, slug: string): Leaf { return { id: `prio-${tier}-${slug}`, match: { type: "channel", value: slug }, weight: 1, maxWorkers: null, }; } function tierGroup(tier: string, slugs: string[]): Group { return { id: `prio-${tier}`, mode: "strict", weight: 1, maxWorkers: null, children: slugs.map((slug) => channelLeaf(tier, slug)), }; } function compiledRoot(lane: string, focus: string[], normal: string[]): Group { const children: (Group | Leaf)[] = []; if (focus.length > 0) children.push(tierGroup("focus", focus)); if (normal.length > 0) children.push(tierGroup("normal", normal)); children.push({ id: "prio-all", match: { type: "all" }, weight: 1, maxWorkers: null, }); return { id: `${lane}-root`, mode: "strict", weight: 1, maxWorkers: null, children, }; } // One hand-authored catch-all, the shape a corpus with no priorities carries. const CATCH_ALL: Group = { id: "root", mode: "strict", weight: 1, maxWorkers: null, children: [ { id: "all", match: { type: "all" }, weight: 1, maxWorkers: null }, ], }; // The digest lane is the one whose work list a spec can seed from a transcript // alone (writeDigestVideo), and the one lane-runner.spec already drives. function settingsDoc(over: Record = {}) { return { adminTitle: "Test Admin", maxTranscriptPageBytes: 8388608, sleepBetweenDownloadsSeconds: 0, minFreeDiskGB: 0, verifyAvailabilityBeforeClean: false, syncScheduler: { fullSweepIntervalMinutes: 0 }, digest: { localAppId: "ollama-direct", remoteAppId: "claude-code", sections: ["chapters"], yieldToTranscription: false, }, ...over, }; } async function seed(page: Page) { await resetData(null); await writeChannelConfig(FOCUSED); await writeChannelConfig(OTHER); await writeDigestVideo({ channelSlug: FOCUSED, videoId: "focusvid0001" }); await writeDigestVideo({ channelSlug: OTHER, videoId: "othervid0001" }); await writeSite(SITE, { siteTitle: SITE_TITLE, channels: [{ slug: FOCUSED }], }); // The snapshot is the lane's WORK LIST — nothing is pending until it exists. await writeSettings( settingsDoc({ autoQueue: { digest: { enabled: true, maxWorkers: 1, root: CATCH_ALL } }, }), ); await generateReport(page, FOCUSED); await generateReport(page, OTHER); } // The lane's console, hydrated. Every assertion below reads state React put // there, so waiting for the section rather than for a timeout is the difference // between a spec and a race. async function openDigestConsole(page: Page) { await page.goto("/operations/digest"); await expect(page.locator('section[data-lane="digest"]')).toHaveAttribute( "data-hydrated", "true", { timeout: 30_000 }, ); } test("a site focus banners the lane, compiles the ladder and freezes the editor", async ({ page, }) => { test.setTimeout(SLOW); await seed(page); // No focus yet: the page is the page it has always been. await openDigestConsole(page); await expect(page.locator("[data-focus-banner]")).toHaveCount(0); await expect( page.getByRole("button", { name: "+ Channel rule" }).first(), ).toBeVisible(); // THE DOCUMENT, AND THE TREES IT COMPILES TO, in one write — which is what // S3's single writer puts on disk. await writeSettings( settingsDoc({ channelPriority: { focus: { kind: "site", siteId: SITE }, channels: {}, }, autoQueue: { digest: { enabled: true, maxWorkers: 1, root: compiledRoot("digest", [FOCUSED], [OTHER]), }, }, }), ); await openDigestConsole(page); // (1) THE BANNER. The site's TITLE, not its id — the model stores a siteId and // the name is resolved on the server. const banner = page.locator('[data-focus-banner="digest"]'); await expect(banner).toBeVisible(); await expect(banner).toContainText(`Focus: ${SITE_TITLE} (1 channel)`); await expect(banner).toContainText("1 pending in this lane"); await expect(banner).toContainText("1 waiting behind it"); // THE HELD COUNT. One non-focus channel has work it is not getting, because // strict descent never reaches the group it is in while the focus group has // anything. This is the display fact the banner exists for. await expect(banner).toContainText("1 channel held"); await expect(banner).toHaveAttribute("data-focus-holding", "true"); // (2) THE LADDER IS THE COMPILED TREE, keyed by the compiled ids. await expect(page.locator('[data-node-id="prio-focus"]')).toHaveCount(1); await expect( page.locator(`[data-node-id="prio-focus-${FOCUSED}"]`), ).toHaveCount(1); await expect( page.locator(`[data-node-id="prio-normal-${OTHER}"]`), ).toHaveCount(1); await expect(page.locator('[data-node-id="prio-all"]')).toHaveCount(1); // The counts landed on the compiled leaves, which is the whole reason the // payload had to stop shipping the stored tree: a leaf the runner does not // have would read zero. await expect( page .locator(`[data-node-id="prio-focus-${FOCUSED}"]`) .getByRole("button", { name: /Show pending videos for rule/ }), ).toHaveText(/1/); // (3) THE EDITOR IS READ-ONLY — the tree only. The rules cannot be rewritten, // added to or removed from. await expect( page.getByRole("button", { name: "+ Channel rule" }), ).toHaveCount(0); await expect(page.getByRole("button", { name: "Remove" })).toHaveCount(0); await expect(page.getByLabel("group mode").first()).toBeDisabled(); await expect(page.getByLabel("rule match type").first()).toBeDisabled(); await expect( page.locator('[data-policy-compiled="true"]'), ).toHaveCount(1); // And only the tree: the lane's own switches still belong to this page. await expect(page.getByLabel("video order for auto-digest")).toBeEnabled(); // THE PROSE FOLLOWS THE STATE. "How priority works" is a disclosure, so it is // opened rather than read through it. The phrase asserted is the one only the // disclosure carries: the ladder's own note opens with the same sentence by // design — it states the same fact as a state rather than as a rule — so a // shorter match resolves to both and fails strict mode. await page.getByText("How priority works", { exact: false }).click(); await expect( page.getByText("All four lanes are compiled from that one document"), ).toBeVisible(); }); test("clearing the priority document puts the console back exactly as it was", async ({ page, }) => { test.setTimeout(SLOW); await seed(page); await writeSettings( settingsDoc({ channelPriority: { focus: { kind: "site", siteId: SITE }, channels: {}, }, autoQueue: { digest: { enabled: true, maxWorkers: 1, root: compiledRoot("digest", [FOCUSED], [OTHER]), }, }, }), ); await openDigestConsole(page); await expect(page.locator('[data-focus-banner="digest"]')).toBeVisible(); // Focus ended, priorities cleared, the hand-authored tree back. await writeSettings( settingsDoc({ autoQueue: { digest: { enabled: true, maxWorkers: 1, root: CATCH_ALL } }, }), ); await openDigestConsole(page); await expect(page.locator("[data-focus-banner]")).toHaveCount(0); await expect(page.locator('[data-node-id="prio-focus"]')).toHaveCount(0); await expect(page.locator('[data-node-id="all"]')).toHaveCount(1); await expect( page.getByRole("button", { name: "+ Channel rule" }).first(), ).toBeVisible(); await expect(page.getByLabel("rule match type").first()).toBeEnabled(); await expect(page.locator('[data-policy-compiled="false"]')).toHaveCount(1); }); test("a focus that resolves to nothing banners nothing and compiles no group", async ({ page, }) => { test.setTimeout(SLOW); await seed(page); // An unknown siteId resolves to no channels, which compiles NO focus group — // deliberately, so a typo leaves the tree as it would be with no focus rather // than holding the whole corpus behind a site that does not exist. await writeSettings( settingsDoc({ channelPriority: { focus: { kind: "site", siteId: "not-a-site" }, channels: {}, }, autoQueue: { digest: { enabled: true, maxWorkers: 1, root: compiledRoot("digest", [], [FOCUSED, OTHER]), }, }, }), ); await openDigestConsole(page); await expect(page.locator("[data-focus-banner]")).toHaveCount(0); await expect(page.locator('[data-node-id="prio-focus"]')).toHaveCount(0); // The document still says something, so the tree is still GENERATED and the // editor still read-only — an unresolvable focus is not an absent document. await expect( page.locator(`[data-node-id="prio-normal-${FOCUSED}"]`), ).toHaveCount(1); await expect( page.getByRole("button", { name: "+ Channel rule" }), ).toHaveCount(0); });