commit 40f37dd04fbbc7c4b8079406709e3a7f944a1c3a
parent 8e4a088be66a168b35c3f2a3af66f13972c89e83
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 11 Sep 2026 12:03:58 -0400
editor: the focus banner and the generated tree, asserted on a lane console
S4's spec. One document changes three things about /operations/<lane> at once
and this is what says so: the banner appears with the site's TITLE and a held
count, the ladder's rungs are the COMPILED ids (`prio-focus-*`, `prio-normal-*`,
`prio-all`) with the pending counts landing on them, and the tree editor is
read-only while the lane's own switches are not.
Clearing the document puts all three back, and that is the half that matters
most: every corpus has no priorities set until one does, so "nothing changed"
is the assertion a corpus that never sets one relies on.
The third case is the unresolvable focus — an unknown siteId 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. It also
pins the distinction the banner turns on: 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.
THE SEEDED SETTINGS CARRY THE COMPILED ROOTS ALONGSIDE THE MODEL, because that
is what lands on disk — S3's one writer persists `channelPriority` and all four
`autoQueue[lane].root` trees in a single save, and seeding the model alone would
be a state no writer produces. The roots are spelled out here rather than
imported from the compiler: the ids and the group order ARE the contract this
page renders, so writing them asserts them a second time instead of re-deriving
them from the code under test.
The digest lane is the fixture lane: it is the one whose work list a spec can
seed from a transcript alone, and the one lane-runner.spec already drives.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 326 insertions(+), 0 deletions(-)
diff --git a/editor/e2e/focus-banner.spec.ts b/editor/e2e/focus-banner.spec.ts
@@ -0,0 +1,326 @@
+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/<lane> 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: <name> (N channels) · <focus>
+// pending in this lane · <rest> 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<string, unknown> = {}) {
+ 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);
+});