Archilyzer · Source

archilyzer

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

commit 470b64ba7bf63ca7538386a70b65952694ca8265
parent c7997a0941093ab7f3dfca65d9b3f9313ce17618
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 20 Sep 2026 19:43:09 -0400

report fetch: poll the job by id, or a failure reads as success

A bare GET /api/report/fetch answers with runningJob(), which is null the
moment the job ends. So a fetch that FAILED between two polls came back as
"nothing running" — indistinguishable from done — and the sweep button counted
it and moved on. The POST already returns the job; poll that.

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

Diffstat:
Mumtool/components/projects/FetchUnfetchedButton.tsx | 16+++++++++++++---
Mumtool/e2e/report-fetch-via-editor.spec.ts | 12++++++++++--
2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/umtool/components/projects/FetchUnfetchedButton.tsx b/umtool/components/projects/FetchUnfetchedButton.tsx @@ -44,10 +44,15 @@ export default function FetchUnfetchedButton({ const total = pending.length; - async function poll(): Promise<JobView | null> { + // POLLED BY ID, not by "whatever is running". A bare GET answers with + // `runningJob()`, which is null the moment the job ends — so a fetch that + // FAILED between two polls would read as nothing running, i.e. as success. + async function poll(id: string): Promise<JobView | null> { for (;;) { await new Promise((r) => setTimeout(r, 1000)); - const r = await fetch("/api/report/fetch", { cache: "no-store" }); + const r = await fetch(`/api/report/fetch?job=${encodeURIComponent(id)}`, { + cache: "no-store", + }); const j = (await r.json()) as { job: JobView | null }; if (!j.job) return null; if (j.job.state !== "running") return j.job; @@ -94,7 +99,12 @@ export default function FetchUnfetchedButton({ setMsg(body.error ?? `HTTP ${res.status}`); break; } - const finished = await poll(); + if (!body.job?.id) { + setFailed(true); + setMsg("the fetch route started no job"); + break; + } + const finished = await poll(body.job.id); if (finished?.state === "failed") { setFailed(true); setMsg(`${clip.id}: ${finished.error ?? "fetch failed"}`); diff --git a/umtool/e2e/report-fetch-via-editor.spec.ts b/umtool/e2e/report-fetch-via-editor.spec.ts @@ -57,15 +57,22 @@ const ytdlpCalls = (): number => { } }; +// BY ID. A bare GET answers with `runningJob()`, which is null the moment the +// job ends -- so a FAILED job polled a beat late reads as "nothing running", +// which is indistinguishable from success. async function waitForJob( request: import("@playwright/test").APIRequestContext, baseURL: string, + jobId?: string, ): Promise<{ state: string; error: string | null }> { let last = { state: "running", error: null as string | null }; + const url = jobId + ? `${baseURL}/api/report/fetch?job=${encodeURIComponent(jobId)}` + : `${baseURL}/api/report/fetch`; await expect .poll( async () => { - const r = await request.get(`${baseURL}/api/report/fetch`); + const r = await request.get(url); const j = (await r.json()) as { job: typeof last | null }; if (j.job) last = j.job; return j.job?.state ?? "done"; @@ -89,8 +96,9 @@ test("the fetch route asks the editor, carries the provenance, and runs no yt-dl data: { project: PROJECT, clip: "e01", padBefore: 2, padAfter: 2 }, }); expect(post.status(), await post.text()).toBe(202); + const started = (await post.json()) as { job: { id: string } }; - const job = await waitForJob(request, baseURL!); + const job = await waitForJob(request, baseURL!, started.job.id); expect(job.state, job.error ?? "").toBe("done"); // WHAT CROSSED THE WIRE. A window with no requester is a window nobody can