Files
ComfyAssets-ComfyUI_PromptM…/tests/js/comfy-metadata.test.js
T
Vito d9537a726c feat(workspace): complete shared inspector and collection viewer (#224)
* feat(inspector): expose bounded physical file evidence

* fix(inspector): distinguish ambiguous saved associations

* feat(inspector): derive conservative generation facts

* fix(inspector): guard fact ancestry and inspection state

* feat(inspector): share complete file details and exports

* fix(inspector): preserve fact states and touch targets

* feat(viewer): navigate the loaded media collection

Refs #216

* fix(viewer): refit after current stage geometry changes

Ref #216. Observe the current media stage after readiness and wrapped title changes without rebuilding focused controls or resetting transforms at unchanged dimensions.

* feat(viewer): add lifecycle-safe slideshow and media preferences

* fix(viewer): resume gallery continuation after close (#216)

* fix(images): guard source recovery and wrap viewer actions

Clear stale root and folder intent, cancel old pages, and wait for current source discovery before loading a refreshed collection. Preserve safe retry through failed, superseded, and disposed discovery.

Wrap narrow viewer footer actions and wait for measured gallery viewport settling in responsive checks. Refs #216; addresses PR #224 review.

* test(viewer): await gallery layout after viewport changes

Require actual inner, root client, and document widths to agree after each responsive-case viewport and CSS zoom transition. Keep the existing exact frame, overflow, focus, and coarse target assertions unchanged.

Observed PR #224 CI initial mobile resize at537px before pending card relayout settled to356px. Refs #216.
2026-10-18 09:41:53 -07:00

781 lines
49 KiB
JavaScript

// Run with: node --test tests/js/
const test = require("node:test");
const assert = require("node:assert/strict");
const { extractGenerationParams, extractGenerationFacts } = require("../../web/js/comfy-metadata.js");
const parsedFactGraph = value => ({ status: "parsed", value });
const absentFactGraph = () => ({ status: "absent", value: null });
const factFields = ["seed", "steps", "cfg", "sampler", "scheduler", "model"];
function factsFor(graph, extra = {}) {
assert.equal(typeof extractGenerationFacts, "function", "the conservative facts contract is exported");
return extractGenerationFacts({ prompt_graph: parsedFactGraph(graph), workflow_graph: absentFactGraph(), metadataStatus: "available", ...extra });
}
function factGraph(inputs = {}) {
return {
s: { class_type: "KSampler", inputs: { seed: "9007199254740993", steps: 28, cfg: "6.5000000000000000001", sampler_name: "dpmpp_2m", scheduler: "karras", model: ["model", 0], ...inputs } },
model: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "model.safetensors" } },
};
}
test("facts keep unsafe seed and decimal strings exact", () => {
const facts = factsFor(factGraph());
assert.deepEqual(Object.keys(facts), factFields);
assert.deepEqual(factFields.map(field => facts[field].value), ["9007199254740993", "28", "6.5000000000000000001", "dpmpp_2m", "karras", "model.safetensors"]);
for (const fact of Object.values(facts)) assert.deepEqual(fact, {
status: "known", value: fact.value, source: "api-graph", evidence: "configuration", reason: null, sampler_id: "s",
});
const zero = factsFor(factGraph({ seed: 0, cfg: "0", steps: "1" }));
assert.equal(zero.seed.value, "0"); assert.equal(zero.cfg.value, "0");
for (const inputs of [{ seed: 9007199254740992 }, { seed: "1.5" }, { steps: "0" }, { cfg: "NaN" }, { cfg: {} }, { cfg: true }]) {
const key = Object.keys(inputs)[0];
assert.equal(factsFor(factGraph(inputs))[key].status, "unresolved");
}
});
test("facts do not choose equal sampler branches or unrelated model", () => {
const graph = factGraph(); graph.other = structuredClone(graph.s);
const ambiguous = factsFor(graph);
assert.ok(Object.values(ambiguous).every(fact => fact.status === "ambiguous" && fact.value === null));
const selected = factsFor(graph, { resolution: { state: "resolved", sampler_id: "other" } });
assert.equal(selected.seed.sampler_id, "other");
const unrelated = factsFor(factGraph({ model: ["missing", 0] }));
assert.equal(unrelated.model.status, "unresolved");
assert.equal(unrelated.model.value, null);
assert.equal(factsFor({ only: graph.model }).seed.status, "unresolved");
});
test("facts respect named scalar outputs and custom scheduler paths", () => {
const graph = factGraph({ seed: ["primitive", 0], steps: ["steps", 0], cfg: ["cfg", 0], sampler_name: ["sampler", 0], scheduler: ["scheduler", 0], model: ["lora", 0] });
graph.primitive = { class_type: "PrimitiveInt", inputs: { value: "0" } };
graph.steps = { class_type: "PrimitiveInt", inputs: { value: "50" } };
graph.cfg = { class_type: "PrimitiveFloat", inputs: { value: "0" } };
graph.sampler = { class_type: "PrimitiveString", inputs: { value: "euler" } };
graph.scheduler = { class_type: "PrimitiveStringMultiline", inputs: { value: "normal" } };
graph.lora = { class_type: "LoraLoader", inputs: { lora_name: "style", model: ["model", 0] } };
assert.deepEqual(factFields.map(field => factsFor(graph)[field].value), ["0", "50", "0", "euler", "normal", "model.safetensors"]);
graph.settings = { class_type: "UnknownSettings", inputs: { first: 20, second: 30 } };
graph.s.inputs.steps = ["settings", 0];
assert.equal(factsFor(graph).steps.status, "unresolved");
const custom = {
s: { class_type: "SamplerCustomAdvanced", inputs: { guider: ["g", 0], noise: ["n", 0], sampler: ["k", 0], sigmas: ["sigmas", 0] } },
g: { class_type: "CFGGuider", inputs: { model: ["m", 0], cfg: "4.0000000000000001" } },
n: { class_type: "RandomNoise", inputs: { noise_seed: "9007199254740993" } },
k: { class_type: "KSamplerSelect", inputs: { sampler_name: "euler" } },
sigmas: { class_type: "BasicScheduler", inputs: { steps: 22, scheduler: "karras" } },
m: { class_type: "UNETLoader", inputs: { unet_name: "flux.safetensors" } },
};
assert.deepEqual(factFields.map(field => factsFor(custom)[field].value), ["9007199254740993", "22", "4.0000000000000001", "euler", "karras", "flux.safetensors"]);
custom.s = { class_type: "SamplerCustom", inputs: { model: ["m", 0], cfg: 0, noise_seed: 0, sampler: ["k", 0], sigmas: ["sigmas", 0] } };
assert.deepEqual([factsFor(custom).seed.value, factsFor(custom).steps.value, factsFor(custom).cfg.value], ["0", "22", "0"]);
custom.sigmas.inputs = { steps: 22 };
assert.equal(factsFor(custom).scheduler.status, "not-recorded");
assert.equal(factsFor(custom).sampler.value, "euler");
});
test("facts require supported scalar producer types and output slots", () => {
const cases = [
["PrimitiveInt", "seed", "42", true], ["PrimitiveFloat", "cfg", "6.5000000000000000001", true],
["PrimitiveString", "sampler_name", "euler", true], ["PrimitiveStringMultiline", "scheduler", "normal", true],
["PrimitiveString", "seed", "42", false], ["PrimitiveStringMultiline", "steps", "42", false],
["PrimitiveInt", "sampler_name", "euler", false], ["PrimitiveFloat", "scheduler", "normal", false],
["PrimitiveBoolean", "seed", "42", false], ["PrimitiveNode", "seed", "42", false],
["UnknownSettings", "seed", "42", false], ["SamplerCombo", "steps", "42", false],
];
for (const [class_type, input, value, supported] of cases) {
for (const slot of [0, 1, 999, -1, 0.5]) {
const graph = factGraph({ [input]: ["settings", slot] });
graph.settings = { class_type, inputs: { value, [input]: value } };
const fact = factsFor(graph)[input === "sampler_name" ? "sampler" : input];
assert.equal(fact.status, supported && slot === 0 ? "known" : "unresolved", `${class_type} output${slot} as ${input}`);
assert.equal(fact.value, supported && slot === 0 ? value : null);
}
}
const graph = factGraph({ seed: ["primitive", 0] });
graph.primitive = { class_type: "PrimitiveInt", inputs: { seed: "987" } };
assert.equal(factsFor(graph).seed.status, "unresolved", "a matching input is not the output's value");
graph.primitive.inputs = { value: "42", seed: "987" };
assert.equal(factsFor(graph).seed.value, "42");
for (const [input, class_type, inputs] of [
["seed", "RandomNoise", { noise_seed: "42", seed: "42" }],
["sampler_name", "KSamplerSelect", { sampler_name: "euler" }],
["steps", "BasicScheduler", { steps: "42" }],
["cfg", "CFGGuider", { cfg: "7" }],
]) {
const graph = factGraph({ [input]: ["provider", 0] }); graph.provider = { class_type, inputs };
assert.equal(factsFor(graph)[input === "sampler_name" ? "sampler" : input].status, "unresolved", `${class_type} is not a generic scalar output`);
}
});
function customFactGraph() {
return {
s: { class_type: "SamplerCustomAdvanced", inputs: { guider: ["g", 0], noise: ["n", 0], sampler: ["k", 0], sigmas: ["sigmas", 0] } },
g: { class_type: "CFGGuider", inputs: { model: ["m", 0], cfg: ["cfg", 0] } },
n: { class_type: "RandomNoise", inputs: { noise_seed: ["seed", 0] } },
k: { class_type: "KSamplerSelect", inputs: { sampler_name: ["sampler", 0] } },
sigmas: { class_type: "BasicScheduler", inputs: { steps: ["steps", 0], scheduler: ["scheduler", 0] } },
seed: { class_type: "PrimitiveInt", inputs: { value: "9007199254740993" } },
cfg: { class_type: "PrimitiveFloat", inputs: { value: "4.0000000000000001" } },
steps: { class_type: "PrimitiveInt", inputs: { value: 22 } },
sampler: { class_type: "PrimitiveString", inputs: { value: "euler" } },
scheduler: { class_type: "PrimitiveString", inputs: { value: "karras" } },
m: { class_type: "UNETLoader", inputs: { unet_name: "flux.safetensors" } },
};
}
test("facts require supported custom providers and guider outputs", () => {
assert.deepEqual(factFields.map(field => factsFor(customFactGraph())[field].value), ["9007199254740993", "22", "4.0000000000000001", "euler", "karras", "flux.safetensors"]);
for (const [input, nodeId, fields] of [["noise", "n", ["seed"]], ["sampler", "k", ["sampler"]], ["sigmas", "sigmas", ["steps", "scheduler"]], ["guider", "g", ["cfg", "model"]]]) {
for (const slot of [1, 999, -1, 0.5]) {
const graph = customFactGraph(); graph.s.inputs[input] = [nodeId, slot];
for (const field of fields) assert.equal(factsFor(graph)[field].status, "unresolved", `${input} output${slot} ${field}`);
}
for (const class_type of ["UnknownProvider", "PrimitiveInt", "PrimitiveString", "BasicGuider"]) {
if (input === "guider" && class_type === "BasicGuider") continue;
const graph = customFactGraph(); graph[nodeId].class_type = class_type;
for (const field of fields) assert.equal(factsFor(graph)[field].status, "unresolved", `${class_type} cannot provide ${input}`);
}
}
const basic = customFactGraph(); basic.g = { class_type: "BasicGuider", inputs: { model: ["m", 0] } };
assert.equal(factsFor(basic).model.value, "flux.safetensors");
assert.equal(factsFor(basic).cfg.status, "not-recorded");
});
test("facts require model output ancestry with supported loaders and passthroughs", () => {
for (const [class_type, key] of [["CheckpointLoader", "ckpt_name"], ["CheckpointLoaderSimple", "ckpt_name"], ["unCLIPCheckpointLoader", "ckpt_name"], ["UNETLoader", "unet_name"]]) {
for (const slot of [0, 1, 2, 999, -1, 0.5]) {
const graph = factGraph({ model: ["loader", slot] });
graph.loader = { class_type, inputs: { [key]: "selected.safetensors" } };
assert.equal(factsFor(graph).model.value, slot === 0 ? "selected.safetensors" : null, `${class_type} output${slot}`);
assert.equal(factsFor(graph).model.status, slot === 0 ? "known" : "unresolved");
}
}
for (const class_type of ["LoraLoader", "LoraLoaderModelOnly", "UnknownModelPassThrough"]) {
for (const slot of [0, 1, 2, 999]) {
const graph = factGraph({ model: ["lora", slot] }); graph.lora = { class_type, inputs: { model: ["model", 0] } };
assert.equal(factsFor(graph).model.status, class_type !== "UnknownModelPassThrough" && slot === 0 ? "known" : "unresolved", `${class_type} output${slot}`);
}
}
for (const [class_type, inputs] of [["UnknownLoader", { ckpt_name: "fake" }], ["PrimitiveString", { model_name: "fake", value: "fake" }], ["__proto__", { "[object Object]": "fake" }]]) {
const graph = factGraph({ model: ["unknown", 0] }); graph.unknown = { class_type, inputs };
assert.equal(factsFor(graph).model.status, "unresolved");
}
for (const [input, field] of [["sampler_name", "sampler"], ["scheduler", "scheduler"]]) assert.equal(factsFor(factGraph({ [input]: 42 }))[field].status, "unresolved");
const numericModel = factGraph(); numericModel.model.inputs.ckpt_name = 42;
assert.equal(factsFor(numericModel).model.status, "unresolved", "model names require string evidence");
});
test("wide workflow lookups retain selected core sampler facts", () => {
const nodes = [{ id: "text", type: "PrimitiveString", widgets_values_named: { value: "unrelated text" } }], links = [];
let linkId = 1;
for (let index = 0; index < 20; index++) {
nodes.push({ id: `r${index}`, type: "Reroute", inputs: [{ name: "value", link: linkId }] });
links.push([linkId++, index ? `r${index - 1}` : "text", 0, `r${index}`, 0, "STRING"]);
}
for (let index = 0; index < 40; index++) {
nodes.push({ id: `encoder${index}`, type: "CLIPTextEncode", inputs: [{ name: "text", link: linkId }] });
links.push([linkId++, "r19", 0, `encoder${index}`, 0, "STRING"]);
}
nodes.push({ id: "model", type: "CheckpointLoaderSimple", widgets_values_named: { ckpt_name: "selected.safetensors" } },
{ id: "steps", type: "PrimitiveInt", widgets_values_named: { value: 28 } },
{ id: "s", type: "KSampler", widgets_values: ["9007199254740993", "fixed", 28, "6.5000000000000000001", "euler", "normal"], inputs: [{ name: "model", link: linkId }, { name: "steps", link: linkId + 1 }] });
links.push([linkId, "model", 0, "s", 0, "MODEL"], [linkId + 1, "steps", 0, "s", 1, "INT"]);
const facts = factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph({ nodes, links }) });
assert.deepEqual(factFields.map(field => facts[field].value), ["9007199254740993", "28", "6.5000000000000000001", "euler", "normal", "selected.safetensors"]);
});
test("workflow facts require supported passthrough output slots and bypass ancestry", () => {
function workflow(type, slot, mode) {
const pass = { id: "pass", type, ...(mode === undefined ? {} : { mode }), widgets_values: ["selected"], inputs: [{ name: type === "Reroute" ? "value" : "model", type: "MODEL", link: 1 }] };
const nodes = [{ id: "m", type: "CheckpointLoaderSimple", widgets_values_named: { ckpt_name: "selected.safetensors" } }, pass,
{ id: "s", type: "KSampler", widgets_values: [0, "fixed", 28, "0", "euler", "normal"], inputs: [{ name: "model", link: 2 }] }];
if (type === "GetNode") {
nodes.push({ id: "set", type: "SetNode", widgets_values: ["selected"], inputs: [{ name: "model", link: 1 }] });
pass.inputs = [];
}
return { nodes, links: [[1, "m", 0, "pass", 0, "MODEL"], [2, "pass", slot, "s", 0, "MODEL"]] };
}
const model = value => factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(value) }).model;
for (const [type, mode] of [["Reroute", undefined], ["GetNode", undefined], ["SetNode", undefined], ["LoraLoader", 4], ["LoraLoaderModelOnly", 4]]) {
for (const slot of [0, 1, 999, -1, 0.5]) {
assert.equal(model(workflow(type, slot, mode)).status, slot === 0 ? "known" : "unresolved", `${type} output${slot}`);
}
}
assert.equal(model(workflow("UnknownBypass", 0, 4)).status, "unresolved");
const duplicate = workflow("LoraLoader", 0, 4);
duplicate.nodes[1].inputs.push({ name: "model", type: "MODEL", link: 1 });
assert.equal(model(duplicate).status, "unresolved", "bypass model input is uniquely named");
const wrongOrigin = workflow("Reroute", 0); wrongOrigin.links[0][2] = 1;
assert.equal(model(wrongOrigin).status, "unresolved", "passthrough cannot turn CLIP into MODEL");
const cycle = workflow("Reroute", 0); cycle.links[0][1] = "pass";
assert.equal(model(cycle).status, "unresolved");
const unnamed = workflow("GetNode", 0);
delete unnamed.nodes.find(node => node.id === "pass").widgets_values;
delete unnamed.nodes.find(node => node.id === "set").widgets_values;
assert.equal(model(unnamed).status, "unresolved", "missing Get/Set names are not a source association");
const scalar = factGraph({ seed: ["bypass", 0] });
scalar.bypass = { class_type: "LoraLoader", inputs: { model: ["primitive", 0] } };
scalar.primitive = { class_type: "PrimitiveInt", inputs: { value: 42 } };
const scalarWorkflow = factWorkflowFromGraph(scalar); scalarWorkflow.nodes.find(node => node.id === "bypass").mode = 4;
assert.equal(factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(scalarWorkflow) }).seed.status, "unresolved", "bypassed MODEL output cannot become a scalar");
for (const [input, field] of [["noise", "seed"], ["sampler", "sampler"], ["sigmas", "steps"], ["guider", "model"]]) {
const graph = customFactGraph(), target = graph.s.inputs[input];
graph.s.inputs[input] = ["bypass", 0]; graph.bypass = { class_type: "LoraLoaderModelOnly", inputs: { model: target } };
const workflow = factWorkflowFromGraph(graph); workflow.nodes.find(node => node.id === "bypass").mode = 4;
assert.equal(factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(workflow) })[field].status, "unresolved", `MODEL output cannot become ${input}`);
}
});
function factWorkflowFromGraph(graph) {
const nodes = [], links = [];
for (const [id, node] of Object.entries(graph)) {
const inputs = [], widgets_values_named = {};
for (const [name, value] of Object.entries(node.inputs)) {
if (Array.isArray(value)) {
const link = links.length + 1;
links.push([link, value[0], value[1], id, inputs.length, "*"]);
inputs.push({ name, link });
} else widgets_values_named[name] = value;
}
nodes.push({ id, type: node.class_type, inputs, widgets_values_named });
}
return { nodes, links };
}
test("workflow facts count combined original ancestry once across passthroughs and providers", () => {
const inspect = graph => factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(factWorkflowFromGraph(graph)) });
function chain(graph, reroutes, primitives, model = false) {
let link = model ? ["model", 0] : null;
for (let index = 0; index < primitives; index++) {
graph[`p${index}`] = { class_type: model ? "LoraLoader" : "PrimitiveInt", inputs: model ? { model: link } : { value: link || "42" } };
link = [`p${index}`, 0];
}
for (let index = 0; index < reroutes; index++) {
graph[`r${index}`] = { class_type: "Reroute", inputs: { value: link } };
link = [`r${index}`, 0];
}
return link;
}
for (const [reroutes, known] of [[32, true], [33, false]]) {
const scalar = factGraph(); scalar.s.inputs.seed = chain(scalar, reroutes, 32);
assert.equal(inspect(scalar).seed.status, known ? "known" : "unresolved", `${reroutes}+32 scalar links`);
const model = factGraph(); model.s.inputs.model = chain(model, reroutes, 31, true);
assert.equal(inspect(model).model.status, known ? "known" : "unresolved", `${reroutes}+31 LoRA+loader links`);
const noise = customFactGraph();
noise.n.inputs.noise_seed = chain(noise, reroutes - 1, 31);
noise.noiseAlias = { class_type: "Reroute", inputs: { value: ["n", 0] } };
noise.s.inputs.noise = ["noiseAlias", 0];
assert.equal(inspect(noise).seed.status, known ? "known" : "unresolved", `noise alias+provider+${reroutes - 1}+31 links`);
const guider = customFactGraph();
guider.model = { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "model.safetensors" } };
guider.g.inputs.model = chain(guider, reroutes - 1, 30, true);
guider.guiderAlias = { class_type: "Reroute", inputs: { value: ["g", 0] } };
guider.s.inputs.guider = ["guiderAlias", 0];
assert.equal(inspect(guider).model.status, known ? "known" : "unresolved", `guider alias+provider+${reroutes - 1}+30 LoRA+loader links`);
}
const cycle = factGraph();
cycle.s.inputs.seed = ["r", 0]; cycle.r = { class_type: "Reroute", inputs: { value: ["p", 0] } }; cycle.p = { class_type: "PrimitiveInt", inputs: { value: ["r", 0] } };
assert.equal(inspect(cycle).seed.status, "unresolved");
cycle.s.inputs.model = ["r", 0]; cycle.p = { class_type: "LoraLoader", inputs: { model: ["r", 0] } };
assert.equal(inspect(cycle).model.status, "unresolved");
for (const [reroutes, known] of [[32, true], [33, false]]) {
const graph = factGraph(), value = chain(graph, reroutes, 31);
const workflow = factWorkflowFromGraph(graph);
const sourceLink = workflow.links.length + 1;
workflow.nodes.push({ id: "set", type: "SetNode", widgets_values: ["seed"], inputs: [{ name: "value", link: sourceLink }] }, { id: "get", type: "GetNode", widgets_values: ["seed"], inputs: [] });
workflow.links.push([sourceLink, value[0], 0, "set", 0, "INT"], [sourceLink + 1, "get", 0, "s", 0, "INT"]);
workflow.nodes.find(node => node.id === "s").inputs.push({ name: "seed", link: sourceLink + 1 });
const fact = factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(workflow) }).seed;
assert.equal(fact.status, known ? "known" : "unresolved", `Get+${reroutes}+31 links, no extra setter expansion`);
}
});
test("workflow sampler selection retains prototype-named node identities", () => {
const workflow = { nodes: [
{ id: "__proto__", type: "KSampler", widgets_values: [1, "fixed", 20, "7", "euler", "normal"] },
{ id: "other", type: "KSampler", widgets_values: [2, "fixed", 20, "7", "euler", "normal"] },
], links: [] };
assert.ok(Object.values(factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(workflow) })).every(fact => fact.status === "ambiguous"));
});
test("workflow-only facts are configuration without creating sendable roles", () => {
const workflow = { nodes: [
{ id: 1, type: "UNETLoader", widgets_values: ["flux.safetensors", "default"] },
{ id: 2, type: "SetNode", widgets_values: ["model"], inputs: [{ name: "MODEL", link: 1 }] },
{ id: 3, type: "GetNode", widgets_values: ["model"] },
{ id: 4, type: "KSamplerAdvanced", widgets_values: ["enable", "9007199254740993", "fixed", 28, "6.5000000000000000001", "euler", "normal", 0, 28, "disable"], inputs: [{ name: "model", link: 2 }] },
], links: [[1, 1, 0, 2, 0, "MODEL"], [2, 3, 0, 4, 0, "MODEL"]] };
const facts = factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(workflow) });
assert.deepEqual(factFields.map(field => facts[field].value), ["9007199254740993", "28", "6.5000000000000000001", "euler", "normal", "flux.safetensors"]);
assert.ok(Object.values(facts).every(fact => fact.source === "workflow" && fact.evidence === "configuration"));
assert.equal(facts.positive, undefined);
workflow.nodes.push({ ...workflow.nodes[3] });
assert.ok(Object.values(factsFor(null, { prompt_graph: absentFactGraph(), workflow_graph: parsedFactGraph(workflow) })).every(fact => fact.status === "unresolved"));
});
test("limited projection stays unresolved", () => {
for (const extra of [
{ prompt_graph: { status: "limited", value: null } },
{ prompt_graph: absentFactGraph(), workflow_graph: { status: "limited", value: null } },
{ prompt_graph: absentFactGraph(), metadataStatus: "limited", parameters: null },
]) {
const facts = factsFor(null, extra);
assert.ok(Object.values(facts).every(fact => fact.status === "unresolved" && fact.value === null && fact.reason === "projection-limited"));
}
const unreadable = factsFor(null, { prompt_graph: absentFactGraph(), metadataStatus: "unreadable" });
assert.equal(unreadable.seed.reason, "metadata-unreadable");
const absent = factsFor(null, { prompt_graph: absentFactGraph(), metadataStatus: "absent" });
assert.equal(absent.seed.status, "not-recorded");
});
test("facts preserve exact video-projected decimals without reading reserialized raw", () => {
const graph = factGraph();
const facts = factsFor(graph, { recordFormats: { prompt: "reserialized-json" }, raw: { prompt: '{"s":{"inputs":{"seed":9007199254740992,"cfg":6.5}}}' } });
assert.equal(facts.seed.value, "9007199254740993");
assert.equal(facts.cfg.value, "6.5000000000000000001");
assert.equal(factsFor(factGraph({ cfg: "1E+1000000000" })).cfg.value, "1E+1000000000");
});
test("nontext parameters remain unresolved", () => {
const parameters = "A portrait\nNegative prompt: blur\nSteps: 28, Sampler: Euler, Schedule type: Karras, CFG scale: 6.5000000000000000001, Seed: 9007199254740993, Model: model";
const base = { prompt_graph: absentFactGraph(), parameters };
for (const format of [undefined, "reserialized-json", "invalid"]) {
const facts = factsFor(null, { ...base, recordFormats: format ? { parameters: format } : {} });
assert.ok(Object.values(facts).every(fact => fact.status === "unresolved" && fact.value === null));
}
const facts = factsFor(null, { ...base, recordFormats: { parameters: "original-text" } });
assert.deepEqual(factFields.map(field => facts[field].value), ["9007199254740993", "28", "6.5000000000000000001", "Euler", "Karras", "model"]);
assert.ok(Object.values(facts).every(fact => fact.source === "embedded-parameters" && fact.evidence === "embedded-parameters"));
for (const invalid of [parameters + "\nSteps: 5, Seed: 2", parameters + ", Seed: 1", parameters + "\ntrailing", parameters.replace("Seed: 9007199254740993", "Seed: -1"), parameters.replace("Karras", "invented"), parameters.replace("Negative prompt: blur", "Negative prompt: blur\nNegative prompt: again")]) {
assert.ok(Object.values(factsFor(null, { ...base, parameters: invalid, recordFormats: { parameters: "original-text" } })).some(fact => fact.status === "unresolved"));
}
});
test("malformed or unsupported API evidence blocks plausible workflow and parameter fallback", () => {
const workflow = { nodes: [{ id: 1, type: "KSampler", widgets_values: [42, "fixed", 20, 7, "euler", "normal"] }], links: [] };
const extra = { workflow_graph: parsedFactGraph(workflow), parameters: "portrait\nSteps: 20, Sampler: Euler, CFG scale: 7, Seed: 42", recordFormats: { parameters: "original-text" } };
for (const prompt_graph of [{ status: "malformed", value: null }, parsedFactGraph({ s: { class_type: "UnknownSampler", inputs: { seed: 99 } } }), parsedFactGraph({ s: { class_type: "KSampler", inputs: [] } })]) {
const facts = factsFor(null, { ...extra, prompt_graph });
assert.ok(Object.values(facts).every(fact => fact.status === "unresolved" && fact.value === null));
assert.ok(Object.values(facts).every(fact => fact.source !== "workflow" && fact.source !== "embedded-parameters"));
}
});
test("conservative facts bound cycles fan-out visits and link depth", () => {
const graph = factGraph({ seed: ["loop", 0], model: ["loop", 0] });
graph.loop = { class_type: "Unknown", inputs: { seed: ["loop", 0], model: ["loop", 0], a: ["loop", 0], b: ["loop", 0] } };
assert.equal(factsFor(graph).seed.status, "unresolved");
assert.equal(factsFor(graph).model.status, "unresolved");
graph.loop = { class_type: "PrimitiveInt", inputs: { value: ["loop", 0] } };
assert.equal(factsFor(graph).seed.status, "unresolved", "registered scalar cycle is bounded");
graph.loop = { class_type: "LoraLoader", inputs: { model: ["loop", 0] } };
assert.equal(factsFor(graph).model.status, "unresolved", "registered model cycle is bounded");
const chain = depth => {
const value = factGraph({ seed: ["p0", 0] });
for (let index = 0; index < depth; index++) value[`p${index}`] = { class_type: "PrimitiveInt", inputs: { value: index === depth - 1 ? "42" : [`p${index + 1}`, 0] } };
return value;
};
assert.equal(factsFor(chain(64)).seed.value, "42");
assert.equal(factsFor(chain(65)).seed.status, "unresolved");
const custom = chain(64);
custom.s = { class_type: "SamplerCustomAdvanced", inputs: { noise: ["noise", 0] } };
custom.noise = { class_type: "RandomNoise", inputs: { noise_seed: ["p0", 0] } };
assert.equal(factsFor(custom).seed.status, "unresolved", "the provider itself consumes one of the 64 allowed links");
const large = factGraph();
for (let index = 0; index < 498; index++) large[`unused${index}`] = { class_type: "PrimitiveInt", inputs: { value: index } };
assert.equal(factsFor(large).seed.value, "9007199254740993");
large.extra = { class_type: "PrimitiveInt", inputs: { value: 1 } };
assert.equal(factsFor(large).seed.status, "unresolved");
});
// Same shape as the workflow attached to #75: PromptManager nodes are saved as
// CLIPTextEncode with extra inputs, and the checkpoint loader is pysssss'.
const issue75Prompt = {
1: {
class_type: "KSampler",
inputs: {
seed: 480655185510613, steps: 25, cfg: 5.5,
sampler_name: "euler_ancestral", scheduler: "normal", denoise: 0.8,
model: ["20", 0], positive: ["39", 0], negative: ["40", 0], latent_image: ["36", 0],
},
},
8: { class_type: "CLIPSetLastLayer", inputs: { stop_at_clip_layer: -2, clip: ["20", 1] } },
20: {
class_type: "CheckpointLoader|pysssss",
inputs: { ckpt_name: "Anime\\Model-XL.safetensors", prompt: "[none]", example: "[none]" },
},
39: {
class_type: "CLIPTextEncode",
inputs: {
text: "1girl, cat_ears, smile", category: "", tags: "", search_text: "",
prepend_text: "masterpiece, best quality, ", append_text: "", clip: ["8", 0],
},
},
40: {
class_type: "CLIPTextEncode",
inputs: {
text: "3d, crowd", category: "", tags: "", search_text: "",
prepend_text: "bad quality, worst quality, ", append_text: "", clip: ["8", 0],
},
},
};
test("#75: traces PromptManager prompts and pysssss checkpoint from the sampler", () => {
const r = extractGenerationParams({ prompt: issue75Prompt });
assert.equal(r.positivePrompt, "masterpiece, best quality, 1girl, cat_ears, smile");
assert.equal(r.negativePrompt, "bad quality, worst quality, 3d, crowd");
assert.equal(r.checkpoint, "Anime\\Model-XL.safetensors");
assert.deepEqual(
[r.seed, r.steps, r.cfgScale, r.sampler],
[480655185510613, 25, 5.5, "euler_ancestral"],
);
});
test("#75: workflow blob does not override values from the prompt graph", () => {
const workflow = { nodes: [{ type: "KSampler", widgets_values: [1, "randomize", 99, 7, "ddim"] }] };
const r = extractGenerationParams({ prompt: issue75Prompt, workflow });
assert.equal(r.steps, 25);
assert.equal(r.cfgScale, 5.5);
});
test("#75: recovers per-node text when SaveImage patch overwrote every PromptManager node", () => {
// Pre-fix images: both nodes carry the final positive text in the prompt graph,
// but the workflow graph still has each node's real widget values.
const positiveFinal = "masterpiece, best quality, 1girl, smile";
const prompt = {
1: { class_type: "KSampler", inputs: { seed: 1, steps: 20, cfg: 7, sampler_name: "euler", positive: ["39", 0], negative: ["40", 0], model: ["20", 0] } },
20: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "m.safetensors" } },
39: { class_type: "PromptManager", inputs: { text: positiveFinal, prepend_text: "masterpiece, best quality,", append_text: "" } },
40: { class_type: "PromptManager", inputs: { text: positiveFinal, prepend_text: "bad quality,", append_text: "" } },
};
const workflow = {
nodes: [
{ id: 39, type: "PromptManager", widgets_values: ["1girl, smile", "", "", "", "masterpiece, best quality,", ""] },
{ id: 40, type: "PromptManager", widgets_values: ["3d, crowd", "", "", "", "bad quality,", ""] },
],
};
const r = extractGenerationParams({ prompt, workflow });
assert.equal(r.positivePrompt, "masterpiece, best quality, 1girl, smile");
assert.equal(r.negativePrompt, "bad quality, 3d, crowd");
});
test("current metadata: API text wins over a stale or unexpanded workflow widget", () => {
// Dynamic prompt expanded at queue time: API holds what ran, workflow holds the template
const prompt = {
1: { class_type: "KSampler", inputs: { positive: ["39", 0], negative: ["40", 0], model: ["20", 0] } },
20: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "m.safetensors" } },
39: { class_type: "PromptManager", inputs: { text: "a red cat", prepend_text: "", append_text: "" } },
40: { class_type: "PromptManager", inputs: { text: "blurry", prepend_text: "", append_text: "" } },
};
const workflow = {
nodes: [
{ id: 39, type: "PromptManager", widgets_values: ["a {red|blue} cat", "", "", "", "", ""] },
{ id: 40, type: "PromptManager", widgets_values: ["blurry", "", "", "", "", ""] },
],
};
const r = extractGenerationParams({ prompt, workflow });
assert.equal(r.positivePrompt, "a red cat");
assert.equal(r.negativePrompt, "blurry");
});
test("current metadata: linked text is followed, not replaced by the widget's stale value", () => {
const prompt = {
1: { class_type: "KSampler", inputs: { positive: ["39", 0], negative: ["40", 0], model: ["20", 0] } },
20: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "m.safetensors" } },
39: { class_type: "PromptManager", inputs: { text: ["50", 0], prepend_text: "", append_text: "" } },
40: { class_type: "PromptManager", inputs: { text: ["50", 0], prepend_text: "bad,", append_text: "" } },
50: { class_type: "PrimitiveString", inputs: { value: "shared text" } },
};
const workflow = {
nodes: [
{ id: 39, type: "PromptManager", widgets_values: ["old value A", "", "", "", "", ""] },
{ id: 40, type: "PromptManager", widgets_values: ["old value B", "", "", "", "bad,", ""] },
],
};
const r = extractGenerationParams({ prompt, workflow });
assert.equal(r.positivePrompt, "shared text");
});
test("prepend/append are not doubled when text is already the combined prompt", () => {
const prompt = {
1: { class_type: "KSampler", inputs: { positive: ["2", 0], negative: ["3", 0], model: ["4", 0] } },
2: { class_type: "PromptManager", inputs: { text: "best quality, a cat, 8k", prepend_text: "best quality,", append_text: "8k" } },
3: { class_type: "CLIPTextEncode", inputs: { text: "blurry" } },
};
assert.equal(extractGenerationParams({ prompt }).positivePrompt, "best quality, a cat, 8k");
});
test("linked prepend/append inputs are resolved (no longer baked into text since 3.2.3)", () => {
const prompt = {
1: { class_type: "KSampler", inputs: { positive: ["2", 0], negative: ["3", 0], model: ["4", 0] } },
2: { class_type: "PromptManager", inputs: { text: "a fox", prepend_text: ["5", 0], append_text: ["6", 0] } },
3: { class_type: "CLIPTextEncode", inputs: { text: "blurry" } },
5: { class_type: "PrimitiveString", inputs: { value: "masterpiece," } },
6: { class_type: "PrimitiveString", inputs: { value: "8k" } },
};
assert.equal(extractGenerationParams({ prompt }).positivePrompt, "masterpiece, a fox 8k");
});
test("negative prompt is identified by link, not by keywords", () => {
const prompt = {
3: { class_type: "KSampler", inputs: { seed: 1, steps: 20, cfg: 7, sampler_name: "euler", positive: ["6", 0], negative: ["7", 0], model: ["4", 0] } },
4: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "sd15.safetensors" } },
6: { class_type: "CLIPTextEncode", inputs: { text: "embedding:goodstyle, a lighthouse" } },
7: { class_type: "CLIPTextEncode", inputs: { text: "blurry" } },
};
const r = extractGenerationParams({ prompt });
assert.equal(r.positivePrompt, "embedding:goodstyle, a lighthouse");
assert.equal(r.negativePrompt, "blurry");
});
test("follows LoRA chains, ControlNet pass-through, concatenated text and primitive inputs", () => {
const prompt = {
1: { class_type: "KSamplerAdvanced", inputs: { noise_seed: 42, steps: ["9", 0], cfg: 6, sampler_name: "dpmpp_2m", positive: ["5", 0], negative: ["5", 1], model: ["3", 0] } },
2: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "base.safetensors" } },
3: { class_type: "LoraLoader", inputs: { lora_name: "style.safetensors", model: ["2", 0], clip: ["2", 1] } },
5: { class_type: "ControlNetApplyAdvanced", inputs: { positive: ["6", 0], negative: ["7", 0], strength: 1 } },
6: { class_type: "CLIPTextEncode", inputs: { text: ["8", 0] } },
7: { class_type: "CLIPTextEncode", inputs: { text: "lowres" } },
8: { class_type: "Text Concatenate", inputs: { delimiter: ", ", text_a: "a castle", text_b: "at dusk" } },
9: { class_type: "PrimitiveInt", inputs: { value: 30 } },
};
const r = extractGenerationParams({ prompt });
assert.equal(r.positivePrompt, "a castle, at dusk");
assert.equal(r.negativePrompt, "lowres");
assert.equal(r.checkpoint, "base.safetensors");
assert.equal(r.seed, 42);
assert.equal(r.steps, 30);
});
test("custom sampler graphs (Flux: SamplerCustomAdvanced + CFGGuider) resolve every field", () => {
const prompt = {
85: { class_type: "UNETLoader", inputs: { unet_name: "flux2-klein.safetensors" } },
135: { class_type: "Lora Loader (LoraManager)", inputs: { model: ["85", 0], clip: ["90", 0] } },
149: { class_type: "PromptManager", inputs: { text: "a red fox", prepend_text: ["133", 0], append_text: "" } },
152: { class_type: "SeedHistory", inputs: { seed: 1725922648 } },
// ControlNet-style node with positive/negative but no model: must not be taken as the sampler
200: { class_type: "ControlNetApplyAdvanced", inputs: { positive: ["149", 0], negative: ["274", 0], strength: 1 } },
265: { class_type: "SamplerCustomAdvanced", inputs: { noise: ["267", 0], guider: ["268", 0], sampler: ["269", 0], sigmas: ["266", 0] } },
266: { class_type: "Flux2Scheduler", inputs: { steps: 50, width: ["153", 1] } },
267: { class_type: "RandomNoise", inputs: { noise_seed: ["152", 0] } },
268: { class_type: "CFGGuider", inputs: { cfg: 5.0, model: ["135", 0], positive: ["149", 0], negative: ["274", 0] } },
269: { class_type: "KSamplerSelect", inputs: { sampler_name: "uni_pc" } },
274: { class_type: "CLIPTextEncode", inputs: { text: "watermark" } },
};
const r = extractGenerationParams({ prompt });
assert.deepEqual(
[r.positivePrompt, r.negativePrompt, r.checkpoint, r.cfgScale, r.seed, r.steps, r.sampler],
["a red fox", "watermark", "flux2-klein.safetensors", 5.0, 1725922648, 50, "uni_pc"],
);
});
test("multi-output settings nodes (SamplerCombo) map each field to its own value", () => {
const prompt = {
5: {
class_type: "KSampler",
inputs: {
seed: ["162", 0], steps: ["165", 2], cfg: ["165", 3], sampler_name: ["165", 0],
positive: ["6", 0], negative: ["7", 0], model: ["4", 0],
},
},
4: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "m.safetensors" } },
6: { class_type: "CLIPTextEncode", inputs: { text: "a cat" } },
7: { class_type: "CLIPTextEncode", inputs: { text: "blurry" } },
162: { class_type: "SeedHistory", inputs: { seed: 1495646836, seed_history_ui: "" } },
165: { class_type: "SamplerCombo", inputs: { sampler_name: "euler_ancestral", scheduler: "normal", steps: 50, cfg: 7 } },
};
const r = extractGenerationParams({ prompt });
assert.deepEqual([r.seed, r.steps, r.cfgScale, r.sampler], [1495646836, 50, 7, "euler_ancestral"]);
});
test("workflow-only images account for the hidden control_after_generate widget", () => {
const workflow = {
nodes: [
{ id: 1, type: "CheckpointLoader|pysssss", widgets_values: ["model.safetensors", "[none]"] },
{ id: 2, type: "KSampler", widgets_values: [123, "randomize", 25, 5.5, "euler", "normal", 1] },
],
};
const r = extractGenerationParams({ workflow });
assert.deepEqual(
[r.checkpoint, r.seed, r.steps, r.cfgScale, r.sampler],
["model.safetensors", 123, 25, 5.5, "euler"],
);
});
test("cyclic, fan-out graphs from crafted PNGs resolve quickly", () => {
// Every node links to itself and its neighbour through several inputs and has
// no scalars: without a visited set this explores ~inputs^MAX_DEPTH paths.
const loop = (id, next) => ({
class_type: "Evil",
inputs: {
text_a: [id, 0], text_b: [next, 0], text_c: [id, 0], text_d: [next, 0],
value_a: [id, 0], value_b: [next, 0], model: [next, 0],
},
});
const prompt = {
1: { class_type: "KSampler", inputs: { seed: ["2", 0], steps: ["3", 0], positive: ["2", 0], negative: ["3", 0], model: ["2", 0] } },
2: loop("2", "3"),
3: loop("3", "2"),
};
const started = Date.now();
const r = extractGenerationParams({ prompt });
assert.ok(Date.now() - started < 500, `took ${Date.now() - started}ms`);
assert.equal(r.positivePrompt, "No prompt found");
assert.equal(r.seed, "Unknown");
});
test("chained encoders whose prepend and append both link onward stay linear", () => {
const prompt = { 1: { class_type: "KSampler", inputs: { positive: ["n0", 0], negative: ["n0", 0], model: ["n0", 0] } } };
for (let i = 0; i < 60; i++) {
prompt[`n${i}`] = {
class_type: "PromptManager",
inputs: { text: `t${i}`, prepend_text: [`n${i + 1}`, 0], append_text: [`n${i + 1}`, 0] },
};
}
const started = Date.now();
extractGenerationParams({ prompt });
assert.ok(Date.now() - started < 500, `took ${Date.now() - started}ms`);
});
// UI-format link: [id, from_node, from_slot, to_node, to_slot, type]
test("workflow-only: traces prompts, model and KSamplerAdvanced settings through Set/Get and Reroute", () => {
const workflow = {
nodes: [
{ id: 1, type: "UNETLoader", widgets_values: ["flux.safetensors", "default"], outputs: [{ links: [1] }] },
{ id: 2, type: "SetNode", widgets_values: ["model"], inputs: [{ name: "MODEL", link: 1 }] },
{ id: 3, type: "GetNode", widgets_values: ["model"], outputs: [{ links: [2] }] },
{ id: 4, type: "PromptManager", widgets_values: ["a castle", "", "", "", "masterpiece,", ""], inputs: [], outputs: [{ links: [3] }] },
{ id: 5, type: "Reroute", inputs: [{ name: "", link: 3 }], outputs: [{ links: [4] }] },
{ id: 6, type: "CLIPTextEncode", widgets_values: ["lowres"], outputs: [{ links: [5] }] },
{
id: 7, type: "KSamplerAdvanced",
inputs: [{ name: "model", link: 2 }, { name: "positive", link: 4 }, { name: "negative", link: 5 }],
widgets_values: ["enable", 77, "randomize", 30, 6.5, "dpmpp_2m", "karras", 0, 10000, "disable"],
},
],
links: [
[1, 1, 0, 2, 0, "MODEL"], [2, 3, 0, 7, 0, "MODEL"], [3, 4, 0, 5, 0, "CONDITIONING"],
[4, 5, 0, 7, 1, "CONDITIONING"], [5, 6, 0, 7, 2, "CONDITIONING"],
],
};
const r = extractGenerationParams({ workflow });
assert.deepEqual(
[r.positivePrompt, r.negativePrompt, r.checkpoint, r.seed, r.steps, r.cfgScale, r.sampler],
["masterpiece, a castle", "lowres", "flux.safetensors", 77, 30, 6.5, "dpmpp_2m"],
);
});
test("workflow-only: widgets_values_named and widget-backed inputs take precedence over positions", () => {
const workflow = {
nodes: [
{ id: 1, type: "CheckpointLoaderSimple", widgets_values: ["m.safetensors"], outputs: [{ links: [1] }] },
{ id: 2, type: "CLIPTextEncode", widgets_values_named: { text: "named text" }, widgets_values: ["ignored"], outputs: [{ links: [2] }] },
{ id: 3, type: "CLIPTextEncode", widgets_values: ["neg"], outputs: [{ links: [3] }] },
{
id: 4, type: "KSampler",
inputs: [
{ name: "model", link: 1 }, { name: "positive", link: 2 }, { name: "negative", link: 3 },
{ name: "seed", widget: { name: "seed" }, link: null },
{ name: "steps", widget: { name: "steps" }, link: null },
{ name: "cfg", widget: { name: "cfg" }, link: null },
{ name: "sampler_name", widget: { name: "sampler_name" }, link: null },
],
widgets_values: [5, "fixed", 12, 4, "euler", "normal", 1],
},
],
links: [[1, 1, 0, 4, 0, "MODEL"], [2, 2, 0, 4, 1, "CONDITIONING"], [3, 3, 0, 4, 2, "CONDITIONING"]],
};
const r = extractGenerationParams({ workflow });
assert.deepEqual([r.positivePrompt, r.seed, r.steps, r.cfgScale, r.sampler], ["named text", 5, 12, 4, "euler"]);
});
test("workflow-only: bypassed nodes pass through, muted nodes are ignored", () => {
const workflow = {
nodes: [
{ id: 1, type: "CheckpointLoaderSimple", widgets_values: ["m.safetensors"], outputs: [{ links: [1] }] },
{ id: 2, type: "LoraLoader", mode: 4, inputs: [{ name: "model", type: "MODEL", link: 1 }], outputs: [{ type: "MODEL", links: [2] }] },
{ id: 3, type: "CLIPTextEncode", widgets_values: ["pos"], outputs: [{ links: [3] }] },
{ id: 4, type: "CLIPTextEncode", widgets_values: ["neg"], outputs: [{ links: [4] }] },
{ id: 5, type: "KSampler", mode: 2, inputs: [{ name: "model", link: 2 }, { name: "positive", link: 3 }, { name: "negative", link: 4 }], widgets_values: [9, "fixed", 99, 9, "ddim"] },
{ id: 6, type: "KSampler", inputs: [{ name: "model", link: 2 }, { name: "positive", link: 3 }, { name: "negative", link: 4 }], widgets_values: [1, "fixed", 20, 7, "euler"] },
],
links: [[1, 1, 0, 2, 0, "MODEL"], [2, 2, 0, 6, 0, "MODEL"], [3, 3, 0, 6, 1, "CONDITIONING"], [4, 4, 0, 6, 2, "CONDITIONING"]],
};
const r = extractGenerationParams({ workflow });
assert.deepEqual([r.checkpoint, r.positivePrompt, r.steps], ["m.safetensors", "pos", 20]);
});
test("missing or empty metadata yields placeholders", () => {
const r = extractGenerationParams({});
assert.equal(r.positivePrompt, "No prompt found");
assert.equal(r.negativePrompt, "No negative prompt found");
assert.equal(r.checkpoint, "Unknown");
assert.equal(extractGenerationParams(null).seed, "Unknown");
});
test("Focused API metadata composes linked positive text and preserves negative role", () => {
const prompt = {
1: { class_type: "KSampler", inputs: { positive: ["2", 0], negative: ["3", 0], model: ["4", 0] } },
2: { class_type: "PromptManagerFocused", inputs: { text: "a fox", prepend_text: ["5", 0], append_text: "8k" } },
3: { class_type: "PromptManagerFocused", inputs: { text: "blurry", prepend_text: "avoid" } },
4: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "model.safetensors" } },
5: { class_type: "PrimitiveString", inputs: { value: "lora trigger" } },
};
const result = extractGenerationParams({ prompt });
assert.equal(result.positivePrompt, "lora trigger a fox 8k");
assert.equal(result.negativePrompt, "avoid blurry");
});
test("Focused workflow-only metadata recovers serialized fields and connected composition", () => {
const workflow = {
nodes: [
{ id: 1, type: "KSampler", inputs: [{ name: "positive", link: 1 }, { name: "negative", link: 2 }, { name: "model", link: 3 }] },
{ id: 2, type: "PromptManagerFocused", widgets_values: ["a fox", "animals", "fox", "ignored search", "stale widget", "8k"], inputs: [{ name: "prepend_text", link: 4 }] },
{ id: 3, type: "PromptManagerFocused", widgets_values: ["blurry", "negative", "bad", "ignored search", "avoid", "watermark"] },
{ id: 4, type: "UNETLoader", widgets_values: ["model.safetensors", "default"] },
{ id: 5, type: "CLIPTextEncode", widgets_values: ["lora trigger"] },
],
links: [
[1, 2, 0, 1, 0, "CONDITIONING"], [2, 3, 0, 1, 1, "CONDITIONING"],
[3, 4, 0, 1, 2, "MODEL"], [4, 5, 0, 2, 0, "STRING"],
],
};
const result = extractGenerationParams({ workflow });
assert.equal(result.positivePrompt, "lora trigger a fox 8k");
assert.equal(result.negativePrompt, "avoid blurry watermark");
assert.equal(result.checkpoint, "model.safetensors");
});
test("Split API metadata composes linked positive text and preserves negative role", () => {
const prompt = {
1: { class_type: "KSampler", inputs: { positive: ["2", 0], negative: ["3", 0], model: ["4", 0] } },
2: { class_type: "PromptManagerSplit", inputs: { text: "a fox", prepend_text: ["5", 0], append_text: "8k" } },
3: { class_type: "PromptManagerSplit", inputs: { text: "blurry", prepend_text: "avoid" } },
4: { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "model.safetensors" } },
5: { class_type: "PrimitiveString", inputs: { value: "lora trigger" } },
};
const result = extractGenerationParams({ prompt });
assert.equal(result.positivePrompt, "lora trigger a fox 8k");
assert.equal(result.negativePrompt, "avoid blurry");
});
test("Split workflow-only metadata recovers serialized fields and connected composition", () => {
const workflow = {
nodes: [
{ id: 1, type: "KSampler", inputs: [{ name: "positive", link: 1 }, { name: "negative", link: 2 }, { name: "model", link: 3 }] },
{ id: 2, type: "PromptManagerSplit", widgets_values: ["a fox", "animals", "fox", "ignored search", "stale widget", "8k"], inputs: [{ name: "prepend_text", link: 4 }] },
{ id: 3, type: "PromptManagerSplit", widgets_values: ["blurry", "negative", "bad", "ignored search", "avoid", "watermark"] },
{ id: 4, type: "UNETLoader", widgets_values: ["model.safetensors", "default"] },
{ id: 5, type: "CLIPTextEncode", widgets_values: ["lora trigger"] },
],
links: [
[1, 2, 0, 1, 0, "CONDITIONING"], [2, 3, 0, 1, 1, "CONDITIONING"],
[3, 4, 0, 1, 2, "MODEL"], [4, 5, 0, 2, 0, "STRING"],
],
};
const result = extractGenerationParams({ workflow });
assert.equal(result.positivePrompt, "lora trigger a fox 8k");
assert.equal(result.negativePrompt, "avoid blurry watermark");
assert.equal(result.checkpoint, "model.safetensors");
});