Archilyzer · Source

archilyzer

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

commit f0aaf37d50313e7d57369a944d642fc663ccbbed
parent 5bcd2c146d63e64dcb63070f9aca1665b53adb32
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 19 Sep 2026 01:47:15 -0400

e2e: the window choice, the description, and the two answers

The fixture grows two more cached windows for vid1, because one file per video
was the reason the wrong-file bug could not be seen: c04 (15.00-18.00) now has
three candidates -- one that does not touch it, one that overlaps by a second
without holding it, and the narrowest one, which does. Sorted by width, the
answer is the first; the spec asserts it is the last, and that the bench's
<video> names that file. A ledger claim, which asks for a VIDEO's files with no
window at all, must still get them.

Three tests for the walk: the description panel says what the clip is supposed
to be, `y` writes `verdict: "confirmed"` and advances, `x` puts the cursor in
the correction box and a saved note reads back as "corrected" -- and `y` on a
corrected clip is refused rather than overwriting the note. Plus the project
page's "Walk the cut" button opening the first clip.

Two deliberate spec changes. The rendered-segment player is behind a <details>
now, so the segment test opens it first. And keyboard tests go through
keyboardLive(): the listener is attached on hydration and a press that lands
before it is simply lost, so `x` -- the one key that writes nothing -- is the
readiness probe for the keys that do.

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

Diffstat:
Mumtool/e2e/clip-bench.spec.ts | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/e2e/fixtures/make-fixture.mjs | 18++++++++++++++++++
2 files changed, 169 insertions(+), 0 deletions(-)

diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts @@ -22,6 +22,11 @@ import { fileURLToPath } from "node:url"; // vid1_0.00-9.00.mp4 is tone / silence / tone / silence / tone with the // silences centred on 3.0s and 6.0s. Verified in the file itself: // silencedetect reports 2.90-3.11 and 5.92-6.11. +// +// vid1 has THREE cached files, because clips-raw is keyed by video and a +// report cites one stream more than once. c04 (15.00-18.00) is held only by +// the NARROWEST of them, which is the whole point -- see the window-choice +// test at the bottom. // --------------------------------------------------------------------------- const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -44,6 +49,7 @@ const readClip = (id: string) => { title?: string; date?: string; correction?: string; + verdict?: string; }[]; }; return m.timeline.find((e) => e.id === id)!; @@ -75,6 +81,23 @@ const setField = async ( await input.blur(); }; +/** + * Wait for the bench's keyboard to be LIVE, and prove it with the harmless key. + * + * The listener is attached on hydration and a press that lands before it is + * simply lost -- `page.goto` resolves long before React has run. `x` is the one + * key that changes nothing on disk (it moves the cursor into the correction + * box), so it doubles as the readiness probe for the keys that do. + */ +const keyboardLive = async (page: import("@playwright/test").Page) => { + const box = page.locator("[data-attrib-field=correction]"); + await expect(async () => { + await page.locator("body").press("x"); + await expect(box).toBeFocused({ timeout: 500 }); + }).toPass({ timeout: 10_000 }); + await box.blur(); +}; + const token = async (request: { get: (u: string) => Promise<{ json: () => Promise<unknown> }> }, clip: string) => { const r = await request.get(`/api/report/clip?project=${encodeURIComponent(PROJECT)}&clip=${clip}`); return (await r.json()) as { token: string }; @@ -337,8 +360,13 @@ test("the segment route serves a built clip with ranges, and 404s when there is // Nothing is rendered for c03, and that is the NORMAL state of a clip nobody // has built -- the bench says so rather than showing a broken player. + // + // It lives behind a <details> now: two 16:9 players is what made this bench + // taller than a laptop, and the rendered one is looked at once per clip, + // after a render. Opening it is the interaction, so the spec does it too. expect((await request.get(`/api/report/segment?${q("c03")}`)).status()).toBe(404); await page.goto(bench("c03")); + await page.locator("[data-rendered-strip] summary").click(); await expect(page.getByTestId("no-segment")).toBeVisible(); await expect(page.getByTestId("segment-video")).toHaveCount(0); @@ -407,3 +435,126 @@ test("a correction is written for the next pass, and collected on the project pa await page.goto(`/browse/${PROJECT}`); await expect(page.locator("[data-corrections]")).toHaveCount(0); }); + + +// --------------------------------------------------------------------------- +// Which cached file the bench plays. +// +// clips-raw is keyed by VIDEO, so a report citing one stream four times leaves +// four files in it. "The widest file for this video" is then another clip's +// file as often as not, and the player seeks `start - from` into it -- for +// ElfpireEva's f01 that was 1146 s into a 44-second file, so the bench showed +// nothing, or somebody else's seconds, on every clip after the first. +// --------------------------------------------------------------------------- + +test("the bench plays the file that HOLDS this clip, not the widest for the video", async ({ + page, + request, +}) => { + const r = await request.get( + `/api/report/clip?project=${encodeURIComponent(PROJECT)}&clip=c04`, + ); + const j = (await r.json()) as { windows: { name: string; from: number; to: number }[] }; + // c04 is 15.00-18.00. vid1_0.00-9.00 does not touch it at all and is gone; + // vid1_10.00-16.00 overlaps by a second without containing it; only + // vid1_14.50-18.50 holds it, and it is the NARROWEST of the three. + expect(j.windows.map((w) => w.name)).toEqual([ + "vid1_14.50-18.50.mp4", + "vid1_10.00-16.00.mp4", + ]); + + await page.goto(bench("c04")); + await expect(page.getByTestId("clip-video")).toHaveAttribute( + "src", + /file=vid1_14\.50-18\.50\.mp4/, + ); + + // And a caller with no window at all -- a ledger claim asks for its VIDEO's + // files and picks by cite -- still gets them. Filtering an absent window by + // overlap would compare against NaN and leave the claim with nothing. + const claim = await request.get( + `/api/report/raw?project=${encodeURIComponent(PROJECT)}&claim=k01`, + ); + expect(claim.status()).toBe(200); + expect(claim.headers()["x-window"]).toBe("vid1_0.00-9.00.mp4"); +}); + +// --------------------------------------------------------------------------- +// The walk: what a clip is supposed to be, and whether it is. +// +// A clip is a CLAIM -- the report said somebody said this, here -- and the +// walk is somebody checking that claim against the audio. Two answers: yes, or +// a note saying what is wrong. An absent verdict is the third state and it is +// the honest one: nobody has been here yet. +// --------------------------------------------------------------------------- + +test("the bench says what the clip is supposed to be", async ({ page }) => { + await page.goto(bench("c01")); + await expect(page.locator("[data-clip-quote]")).toContainText("and because"); + // The fixture's clips carry no `note`, and that is a thing to SAY rather + // than an empty box. + await expect(page.locator("[data-clip-note]")).toHaveText("no note"); + await expect(page.locator("[data-verdict-state]")).toHaveText("not yet reviewed"); +}); + +test("`y` confirms the clip and walks on; the manifest says so", async ({ page, request }) => { + await page.goto(bench("c01")); + await keyboardLive(page); + await page.locator("body").press("y"); + + // Confirming ADVANCES: in the yes case the next clip is what you want, and + // a walk of sixty clips is one finger. + await expect(page.locator("[data-bench=c02]")).toBeVisible(); + await expect.poll(() => readClip("c01").verdict).toBe("confirmed"); + + await page.goto(bench("c01")); + await expect(page.locator("[data-verdict-state]")).toHaveText("confirmed"); + await expect(page.locator("[data-bench=c01]")).toHaveAttribute("data-verdict", "confirmed"); + + // Empty clears it, the way every other field in this manifest clears. + const { token: t } = await token(request, "c01"); + await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c01", verdict: "", token: t }, + }); + await expect.poll(() => readClip("c01").verdict).toBeUndefined(); +}); + +test("`x` puts the cursor in the note, and the note IS the no answer", async ({ page }) => { + await page.goto(bench("c03")); + // The key IS the assertion: `x` answers "no" by putting the cursor where the + // answer goes. + await keyboardLive(page); + + await setField(page, "correction", "The speaker here is the caller, not the host."); + await expect.poll(() => readClip("c03").correction).toContain("the caller"); + // No verdict value is needed: a correction IS "this is not accurate". + await expect(page.locator("[data-verdict-state]")).toHaveText("corrected"); + expect(readClip("c03").verdict).toBeUndefined(); + + // And `y` on a corrected clip does NOT quietly overwrite the note. A clip + // cannot be both, so the bench says which one to resolve first. + await page.locator("[data-verdict-yes]").click(); + await expect(page.locator("[data-bench-note]")).toContainText("clear it first"); + expect(readClip("c03").verdict).toBeUndefined(); + expect(readClip("c03").correction).toContain("the caller"); + // Still here: the "no" answer does not walk on, because the note is the work. + await expect(page.locator("[data-bench=c03]")).toBeVisible(); + + // Cleared, so the fixture is left as the next spec expects to find it. + await setField(page, "correction", ""); + await expect.poll(() => readClip("c03").correction).toBeUndefined(); +}); + +test("the project page opens the walk at the first clip", async ({ page }) => { + await page.goto(`/browse/${PROJECT}`); + const cta = page.locator("[data-walk-cut]"); + await expect(cta).toBeVisible(); + await expect(cta).toContainText("Walk the cut"); + // Named, so it is a promise about where it goes rather than a mystery. + await expect(page.locator("[data-walk-start=c01]")).toHaveAttribute( + "href", + `/browse/${PROJECT}/clip/c01`, + ); + await page.locator("[data-walk-start=c01]").click(); + await expect(page.locator("[data-bench=c01]")).toBeVisible(); +}); diff --git a/umtool/e2e/fixtures/make-fixture.mjs b/umtool/e2e/fixtures/make-fixture.mjs @@ -998,6 +998,24 @@ for (const dir of [BENCH, BUILD]) { path.join(dir, "out", "clips-raw", "vid1_0.00-9.00.mp4"), ); } + +// TWO MORE cached windows for vid1, and they are the regression rather than +// decoration. +// +// clips-raw is keyed by VIDEO and a report cites the same stream more than +// once, so the directory holds a file per clip. c04 is 15.00-18.00 and these +// give it three candidates: the 9-second file (no overlap at all), a 6-second +// one that overlaps by a second without containing it, and the 4-second one +// that actually holds it. Sorted by WIDTH -- which is what the bench did -- +// the answer is the first, and the player seeks 15 s into a file that ends at +// 9. The content is the same nine seconds of tone in all three: what is being +// tested is which NAME is chosen. +for (const name of ["vid1_10.00-16.00.mp4", "vid1_14.50-18.50.mp4"]) { + copyFileSync( + path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), + path.join(BENCH, "out", "clips-raw", name), + ); +} copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(reports, "no-origin-fixture", "out", "no-origin-fixture.mp4"),