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:
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"),