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:
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));
+});