Adversarial review of the previous commit found four real defects, all confirmed: 1. A run covering more than one scene (the whole-timeline H3 shape, or any single-scene click whose gen-unit has sibling subclips) only ever sent the FIRST scene id as extra_data.funpack_scene_id. On reload mid-run, reattach recovered just that one id, so _recordSegment only ever wrote the finished media onto scene 1 -- every other scene the job actually rendered silently got nothing recorded, no error shown. Fixed by adding funpack_scene_ids (plural) alongside the existing singular field, threaded through client.js's queuedFor and session.js's reattach() (both additive -- a new optional third onAdopt argument and a new optional response field, ignored by every existing caller such as app/boot.js) down to generate_bridge.js/store.js, which now record onto every adopted scene. 2 & 3. A run adopted on reload never primed pollStart/genWallStart, so _elapsed() and the transport button's own clock read time since the Unix epoch instead of real elapsed time, and never stopped afterward -- wedging genElapsed() for the rest of the session (its own guard is "if already started, do nothing"). Fixed: the "adopt" handler now primes both clocks (flagged approximate, same as the deleted resumeRunningGeneration used to), and every terminal run phase now stops them. 4. check()'s `notes` field (e.g. "N settings didn't reach the pipeline") was fired through transport.warn() but nothing ever subscribed to GenerateBridge's "warn" event -- silently dropped, reproducing the exact bug class the deleted code's own comment said must not recur. Fixed: wired into state.notice, same as the old code did. New tests added for both the plural-scene-id path (client.test.js, session.test.js) and the fallback-to-one-scene case. 691/692 JS tests and 548/548 Python tests pass (the one JS failure is the pre-existing, unrelated projects.test.js import-race flake). Live-verified against the dev server: a multi-scene GenerateBridge.generate() call still correctly surfaces a real pipeline refusal with no console errors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
200 lines
7.6 KiB
JavaScript
200 lines
7.6 KiB
JavaScript
// Reattaching to a run that outlived the page.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { queuedFor, finishedFor } from "../client.js";
|
|
|
|
const answering = (body, ok = true) => async () => ({
|
|
ok, status: ok ? 200 : 500, json: async () => body,
|
|
});
|
|
|
|
test("the run this browser queued is found by its client id", async () => {
|
|
const found = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [
|
|
[1, "someone-elses", {}, { client_id: "them" }, []],
|
|
[2, "mine", {}, { client_id: "me" }, []],
|
|
],
|
|
queue_pending: [],
|
|
}),
|
|
});
|
|
assert.deepEqual(found, { promptId: "mine", running: true, sceneId: null, sceneIds: [], projectId: null });
|
|
});
|
|
|
|
test("another browser's run is not adopted", async () => {
|
|
const promptId = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [[1, "theirs", {}, { client_id: "them" }, []]],
|
|
queue_pending: [[2, "also-theirs", {}, { client_id: "them" }, []]],
|
|
}),
|
|
});
|
|
assert.equal(promptId, null);
|
|
});
|
|
|
|
test("nothing running means nothing to reattach to", async () => {
|
|
assert.equal(await queuedFor("me", {
|
|
fetch: answering({ queue_running: [], queue_pending: [] }),
|
|
}), null);
|
|
});
|
|
|
|
test("a queue that cannot be reached is not an error the app has to show", async () => {
|
|
// The dev server has no queue behind it, which is the ordinary case while
|
|
// working on the UI.
|
|
const promptId = await queuedFor("me", {
|
|
fetch: async () => { throw new TypeError("Failed to fetch"); },
|
|
});
|
|
assert.equal(promptId, null);
|
|
});
|
|
|
|
test("a malformed queue entry is skipped rather than crashing the load", async () => {
|
|
const found = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [null, "nonsense", [1], [2, "mine", {}, { client_id: "me" }, []]],
|
|
queue_pending: [undefined, {}, [3, null, {}, { client_id: "me" }, []]],
|
|
}),
|
|
});
|
|
assert.deepEqual(found, { promptId: "mine", running: true, sceneId: null, sceneIds: [], projectId: null });
|
|
});
|
|
|
|
test("where a run belongs travels with it, when it said so at queue time", async () => {
|
|
const found = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [[1, "mine", {}, { client_id: "me", funpack_scene_id: "s1",
|
|
funpack_project_id: "p1" }, []]],
|
|
queue_pending: [],
|
|
}),
|
|
});
|
|
assert.deepEqual(found, { promptId: "mine", running: true, sceneId: "s1", sceneIds: [], projectId: "p1" });
|
|
});
|
|
|
|
test("a run covering more than one scene carries the whole list, not just the first", async () => {
|
|
// No chain sampler to split a multi-scene selection into several /prompt
|
|
// calls, so one run can cover several scenes at once -- reattach needs
|
|
// every one of them, or a reload only ever recovers the first (see this
|
|
// field's own comment in client.js for why sceneId alone is not enough).
|
|
const found = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [[1, "mine", {}, { client_id: "me", funpack_scene_id: "s1",
|
|
funpack_scene_ids: ["s1", "s2", "s3"],
|
|
funpack_project_id: "p1" }, []]],
|
|
queue_pending: [],
|
|
}),
|
|
});
|
|
assert.deepEqual(found, {
|
|
promptId: "mine", running: true, sceneId: "s1", sceneIds: ["s1", "s2", "s3"], projectId: "p1",
|
|
});
|
|
});
|
|
|
|
test("a run queued before this existed has nowhere it said it belongs, and says so with null", async () => {
|
|
const found = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [[1, "mine", {}, { client_id: "me" }, []]],
|
|
queue_pending: [],
|
|
}),
|
|
});
|
|
assert.equal(found.sceneId, null);
|
|
assert.equal(found.projectId, null);
|
|
});
|
|
|
|
test("a finished run is recognised as this browser's from its history entry", async () => {
|
|
const asked = [];
|
|
const promptId = await finishedFor("me", ["theirs", "mine"], {
|
|
fetch: async (url) => {
|
|
asked.push(url);
|
|
const id = url.split("/").pop();
|
|
const owner = id === "mine" ? "me" : "them";
|
|
return { ok: true, status: 200, json: async () => ({
|
|
[id]: { prompt: [1, id, {}, { client_id: owner }, []], outputs: {}, status: {} },
|
|
}) };
|
|
},
|
|
});
|
|
assert.equal(promptId, "mine");
|
|
// Newest first: the run that just ended is the one being looked for.
|
|
assert.equal(asked[0], "/history/mine");
|
|
});
|
|
|
|
test("onFound hands back the extra_data of a match, without changing what this returns", async () => {
|
|
const found = [];
|
|
const promptId = await finishedFor("me", ["mine"], {
|
|
fetch: async () => ({ ok: true, status: 200, json: async () => ({
|
|
mine: { prompt: [1, "mine", {}, { client_id: "me", funpack_scene_id: "s1",
|
|
funpack_project_id: "p1" }, []] },
|
|
}) }),
|
|
onFound: (extra) => found.push(extra),
|
|
});
|
|
assert.equal(promptId, "mine");
|
|
assert.deepEqual(found, [{ client_id: "me", funpack_scene_id: "s1", funpack_project_id: "p1" }]);
|
|
});
|
|
|
|
test("onFound is never called when nothing matches", async () => {
|
|
let called = false;
|
|
await finishedFor("me", ["theirs"], {
|
|
fetch: async () => ({ ok: true, status: 200, json: async () => ({
|
|
theirs: { prompt: [1, "theirs", {}, { client_id: "them" }, []] },
|
|
}) }),
|
|
onFound: () => { called = true; },
|
|
});
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
test("a finished run belonging to someone else is not adopted", async () => {
|
|
const promptId = await finishedFor("me", ["theirs"], {
|
|
fetch: async (url) => ({ ok: true, status: 200, json: async () => ({
|
|
theirs: { prompt: [1, "theirs", {}, { client_id: "them" }, []] },
|
|
}) }),
|
|
});
|
|
assert.equal(promptId, null);
|
|
});
|
|
|
|
test("a history entry with nothing in it is skipped, not trusted", async () => {
|
|
for (const body of [{}, { mine: {} }, { mine: { prompt: [] } }, { mine: { prompt: [1, "mine", {}] } }]) {
|
|
const promptId = await finishedFor("me", ["mine"], {
|
|
fetch: async () => ({ ok: true, status: 200, json: async () => body }),
|
|
});
|
|
assert.equal(promptId, null, JSON.stringify(body));
|
|
}
|
|
});
|
|
|
|
test("no history to ask is not an error", async () => {
|
|
const promptId = await finishedFor("me", ["mine"], {
|
|
fetch: async () => { throw new TypeError("Failed to fetch"); },
|
|
});
|
|
assert.equal(promptId, null);
|
|
});
|
|
|
|
test("a run still waiting its turn is found too", async () => {
|
|
// /queue answers with both halves, and a job sits in the pending one from the
|
|
// moment /prompt returns until the worker picks it up -- always briefly, and
|
|
// for as long as it takes whenever something is running ahead of it. A reload
|
|
// in that window found nothing and let the same job be queued twice.
|
|
const promptId = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [[1, "theirs", {}, { client_id: "them" }, []]],
|
|
queue_pending: [[2, "mine", {}, { client_id: "me" }, []]],
|
|
}),
|
|
});
|
|
assert.deepEqual(promptId, { promptId: "mine", running: false, sceneId: null, sceneIds: [], projectId: null },
|
|
"a job waiting its turn was reported as one already under way");
|
|
});
|
|
|
|
test("a run under way is preferred over one still waiting", async () => {
|
|
const promptId = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [[1, "under-way", {}, { client_id: "me" }, []]],
|
|
queue_pending: [[2, "waiting", {}, { client_id: "me" }, []]],
|
|
}),
|
|
});
|
|
assert.deepEqual(promptId, { promptId: "under-way", running: true, sceneId: null, sceneIds: [], projectId: null });
|
|
});
|
|
|
|
test("somebody else's pending run is not adopted either", async () => {
|
|
const promptId = await queuedFor("me", {
|
|
fetch: answering({
|
|
queue_running: [],
|
|
queue_pending: [[1, "theirs", {}, { client_id: "them" }, []]],
|
|
}),
|
|
});
|
|
assert.equal(promptId, null);
|
|
});
|