commit 0a11ff20a3c61dbd15d3155fa648ec54e67fd70d
parent 79df947924d5ea3bdab9234b011501e9e5fa011c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 19 Sep 2026 03:07:58 -0400
e2e: siblings, the overlap hatch, and one-sided fetching
Three for the siblings: the payload and the list for c01 (c02 "next in cut",
c04 "3 later", both after it on the clock), the dimmed peek cue inside c02's
window carrying "already in the cut as c02", the near-sibling sentence, and
c03 saying no other clip uses vid2. The overlap test moves c02 to 8.00 first:
a drag never downloads, so the overlap has to be reachable inside the cached
file, and it is `Shift+.` that carries c01's end into it -- the coarse nudge
that until now did nothing.
Three for one-sided fetching: the "already cached" refusal now reads per side,
each button names its own pad (+23 after, and nothing before, because c01's
cache already starts at the beginning of the recording), a dimmed cue past the
end sends padAfter with the before side passed through unchanged, and an
after-only fetch -- run for real through the fixture's offline yt-dlp stub --
leaves `from` exactly where it was while `to` grows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 151 insertions(+), 15 deletions(-)
diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts
@@ -731,9 +731,11 @@ test("a fetch the cache already covers is refused, not run", async ({ request })
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);
+ const j = (await res.json()) as { error: string; cachedBefore: number; cachedAfter: number };
+ // Per SIDE, because that is the unit a fetch is asked for in now.
+ expect(j.error).toContain("already cached to −3 s / +3 s");
+ expect(j.cachedBefore).toBe(3);
+ expect(j.cachedAfter).toBe(3);
// And nothing started: a no-op job is worse than an answer, because it takes
// a minute to say the same thing.
@@ -743,14 +745,19 @@ test("a fetch the cache already covers is refused, not run", async ({ request })
expect(running.job).toBeNull();
});
-test("the button names the pad it will ask for, from what is on disk", async ({ page }) => {
+test("each side names its own pad, and a side that cannot grow says so", 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");
+ // The cached file reaches 3 s past the clip's end, so the after button asks
+ // for 23 -- never the flat 20 that was already there.
+ const after = page.locator("[data-fetch-after]");
+ await expect(after).toBeVisible();
+ await expect(after).toHaveAttribute("data-fetch-after", "23");
+ await expect(after).toContainText("fetch to +23 s");
+
+ // And there is nothing before 0.00 to fetch: c01 starts at 3 s into a
+ // recording whose cached file already begins at the beginning.
+ await expect(page.locator("[data-fetch-before]")).toHaveCount(0);
+ await expect(page.locator("[data-fetch-before-maxed]")).toContainText("the recording starts here");
});
test("the rail reads past the cache, dimmed, and a dimmed cue fetches to itself", async ({
@@ -768,17 +775,146 @@ test("the rail reads past the cache, dimmed, and a dimmed cue fetches to itself"
// 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 } = {};
+ let body: { padBefore?: number; padAfter?: number } = {};
await page.route("**/api/report/fetch", async (route) => {
if (route.request().method() !== "POST") return route.continue();
- body = route.request().postDataJSON() as { pad?: number };
+ body = route.request().postDataJSON() as { padBefore?: number; padAfter?: 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));
+ await expect.poll(() => body.padAfter).toBeGreaterThan(0);
+ // Far enough to hold that cue's start plus a breath, measured from the clip
+ // -- and ONE SIDE: the cue is past the end, so the before side is passed
+ // through exactly as cached rather than re-fetched.
+ expect(body.padAfter!).toBeGreaterThanOrEqual(Math.ceil(at + 2 - 6));
+ expect(body.padBefore).toBe(3);
+});
+
+
+// ---------------------------------------------------------------------------
+// The other clips from the same recording.
+//
+// vid1 carries c01, c02 and c04; vid2 carries only c03. "Is the context this
+// clip is missing already in the cut?" is a question about the MANIFEST, and
+// the two answers -- a hole and a duplicate -- look identical from inside one
+// clip.
+// ---------------------------------------------------------------------------
+
+test("a clip lists the other clips cut from its recording", async ({ page, request }) => {
+ const r = await request.get(
+ `/api/report/clip?project=${encodeURIComponent(PROJECT)}&clip=c01`,
+ );
+ const j = (await r.json()) as { siblings: { id: string; where: string; side: string }[] };
+ // Sorted by start, positioned by distance in the CUT.
+ expect(j.siblings.map((sb) => sb.id)).toEqual(["c02", "c04"]);
+ expect(j.siblings[0].where).toBe("next in cut");
+ expect(j.siblings[1].where).toBe("3 later");
+ expect(j.siblings.every((sb) => sb.side === "after")).toBe(true);
+
+ await page.goto(bench("c01"));
+ await expect(page.locator("[data-siblings]")).toHaveAttribute("data-siblings", "2");
+ const row = page.locator("[data-sibling=c02]");
+ await expect(row).toContainText("next in cut");
+ await expect(row.getByRole("link", { name: "c02" })).toHaveAttribute(
+ "href",
+ `/browse/${PROJECT}/clip/c02`,
+ );
+ // A band per sibling on the rail, labelled, so where the cut already reaches
+ // is visible against the window being dragged.
+ await expect(page.locator("[data-sibling-band=c02]")).toBeVisible();
+ await expect(page.locator("[data-sibling-band=c02]")).toContainText("next in cut");
+ await expect(page.locator("[data-sibling-band=c04]")).toHaveCount(1);
+
+ // And a dimmed cue that falls inside c02's window says so: those words are
+ // not missing, they are the next clip.
+ const inside = page.locator("[data-cue][data-peek='1'][data-in-sibling=c02]");
+ await expect(inside.first()).toBeVisible();
+ await expect(inside.first()).toHaveAttribute("title", /already in the cut as c02/);
+});
+
+test("a near sibling is said in words, and a lone clip says it has none", async ({
+ page,
+ request,
+}) => {
+ // c01 3.00-6.00 and c02 at 9.00: three seconds nobody is cutting.
+ const { token: t } = await token(request, "c01");
+ await request.put("/api/report/window", {
+ data: { project: PROJECT, clip: "c01", start: 3, end: 6, token: t },
+ });
+ await page.goto(bench("c01"));
+ await expect(page.locator("[data-sibling-near=c02]")).toContainText(
+ "the next 3 s are not in the cut; c02 starts at 0:09",
+ );
+
+ // c03 is the only clip from vid2, and that is a thing to SAY: the silence
+ // otherwise reads as "not computed".
+ await page.goto(bench("c03"));
+ await expect(page.locator("[data-siblings]")).toContainText(
+ "no other clip in the cut uses this video",
+ );
+ await expect(page.locator("[data-sibling-band]")).toHaveCount(0);
+});
+
+test("a selection widened into another clip is marked as an overlap", async ({
+ page,
+ request,
+}) => {
+ // Put c02 at 8.00, a second inside what c01 can reach: the cached file ends
+ // at 9.00 and a drag never downloads, so the overlap has to be reachable
+ // without one.
+ const { token: t } = await token(request, "c02");
+ await request.put("/api/report/window", {
+ data: { project: PROJECT, clip: "c02", start: 8, end: 11.5, token: t },
+ });
+
+ await page.goto(bench("c01"));
+ await expect(page.locator("[data-sibling-band=c02][data-sibling-clash='0']")).toHaveCount(1);
+
+ // `Shift+.` is the 0.5 s nudge -- and it arrives as ">", which is why the
+ // coarse step the hint advertises used to do nothing at all. Six of them
+ // carry c01's end from 6.00 into c02's window, and the band goes hatched:
+ // from inside c01 those seconds look like context, and they are a duplicate.
+ await keyboardLive(page);
+ for (let i = 0; i < 6; i += 1) await page.locator("body").press("Shift+Period");
+ await expect(page.locator("[data-sibling-band=c02][data-sibling-clash='1']")).toHaveCount(1);
+});
+
+
+test("an after-only fetch leaves the before edge exactly where it was", async ({ request }) => {
+ // c01 is 3.00-6.00 inside vid1_0.00-9.00: −3 s / +3 s.
+ const before = (await (
+ await request.get(`/api/report/clip?project=${encodeURIComponent(PROJECT)}&clip=c01`)
+ ).json()) as { windows: { from: number; to: number }[] };
+ expect(before.windows[0].from).toBe(0);
+
+ const start = await request.post("/api/report/fetch", {
+ data: { project: PROJECT, clip: "c01", padBefore: 3, padAfter: 8 },
+ });
+ expect(start.status()).toBe(202);
+ const { job } = (await start.json()) as { job: { id: string } };
+
+ // The fixture's yt-dlp is a deterministic offline stub, so this is a real
+ // run of the pipeline's own fetch path and not a mock of it.
+ await expect
+ .poll(
+ async () => {
+ const s = (await (await request.get(`/api/report/fetch?job=${job.id}`)).json()) as {
+ job: { state: string } | null;
+ };
+ return s.job?.state ?? "gone";
+ },
+ { timeout: 120_000 },
+ )
+ .not.toBe("running");
+
+ const after = (await (
+ await request.get(`/api/report/clip?project=${encodeURIComponent(PROJECT)}&clip=c01`)
+ ).json()) as { windows: { from: number; to: number }[] };
+ // The widest containing file now reaches further ON ONE SIDE. Asking for
+ // twenty seconds of lead-in nobody wanted is what this stopped doing.
+ expect(after.windows[0].from).toBe(before.windows[0].from);
+ expect(after.windows[0].to).toBeGreaterThan(before.windows[0].to);
});