Review fixes for the pack's offline checks. - The startup version reader and the checks read pyproject.toml as TOML instead of a regex. - validate_nodes and registry_contract catch more ways a node mapping or the module list could be hidden from registry scanners. - release_preflight flags network-client imports and spaced or qualified fetch calls in shipped code. - workflow_contract verifies saved widget values against the real widget order, covers subgraphs and validates model download info. - Krea 2 Inpaint Masked example: two Note nodes' named widget values now match their positional values, which the new check requires, and the file ends with a newline. The graph itself is unchanged. Tests: tests/test_check_regressions.py, with one adjusted case in tests/test_workflow_contract.py. No inputs, outputs or mapping keys change. No version change.
134 lines
6.6 KiB
Python
134 lines
6.6 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_a_tile_can_hold_the_setup_instructions(self):
|
|
graph = good_graph()
|
|
graph["nodes"][2]["type"] = "AUSBOSS_NODES_WorkflowNoteTile"
|
|
self.assertEqual(workflow_problems(graph), [])
|
|
graph["nodes"][2]["widgets_values"] = ['{"title":"Example","layout":"tile"}']
|
|
self.assertIn("needs a title and instructions", " ".join(workflow_problems(graph)))
|
|
|
|
def test_named_widget_copy_must_match_the_loaded_values(self):
|
|
graph = good_graph()
|
|
graph["nodes"][0]["type"] = "AUSBOSS_NODES_Seed"
|
|
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), [])
|
|
graph["nodes"][3]["type"] = "AUSBOSS_NODES_CalloutTile"
|
|
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()
|