Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit ffdaa5fc8dcf8e43e111b1961b9cc2a254994ee4
parent 4ed2bd9510c85d7a50fb637741001a1ba78ac1d9
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  7 Sep 2026 22:35:38 -0400

editor: the e2e suite asks the runner what it used to ask the sweep

Slice 1.3, fourth commit. Three sweep tests in `backfill.spec.ts` are rewritten
to the lane runner, three arbiter tests in `auto-queue.spec.ts` are deleted, and
every fixture that carried the ten retired settings fields is stripped to the
shape settings.json now has.

THE PROPERTIES MOVED, THEY DID NOT VANISH. The sweep pair pinned two bugs —
"the flag is persisted without the scope" and "stopping leaves the scope
behind". Both are unreachable now, because the scope IS the tree and it is
written in the same settings write as the switch. So the rewritten test pins the
other half of that bargain instead: disarming must NOT clear the tree, because
an operator authored it and switching a lane off is not throwing it away. The
scope test drives the ladder's operation select rather than a checkbox list and
asserts a leaf's `match.operation`; the restart test goes through
/api/test/resume-lane.

`Reach is disabled until an order is chosen` becomes `the digest lane offers
Shortest first, and has no Reach axis` — the retired-routes rule in both
directions. Reach is asserted GONE (a runner's order already applies across
every channel a rule claims; which rule goes first is the tree), `cheapest` is
asserted present on the one lane whose runner can price its work, and the order
is asserted to land on `autoQueue.digest.order` with `digest.recencyOrder` and
`recencyReach` gone from the file.

`section[data-sweep-lane]` is asserted to have COUNT 0 on both an operation page
and the digest page, rather than merely being unused. The figures that lived on
the sweep panel are read off the rail's row for the operation — one band per
operation, populations stated separately and never summed — and the channel a
lane would touch is read off Next up, which is the itinerary answered by the
thing that actually dispatches.

Fixtures lose `weight: 1`, which several specs set to mean "assert that work
HAPPENS". They no longer need to: the idle-only rule is the operation's declared
`contendsFor`, and attribution contends for the network, so it keeps its slots
whatever transcription is doing.

Also here, because it is the same sentence: the digest pause tooltip stops
promising not to end "the sweep".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/app/components/lanes/pauseControl.tsx | 14+++++++-------
Meditor/e2e/attribution.spec.ts | 37+++++++++++++++++++++----------------
Meditor/e2e/auto-queue.spec.ts | 185+++++++++++++++++++------------------------------------------------------------
Meditor/e2e/backfill.spec.ts | 271++++++++++++++++++++++++++++++++++++-------------------------------------------
Meditor/e2e/channel-line.spec.ts | 4----
Meditor/e2e/lane-runner.spec.ts | 7-------
Meditor/e2e/navigation.spec.ts | 6+++---
Meditor/e2e/widget.spec.ts | 4----
8 files changed, 198 insertions(+), 330 deletions(-)

diff --git a/editor/app/components/lanes/pauseControl.tsx b/editor/app/components/lanes/pauseControl.tsx @@ -22,12 +22,12 @@ import { pauseLaneAction, resumeLaneAction } from "../../operations/actions"; // case-insensitive SUBSTRING unless `exact` is passed (none of them pass it), // which is why a spec asking for "Pause Digest" finds "pause digests". // -// THE RUNNER LANES SAY *PAUSE*, THE SWEEP LANES SAY *HOLD*, on purpose. The feed -// SWEEPS and the gate HOLDS: on a lane that also has a sweep button, "Start -// sweep" and "Pause backfill" are the same shape of phrase for two acts whose -// costs to undo differ by a week of GPU time, and different verbs are the -// cheapest thing that keeps them apart. A lane with no sweep has nothing to be -// confused with, so it keeps the plain word. +// THE TWO OPERATION LANES SAY *HOLD* IN THEIR TOOLTIPS, on purpose. The lane's +// arm switch RUNS it and the gate HOLDS it: on a card that carries both, "Run +// every channel" and "Pause backfill" are two acts whose costs to undo differ by +// a week of GPU time, and different verbs are the cheapest thing that keeps them +// apart. A lane with no arm switch beside it has nothing to be confused with, so +// it keeps the plain word. type PauseControlInput = { lane: PauseLane; @@ -67,7 +67,7 @@ const PAUSE_COPY: Record<PauseLane, { held: PauseCopy; free: PauseCopy }> = { label: "Hold the lane", ariaLabel: "pause digests", title: - "Hold digest generation without ending the sweep. The running job idles at zero and resumes instantly.", + "Hold digest generation without stopping the lane. The runner idles at zero and resumes instantly.", }, held: { label: "Resume the lane", diff --git a/editor/e2e/attribution.spec.ts b/editor/e2e/attribution.spec.ts @@ -91,13 +91,14 @@ function attributionRecord(over: Record<string, unknown> = {}) { }; } -// Settings with both attribution lanes armed and the backfill lane at a real -// share. +// Settings with both attribution lanes armed and the backfill lane switched on. // -// weight 1, not the idle-only 0: these specs assert that work HAPPENS, and at -// weight 0 anything transcribing in the same run would legitimately park the -// lane. The idle-only default has a pure unit test, which is the right place for -// it — no pool, no GPU, no timing. +// Nothing here has to force a share any more: the `weight` scalar retired in +// slice 1.3, and the idle-only rule is now the operation's declared +// `contendsFor` — attribution contends for the network, so it keeps its slots +// whatever transcription is doing. That is exactly what these specs need, and +// the GPU carve-out has a pure unit test (operationBatch.test.ts), which is the +// right place for it — no pool, no GPU, no timing. function attributionSettings(over: { attribution?: Record<string, unknown>; } = {}) { @@ -115,11 +116,7 @@ function attributionSettings(over: { // protect. backfill: { enabled: true, - weight: 1, concurrency: 1, - sweepEnabled: false, - sweepKinds: [], - sweepChannels: [], allowRedownload: false, }, attribution: { @@ -452,12 +449,20 @@ test("the stage card and the operation pages show attribution beside diarization // not re-asserted — it still lives on the channel page's speakers stage, // asserted a few lines above. await page.goto("/operations/attribution-diarized"); - const diarized = page.locator('section[data-sweep-lane="backfill"]'); - await expect(diarized).toContainText(CHANNEL); - await expect(diarized.getByText("1 reachable now")).toBeVisible(); - await expect(diarized.getByText("1 blocked upstream")).toBeVisible(); + // The lane's console is where the channel appears — as the video it would + // dispatch next, which is the sweep itinerary's job done by the dispatcher. + const lane = page.locator('section[data-lane="backfill"]'); + await expect(lane.getByText(new RegExp(`${CHANNEL}/`))).toBeVisible({ + timeout: 30_000, + }); + // The per-operation figures are the RAIL'S row for this operation: one band + // per operation, its populations stated separately and never summed. + const diarized = page.locator('li[data-operation="attribution-diarized"]'); + await expect(diarized.getByText("1 reachable")).toBeVisible(); + await expect(diarized.getByText("1 blocked")).toBeVisible(); await page.goto("/operations/attribution-text"); - const text = page.locator('section[data-sweep-lane="backfill"]'); - await expect(text.getByText("2 reachable now")).toBeVisible(); + await expect( + page.locator('li[data-operation="attribution-text"]').getByText("2 reachable"), + ).toBeVisible(); }); diff --git a/editor/e2e/auto-queue.spec.ts b/editor/e2e/auto-queue.spec.ts @@ -1205,22 +1205,32 @@ test("UI: each operation page carries its own lane, and the rail as context", as await resume.click(); await expect.poll(downloadsPaused, { timeout: 20_000 }).toBe(false); - // The digest lane is a page of its own, with both of its controls. + // The digest lane is a page of its own, and it is a RUNNER console like the + // two above it — the same section, the same four controls. Until slice 1.3 it + // was a sweep panel on `data-sweep-lane`; that attribute is gone, and no + // second section on the page answers to a lane. await page.goto("/operations/digest"); - const digest = page.locator('section[data-sweep-lane="digest"]'); + const digest = page.locator('section[data-lane="digest"]'); await awaitHydration(digest); - await expect(digest.getByRole("heading", { name: "Digest" })).toBeVisible(); + await expect(page.locator("section[data-sweep-lane]")).toHaveCount(0); await expect( - digest.getByRole("button", { name: "Start Digest sweep" }), + digest.getByRole("heading", { name: "Auto-digest" }), ).toBeVisible(); - // SWEEP AND PAUSE ARE TWO CONTROLS, and both must be here — conflating them - // is how an operator loses a week of GPU time. await expect( - digest.getByRole("button", { name: "Pause Digest" }), + digest.getByRole("button", { name: "Start Auto-digest" }), ).toBeVisible(); + // THE RUNNER AND THE GATE ARE DIFFERENT CONTROLS, and both must be here — + // conflating them is how an operator loses a week of GPU time. + await expect( + digest.getByRole("button", { name: "Pause Digests" }), + ).toBeVisible(); + // The sweep's arm button is gone from this page and from everywhere else. + await expect( + page.getByRole("button", { name: "Start Digest sweep" }), + ).toHaveCount(0); }); -test("UI: Reach is disabled until an order is chosen, and it persists", async ({ +test("the digest lane offers Shortest first, and has no Reach axis", async ({ page, }) => { await resetData(null); @@ -1235,41 +1245,45 @@ test("UI: Reach is disabled until an order is chosen, and it persists", async ({ }); await page.goto("/operations/digest"); - await awaitHydration(page.locator('section[data-sweep-lane="digest"]')); - - // Scoped to the lane. One lane per page now, but the scoping stays: Order and - // Reach are a pair every lane carries, and an unscoped getByLabel would be - // ambiguous the moment a second one shares a page again. - const digest = page.locator('section[data-sweep-lane="digest"]'); - const order = digest.getByLabel("Order", { exact: true }); - const reach = digest.getByLabel("Reach", { exact: true }); - // Reach is meaningless without an order, so it is disabled rather than - // silently ignored. - await expect(reach).toBeDisabled(); + const digest = page.locator('section[data-lane="digest"]'); + await awaitHydration(digest); + // REACH RETIRED WITH THE SWEEP. A runner's order already applies across every + // channel and bucket a rule claims, so there is no second axis to offer — + // which rule goes FIRST is the tree's job. The control is gone, not disabled: + // a disabled dropdown would imply a setting that does not exist. + await expect(digest.getByLabel("Reach", { exact: true })).toHaveCount(0); + + // "Shortest first" is offered on this lane ALONE, because it is the only lane + // whose runner can price its candidates. + const order = digest.getByLabel("video order for auto-digest"); + await expect(order.locator("option")).toHaveText([ + "Listed order", + "Newest first", + "Oldest first", + "Shortest first", + ]); await order.selectOption("newest"); - await expect(reach).toBeEnabled(); - await reach.selectOption("corpus"); + await digest.getByRole("button", { name: "Save policy" }).click(); - // Both land in settings.digest — NOT in autoQueue, which has no digest key. + // It lands on the LANE POLICY, not in settings.digest — `recencyOrder` and + // `recencyReach` retired with the sweep that read them. await expect .poll( async () => { const s = await readJson<{ + autoQueue?: { digest?: { order?: string } }; digest?: { recencyOrder?: string; recencyReach?: string }; }>("test-settings.json").catch(() => null); - return `${s?.digest?.recencyOrder}/${s?.digest?.recencyReach}`; + return [ + s?.autoQueue?.digest?.order, + s?.digest?.recencyOrder ?? "gone", + s?.digest?.recencyReach ?? "gone", + ].join("/"); }, { timeout: 15_000 }, ) - .toBe("newest/corpus"); - - // The armed sweep and its scope are UNTOUCHED by an order save — a rebuilt - // settings block here would disarm a multi-week run. - const after = await readJson<{ digest?: { sweepEnabled?: boolean } }>( - "test-settings.json", - ); - expect(after.digest?.sweepEnabled ?? false).toBe(false); + .toBe("newest/gone/gone"); }); test("/jobs: a runner is a lane on a strip, not a card", async ({ @@ -1301,112 +1315,3 @@ test("/jobs: a runner is a lane on a strip, not a card", async ({ // And the generic bucket stays gone. await expect(page.getByRole("heading", { name: "Other" })).toHaveCount(0); }); - - -// --- The arbiter ------------------------------------------------------------- - -test("the arbiter refuses to start beside an armed sweep, and says which one", async ({ - page, -}) => { - await resetData(null); - await makeChannel("alpha", ["a1"]); - await writeSettings({ - adminTitle: "Test Admin", - maxTranscriptPageBytes: 8388608, - sleepBetweenDownloadsSeconds: 0, - minFreeDiskGB: 0, - workers: ONE_WORKER, - // A rule DOES name an operation, so the only thing in the way is the sweep. - autoQueue: transcriptionAutoQueue({ - id: "root", - mode: "strict", - children: [{ id: "dig", match: { type: "all", operation: "digest" } }], - }), - digest: { sweepEnabled: true }, - }); - - await page.goto("/operations"); - // THE ARBITER IS THE BOARD'S CONTROL, not a lane's: it is the thing that - // decides BETWEEN lanes, so it sits on /operations beside the rail. The board - // carries its own hydration signal — its buttons are inert until React is - // live on it, exactly as a runner section's are. - await awaitHydration(page.locator('[data-board="operations"]')); - const start = page.getByRole("button", { name: "Start the arbiter" }); - await expect(start).toBeDisabled(); - // Two dispatchers on one lane would start the same channel twice, so the - // refusal names the switch that is in the way rather than failing silently. - await expect(page.getByText(/digest sweep is armed/i)).toBeVisible(); -}); - -test("the arbiter will not start with no rule naming an operation", async ({ - page, -}) => { - await resetData(null); - await makeChannel("alpha", ["a1"]); - await writeSettings({ - adminTitle: "Test Admin", - maxTranscriptPageBytes: 8388608, - sleepBetweenDownloadsSeconds: 0, - minFreeDiskGB: 0, - workers: ONE_WORKER, - autoQueue: transcriptionAutoQueue(ALPHA_ROOT), - }); - - await page.goto("/operations"); - // THE ARBITER IS THE BOARD'S CONTROL, not a lane's: it is the thing that - // decides BETWEEN lanes, so it sits on /operations beside the rail. The board - // carries its own hydration signal — its buttons are inert until React is - // live on it, exactly as a runner section's are. - await awaitHydration(page.locator('[data-board="operations"]')); - // It would come up, find nothing to dispatch and stop. Saying so before the - // click is the difference between a disabled button and a mystery. - await expect( - page.getByRole("button", { name: "Start the arbiter" }), - ).toBeDisabled(); - // The explanatory line, not the status chip beside it — both say the same - // thing, deliberately, so the assertion names which one it means. - await expect( - page.getByText(/No rule names an operation yet, so there would be nothing/), - ).toBeVisible(); -}); - -test("the arbiter starts, appears as a job, and stops", async ({ page }) => { - await resetData(null); - await makeChannel("alpha", ["a1"]); - await writeSettings({ - adminTitle: "Test Admin", - maxTranscriptPageBytes: 8388608, - sleepBetweenDownloadsSeconds: 0, - minFreeDiskGB: 0, - workers: ONE_WORKER, - autoQueue: transcriptionAutoQueue({ - id: "root", - mode: "strict", - children: [{ id: "dig", match: { type: "all", operation: "digest" } }], - }), - }); - - await page.goto("/operations"); - // THE ARBITER IS THE BOARD'S CONTROL, not a lane's: it is the thing that - // decides BETWEEN lanes, so it sits on /operations beside the rail. The board - // carries its own hydration signal — its buttons are inert until React is - // live on it, exactly as a runner section's are. - await awaitHydration(page.locator('[data-board="operations"]')); - await page.getByRole("button", { name: "Start the arbiter" }).click(); - const stop = page.getByRole("button", { name: "Stop the arbiter" }); - await expect(stop).toBeVisible({ timeout: 15_000 }); - - // It runs on queueKey "" — a long-lived job that WAITS on jobs needing the - // real keys must not hold one, or it deadlocks against its own work. - const job = await page.request - .get(`${baseUrl}/api/jobs/active`) - .then((r) => r.json() as Promise<{ jobs: { kind: string; queueKey: string }[] }>); - const arbiter = job.jobs.find((j) => j.kind === "operations-arbiter"); - expect(arbiter).toBeTruthy(); - expect(arbiter?.queueKey).toBe(""); - - await stop.click(); - await expect( - page.getByRole("button", { name: "Start the arbiter" }), - ).toBeVisible({ timeout: 15_000 }); -}); diff --git a/editor/e2e/backfill.spec.ts b/editor/e2e/backfill.spec.ts @@ -84,16 +84,7 @@ function backfillSettings(over: { }, backfill: { enabled: true, - // 1, not the idle-only 0: these specs assert that work HAPPENS, and at - // weight 0 a transcription running in the same test would legitimately - // park the lane forever. The idle-only default is covered by the pure - // unit test (common/controller/operationBatch.test.ts), which is the right - // place for it — no pool, no GPU, no timing. - weight: 1, concurrency: 1, - sweepEnabled: false, - sweepKinds: [], - sweepChannels: [], allowRedownload: false, ...over.backfill, }, @@ -118,13 +109,25 @@ async function audioFiles(videoId: string): Promise<string[]> { ); } -// settings.backfill.sweepEnabled, straight off disk — the persisted intent the -// boot hook reads. -async function sweepFlag(): Promise<boolean> { - const s = await readJson<{ backfill?: { sweepEnabled?: boolean } }>( +// The backfill lane as settings.json spells it — the persisted intent the boot +// hook reads. It was `backfill.sweepEnabled` until slice 1.3; a lane is armed by +// its policy now, and its SCOPE is the tree beside the switch, so both are read +// together here rather than one being watched while the other silently moves. +type LanePolicy = { + enabled?: boolean; + order?: string; + root?: { children?: { id?: string; match?: Record<string, unknown> }[] }; +}; + +async function backfillLane(): Promise<LanePolicy> { + const s = await readJson<{ autoQueue?: { backfill?: LanePolicy } }>( "test-settings.json", - ).catch(() => ({}) as { backfill?: { sweepEnabled?: boolean } }); - return s.backfill?.sweepEnabled ?? false; + ).catch(() => ({}) as { autoQueue?: { backfill?: LanePolicy } }); + return s.autoQueue?.backfill ?? {}; +} + +async function laneArmed(): Promise<boolean> { + return (await backfillLane()).enabled ?? false; } const SLOW = 120_000; @@ -378,12 +381,16 @@ test("the disk floor refuses to re-acquire anything", async ({ page }) => { expect(await pathExists(dataRel("vidB", "diarization.json"))).toBe(false); }); -// (8) The sweep arms, persists its SCOPE with the flag, and disarms clearing -// both. Persisting the flag without the scope is what turns a deliberately -// bounded run into a corpus-wide one on the next restart; leaving the scope -// behind on stop is what silently narrows the NEXT sweep. Both were real bugs in -// the digest sweep, so both are pinned here. -test("the sweep arms with its scope and disarms clearing it", async ({ +// (8) THE LANE ARMS FROM THE DASHBOARD, AND DISARMING LEAVES ITS RULES ALONE. +// +// This was the sweep's arm/disarm pair. The two bugs it pinned were "the flag is +// persisted without the scope" (a bounded run comes back corpus-wide after a +// restart) and "stopping leaves the scope behind" (the next run is silently +// narrowed). Both are gone by construction now — the scope IS the tree, written +// in the same settings write as the switch — so what has to be pinned instead is +// the other half of that: disarming must NOT clear the tree, because an operator +// authored it and switching a lane off is not throwing it away. +test("the lane arms from the dashboard and disarms without losing its rules", async ({ page, }) => { test.setTimeout(SLOW); @@ -391,72 +398,61 @@ test("the sweep arms with its scope and disarms clearing it", async ({ await writeSettings(backfillSettings()); await page.goto("/"); - const start = page.getByRole("button", { name: "start backfill sweep" }); - await expect(start).toBeEnabled({ timeout: 30_000 }); - await start.click(); + const arm = page.getByRole("button", { name: "arm backfill lane" }); + await expect(arm).toBeEnabled({ timeout: 30_000 }); + await arm.click(); - await expect - .poll( - async () => - ( - await readJson<{ backfill?: { sweepEnabled?: boolean } }>( - "test-settings.json", - ).catch(() => ({}) as { backfill?: { sweepEnabled?: boolean } }) - ).backfill?.sweepEnabled ?? false, - { timeout: 30_000 }, - ) - .toBe(true); + await expect.poll(laneArmed, { timeout: 30_000 }).toBe(true); + // An unscoped arm is one catch-all leaf — what an empty `sweepChannels` meant. + expect( + (await backfillLane()).root?.children?.map((c) => c.match), + ).toEqual([{ type: "all" }]); - const stop = page.getByRole("button", { name: "stop backfill sweep" }); - await expect(stop).toBeEnabled({ timeout: 30_000 }); - await stop.click(); + const disarm = page.getByRole("button", { name: "disarm backfill lane" }); + await expect(disarm).toBeEnabled({ timeout: 30_000 }); + await disarm.click(); await expect .poll( async () => { - const s = await readJson<{ - backfill?: { - sweepEnabled?: boolean; - sweepChannels?: string[]; - sweepKinds?: string[]; - }; - }>("test-settings.json").catch(() => ({}) as Record<string, never>); - const b = s.backfill ?? {}; + const lane = await backfillLane(); return { - enabled: b.sweepEnabled ?? false, - channels: (b.sweepChannels ?? []).length, - kinds: (b.sweepKinds ?? []).length, + enabled: lane.enabled ?? false, + rules: lane.root?.children?.length ?? 0, }; }, { timeout: 30_000 }, ) - .toEqual({ enabled: false, channels: 0, kinds: 0 }); + .toEqual({ enabled: false, rules: 1 }); + + // The sweep's own button is gone from this surface, not merely relabelled. + await expect( + page.getByRole("button", { name: "start backfill sweep" }), + ).toHaveCount(0); }); -// (8c) THE SCOPE IS SET FROM A SCREEN, AND IT IS THE ARM ACTION THAT WRITES IT. +// (8c) THE SCOPE IS SET FROM A SCREEN, AND IT IS A TREE. // -// `backfill.sweepKinds` has been honoured by the run since the sweep was written -// and settable by nothing but a hand-edit of settings.json. That mattered here -// rather than merely being untidy: an unscoped sweep runs EVERY enabled -// operation, and on the live corpus that includes speaker-names-from-the- -// transcript at ~1 model call per transcript chunk — on the order of 194,000 -// calls. "Diarization only" had no expression in the product. +// `backfill.sweepKinds` was honoured by the run since the sweep was written and +// settable by nothing but a hand-edit of settings.json. That mattered rather +// than merely being untidy: an unscoped run does EVERY enabled operation, and on +// the live corpus that includes speaker-names-from-the-transcript at ~1 model +// call per transcript chunk — on the order of 194,000 calls. "Diarization only" +// had no expression in the product. // -// Two halves are pinned, and the second is the one with a bug behind it. Arming -// a scoped sweep must RECORD the scope, because the boot hook re-launches from -// settings alone and a scope that was not recorded resurrects a bounded run as -// a corpus-wide one. Stopping must CLEAR it, because a stale scope silently -// narrows the next sweep. -test("a sweep can be scoped to one operation from the console, and the scope is persisted", async ({ +// It has one now, and it is the same control every lane uses: a rule that names +// an operation. The scope is persisted as the lane's ROOT, in the same write as +// the switch — so a scope that is not recorded is not a thing that can happen. +test("a lane can be scoped to one operation from its console, and the scope is a tree", async ({ page, }) => { test.setTimeout(SLOW); await resetData("one-transcribe-channel-with-audio"); await writeSettings({ ...backfillSettings(), - // Three operations, so there is a choice to make. With diarization alone - // the console offers no scope control at all — one operation is not a - // choice — which is itself the right behaviour and not what this pins. + // Three operations, so there is a choice to make — and so the ladder draws + // an operation select at all. With diarization alone it draws none, because + // one operation is not a choice and a leaf naming none claims it anyway. attribution: { enabled: true, diarizedEnabled: true, @@ -466,107 +462,80 @@ test("a sweep can be scoped to one operation from the console, and the scope is // ONE OPERATION, ONE PAGE. Diarization's page renders the lane it is // dispatched by — the SHARED backfill lane, which is also attribution's — so - // the scope control below is the same one, reached by an address instead of a + // the rules below are the same rules, reached by an address instead of a // dropdown. - // - // The poll this replaced re-selected the lane until it took: a selectOption - // before hydration sets the DOM value and fires nothing at all — no state - // change, no error, no clue. There is no <select> to lose a change on now, so - // the wait is the ordinary one: React is live on the section. await page.goto("/operations/diarization"); - const lane = page.locator('section[data-sweep-lane="backfill"]'); + const lane = page.locator('section[data-lane="backfill"]'); await expect(lane).toHaveAttribute("data-hydrated", "true", { timeout: 30_000, }); + // The retired panel took its attribute with it. + await expect(page.locator("section[data-sweep-lane]")).toHaveCount(0); - // THE PLAN IS DRAWN BEFORE ANYTHING IS ARMED. That is the whole point: the - // commitment is readable before it is made. - await expect(lane.getByText("The plan")).toBeVisible(); + // WHAT THE LANE WOULD DO NEXT is drawn before anything is armed. That is the + // sweep plan's job, done by the thing that actually dispatches. + await expect(lane.getByText("Next up")).toBeVisible(); - // Leave diarization ticked and drop the two expensive lanes. - await lane.getByLabel("Speaker names (from the audio)").uncheck(); - await lane.getByLabel("Speaker names (from the transcript)").uncheck(); - await expect(lane.getByText("1 of 3 selected")).toBeVisible(); - - // Addressed by its ARIA name, which is unchanged internal addressing — and on - // this panel it is derived from the lane's group ("Start Speakers sweep"), - // not from the queue key the dashboard card uses. The regex keeps this spec - // from re-breaking if a fourth operation moves the group label. - const arm = lane.getByRole("button", { name: /sweep$/i }); - await expect(arm).toBeEnabled({ timeout: 30_000 }); - await arm.click(); + await lane.getByLabel("rule operation").selectOption("diarization"); + await lane.getByLabel("Enable auto-backfill").check(); + await lane.getByRole("button", { name: "Save policy" }).click(); await expect .poll( async () => { - const s = await readJson<{ - backfill?: { sweepEnabled?: boolean; sweepKinds?: string[] }; - }>("test-settings.json").catch(() => ({}) as Record<string, never>); + const b = await backfillLane(); return { - enabled: s.backfill?.sweepEnabled ?? false, - kinds: s.backfill?.sweepKinds ?? [], + enabled: b.enabled ?? false, + operations: (b.root?.children ?? []).map( + (c) => (c.match ?? {}).operation ?? null, + ), }; }, { timeout: 30_000 }, ) - .toEqual({ enabled: true, kinds: ["diarization"] }); - - const stop = lane.getByRole("button", { name: /sweep$/i }); - await expect(stop).toBeEnabled({ timeout: 30_000 }); - await expect(stop).toHaveText(/stop sweeping/i); - await stop.click(); + .toEqual({ enabled: true, operations: ["diarization"] }); - await expect - .poll( - async () => { - const s = await readJson<{ - backfill?: { sweepEnabled?: boolean; sweepKinds?: string[] }; - }>("test-settings.json").catch(() => ({}) as Record<string, never>); - return { - enabled: s.backfill?.sweepEnabled ?? false, - kinds: (s.backfill?.sweepKinds ?? []).length, - }; - }, - { timeout: 30_000 }, - ) - .toEqual({ enabled: false, kinds: 0 }); + // Switch it back off so an armed lane does not bleed into the next spec. + await lane.getByLabel("Enable auto-backfill").uncheck(); + await lane.getByRole("button", { name: "Save policy" }).click(); + await expect.poll(laneArmed, { timeout: 30_000 }).toBe(false); }); -// (8b) …AND SURVIVES A RESTART. This is the reason the flag is persisted at all: -// a sweep is days of work and will outlive several restarts by construction. -// The suite cannot restart the dev server mid-run, so /api/test/ -// resume-backfill-sweep does the two halves a restart does — drop the live -// sweep, then call the boot hook — and the sweep must come back from the -// persisted intent alone. -test("an armed sweep comes back after a restart", async ({ page, request }) => { +// (8b) …AND SURVIVES A RESTART. This is the reason the switch is persisted at +// all: a corpus pass is days of work and will outlive several restarts by +// construction. The suite cannot restart the dev server mid-run, so +// /api/test/resume-lane does the two halves a restart does — drop the live +// runner, then call the boot hook — and the lane must come back from the +// persisted policy alone. +test("an armed lane comes back after a restart", async ({ page, request }) => { test.setTimeout(SLOW); await resetData("one-transcribe-channel-with-audio"); await writeSettings(backfillSettings()); await page.goto("/"); - const start = page.getByRole("button", { name: "start backfill sweep" }); - await expect(start).toBeEnabled({ timeout: 30_000 }); - await start.click(); - await expect - .poll(async () => sweepFlag(), { timeout: 30_000 }) - .toBe(true); + const arm = page.getByRole("button", { name: "arm backfill lane" }); + await expect(arm).toBeEnabled({ timeout: 30_000 }); + await arm.click(); + await expect.poll(laneArmed, { timeout: 30_000 }).toBe(true); - const res = await request.get(`${baseUrl}/api/test/resume-backfill-sweep`); + const res = await request.get( + `${baseUrl}/api/test/resume-lane?lane=backfill`, + ); const body = (await res.json()) as { cancelled: string | null; resumed: string | null; }; // A NEW job id: the old one is gone (as a restart would leave it) and the hook - // started a fresh sweep rather than the test merely observing the old one. + // started a fresh runner rather than the test merely observing the old one. expect(body.resumed).toBeTruthy(); expect(body.resumed).not.toBe(body.cancelled); - // Clean up, so an armed sweep does not bleed into the next spec. + // Clean up, so an armed lane does not bleed into the next spec. await page.goto("/"); - const stop = page.getByRole("button", { name: "stop backfill sweep" }); - await expect(stop).toBeEnabled({ timeout: 30_000 }); - await stop.click(); - await expect.poll(async () => sweepFlag(), { timeout: 30_000 }).toBe(false); + const disarm = page.getByRole("button", { name: "disarm backfill lane" }); + await expect(disarm).toBeEnabled({ timeout: 30_000 }); + await disarm.click(); + await expect.poll(laneArmed, { timeout: 30_000 }).toBe(false); }); // (9) THE WHOLE POINT OF THE SEPARATE QUEUE KEY: a backfill job and a @@ -769,16 +738,19 @@ test("the diarization page shows the channel and both populations", async ({ await generateReport(page, SLUG); await page.goto("/operations/diarization"); - const lane = page.locator('section[data-sweep-lane="backfill"]'); + const lane = page.locator('section[data-lane="backfill"]'); await expect(lane).toBeVisible(); - // The channel is a row in the plan (the fixture has a snapshot, so it lands - // in `working` rather than in the unknown-channel count). - await expect(lane).toContainText(SLUG); + // The channel is what the lane would dispatch next — the plan the sweep's + // itinerary used to draw, answered by the thing that actually dispatches. + await expect(lane.getByText(new RegExp(`${SLUG}/`))).toBeVisible({ + timeout: 30_000, + }); // vidB's audio was removed above, so the re-acquire population is non-zero — - // and it is stated separately, never added to the count beside it. - await expect( - lane.getByText(/need their media back first/), - ).toBeVisible(); + // and it is STATED SEPARATELY on the rail's row, never added to the count + // beside it. On the measured corpus these two are 91x apart. + const row = page.locator('li[data-operation="diarization"]'); + await expect(row).toContainText(/reachable/); + await expect(row).toContainText(/no media/); }); // PAUSE IS A HOLD, NOT A STOP, and it is reachable from where the work is @@ -792,7 +764,7 @@ test("the diarization page shows the channel and both populations", async ({ // `backfillPaused` flag, so the two cannot drift; this asserts the field, // not just the label, for exactly that reason. It also asserts the button // renders WITHOUT a sweep armed — an operator pauses a hand-clicked -// backfill-channel job too, not only a sweep. +// backfill-channel job too, not only an armed lane. test("the dashboard pauses and resumes the backfill lane", async ({ page }) => { test.setTimeout(SLOW); await resetData("one-transcribe-channel-with-audio"); @@ -812,10 +784,11 @@ test("the dashboard pauses and resumes the backfill lane", async ({ page }) => { await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(false); - // The sweep flag is untouched: pausing the lane must not disarm a sweep, or + // 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 - // re-arming. - expect(await sweepFlag()).toBe(false); + // re-arming. (The fixture leaves it disarmed, so `false` is what "unchanged" + // looks like here.) + expect(await laneArmed()).toBe(false); const resume = page.getByRole("button", { name: "resume backfill" }); await expect(resume).toBeEnabled({ timeout: 30_000 }); @@ -847,17 +820,17 @@ test("an operation page holds the same lane the dashboard does", async ({ ).backfill?.enabled ?? false; await page.goto("/operations/diarization"); - // Scoped to the runner-less lane section so this cannot accidentally match a + // Scoped to the lane's own section so this cannot accidentally match a // control the rail draws elsewhere on the page. - const lane = page.locator('section[data-sweep-lane="backfill"]'); + const lane = page.locator('section[data-lane="backfill"]'); const hold = lane.getByRole("button", { name: "pause backfill" }); // The hydration wait: 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(); await expect.poll(laneEnabled, { timeout: 30_000 }).toBe(false); - // A hold is not a stop: the sweep flag is untouched. - expect(await sweepFlag()).toBe(false); + // A hold is not a stop: the lane's arm switch is untouched. + expect(await laneArmed()).toBe(false); // The dashboard shows the same lane held, and releases it. await page.goto("/"); diff --git a/editor/e2e/channel-line.spec.ts b/editor/e2e/channel-line.spec.ts @@ -54,11 +54,7 @@ function laneSettings(enabled: boolean) { }, backfill: { enabled, - weight: 1, concurrency: 1, - sweepEnabled: false, - sweepKinds: [], - sweepChannels: [], allowRedownload: false, }, }; diff --git a/editor/e2e/lane-runner.spec.ts b/editor/e2e/lane-runner.spec.ts @@ -126,14 +126,7 @@ function laneSettings(over: Record<string, unknown> = {}) { }, backfill: { enabled: true, - // weight 1, not the idle-only 0: these specs assert that work HAPPENS. - // The idle-only default has a pure unit test (operationBatch.test.ts), - // which is the right place for it — no pool, no GPU, no timing. - weight: 1, concurrency: 1, - sweepEnabled: false, - sweepKinds: [], - sweepChannels: [], allowRedownload: false, ...((over.backfill as Record<string, unknown>) ?? {}), }, diff --git a/editor/e2e/navigation.spec.ts b/editor/e2e/navigation.spec.ts @@ -37,9 +37,9 @@ import { resetData, writeSite } from "./helpers"; // corpus walk. const HEAVY_ROUTES = [ { link: "Channels", heading: "Channels", path: "/channels" }, - // The board: one row per operation, built from the registry catalog, with the - // arbiter above them. It reads every channel's snapshot to draw the rail, so - // it belongs with the heavy routes rather than beside the cheap ones. + // The board: one row per operation, built from the registry catalog. It reads + // every channel's snapshot to draw the rail, so it belongs with the heavy + // routes rather than beside the cheap ones. { link: "Operations", heading: "Operations", path: "/operations" }, { link: "Jobs", heading: "Jobs", path: "/jobs" }, ] as const; diff --git a/editor/e2e/widget.spec.ts b/editor/e2e/widget.spec.ts @@ -75,11 +75,7 @@ const BACKFILL_SETTINGS = { }, backfill: { enabled: true, - weight: 1, concurrency: 1, - sweepEnabled: false, - sweepKinds: [], - sweepChannels: [], allowRedownload: false, }, };