Files
ausboss-ComfyUI-AusBoss/tests/test_registry_contract.py
T
ausboss 4e435e3013 Trade the display-name suffix for the 🆎 signature and unclip the chat toolbar
Every display name swaps ' (AusBoss)' for ' 🆎' - menus, docs, tooltips,
the README, and the example workflow's notes. Mapping keys never change,
so saved workflows keep loading; 'ausboss' keeps finding everything via
the category, the AUSBOSS_NODES_ prefix, and SEARCH_ALIASES. Python error
messages drop the suffix entirely so exception text stays ASCII for
cp1252 consoles.

The LM Studio endpoint toolbar declared no getMinHeight, so the frontend
gave its wrapper a 12px sliver and the buttons clipped; it now declares
32px through every sizing path, and the buttons breathe at 22px.
2026-08-16 20:29:38 -05:00

262 lines
10 KiB
Python

"""The registry contract, checked against fixtures that really break it.
Every ``bad_*.py`` under tests/fixtures/registry is a way a node module can
load and run perfectly while a registry scanner -- which parses the source
rather than importing it -- sees no nodes at all. That failure is invisible
from inside ComfyUI, so each shape is pinned here.
"""
from __future__ import annotations
import importlib.util
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "registry"
def _load_contract():
spec = importlib.util.spec_from_file_location(
"ausboss_registry_contract", ROOT / "scripts" / "registry_contract.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
contract = _load_contract()
def problems_for(fixture: str) -> list[str]:
path = FIXTURES / fixture
return contract.mapping_problems(path.read_text(encoding="utf-8"), fixture)
class GoodModuleTests(unittest.TestCase):
def test_a_conforming_module_reports_nothing(self):
self.assertEqual(problems_for("good_module.py.txt"), [])
def test_its_keys_are_readable_without_importing_it(self):
source = (FIXTURES / "good_module.py.txt").read_text(encoding="utf-8")
self.assertEqual(contract.class_mapping_keys(source), {"AUSBOSS_NODES_Good"})
def test_an_id_named_only_in_prose_is_not_a_registration(self):
# What separates the parsed keys from grepping the file for
# AUSBOSS_NODES_*: a retired id can go on being mentioned in a
# docstring or a search alias long after it stopped being registered.
source = (
'"""Replaces AUSBOSS_NODES_Old."""\n'
"\n"
"class New:\n"
' SEARCH_ALIASES = ["AUSBOSS_NODES_Old", "ausboss"]\n'
"\n"
'# AUSBOSS_NODES_Old moved here in wave three.\n'
'NODE_CLASS_MAPPINGS = {"AUSBOSS_NODES_New": New}\n'
'NODE_DISPLAY_NAME_MAPPINGS = {"AUSBOSS_NODES_New": "New 🆎"}\n'
)
self.assertEqual(contract.class_mapping_keys(source), {"AUSBOSS_NODES_New"})
self.assertEqual(contract.mapping_problems(source, "inline"), [])
def test_every_shipped_node_module_conforms(self):
modules = sorted((ROOT / "nodes").glob("node_*.py"))
self.assertTrue(modules, "no node modules found")
for path in modules:
with self.subTest(module=path.name):
source = path.read_text(encoding="utf-8")
self.assertEqual(contract.mapping_problems(source, path.name), [])
self.assertTrue(
contract.class_mapping_keys(source),
"a scanner would find no keys in this module",
)
class BadModuleTests(unittest.TestCase):
"""Each fixture must be rejected, and for the stated reason."""
def assert_rejected(self, fixture: str, *expected: str):
problems = problems_for(fixture)
self.assertTrue(problems, f"{fixture} was accepted")
joined = " | ".join(problems)
for fragment in expected:
self.assertIn(fragment, joined)
def test_a_variable_key_is_rejected(self):
# The original bug: the pack loaded fine and was invisible to Manager.
self.assert_rejected(
"bad_variable_key.py.txt", "NODE_ID", "string literal")
def test_a_dict_call_is_rejected(self):
self.assert_rejected(
"bad_dict_call.py.txt", "dictionary literal", "dict(...)")
def test_a_helper_return_is_rejected(self):
self.assert_rejected(
"bad_helper_return.py.txt", "dictionary literal", "_build(...)")
def test_reassignment_is_rejected(self):
self.assert_rejected(
"bad_reassigned.py.txt", "exactly once")
def test_a_later_update_is_rejected(self):
self.assert_rejected(
"bad_update.py.txt", "update()")
def test_item_assignment_is_rejected(self):
self.assert_rejected(
"bad_item_assignment.py.txt", "has an entry assigned after assignment"
)
def test_empty_mappings_are_rejected(self):
# Two empty literals agree with each other, so the key-match rule is
# happy: only the emptiness check catches a module registering nothing.
self.assert_rejected(
"bad_empty.py.txt",
"NODE_CLASS_MAPPINGS is empty",
"NODE_DISPLAY_NAME_MAPPINGS is empty",
)
def test_a_delete_is_rejected(self):
self.assert_rejected(
"bad_delete.py.txt", "has an entry deleted after assignment"
)
def test_an_alias_is_rejected_where_it_is_made(self):
# _registry.update(...) never says NODE_CLASS_MAPPINGS, so the only
# place this is still visible is the line that binds the alias.
problems = problems_for("bad_alias.py.txt")
joined = " | ".join(problems)
self.assertIn("NODE_CLASS_MAPPINGS is aliased after assignment", joined)
self.assertIn("NODE_DISPLAY_NAME_MAPPINGS is aliased after assignment", joined)
def test_a_bare_read_after_assignment_is_rejected(self):
# Strict on purpose: once the name can travel, no checker can promise
# where it ends up, so the contract is that it does not travel at all.
source = (
"class Node:\n"
" pass\n"
'NODE_CLASS_MAPPINGS = {"AUSBOSS_NODES_A": Node}\n'
'NODE_DISPLAY_NAME_MAPPINGS = {"AUSBOSS_NODES_A": "A 🆎"}\n'
"print(len(NODE_CLASS_MAPPINGS))\n"
)
problems = contract.mapping_problems(source, "inline")
self.assertEqual(len(problems), 1)
self.assertIn("NODE_CLASS_MAPPINGS is used after assignment", problems[0])
def test_a_delete_of_the_whole_mapping_is_rejected(self):
source = (
"class Node:\n"
" pass\n"
'NODE_CLASS_MAPPINGS = {"AUSBOSS_NODES_A": Node}\n'
'NODE_DISPLAY_NAME_MAPPINGS = {"AUSBOSS_NODES_A": "A 🆎"}\n'
"del NODE_CLASS_MAPPINGS\n"
)
problems = contract.mapping_problems(source, "inline")
self.assertEqual(len(problems), 1)
self.assertIn("NODE_CLASS_MAPPINGS is deleted after assignment", problems[0])
def test_mismatched_keys_are_rejected(self):
self.assert_rejected(
"bad_key_mismatch.py.txt",
"AUSBOSS_NODES_Two has no NODE_DISPLAY_NAME_MAPPINGS entry",
)
def test_a_repeated_key_is_rejected(self):
self.assert_rejected(
"bad_duplicate_key.py.txt", "twice")
def test_unpacking_another_mapping_is_rejected(self):
self.assert_rejected(
"bad_unpacked.py.txt", "unpacks another")
def test_a_missing_mapping_is_rejected(self):
self.assert_rejected(
"bad_missing_display.py.txt", "NODE_DISPLAY_NAME_MAPPINGS", "exactly once"
)
def test_every_bad_fixture_is_rejected(self):
fixtures = sorted(FIXTURES.glob("bad_*.py.txt"))
self.assertGreaterEqual(len(fixtures), 13)
for path in fixtures:
with self.subTest(fixture=path.name):
self.assertTrue(problems_for(path.name), "fixture was accepted")
class DuplicateKeyTests(unittest.TestCase):
def test_one_key_claimed_by_two_modules_is_reported(self):
problems = contract.duplicate_key_problems(
{
"node_a.py": {"AUSBOSS_NODES_Shared", "AUSBOSS_NODES_OnlyA"},
"node_b.py": {"AUSBOSS_NODES_Shared"},
}
)
self.assertEqual(len(problems), 1)
self.assertIn("AUSBOSS_NODES_Shared", problems[0])
self.assertIn("node_a.py", problems[0])
self.assertIn("node_b.py", problems[0])
def test_distinct_keys_report_nothing(self):
self.assertEqual(
contract.duplicate_key_problems(
{"node_a.py": {"AUSBOSS_NODES_A"}, "node_b.py": {"AUSBOSS_NODES_B"}}
),
[],
)
def test_the_shipped_pack_has_no_collisions(self):
keys = {
path.name: contract.class_mapping_keys(path.read_text(encoding="utf-8"))
for path in sorted((ROOT / "nodes").glob("node_*.py"))
}
self.assertEqual(contract.duplicate_key_problems(keys), [])
class ModuleListTests(unittest.TestCase):
"""NODE_MODULES is parsed, not matched anywhere in the text.
A name left behind in a comment used to satisfy the on-disk check, so a
module nobody imports passed with no output at all.
"""
def test_the_real_init_declares_every_node_module(self):
listed = contract.module_list((ROOT / "__init__.py").read_text(encoding="utf-8"))
on_disk = {path.stem for path in sorted((ROOT / "nodes").glob("node_*.py"))}
self.assertEqual(listed, on_disk)
def test_a_name_only_in_a_comment_is_not_a_registration(self):
source = 'NODE_MODULES = ["node_a"]\n# TODO re-enable "node_b" after the refactor\n'
self.assertEqual(contract.module_list(source), {"node_a"})
def test_quoting_style_does_not_change_the_answer(self):
self.assertEqual(
contract.module_list("NODE_MODULES = ['node_a', 'node_b']\n"),
{"node_a", "node_b"},
)
def test_an_unreadable_declaration_reports_none_rather_than_empty(self):
# None means "cannot be read" and is an error; an empty set would be
# indistinguishable from a pack that registers nothing.
self.assertIsNone(contract.module_list("NODE_MODULES = [name for name in x]\n"))
self.assertIsNone(contract.module_list("NODE_MODULES = build()\n"))
self.assertIsNone(contract.module_list("SOMETHING_ELSE = []\n"))
def test_no_module_outside_nodes_declares_mapping_keys(self):
# Registry scanners read the whole checkout: a fixture or sample that
# names AUSBOSS_NODES_* in a mapping literal is advertised as an
# installable node the pack never registers.
stray = {}
for path in sorted(ROOT.rglob("*.py")):
parts = path.relative_to(ROOT).parts
if any(part.startswith(".") or part == "__pycache__" for part in parts):
continue
if path.parent == ROOT / "nodes":
continue
keys = contract.class_mapping_keys(path.read_text(encoding="utf-8"))
if keys:
stray[str(path.relative_to(ROOT))] = sorted(keys)
self.assertEqual(stray, {})
if __name__ == "__main__":
unittest.main()