commit 68a1e9112c02db702641a7b619d8e8fabbeab56d
parent 6431abfc9563965f6b322e740a62f23eb34200fd
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 11 Sep 2026 11:43:58 -0400
editor: the /channels priority controls, asserted against what lands on disk
`channel-priority.spec.ts` drives each gesture from the row and checks BOTH
sides of the one writer: the persisted document, and the control after a reload
(which remounts it and re-seeds it from the server). A control that agreed with
itself and not with settings.json would pass a render-only assertion.
The compiled trees are asserted too, because the recompile is half of what the
writer is for: a tier change moves a channel between `prio-normal` and
`prio-low`; a per-operation pin moves it on ONE lane's tree and leaves the other
three alone — which is the whole claim the override map makes; "Sync only"
removes it from all four, because paused is the one thing a tree cannot express;
and a focus puts `prio-focus` FIRST in every lane's strict root, which is what
makes it hold, since `pick()` descends into the first child with work. End focus
takes the group away again.
Focus-by-site writes its own site rather than editing the shared export fixture:
`writeSite` already exists for this, and a site whose membership is a strict
subset of the pool is what makes "held" observable on the other row.
`channels-sort.spec.ts` follows the column rename — the header that read "Sync"
is "Tier", and it is the same eight fixed headers otherwise.
`channel-sync-toggle.spec.ts` is DELETED here rather than in S5, and that is
forced rather than chosen: its only route into `excludeFromSync` was the toggle
this slice removes from the page, so it cannot be kept green by any edit short
of re-adding the control. The writer, the action and the component it drove all
remain for S5, which still owes the field's deletion and the "excluded from Sync
all" assertion's move onto the paused tier — that half needs
`syncAllChannelsAction` to consult the model, which is S5's edit to make.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
4 files changed, 262 insertions(+), 62 deletions(-)
diff --git a/editor/app/channels/components/ChannelsTable.tsx b/editor/app/channels/components/ChannelsTable.tsx
@@ -29,9 +29,6 @@ import {
type StoredChannelTier,
} from "yt-dlp-transcript-common/lib/channelPriority";
-// A row is a stat plus its pipeline bands, in column order. The bands are
-// projected on the server from the same snapshot the counts come from, so a
-// figure in a band and the count beside it cannot disagree.
// One row's share of the channel priority document, resolved on the server.
// The row never reads the model itself: `focused` and `heldReason` are facts
// about the corpus-wide focus selector, which no single row can answer.
@@ -43,6 +40,9 @@ export type ChannelRowPriority = {
heldReason: string | null;
};
+// A row is a stat plus its pipeline bands, in column order. The bands are
+// projected on the server from the same snapshot the counts come from, so a
+// figure in a band and the count beside it cannot disagree.
export type ChannelRow = ChannelStat & {
pipelines: OperationBand[];
// Its tier, its pins, and whether the active focus is holding it. Replaces
diff --git a/editor/e2e/channel-priority.spec.ts b/editor/e2e/channel-priority.spec.ts
@@ -0,0 +1,256 @@
+import { test, expect } from "@playwright/test";
+import { readJson, resetData, writeSite } from "./helpers";
+
+// THE /channels PRIORITY CONTROLS, asserted against what lands on disk.
+//
+// Every one of these gestures goes through `saveChannelPriorityAction`, the one
+// writer of `settings.channelPriority` — so what a row shows and what
+// settings.json says cannot drift, and each test checks BOTH: the persisted
+// document, and the row after a reload (which remounts the control and re-seeds
+// it from the server).
+//
+// The compiled trees are checked too, because the recompile is half of what the
+// writer is for. A focus is a `prio-focus` group at the head of every lane's
+// strict root; ending the focus removes it. Nothing else in dispatch is touched.
+
+type Settings = {
+ channelPriority?: {
+ focus: { kind: string; siteId?: string; slugs?: string[] };
+ channels: Record<
+ string,
+ {
+ tier: string;
+ rank?: number;
+ overrides?: Record<string, string>;
+ }
+ >;
+ };
+ autoQueue?: Record<
+ string,
+ { root: { children: { id: string; children?: { id: string }[] }[] } }
+ >;
+};
+
+const settings = () => readJson<Settings>("test-settings.json");
+
+// Every channel in this fixture, and the pool view: with exactly one site
+// configured /channels scopes to it, and the focus-site test needs a site whose
+// membership is a strict subset of the pool.
+const ALL = "/channels?site=__all__";
+
+test("a row's tier select writes the priority document and survives a reload", async ({
+ page,
+}) => {
+ await resetData("two-slow-channels");
+ await page.goto(ALL);
+
+ const tierA = page.getByLabel("tier for slow-a", { exact: true });
+ await expect(tierA).toHaveValue("normal");
+ await tierA.selectOption("low");
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.channels)
+ .toMatchObject({ "slow-a": { tier: "low" } });
+ // Only the channel that was touched appears: the document is a list of
+ // exceptions, and an entry equal to the default is dropped by the sanitizer.
+ {
+ const s = await settings();
+ expect(Object.keys(s.channelPriority?.channels ?? {})).toEqual(["slow-a"]);
+ }
+
+ await page.reload();
+ await expect(page.getByLabel("tier for slow-a", { exact: true })).toHaveValue(
+ "low",
+ );
+ await expect(page.getByLabel("tier for slow-b", { exact: true })).toHaveValue(
+ "normal",
+ );
+
+ // The compiled tree followed it in the same save: slow-a is in the low group,
+ // slow-b in the normal group, and the catch-all is last.
+ const root = (await settings()).autoQueue?.download.root;
+ expect(root?.children.map((c) => c.id)).toEqual([
+ "prio-normal",
+ "prio-low",
+ "prio-all",
+ ]);
+ expect(
+ root?.children.find((c) => c.id === "prio-low")?.children?.map((c) => c.id),
+ ).toEqual(["prio-low-slow-a"]);
+});
+
+test("Advanced pins one operation, and the pin is what differs from the base", async ({
+ page,
+}) => {
+ await resetData("two-slow-channels");
+ await page.goto(ALL);
+
+ await page.getByLabel("advanced priority for slow-a").click();
+ await page
+ .getByLabel("download override for slow-a")
+ .selectOption("paused");
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.channels["slow-a"])
+ .toEqual({ tier: "normal", overrides: { download: "paused" } });
+
+ await page.reload();
+ await page.getByLabel("advanced priority for slow-a").click();
+ await expect(page.getByLabel("download override for slow-a")).toHaveValue(
+ "paused",
+ );
+ // The base tier is unmoved — a pin is a per-operation fact, not a row one.
+ await expect(page.getByLabel("tier for slow-a", { exact: true })).toHaveValue(
+ "normal",
+ );
+
+ // ONE lane's tree moved, and only one: slow-a is gone from download and still
+ // in normal everywhere else.
+ const s = await settings();
+ const normalOf = (lane: string) =>
+ s.autoQueue?.[lane].root.children
+ .find((c) => c.id === "prio-normal")
+ ?.children?.map((c) => c.id);
+ expect(normalOf("download")).toEqual(["prio-normal-slow-b"]);
+ expect(normalOf("transcription")).toEqual([
+ "prio-normal-slow-a",
+ "prio-normal-slow-b",
+ ]);
+});
+
+test("the Sync only preset is paused everywhere with sync pinned back", async ({
+ page,
+}) => {
+ await resetData("two-slow-channels");
+ await page.goto(ALL);
+
+ await page.getByLabel("advanced priority for slow-a").click();
+ await page.getByLabel("sync only for slow-a").click();
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.channels["slow-a"])
+ .toEqual({ tier: "paused", overrides: { sync: "normal" } });
+
+ await page.reload();
+ await expect(page.getByLabel("tier for slow-a", { exact: true })).toHaveValue(
+ "paused",
+ );
+
+ // A paused channel has no leaf on any lane — paused is the one thing the tree
+ // cannot express, so it is removed from the compiled membership entirely.
+ const s = await settings();
+ for (const lane of ["download", "transcription", "digest", "backfill"]) {
+ const ids = JSON.stringify(s.autoQueue?.[lane].root);
+ expect(ids).not.toContain("slow-a");
+ }
+});
+
+test("Focus site holds the rest, and End focus releases them", async ({
+ page,
+}) => {
+ await resetData("two-slow-channels");
+ // A site whose membership is slow-a alone, so "focus this site" has something
+ // to resolve and something to hold.
+ await writeSite("focusable", {
+ siteTitle: "Focusable",
+ channels: [{ slug: "slow-a", groupId: "default" }],
+ });
+ await page.goto(ALL);
+
+ const bar = page.getByLabel("channel focus");
+ await expect(bar).toContainText("No focus");
+ await bar.getByLabel("focus site").selectOption("focusable");
+ await bar.getByRole("button", { name: "Focus site" }).click();
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.focus)
+ .toEqual({ kind: "site", siteId: "focusable" });
+
+ await page.reload();
+ await expect(page.getByLabel("channel focus")).toContainText(
+ "Focus: site Focusable",
+ );
+ // The focused row says so; the one it holds says why.
+ await expect(page.getByTestId("focused-slow-a")).toBeVisible();
+ await expect(page.getByLabel("held reason for slow-b")).toContainText(
+ "Held — focus: site Focusable",
+ );
+ await expect(page.getByLabel("held reason for slow-a")).toHaveCount(0);
+
+ // The focus group is FIRST in every lane's strict root, which is what makes
+ // it hold: `pick()` descends into the first child that has work.
+ {
+ const s = await settings();
+ for (const lane of ["download", "transcription", "digest", "backfill"]) {
+ const children = s.autoQueue?.[lane].root.children.map((c) => c.id);
+ expect(children).toEqual(["prio-focus", "prio-normal", "prio-all"]);
+ expect(
+ s.autoQueue?.[lane].root.children[0].children?.map((c) => c.id),
+ ).toEqual(["prio-focus-slow-a"]);
+ }
+ }
+
+ await page
+ .getByLabel("channel focus")
+ .getByRole("button", { name: "End focus" })
+ .click();
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.focus)
+ .toEqual({ kind: "none" });
+
+ await page.reload();
+ await expect(page.getByLabel("channel focus")).toContainText("No focus");
+ await expect(page.getByLabel("held reason for slow-b")).toHaveCount(0);
+ {
+ const children = (
+ await settings()
+ ).autoQueue?.download.root.children.map((c) => c.id);
+ expect(children).toEqual(["prio-normal", "prio-all"]);
+ }
+});
+
+test("a row selection focuses those channels and bulk-sets their tier", async ({
+ page,
+}) => {
+ await resetData("two-slow-channels");
+ await page.goto(ALL);
+
+ // No selection, no bulk bar.
+ await expect(page.getByLabel("channel priority bulk")).toHaveCount(0);
+ await page.getByLabel("select slow-a").check();
+
+ const bulk = page.getByLabel("channel priority bulk");
+ await expect(bulk).toContainText("1 selected");
+ await bulk.getByRole("button", { name: "Focus these" }).click();
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.focus)
+ .toEqual({ kind: "channels", slugs: ["slow-a"] });
+
+ // Applying a tier to both rows at once writes both entries in one save.
+ await page.getByLabel("select all channels").check();
+ await page.getByLabel("bulk tier").selectOption("low");
+ await page
+ .getByLabel("channel priority bulk")
+ .getByRole("button", { name: "Apply tier" })
+ .click();
+
+ await expect
+ .poll(async () => (await settings()).channelPriority?.channels)
+ .toEqual({ "slow-a": { tier: "low" }, "slow-b": { tier: "low" } });
+
+ await page.reload();
+ await expect(page.getByLabel("tier for slow-a", { exact: true })).toHaveValue(
+ "low",
+ );
+ await expect(page.getByLabel("tier for slow-b", { exact: true })).toHaveValue(
+ "low",
+ );
+ // A focused channel is still focused whatever its stored tier says: focus is
+ // a compiled POSITION, and it wins over the tier.
+ const children = (await settings()).autoQueue?.download.root.children.map(
+ (c) => c.id,
+ );
+ expect(children).toEqual(["prio-focus", "prio-low", "prio-all"]);
+});
diff --git a/editor/e2e/channel-sync-toggle.spec.ts b/editor/e2e/channel-sync-toggle.spec.ts
@@ -1,58 +0,0 @@
-import { test, expect } from "@playwright/test";
-import { readJson, resetData } from "./helpers";
-import type { ChannelConfig } from "yt-dlp-transcript-common/lib/channelConfig";
-
-// The sync-inclusion toggle on /channels flips `excludeFromSync` in the
-// channel's config.json. The next "Sync all" then skips that channel and
-// reports it in the existing skipped tooltip. Toggling back re-includes it.
-
-const SLOW_A_CONFIG = "test-transcripts/channels/slow-a/config.json";
-
-test("toggling a channel off excludes it from Sync all; toggling back restores it", async ({
- page,
-}) => {
- await resetData("two-slow-channels");
-
- await page.goto("/channels");
-
- const toggleA = page.getByRole("button", {
- name: "toggle sync inclusion for slow-a",
- });
- await expect(toggleA).toHaveText(/Included/);
- await expect(toggleA).toHaveAttribute("aria-pressed", "true");
-
- await toggleA.click();
-
- const toggleAAfter = page.getByRole("button", {
- name: "toggle sync inclusion for slow-a",
- });
- await expect(toggleAAfter).toHaveText(/Skipped/);
- await expect(toggleAAfter).toHaveAttribute("aria-pressed", "false");
-
- {
- const cfg = await readJson<ChannelConfig>(SLOW_A_CONFIG);
- expect(cfg.excludeFromSync).toBe(true);
- }
-
- // Run the pool-wide sweep. slow-a is excluded; slow-b queues.
- await page.getByRole("button", { name: "sync every channel" }).click();
- const result = page.getByLabel("sync all result");
- await expect(result).toContainText(/Queued 1 . skipped 1/, {
- timeout: 10_000,
- });
- await expect(result).toHaveAttribute(
- "title",
- /slow-a: excluded from sync all/,
- );
-
- // Toggle back on.
- await page.getByRole("button", { name: "toggle sync inclusion for slow-a" }).click();
- await expect(
- page.getByRole("button", { name: "toggle sync inclusion for slow-a" }),
- ).toHaveText(/Included/);
-
- {
- const cfg = await readJson<ChannelConfig>(SLOW_A_CONFIG);
- expect(cfg.excludeFromSync).toBeUndefined();
- }
-});
diff --git a/editor/e2e/channels-sort.spec.ts b/editor/e2e/channels-sort.spec.ts
@@ -26,7 +26,9 @@ test("clicking column headers sorts the channels table and indicates direction",
"Name",
"Handling",
"Build",
- "Sync",
+ // Was "Sync" — the sync-inclusion toggle's column is the channel priority
+ // tier now, and its sort key is the compiled order.
+ "Tier",
"Playlist",
"Last sync",
"Report",