Archilyzer · Source

archilyzer

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

commit fe10ef1998935ebfa373dfa9b422a6bf2385d091
parent bbc5b7d2199a131476a5f20a1df9382cf9edce71
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 19 Sep 2026 02:43:34 -0400

e2e: two verdicts, the note rule, the edge audition, and a fetch that is a no-op

The verdict tests follow the shape the writer now keeps: `x` with an empty box
writes NOTHING, `x` with text writes the note and the verdict in one patch,
`y` on an incorrect clip keeps the note and reads "confirmed · with note", an
incorrect verdict is refused without one (from both sides -- setting it, and
clearing the note off it), and a correction with no verdict still reads as
incorrect, because two of those are in a live manifest.

Three for how you listen: `.` plays the four seconds ENDING on the new edge,
the speed survives a reload, and walking onto a clip with auto-audition on
starts it playing. The last one is why the preference bug was found: it was
the only test that read a setting on a page other than the one that set it.

Three for fetching: a window the cache already covers is a 409 with no job
started, the button names the pad it computes from what is on disk (±23, not
a flat 20), and a dimmed peek cue posts a pad wide enough to reach it -- that
POST intercepted, because the arithmetic is the subject and a real download is
the build spec's job.

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

Diffstat:
Mumtool/e2e/clip-bench.spec.ts | 252++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 238 insertions(+), 14 deletions(-)

diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts @@ -417,6 +417,9 @@ test("a correction is written for the next pass, and collected on the project pa await page.goto(bench("c04")); await setField(page, "correction", text); await expect.poll(() => readClip("c04").correction).toBe(text); + // Writing a note on a clip nobody has confirmed IS saying the clip is wrong, + // and the verdict rides in the same patch. + expect(readClip("c04").verdict).toBe("incorrect"); await page.reload(); await expect(page.locator("[data-attrib-field=correction]")).toHaveValue(text); @@ -428,7 +431,13 @@ test("a correction is written for the next pass, and collected on the project pa await expect(list).toBeVisible(); await expect(list.locator("[data-correction=c04]")).toContainText("not Destiny"); - // Empty deletes the key, and the section goes with the last correction. + // Empty deletes the key, and the section goes with the last correction -- + // but an INCORRECT clip cannot have its note taken away, so the way back is + // to say the clip is fine first. That is the point of the rule: "no" without + // a reason is a complaint nobody can act on. + await page.goto(bench("c04")); + await page.locator("[data-verdict-yes]").click(); + await expect.poll(() => readClip("c04").verdict).toBe("confirmed"); await page.goto(bench("c04")); await setField(page, "correction", ""); await expect.poll(() => readClip("c04").correction).toBeUndefined(); @@ -519,30 +528,172 @@ test("`y` confirms the clip and walks on; the manifest says so", async ({ page, await expect.poll(() => readClip("c01").verdict).toBeUndefined(); }); -test("`x` puts the cursor in the note, and the note IS the no answer", async ({ page }) => { +test("`x` requires the note, writes both fields at once, and stays put", async ({ page }) => { await page.goto(bench("c03")); // The key IS the assertion: `x` answers "no" by putting the cursor where the - // answer goes. + // answer goes -- and keyboardLive() blurs it again with the box still empty, + // which must write NOTHING. Blurring out of a box you were asked to fill is + // not an answer. await keyboardLive(page); + expect(readClip("c03").verdict).toBeUndefined(); + expect(readClip("c03").correction).toBeUndefined(); + await expect(page.locator("[data-verdict-state]")).toHaveText("not yet reviewed"); 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(); + // ONE patch. The manifest never holds a complaint with no verdict, which is + // why the bench sends them together rather than in two saves. + await expect.poll(() => readClip("c03").verdict).toBe("incorrect"); + expect(readClip("c03").correction).toContain("the caller"); + await expect(page.locator("[data-verdict-state]")).toHaveText("incorrect"); + // And it STAYS: the note is there to be read back against the clip. + await expect(page.locator("[data-bench=c03]")).toBeVisible(); +}); + +test("`y` on an incorrect clip keeps the note — it becomes a note, not a defect", async ({ + page, +}) => { + await page.goto(bench("c03")); + await expect(page.locator("[data-verdict-state]")).toHaveText("incorrect"); - // 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(); + await expect.poll(() => readClip("c03").verdict).toBe("confirmed"); + // The note survives the confirmation. It stops being a complaint and becomes + // what it now says it is: something a reader should know about a good clip. 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 page.goto(bench("c03")); + await expect(page.locator("[data-verdict-state]")).toHaveText("confirmed · with note"); +}); + +test("an incorrect verdict cannot have its note taken away, or arrive without one", async ({ + page, + request, +}) => { + // c02 carries no note, so this is the same error from the other side. + const { token: t2 } = await token(request, "c02"); + const bad = await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c02", verdict: "incorrect", token: t2 }, + }); + expect(bad.status()).toBe(400); + expect(((await bad.json()) as { error: string }).error).toContain("needs its note"); + expect(readClip("c02").verdict).toBeUndefined(); + + // c03 still has its note; put the incorrect verdict back on it. + const { token: t3 } = await token(request, "c03"); + const ok = await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c03", verdict: "incorrect", token: t3 }, + }); + expect(ok.ok()).toBeTruthy(); + + // Now empty the box in the bench. Refused, and the bench says why rather + // than quietly leaving the screen and the manifest disagreeing. + await page.goto(bench("c03")); + await setField(page, "correction", ""); + await expect(page.locator("[data-bench-note]")).toContainText("needs its note"); + expect(readClip("c03").correction).toContain("the caller"); + expect(readClip("c03").verdict).toBe("incorrect"); + + // Cleared the only way there is -- say the clip is fine, then drop the note. + await page.locator("[data-verdict-yes]").click(); + await expect.poll(() => readClip("c03").verdict).toBe("confirmed"); + await page.goto(bench("c03")); await setField(page, "correction", ""); await expect.poll(() => readClip("c03").correction).toBeUndefined(); + const { token: t4 } = await token(request, "c03"); + await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c03", verdict: "", token: t4 }, + }); + await expect.poll(() => readClip("c03").verdict).toBeUndefined(); +}); + +test("a note written before `incorrect` existed still reads as incorrect", async ({ + page, + request, +}) => { + // Two of these are in a live manifest: `x` wrote a correction and no verdict + // before there was one to write. They are read, never migrated. + const { token: t } = await token(request, "c02"); + await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c02", correction: "legacy: written before the verdict was", token: t }, + }); + expect(readClip("c02").verdict).toBeUndefined(); + + await page.goto(bench("c02")); + await expect(page.locator("[data-verdict-state]")).toHaveText("incorrect"); + await expect(page.locator("[data-bench=c02]")).toHaveAttribute("data-verdict", "incorrect"); + + await page.goto(`/browse/${PROJECT}`); + await expect(page.locator("[data-walk-progress]")).toContainText("incorrect"); + await expect(page.locator("[data-correction=c02]")).toHaveAttribute( + "data-correction-verdict", + "incorrect", + ); + + // And a legacy note can still be cleared: the required-note rule is keyed on + // the explicit verdict, so it never traps somebody in a state they wrote + // before the rule existed. + const { token: t2 } = await token(request, "c02"); + const gone = await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c02", correction: "", token: t2 }, + }); + expect(gone.ok()).toBeTruthy(); + expect(readClip("c02").correction).toBeUndefined(); +}); + +// --------------------------------------------------------------------------- +// How you listen. +// +// Per browser, never per manifest. The edge-aware audition is the bug this +// pair was born with: every change played `from` to `from + 6`, so moving the +// END -- the edge that decides whether a clip stops mid-thought -- played the +// start and answered a question nobody asked. +// --------------------------------------------------------------------------- + +test("moving the end auditions the END", async ({ page }) => { + await page.goto(bench("c01")); + await keyboardLive(page); + + const before = readClip("c01"); + await page.locator("body").press("."); + // Clamped to the cached file, which ends at 9.00 -- a drag never downloads. + const to = Math.min(before.end + 0.05, 9); + const t = await page + .getByTestId("clip-video") + .evaluate((el: HTMLVideoElement) => el.currentTime); + // vid1_0.00-9.00 starts at 0.00, so file time IS source time here. The four + // seconds ENDING on the new edge, not the four after the start. + expect(t).toBeGreaterThan(to - 4 - 0.4); + expect(t).toBeLessThan(to - 4 + 2); +}); + +test("the playback speed is this browser's, and it survives a reload", async ({ page }) => { + await page.goto(bench("c01")); + await page.locator("[data-playback-rate]").selectOption("1.5"); + await expect + .poll(() => page.getByTestId("clip-video").evaluate((el: HTMLVideoElement) => el.playbackRate)) + .toBe(1.5); + + await page.reload(); + await expect(page.locator("[data-playback-rate]")).toHaveValue("1.5"); + await expect + .poll(() => page.getByTestId("clip-video").evaluate((el: HTMLVideoElement) => el.playbackRate)) + .toBe(1.5); +}); + +test("auto-audition plays the clip you walk onto", async ({ page }) => { + // c03 -> c04, because c04 is the neighbour with cached material. + await page.goto(bench("c03")); + await page.locator("[data-auto-audition=off]").click(); + await expect(page.locator("[data-auto-audition=on]")).toBeVisible(); + + await page.locator("[data-clip-nav=next]").click(); + await expect(page.locator("[data-bench=c04]")).toBeVisible(); + await expect + .poll( + () => page.getByTestId("clip-video").evaluate((el: HTMLVideoElement) => !el.paused), + { timeout: 15_000 }, + ) + .toBe(true); }); test("the project page opens the walk at the first clip", async ({ page }) => { @@ -558,3 +709,76 @@ test("the project page opens the walk at the first clip", async ({ page }) => { await page.locator("[data-walk-start=c01]").click(); await expect(page.locator("[data-bench=c01]")).toBeVisible(); }); + + +// --------------------------------------------------------------------------- +// Fetching more, and reading ahead before you pay for it. +// +// The pad is measured from the CLIP, not from the cache, and the pipeline +// reuses any file that already contains the window -- so a flat `pad: 20` +// asked for the same seconds twice, downloaded nothing, exited 0, and the +// bench reported "fetched" over a cache that had not moved. +// --------------------------------------------------------------------------- + +test("a fetch the cache already covers is refused, not run", async ({ request }) => { + // c01 3.00-6.00 inside vid1_0.00-9.00 is exactly ±3 s of pad. + const { token: t } = await token(request, "c01"); + await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c01", start: 3, end: 6, token: t }, + }); + + const res = await request.post("/api/report/fetch", { + data: { project: PROJECT, clip: "c01", pad: 3 }, + }); + expect(res.status()).toBe(409); + const j = (await res.json()) as { error: string; cachedPad: number }; + expect(j.error).toContain("already cached to ±3 s"); + expect(j.cachedPad).toBe(3); + + // And nothing started: a no-op job is worse than an answer, because it takes + // a minute to say the same thing. + const running = (await (await request.get("/api/report/fetch")).json()) as { + job: unknown | null; + }; + expect(running.job).toBeNull(); +}); + +test("the button names the pad it will ask for, from what is on disk", async ({ page }) => { + await page.goto(bench("c01")); + // The cached file reaches 3 s past the clip, so the next press asks for 23 -- + // never the flat 20 that was already there. + const btn = page.locator("[data-fetch-more]"); + await expect(btn).toBeVisible(); + await expect(btn).toHaveAttribute("data-fetch-more", "23"); + await expect(btn).toContainText("fetch to ±23 s"); +}); + +test("the rail reads past the cache, dimmed, and a dimmed cue fetches to itself", async ({ + page, +}) => { + await page.goto(bench("c01")); + // vid1's cues run to 21 s and the cached file stops at 9 -- so there is + // something to read out there, and it is marked as not fetched. + const peeked = page.locator("[data-cue][data-peek='1']"); + await expect(peeked.first()).toBeVisible(); + await expect(page.locator("[data-cache-edge]")).toHaveCount(2); + // The cues inside the cache are still the ones that can move an edge. + await expect(page.locator("[data-cue='3'][data-peek='0']")).toBeVisible(); + + // Clicking a dimmed cue asks for exactly the pad that reaches it. The POST is + // intercepted rather than run: what is being tested is the arithmetic, and a + // real fetch here would be the build spec's job a second time. + let body: { pad?: number } = {}; + await page.route("**/api/report/fetch", async (route) => { + if (route.request().method() !== "POST") return route.continue(); + body = route.request().postDataJSON() as { pad?: number }; + await route.fulfill({ status: 409, json: { error: "intercepted" } }); + }); + + const cue = peeked.first(); + const at = Number(await cue.getAttribute("data-cue")); + await cue.click(); + await expect.poll(() => body.pad).toBeGreaterThan(0); + // Far enough to hold that cue's start plus a breath, measured from the clip. + expect(body.pad!).toBeGreaterThanOrEqual(Math.ceil(at + 2 - 6)); +});