commit c62b9c5e3e2a2d7c980bbd8c2c1c9cfaa825185f
parent c905fc26f0df3abc382d086a308f9953204eda81
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 8 Sep 2026 00:03:28 -0400
editor: the suite reads a lane's gate where the lane keeps it, and asks three surfaces to agree
Six specs that pinned a pause by reading one of the four retired fields off disk
now read `autoQueue.<lane>.held`, and two new tests cover the halves a rename
does not: an unmigrated file — the retired field, no key — still reads as held
and gains the key on its first toggle, and the dashboard, the widget and the
runner console each hold the digest lane by writing that one key while the
runner stays running at `lane-held`. The policy save is asserted to carry a hold
through a rule reorder, which is the one writer that spells its fields out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
6 files changed, 275 insertions(+), 43 deletions(-)
diff --git a/editor/e2e/auto-queue.spec.ts b/editor/e2e/auto-queue.spec.ts
@@ -1185,17 +1185,19 @@ test("UI: each operation page carries its own lane, and the rail as context", as
// the runner; this holds the LANE, which outlives it — and until slice 7 the
// only way to hold a runner lane was the dashboard.
//
- // It writes the same settings field the dashboard's button does, so the field
- // is what is asserted, not just the label. The pause is released again at the
- // end so this test leaves the fixture as it found it.
+ // It writes the same settings key the dashboard's button does, so the key is
+ // what is asserted, not just the label. Since slice 1.4 that key is the lane's
+ // own `autoQueue.download.held`, not the loose `downloadsPaused` flag. The
+ // pause is released again at the end so this test leaves the fixture as it
+ // found it.
const download = page.locator('section[data-lane="download"]');
await awaitHydration(download);
const downloadsPaused = async () =>
(
- await readJson<{ downloadsPaused?: boolean }>("test-settings.json").catch(
- () => ({}) as { downloadsPaused?: boolean },
- )
- ).downloadsPaused ?? false;
+ await readJson<{ autoQueue?: { download?: { held?: boolean } } }>(
+ "test-settings.json",
+ ).catch(() => ({}) as { autoQueue?: { download?: { held?: boolean } } })
+ ).autoQueue?.download?.held ?? false;
const pause = download.getByRole("button", { name: "Pause Downloads" });
await expect(pause).toBeVisible();
await pause.click();
@@ -1282,27 +1284,105 @@ test("the digest lane offers Shortest first, and has no Reach axis", async ({
"Oldest first",
"Shortest first",
]);
+ // HOLD THE LANE FIRST, and this is not decoration. `saveAutoQueueAction`
+ // spells every policy field explicitly — its own comment says why — so a
+ // field it forgets is dropped on every save, and since slice 1.4 the lane's
+ // GATE is such a field. A rule reorder that silently resumed a held lane is
+ // the exact regression this half is here to catch.
+ await digest.getByRole("button", { name: "pause digests" }).click();
+ await expect
+ .poll(
+ async () =>
+ (
+ await readJson<{
+ autoQueue?: { digest?: { held?: boolean } };
+ }>("test-settings.json").catch(() => null)
+ )?.autoQueue?.digest?.held ?? null,
+ { timeout: 15_000 },
+ )
+ .toBe(true);
+
await order.selectOption("newest");
await digest.getByRole("button", { name: "Save policy" }).click();
// It lands on the LANE POLICY, not in settings.digest — `recencyOrder` and
- // `recencyReach` retired with the sweep that read them.
+ // `recencyReach` retired with the sweep that read them — and the hold above
+ // is still there afterwards.
await expect
.poll(
async () => {
const s = await readJson<{
- autoQueue?: { digest?: { order?: string } };
+ autoQueue?: { digest?: { order?: string; held?: boolean } };
digest?: { recencyOrder?: string; recencyReach?: string };
}>("test-settings.json").catch(() => null);
return [
s?.autoQueue?.digest?.order,
s?.digest?.recencyOrder ?? "gone",
s?.digest?.recencyReach ?? "gone",
+ `held=${String(s?.autoQueue?.digest?.held)}`,
].join("/");
},
{ timeout: 15_000 },
)
- .toBe("newest/gone/gone");
+ .toBe("newest/gone/gone/held=true");
+});
+
+// THE MIGRATION, FROM THE BROWSER. Every settings.json on disk today spells a
+// lane's pause in one of the four RETIRED fields and carries no `held` at all,
+// so "an unmigrated file still reads as held" is not a historical curiosity —
+// it is how the live corpus reads until something writes it. The unit tests pin
+// all four fields (common/lib/pauseGates.test.ts); this pins the half a unit
+// test cannot: that a real editor, reading a real file, holds the lane, and
+// that the first toggle moves the answer onto the lane where every writer now
+// looks.
+//
+// The DIGEST lane, not transcription, and that is the asymmetry rather than a
+// shortcut: transcription's live hold is the worker pool, and no UI surface may
+// read its stored value (see lib/pauseGates.ts), so a fixture flag could not be
+// observed through a page without contradicting that rule. Its fallback is
+// covered by the unit tests and by the phase-1 numbers script, which prints
+// `held transcription` off the live file through isGateHeld.
+test("a settings.json with no `held` is read through its retired pause field", async ({
+ page,
+}) => {
+ await resetData(null);
+ await makeChannel("alpha", ["a1"]);
+ await writeSettings({
+ adminTitle: "Test Admin",
+ maxTranscriptPageBytes: 8388608,
+ sleepBetweenDownloadsSeconds: 0,
+ minFreeDiskGB: 0,
+ workers: ONE_WORKER,
+ // The pre-1.4 spelling, and nothing else: no `autoQueue.digest` block, so
+ // no `held` for the sanitizer to find.
+ digest: { digestsPaused: true },
+ });
+
+ const res = await page.request.get(`${baseUrl}/api/auto-queue/status`);
+ expect(res.ok()).toBeTruthy();
+ const payload = (await res.json()) as Record<string, { held?: boolean }>;
+ expect(payload.digest?.held).toBe(true);
+
+ await page.goto("/operations/digest");
+ const digest = page.locator('section[data-lane="digest"]');
+ await awaitHydration(digest);
+ const resume = digest.getByRole("button", { name: "resume digests" });
+ await expect(resume).toBeEnabled({ timeout: 30_000 });
+ await resume.click();
+
+ // The first toggle persists the KEY. From here the retired field is dead
+ // config — which is why every writer of it was rewritten in the same slice.
+ await expect
+ .poll(
+ async () =>
+ (
+ await readJson<{
+ autoQueue?: { digest?: { held?: boolean } };
+ }>("test-settings.json").catch(() => null)
+ )?.autoQueue?.digest?.held ?? null,
+ { timeout: 20_000 },
+ )
+ .toBe(false);
});
test("/jobs: a runner is a lane on a strip, not a card", async ({
diff --git a/editor/e2e/backfill.spec.ts b/editor/e2e/backfill.spec.ts
@@ -781,19 +781,22 @@ test("the dashboard pauses and resumes the backfill lane", async ({ page }) => {
await resetData("one-transcribe-channel-with-audio");
await writeSettings(backfillSettings());
- const laneEnabled = async () =>
+ // THE LANE'S GATE, on the lane. Slice 1.4 moved it off the inverted
+ // `backfill.enabled` and onto `autoQueue.backfill.held`, so the polarity here
+ // is the plain one: held means held.
+ const laneHeld = async () =>
(
- await readJson<{ backfill?: { enabled?: boolean } }>(
+ await readJson<{ autoQueue?: { backfill?: { held?: boolean } } }>(
"test-settings.json",
- ).catch(() => ({}) as { backfill?: { enabled?: boolean } })
- ).backfill?.enabled ?? false;
+ ).catch(() => ({}) as { autoQueue?: { backfill?: { held?: boolean } } })
+ ).autoQueue?.backfill?.held ?? false;
await page.goto("/");
const pause = page.getByRole("button", { name: "pause backfill" });
await expect(pause).toBeEnabled({ timeout: 30_000 });
await pause.click();
- await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(false);
+ await expect.poll(laneHeld, { timeout: 30_000 }).toBe(true);
// THE LANE'S ARM SWITCH IS UNTOUCHED: pausing must not disarm the lane, or
// "pause" would quietly become "stop" and days of queued work would need
@@ -804,7 +807,7 @@ test("the dashboard pauses and resumes the backfill lane", async ({ page }) => {
const resume = page.getByRole("button", { name: "resume backfill" });
await expect(resume).toBeEnabled({ timeout: 30_000 });
await resume.click();
- await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(true);
+ await expect.poll(laneHeld, { timeout: 30_000 }).toBe(false);
});
// ONE GATE, TWO SURFACES. /operations/diarization and the dashboard draw the
@@ -823,12 +826,15 @@ test("an operation page holds the same lane the dashboard does", async ({
await resetData("one-transcribe-channel-with-audio");
await writeSettings(backfillSettings());
- const laneEnabled = async () =>
+ // THE LANE'S GATE, on the lane. Slice 1.4 moved it off the inverted
+ // `backfill.enabled` and onto `autoQueue.backfill.held`, so the polarity here
+ // is the plain one: held means held.
+ const laneHeld = async () =>
(
- await readJson<{ backfill?: { enabled?: boolean } }>(
+ await readJson<{ autoQueue?: { backfill?: { held?: boolean } } }>(
"test-settings.json",
- ).catch(() => ({}) as { backfill?: { enabled?: boolean } })
- ).backfill?.enabled ?? false;
+ ).catch(() => ({}) as { autoQueue?: { backfill?: { held?: boolean } } })
+ ).autoQueue?.backfill?.held ?? false;
await page.goto("/operations/diarization");
// Scoped to the lane's own section so this cannot accidentally match a
@@ -839,7 +845,7 @@ test("an operation page holds the same lane the dashboard does", async ({
// a click before that fires nothing at all.
await expect(hold).toBeEnabled({ timeout: 30_000 });
await hold.click();
- await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(false);
+ await expect.poll(laneHeld, { timeout: 30_000 }).toBe(true);
// A hold is not a stop: the lane's arm switch is untouched.
expect(await laneArmed()).toBe(false);
@@ -850,7 +856,7 @@ test("an operation page holds the same lane the dashboard does", async ({
});
await expect(resumeOnDashboard).toBeEnabled({ timeout: 30_000 });
await resumeOnDashboard.click();
- await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(true);
+ await expect.poll(laneHeld, { timeout: 30_000 }).toBe(false);
});
// (11) THE RE-DOWNLOAD ON A SUBTITLE CHANNEL: audio, not captions.
diff --git a/editor/e2e/dashboard.spec.ts b/editor/e2e/dashboard.spec.ts
@@ -89,13 +89,15 @@ test("pausing downloads persists and blocks a manual sync", async ({ page }) =>
page.getByRole("button", { name: "Resume Downloads" }),
).toBeVisible();
- // The flag is written to settings.json (survives a restart).
+ // The gate is written to settings.json (survives a restart) — on the LANE,
+ // `autoQueue.download.held`, which is where slice 1.4 moved it from the loose
+ // `downloadsPaused` flag.
await expect
.poll(async () => {
- const s = await readJson<{ downloadsPaused?: boolean }>(
- "test-settings.json",
- );
- return s.downloadsPaused;
+ const s = await readJson<{
+ autoQueue?: { download?: { held?: boolean } };
+ }>("test-settings.json");
+ return s.autoQueue?.download?.held;
})
.toBe(true);
diff --git a/editor/e2e/disk-space.spec.ts b/editor/e2e/disk-space.spec.ts
@@ -187,10 +187,14 @@ function page_waitMs(ms: number): Promise<void> {
test("the dashboard tells a disk stop apart from the manual pause", async ({
page,
}) => {
- // The defect: PipelineBand rendered one red "downloads paused" that read
- // settings.downloadsPaused — the MANUAL toggle. A real disk stop said
- // nothing, and when both were true the operator would un-pause and watch
- // nothing happen.
+ // The defect: PipelineBand rendered one red "downloads paused" that read the
+ // MANUAL toggle. A real disk stop said nothing, and when both were true the
+ // operator would un-pause and watch nothing happen.
+ //
+ // The manual toggle is `autoQueue.download.held` since slice 1.4 — the lane's
+ // own gate, spelled here the way a settings.json written by the editor now
+ // spells it. (The field it replaced still reads as a fallback; the spec that
+ // proves THAT is in auto-queue.spec.ts.)
await resetData("test-pipeline");
// Two distinct labels, because they are two distinct facts. (getByLabel
@@ -203,19 +207,25 @@ test("the dashboard tells a disk stop apart from the manual pause", async ({
// Disk stop only. The band must name the disk, and must NOT claim a manual
// pause — reading a disk stop as the manual toggle is the original defect.
- await writeSettings({ minFreeDiskGB: HUGE_FLOOR_GB, downloadsPaused: false });
+ await writeSettings({
+ minFreeDiskGB: HUGE_FLOOR_GB,
+ autoQueue: { download: { held: false } },
+ });
await page.goto("/");
await expect(diskStop).toContainText("disk", { timeout: 15_000 });
await expect(manualPause).toHaveCount(0);
// Both at once: two instruments, so un-pausing is visibly not enough.
- await writeSettings({ minFreeDiskGB: HUGE_FLOOR_GB, downloadsPaused: true });
+ await writeSettings({
+ minFreeDiskGB: HUGE_FLOOR_GB,
+ autoQueue: { download: { held: true } },
+ });
await page.reload();
await expect(manualPause).toBeVisible({ timeout: 15_000 });
await expect(diskStop).toBeVisible();
// Gate off, still manually paused: only the manual instrument remains.
- await writeSettings({ minFreeDiskGB: 0, downloadsPaused: true });
+ await writeSettings({ minFreeDiskGB: 0, autoQueue: { download: { held: true } } });
await page.reload();
await expect(manualPause).toBeVisible({ timeout: 15_000 });
await expect(diskStop).toHaveCount(0);
diff --git a/editor/e2e/lane-runner.spec.ts b/editor/e2e/lane-runner.spec.ts
@@ -6,6 +6,7 @@ import { baseUrl } from "./baseUrl";
import {
generateReport,
pathExists,
+ readJson,
resetData,
resolvePath,
writeChannelConfig,
@@ -43,6 +44,11 @@ import {
// sampled instant. The second is the same property measured by a coin
// toss, and a loaded machine loses it to sampling rather than to a
// regression.
+// 6. ONE GATE, THREE SURFACES. The dashboard deck, the widget rail and the
+// lane's own console all hold the SAME lane by writing the SAME key
+// (`autoQueue.digest.held`, since slice 1.4), and none of them touches
+// another lane's. Asserted from the file, because every one of those
+// buttons flips its own label optimistically.
//
// Engines: the digest and attribution lanes use the ollama HTTP stub
// (e2e/fixtures/ollama-stub.mjs, wired via OLLAMA_URL), diarization uses
@@ -410,3 +416,129 @@ test("the digest and backfill lanes dispatch at the same time", async ({
`digest picks ${dFrom}-${dTo} vs backfill picks ${bFrom}-${bTo} do not overlap`,
).toBe(true);
});
+
+// ---------------------------------------------------------------------------
+// (5) THREE SURFACES, ONE KEY.
+//
+// A lane's pause is drawn on the dashboard deck, in the monitor widget's rail
+// and on the lane's own runner console, and until slice 1.4 those three wrote a
+// settings FIELD chosen per lane — one of four, with three polarities. They
+// write `autoQueue[lane].held` now, and this is the spec that says all three
+// mean it: the file is read back after each click, the OTHER three lanes' keys
+// are compared before and after, and the runner is asked whether it is still
+// running each time.
+//
+// WHY THE FILE AND NOT THE LABEL. Every one of these buttons flips its own label
+// optimistically, so three green label assertions would pass over three
+// surfaces writing three different things. The key on disk is the thing they
+// have to agree about.
+//
+// A HOLD, NEVER A STOP, at every one of them: `running` stays true and the idle
+// reason is `lane-held` — the same pair test (1) asserts for the console, here
+// for the two surfaces that are not the console.
+
+type HeldByLane = Record<string, boolean | null>;
+
+async function heldByLane(): Promise<HeldByLane> {
+ const s = await readJson<{
+ autoQueue?: Record<string, { held?: boolean }>;
+ }>("test-settings.json").catch(() => ({}) as { autoQueue?: undefined });
+ return Object.fromEntries(
+ ["transcription", "download", "digest", "backfill"].map((lane) => [
+ lane,
+ s.autoQueue?.[lane]?.held ?? null,
+ ]),
+ );
+}
+
+test("a hold from the dashboard, the widget and the console writes one key", async ({
+ page,
+ request,
+}) => {
+ test.setTimeout(SLOW);
+ await seedDigestChannel(page, ["lanevid0005"]);
+ await writeSettings(
+ laneSettings({
+ autoQueue: { digest: { enabled: true, maxWorkers: 1, root: CATCH_ALL } },
+ }),
+ );
+ await generateReport(page, CHANNEL);
+ await control(request, "digest", "start");
+ await expect
+ .poll(async () => (await status(request, "digest")).runner.running, {
+ timeout: 30_000,
+ intervals: [250],
+ })
+ .toBe(true);
+
+ // The other three lanes, as the fixture leaves them. Nothing below may move
+ // any of these — a surface that held "the lane" by holding the wrong one is
+ // the failure this comparison exists for, and it is silent in every label.
+ const settle = async (want: boolean | null) => {
+ await expect
+ .poll(async () => (await heldByLane()).digest, {
+ timeout: 30_000,
+ intervals: [250],
+ })
+ .toBe(want);
+ return heldByLane();
+ };
+ // The first write through the editor migrates every lane's gate onto its
+ // policy, so the neighbours are compared from AFTER that write, not before.
+ //
+ // `scope` is not decoration on the console: an operation page draws the rail
+ // as well as the lane's own section, and both carry this control (the same
+ // reason backfill.spec scopes its hold to `section[data-lane]`). The other
+ // two pages have one.
+ const surfaces: { name: string; url: string; scope: string | null }[] = [
+ { name: "console", url: "/operations/digest", scope: 'section[data-lane="digest"]' },
+ { name: "dashboard", url: "/", scope: null },
+ { name: "widget", url: "/widget?controls=1", scope: null },
+ ];
+ let neighbours: HeldByLane | null = null;
+
+ for (const surface of surfaces) {
+ await page.goto(surface.url);
+ const within = surface.scope ? page.locator(surface.scope) : page;
+ const hold = within.getByRole("button", { name: "pause digests" });
+ // LaneActionButton is disabled until React attaches, and a click before
+ // that fires nothing at all.
+ await expect(hold).toBeEnabled({ timeout: 30_000 });
+ await hold.click();
+ const afterHold = await settle(true);
+
+ // HELD IS A HOLD. The runner is still up, idling at zero with the lane's
+ // own reason — not `stopped`, and not the download lane's
+ // `downloads-paused`, which names a different gate.
+ await expect
+ .poll(async () => (await status(request, "digest")).runner.idleReason, {
+ timeout: 30_000,
+ intervals: [250],
+ })
+ .toBe("lane-held");
+ expect(
+ (await status(request, "digest")).runner.running,
+ `${surface.name} stopped the runner`,
+ ).toBe(true);
+
+ const release = within.getByRole("button", { name: "resume digests" });
+ await expect(release).toBeEnabled({ timeout: 30_000 });
+ await release.click();
+ const afterRelease = await settle(false);
+
+ // ONE KEY MOVED, and only one. Held and released are compared separately so
+ // a surface writing a second lane on either edge is caught.
+ const others = (h: HeldByLane) => ({ ...h, digest: null });
+ if (neighbours === null) neighbours = others(afterHold);
+ expect(others(afterHold), `${surface.name} hold moved another lane`).toEqual(
+ neighbours,
+ );
+ expect(
+ others(afterRelease),
+ `${surface.name} release moved another lane`,
+ ).toEqual(neighbours);
+ }
+
+ // And it survived all three round trips as a runner, not as a restart.
+ expect((await status(request, "digest")).runner.running).toBe(true);
+});
diff --git a/editor/e2e/widget.spec.ts b/editor/e2e/widget.spec.ts
@@ -785,27 +785,29 @@ test("the widget's controls hold and release the backfill lane", async ({
}) => {
await writeSettings({ ...TWO_WORKERS, ...BACKFILL_SETTINGS });
- // Assert the FIELD, not just the label: the button writes the same
- // settings.backfill.enabled the Settings checkbox does, and the point of that
- // choice is that the two cannot drift.
- const laneEnabled = async () =>
+ // Assert the KEY, not just the label: the button writes the same
+ // `autoQueue.backfill.held` the "Run the backfill lane" checkbox does, and
+ // the point of that choice is that the two cannot drift. (It was
+ // `backfill.enabled` until slice 1.4 moved the gate onto the lane; the
+ // polarity is no longer inverted.)
+ const laneHeld = async () =>
(
- await readJson<{ backfill?: { enabled?: boolean } }>(
+ await readJson<{ autoQueue?: { backfill?: { held?: boolean } } }>(
"test-settings.json",
- ).catch(() => ({}) as { backfill?: { enabled?: boolean } })
- ).backfill?.enabled ?? false;
+ ).catch(() => ({}) as { autoQueue?: { backfill?: { held?: boolean } } })
+ ).autoQueue?.backfill?.held ?? false;
await page.goto("/widget?controls=1&backfill=1");
const pause = page.getByRole("button", { name: "pause backfill" });
await expect(pause).toBeEnabled({ timeout: 30_000 });
await pause.click();
- await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(false);
+ await expect.poll(laneHeld, { timeout: 30_000 }).toBe(true);
// The label flips immediately, because the click refetches the payload it
// reads rather than waiting out the 15s poll floor.
const resume = page.getByRole("button", { name: "resume backfill" });
await expect(resume).toBeVisible({ timeout: 15_000 });
await resume.click();
- await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(true);
+ await expect.poll(laneHeld, { timeout: 30_000 }).toBe(false);
});