Click a thing in your picture on Mask by Touch, say what to change, and Qwen repaints only that part. It is the workflow of its page, with the Seed on fixed as every example here has it. The workflow carries a note above its Workflow Note that links to the video channel. The example check wanted every node but the Workflow Note inside a numbered group, so that note counted as a stray node. A Callout in the Workflow Note's own column now belongs to the note. A Callout anywhere else outside the groups, and any other node in that column, is still refused.
124 lines
6.0 KiB
Python
124 lines
6.0 KiB
Python
"""Catch saved graphs that parse successfully but fail to load or read cleanly."""
|
|
from pathlib import Path
|
|
import sys
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
from workflow_contract import workflow_problems, example_problems
|
|
|
|
|
|
def good_graph():
|
|
return {
|
|
"last_node_id": 3, "last_link_id": 1,
|
|
"nodes": [
|
|
{"id": 1, "type": "Source", "pos": [0, 100], "size": [100, 80], "outputs": [{"links": [1]}]},
|
|
{"id": 2, "type": "Target", "pos": [180, 100], "size": [100, 80], "inputs": [{"link": 1}]},
|
|
{"id": 3, "type": "AUSBOSS_NODES_WorkflowNote", "pos": [-200, 100], "size": [100, 80],
|
|
"widgets_values": ['{"title":"Example","body":"Choose a source and queue."}']},
|
|
],
|
|
"links": [[1, 1, 0, 2, 0, "IMAGE"]],
|
|
"groups": [{"bounding": [-20, 40, 320, 160]}],
|
|
}
|
|
|
|
|
|
class WorkflowContractTests(unittest.TestCase):
|
|
def test_complete_graph(self):
|
|
self.assertEqual(workflow_problems(good_graph()), [])
|
|
|
|
def test_named_widget_copy_must_match_the_loaded_values(self):
|
|
graph = good_graph()
|
|
graph["nodes"][0]["widgets_values"] = [7, "fixed", ""]
|
|
graph["nodes"][0]["widgets_values_named"] = {"seed": 7, "control_after_generate": "fixed", "ausboss_seed_panel": ""}
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
graph["nodes"][0]["widgets_values_named"]["control_after_generate"] = "randomize"
|
|
self.assertIn("stale named widget values: control_after_generate", " ".join(workflow_problems(graph)))
|
|
graph = good_graph()
|
|
graph["nodes"][0]["widgets_values"] = {"video": "sample.mp4", "start_seconds": 0}
|
|
graph["nodes"][0]["widgets_values_named"] = {"video": "someone_else.mp4", "start_seconds": 0, "upload": ""}
|
|
self.assertIn("stale named widget values: video", " ".join(workflow_problems(graph)))
|
|
|
|
def test_workflow_id_and_zoom(self):
|
|
graph = good_graph()
|
|
graph["id"] = "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b"
|
|
graph["extra"] = {"ds": {"scale": 0.6, "offset": [0, 0]}}
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
for bad in ("", "ausboss-showcase", "00000000-0000-0000-0000-000000000000"):
|
|
graph["id"] = bad
|
|
self.assertIn("workflow id", " ".join(workflow_problems(graph)))
|
|
graph["id"] = "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b"
|
|
graph["extra"]["ds"]["scale"] = 0.45
|
|
self.assertIn("saved zoom", " ".join(workflow_problems(graph)))
|
|
|
|
def test_download_info_and_save_prefix(self):
|
|
graph = good_graph()
|
|
graph["nodes"][0]["widgets_values"] = ["model.safetensors", "default"]
|
|
graph["nodes"][0]["properties"] = {"models": [{"name": "model.safetensors", "url": "https://example.invalid/model.safetensors", "directory": "vae"}]}
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
graph["nodes"][0]["properties"]["models"][0]["name"] = "other.safetensors"
|
|
self.assertIn("does not select", " ".join(workflow_problems(graph)))
|
|
graph = good_graph()
|
|
graph["nodes"][1]["type"] = "AUSBOSS_NODES_SaveImage"
|
|
graph["nodes"][1]["widgets_values"] = ["AusBoss/image", "png"]
|
|
self.assertIn("saves into a folder", " ".join(workflow_problems(graph)))
|
|
graph["nodes"][1]["widgets_values"] = ["image", "png"]
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
|
|
def test_link_must_agree_in_all_three_places(self):
|
|
graph = good_graph()
|
|
graph["nodes"][1]["inputs"][0]["link"] = 99
|
|
self.assertIn("target input", " ".join(workflow_problems(graph)))
|
|
self.assertIn("stale link", " ".join(workflow_problems(graph)))
|
|
graph = good_graph()
|
|
graph["nodes"][0]["outputs"][0]["links"] = []
|
|
self.assertIn("source output", " ".join(workflow_problems(graph)))
|
|
|
|
def test_missing_nodes_and_stale_counters(self):
|
|
graph = good_graph()
|
|
graph["links"][0][1] = 99
|
|
graph["last_link_id"] = 0
|
|
problems = " ".join(workflow_problems(graph))
|
|
self.assertIn("missing node", problems)
|
|
self.assertIn("last_link_id", problems)
|
|
|
|
def test_core_lora_loaders_are_refused(self):
|
|
graph = good_graph()
|
|
graph["nodes"][0]["type"] = "LoraLoaderModelOnly"
|
|
self.assertIn("examples use LoRA Loader", " ".join(workflow_problems(graph)))
|
|
graph["nodes"][0]["type"] = "AUSBOSS_NODES_LoraLoader"
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
|
|
def test_title_bars_count_as_overlap(self):
|
|
graph = good_graph()
|
|
graph["nodes"][1]["pos"] = [0, 195] # Body clears; title collides.
|
|
self.assertIn("overlap", " ".join(workflow_problems(graph)))
|
|
|
|
def test_notes_and_group_containment(self):
|
|
graph = good_graph()
|
|
graph["nodes"][2]["widgets_values"] = ['{"title":"Example","body":"one\\\\ntwo"}']
|
|
graph["nodes"][1]["pos"] = [500, 100]
|
|
problems = " ".join(workflow_problems(graph))
|
|
self.assertIn("escaped line breaks", problems)
|
|
self.assertIn("outside its stage", problems)
|
|
|
|
def test_a_callout_in_the_notes_own_column_needs_no_group(self):
|
|
graph = good_graph()
|
|
graph["last_node_id"] = 4
|
|
# above the Workflow Note, as wide as it: a link to a video
|
|
graph["nodes"].append({"id": 4, "type": "AUSBOSS_NODES_Callout", "pos": [-200, -60], "size": [100, 60],
|
|
"widgets_values": ["[Watch the video](https://example.invalid/)"]})
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
# anywhere else outside the stage groups it is still a stray node
|
|
graph["nodes"][3]["pos"] = [600, -60]
|
|
self.assertIn("node 4 is outside its stage", " ".join(workflow_problems(graph)))
|
|
# and any other node in the note's column is one too
|
|
graph["nodes"][3].update({"pos": [-200, -60], "type": "Source"})
|
|
self.assertIn("node 4 is outside its stage", " ".join(workflow_problems(graph)))
|
|
|
|
def test_all_shipped_examples(self):
|
|
self.assertEqual(example_problems(ROOT / "example_workflows"), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|