Compare commits

...
Author SHA1 Message Date
Vito ef7fa04bba Merge branch 'main' into fix/253-js-string-depth 2026-10-19 21:50:47 -07:00
Vito 673433b2ae fix(inspect): keep the server result when the legacy parser throws (#254)
#251 fills undecided roles from the browser parser, which throws on some
malformed graphs (for example a null workflow node). The error escaped
the record builder, so the inspector could not load such files at all.
The fill-in is now skipped when the parser throws.
2026-10-19 21:43:01 -07:00
Vito Sansevero b0ca3ced5f fix(parse): build array String() without recursion
utils.generation_params converted nested arrays to strings recursively,
so a crafted file with a value nested about 900 arrays deep raised
RecursionError, where the browser parser returns a result. The join now
runs on an explicit stack. A JS-parity case with 900-deep values pins it.
2026-10-19 21:34:47 -07:00
6 changed files with 80 additions and 2 deletions
File diff suppressed because one or more lines are too long
+17
View File
@@ -244,6 +244,14 @@ def _workflow_only():
}
def _nested(depth):
"""A value nested ``depth`` arrays deep, as a crafted file can hold."""
value = ["leaf", 7]
for _ in range(depth):
value = [value, None]
return value
def _chain(length):
graph = {"4": CHECKPOINT, "1": encoder("deep chain text")}
previous = "1"
@@ -440,5 +448,14 @@ CASES = {
"3": ksampler(("6", 0), ("6", 0), cfg=1.0, seed=4974052037430000001),
}
),
"deeply-nested-values": case(
{
"4": CHECKPOINT,
"6": encoder("nested neighbours"),
"3": ksampler(("6", 0), ("6", 0)),
"8": {"class_type": _nested(900), "inputs": {"text": "x"}},
},
{"nodes": [{"id": _nested(900), "type": _nested(900)}], "links": []},
),
"nothing": case(),
}
@@ -555,3 +555,16 @@ test('a setting the file does not record is never filled from the legacy parse',
assert.equal(record.generation.cfg.status, 'not-recorded');
assert.equal(record.generation.cfg.value, null);
});
test('a workflow the legacy parser throws on keeps the server result instead of failing the record', t => {
globalThis.ComfyMetadata = require('../../web/js/comfy-metadata.js');
t.after(() => { delete globalThis.ComfyMetadata; });
const entry = image(), data = envelope(entry);
// The browser parser throws on a null workflow node (it reads properties of it).
data.metadata.workflow_graph = { status: 'parsed', value: { nodes: [null], links: [] } };
data.metadata.raw.workflow = '{"nodes":[null],"links":[]}';
data.metadata.record_formats.workflow = 'reserialized-json';
const record = contract('toInspectionRecord')(data, entry, extractGenerationFacts);
assert.deepEqual(record.prompts.positive, { status: 'resolved', text: 'exact\npositive', source: 'execution' });
assert.equal(record.prompts.negative.status, 'ambiguous');
});
+6
View File
@@ -176,6 +176,12 @@ class StaysBounded(unittest.TestCase):
self.assertEqual(result["negativePrompt"], "t")
class DeeplyNestedValues(unittest.TestCase):
def test_deeply_nested_values_do_not_exhaust_the_python_stack(self):
result = run("deeply-nested-values")
self.assertEqual(result["positivePrompt"], "nested neighbours")
class Placeholders(unittest.TestCase):
def test_blank_text_is_a_placeholder_and_zero_seed_is_kept(self):
result = run("blank-text-and-zero-seed")
+24 -1
View File
@@ -217,10 +217,33 @@ def _js_string(value):
if isinstance(value, str):
return value
if isinstance(value, list):
return ",".join("" if _nullish(item) else _js_string(item) for item in value)
return _array_string(value)
return "[object Object]"
def _array_string(value):
"""``String(array)``: ``join(",")`` applied to nested arrays, on an explicit stack.
null and undefined elements join as "". A crafted file can nest arrays far
deeper than the Python stack allows, so this never recurses.
"""
out = []
stack = [(value, 0)]
while stack:
items, index = stack.pop()
if index == len(items):
continue
if index:
out.append(",")
stack.append((items, index + 1))
item = items[index]
if isinstance(item, list):
stack.append((item, 0))
elif not _nullish(item):
out.append(_js_string(item))
return "".join(out)
def _key(value):
return value if isinstance(value, str) else _js_string(value)
+3 -1
View File
@@ -176,7 +176,9 @@ function withWorkflowParams(record, parser = comfyMetadata()) {
const graph = record.metadata.prompt_graph.status === 'parsed' ? record.metadata.prompt_graph.value : null;
const workflow = record.metadata.workflow_graph.status === 'parsed' ? record.metadata.workflow_graph.value : null;
if (typeof parser?.extractGenerationParams !== 'function' || (!graph && !workflow)) return;
const params = parser.extractGenerationParams({ prompt: graph, workflow });
let params;
// The browser parser throws on some malformed graphs; then only the server result shows.
try { params = parser.extractGenerationParams({ prompt: graph, workflow }); } catch { return; }
const found = key => typeof params[key] === 'string' || typeof params[key] === 'number' ?
String(params[key]) !== '' && params[key] !== parser.PLACEHOLDERS?.[key] : false;
const prompts = { ...record.prompts };