Files
olivv-cs-ComfyUI-FunPack/app/shell/tests/client.test.js
T
digital-garbageandClaude Sonnet 5 aa1cec7817 fix: carry every scene a multi-scene run covers through reattach, not just the first
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>
2026-09-15 11:47:08 +03:00

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