* fix(nodes): handle a failed upstream stage without crashing When a stage fails it passes None down its data output. Downstream nodes called `.to_json()` / iterated the None and raised an unhelpful AttributeError (found in ComfyUI: a failed Scene Analyzer crashed the Scene Validator on `None.to_json()`). Each consuming node now short-circuits on a missing required input: the Validator, Resolver, and Category Splitter return None plus an error string that points at the upstream node's errors output; the Prompt Builder returns empty strings. Adds a shared upstream_failure_message helper and regression tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(analyzer): unwrap Markdown code fences in model responses Most local models (e.g. gemma) return valid Scene JSON wrapped in a ```json ... ``` code fence despite the system prompt forbidding markdown. Strict json.loads then rejected it (SC0011), and at temperature 0 every retry returned the identical fenced output, so the Analyzer always failed and produced a None scene. The response parser now unwraps a leading/trailing code fence before parsing. This is not the heuristic JSON repair forbidden by MASTER_SPEC §12.9: the JSON inside is left byte-for-byte unchanged; only the surrounding ``` markers are removed. Malformed JSON inside a fence is still rejected. Verified end-to-end against a live Ollama (gemma): the fenced response now parses and the full pipeline produces prompt outputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: kunail0804 <kunail0804@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
"""Regression: nodes must not crash when an upstream stage produced no output.
|
|
|
|
A failed upstream node passes ``None`` down its data output; downstream nodes must
|
|
surface an error instead of raising AttributeError (found in ComfyUI: a failed
|
|
Scene Analyzer crashed the Scene Validator on ``None.to_json()``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from nodes.category_splitter_node import CategorySplitterNode
|
|
from nodes.prompt_builder_node import PromptBuilderNode
|
|
from nodes.resolver_node import ResolverNode
|
|
from nodes.scene_validator_node import SceneValidatorNode
|
|
|
|
|
|
def test_validator_handles_none_scene() -> None:
|
|
scene, warnings, errors = SceneValidatorNode().run(None)
|
|
assert scene is None
|
|
assert "no output" in errors.lower()
|
|
|
|
|
|
def test_resolver_handles_none_scene() -> None:
|
|
resolved, _, errors = ResolverNode().run(None, knowledge_base=object())
|
|
assert resolved is None
|
|
assert errors
|
|
|
|
|
|
def test_resolver_handles_none_knowledge_base() -> None:
|
|
resolved, _, errors = ResolverNode().run(scene=object(), knowledge_base=None)
|
|
assert resolved is None
|
|
assert errors
|
|
|
|
|
|
def test_category_splitter_handles_none_tags() -> None:
|
|
category_map, _, errors = CategorySplitterNode().run(None)
|
|
assert category_map is None
|
|
assert errors
|
|
|
|
|
|
def test_prompt_builder_handles_none_category_map() -> None:
|
|
outputs = PromptBuilderNode().run(None)
|
|
assert len(outputs) == len(PromptBuilderNode.RETURN_NAMES)
|
|
assert all(value == "" for value in outputs)
|