Features: BV Text node with management-gated writing assistance (translate, improve, undo, prompt profiles); reuse of prepared MODEL/CLIP state for Krea 2 and Anima LLLite consumers; Regional refactoring. Security boundaries: one wall-clock budget per Remote LLM request enforced by a socket-closing watchdog across connect, TLS handshake, headers, chunk framing and body; writing assistance uses the profile's configured timeout; a shared bounded JSON reader with route-specific limits on every JSON route; endpoint binding negative tests for the assist path. Release and packaging: SECURITY.md; bundled-library notices and a readable bundle header; Registry scan replica as a hard test with a pinned count; a packaged-file-set test; publish only from a validated push to main with provenance checks, pinned comfy-cli, an input manifest and a byte-exact comparison of the stored archive; npm audit and a whole js/ rebuild gate in Validate; design references, brand sources and a type declaration excluded from the package. README and changelog for 1.4.4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
295 lines
10 KiB
Python
295 lines
10 KiB
Python
import ast
|
|
import json
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).parents[1]
|
|
|
|
EXPECTED_NODE_NAMES_BY_CATEGORY = {
|
|
"__hidden__": {
|
|
"BV Detailer Loop Advance (internal)",
|
|
"BV Detailer Loop Result (internal)",
|
|
"BV Detailer While End (internal)",
|
|
"BV Detailer While Start (internal)",
|
|
"BV LUT Loop Advance (Prototype internal)",
|
|
"BV LUT Loop Result (Prototype internal)",
|
|
"BV LUT While End (Prototype internal)",
|
|
"BV LUT While Start (Prototype internal)",
|
|
},
|
|
"🌀 BV Node Pack/advanced/integrations": {"BV Detector Binding"},
|
|
"🌀 BV Node Pack/control": {"BV Control Center"},
|
|
"🌀 BV Node Pack/image": {"BV Image Dimensions"},
|
|
"🌀 BV Node Pack/latent": {
|
|
"BV Empty Latent Random Ratio",
|
|
"BV Latent Random Rotate 90",
|
|
},
|
|
"🌀 BV Node Pack/latent/Deprecated Legacy Nodes": {
|
|
"BV Latent Random Aspect Ratio",
|
|
},
|
|
"🌀 BV Node Pack/pipe": {"BV Smart Pipe", "BV Smart Pipe Merge"},
|
|
"🌀 BV Node Pack/pipe/Deprecated Legacy Nodes": {
|
|
"BV Pipe",
|
|
"BV Pipe Config",
|
|
},
|
|
"🌀 BV Node Pack/prompting": {
|
|
"BV Prompt AST Debug",
|
|
"BV Prompt Category Switch",
|
|
"BV Prompt Decode",
|
|
"BV Prompt Encode",
|
|
},
|
|
"🌀 BV Node Pack/regional/core": {
|
|
"BV Regional Debug",
|
|
"BV Regional Deconstructor",
|
|
"BV Regional Mask Render",
|
|
"BV Regional Prompt",
|
|
"BV Regional Prompt Extract",
|
|
"BV Regional Select",
|
|
},
|
|
"🌀 BV Node Pack/regional/detailer": {
|
|
"BV Detailer Loop End",
|
|
"BV Detailer Loop Job Resolver",
|
|
"BV Detailer Loop Start",
|
|
"BV Detector Registry",
|
|
"BV Regional Detailer Plan",
|
|
},
|
|
"🌀 BV Node Pack/regional/integrations/Impact Pack": {
|
|
"BV Detailer Loop Detect to SEGS (Impact)",
|
|
"BV Regional Detailer Mask",
|
|
},
|
|
"🌀 BV Node Pack/regional/LoRA": {"BV LoRA Registry", "BV Regional LoRA"},
|
|
"🌀 BV Node Pack/regional/LoRA/Manual Chains (Optional)": {
|
|
"BV LoRA Stack Collector",
|
|
"BV Named LoRA Stack",
|
|
},
|
|
"🌀 BV Node Pack/regional/LUT": {
|
|
"BV Apply LUT",
|
|
"BV Impact Detector Mask",
|
|
"BV LUT Loop End",
|
|
"BV LUT Loop Job Resolver",
|
|
"BV LUT Loop Start",
|
|
"BV LUT Registry",
|
|
"BV Regional LUT Plan",
|
|
},
|
|
"🌀 BV Node Pack/regional/LUT/Manual Chains (Optional)": {
|
|
"BV LUT Job",
|
|
"BV LUT Loader",
|
|
},
|
|
"🌀 BV Node Pack/regional/models/Anima": {
|
|
"BV Regional Anima Adapter",
|
|
"BV Regional Anima Conditioning",
|
|
"BV Regional Anima LLLite",
|
|
"BV Regional Color Control Image",
|
|
},
|
|
"🌀 BV Node Pack/regional/models/FLUX.2 Klein 9B": {
|
|
"BV Regional FLUX.2 Klein 9B Attention",
|
|
},
|
|
"🌀 BV Node Pack/regional/models/Generic": {
|
|
"BV Regional Native Conditioning",
|
|
},
|
|
"🌀 BV Node Pack/regional/models/Krea 2": {
|
|
"BV Regional Krea 2 Attention",
|
|
},
|
|
"🌀 BV Node Pack/regional/models/SDXL": {
|
|
"BV Regional SDXL Attention",
|
|
},
|
|
"🌀 BV Node Pack/regional/models/Z-Image": {
|
|
"BV Regional Z-Image Attention",
|
|
},
|
|
"🌀 BV Node Pack/regional/output": {
|
|
"BV Regional Image Save",
|
|
"BV Regional Image Send",
|
|
},
|
|
"🌀 BV Node Pack/regional/prompt enhancement": {
|
|
"BV Apply Regional Enhancement",
|
|
"BV Comfy CLIP LLM Provider",
|
|
"BV Regional Prompt Enhancer",
|
|
"BV Remote LLM Provider",
|
|
},
|
|
"🌀 BV Node Pack/subgraph": {
|
|
"BV Dynamic Combo",
|
|
"BV Subgraph Divider",
|
|
"BV Subgraph Heading",
|
|
"BV Subgraph Spacer",
|
|
},
|
|
"🌀 BV Node Pack/utils": {
|
|
"BV Hex Color To Int",
|
|
"BV Inspect Any",
|
|
"BV Seed",
|
|
"BV Text",
|
|
"BV Text Log Writer",
|
|
},
|
|
}
|
|
|
|
|
|
def collect_literal_node_mapping_keys() -> set[str]:
|
|
names: set[str] = set()
|
|
for path in (ROOT / "py" / "nodes").glob("*.py"):
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
for statement in tree.body:
|
|
if not isinstance(statement, ast.Assign):
|
|
continue
|
|
if not any(
|
|
isinstance(target, ast.Name)
|
|
and target.id == "NODE_CLASS_MAPPINGS"
|
|
for target in statement.targets
|
|
):
|
|
continue
|
|
if not isinstance(statement.value, ast.Dict):
|
|
continue
|
|
for key in statement.value.keys:
|
|
if isinstance(key, ast.Constant) and isinstance(key.value, str):
|
|
names.add(key.value)
|
|
return names
|
|
|
|
|
|
def resolve_string_expression(
|
|
expression: ast.expr,
|
|
constants: dict[str, str],
|
|
) -> str | None:
|
|
if isinstance(expression, ast.Constant) and isinstance(expression.value, str):
|
|
return expression.value
|
|
if isinstance(expression, ast.Name):
|
|
return constants.get(expression.id)
|
|
if isinstance(expression, ast.JoinedStr):
|
|
parts: list[str] = []
|
|
for value in expression.values:
|
|
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
|
parts.append(value.value)
|
|
elif isinstance(value, ast.FormattedValue):
|
|
resolved = resolve_string_expression(value.value, constants)
|
|
if resolved is None:
|
|
return None
|
|
parts.append(resolved)
|
|
else:
|
|
return None
|
|
return "".join(parts)
|
|
return None
|
|
|
|
|
|
def collect_declared_node_categories(filename: str) -> dict[str, str]:
|
|
path = ROOT / "py" / "nodes" / filename
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
constants: dict[str, str] = {}
|
|
class_categories: dict[str, str] = {}
|
|
node_classes: dict[str, str] = {}
|
|
|
|
for statement in tree.body:
|
|
if isinstance(statement, ast.Assign):
|
|
value = resolve_string_expression(statement.value, constants)
|
|
if value is not None:
|
|
for target in statement.targets:
|
|
if isinstance(target, ast.Name):
|
|
constants[target.id] = value
|
|
if any(
|
|
isinstance(target, ast.Name) and target.id == "NODE_CLASS_MAPPINGS"
|
|
for target in statement.targets
|
|
) and isinstance(statement.value, ast.Dict):
|
|
for key, mapped_class in zip(
|
|
statement.value.keys,
|
|
statement.value.values,
|
|
strict=True,
|
|
):
|
|
if (
|
|
isinstance(key, ast.Constant)
|
|
and isinstance(key.value, str)
|
|
and isinstance(mapped_class, ast.Name)
|
|
):
|
|
node_classes[key.value] = mapped_class.id
|
|
elif isinstance(statement, ast.ClassDef):
|
|
for class_statement in statement.body:
|
|
if not isinstance(class_statement, ast.Assign):
|
|
continue
|
|
if not any(
|
|
isinstance(target, ast.Name) and target.id == "CATEGORY"
|
|
for target in class_statement.targets
|
|
):
|
|
continue
|
|
category = resolve_string_expression(class_statement.value, constants)
|
|
if category is not None:
|
|
class_categories[statement.name] = category
|
|
|
|
return {
|
|
node_name: class_categories[class_name]
|
|
for node_name, class_name in node_classes.items()
|
|
if class_name in class_categories
|
|
}
|
|
|
|
|
|
class NodeListTests(unittest.TestCase):
|
|
def test_node_list_matches_all_declared_node_mapping_keys(self):
|
|
node_list = json.loads(
|
|
(ROOT / "node_list.json").read_text(encoding="utf-8")
|
|
)
|
|
self.assertEqual(set(node_list), collect_literal_node_mapping_keys())
|
|
|
|
def test_node_list_descriptions_are_non_empty_strings(self):
|
|
node_list = json.loads(
|
|
(ROOT / "node_list.json").read_text(encoding="utf-8")
|
|
)
|
|
self.assertTrue(node_list)
|
|
for node_name, description in node_list.items():
|
|
with self.subTest(node=node_name):
|
|
self.assertIsInstance(description, str)
|
|
self.assertTrue(description.strip())
|
|
|
|
def test_deprecated_nodes_live_below_their_product_families(self):
|
|
from py.nodes.bv_latent_random_ratio import BVLatentRandomAspectRatio
|
|
from py.nodes.bv_pipe import BVPipeNode
|
|
from py.nodes.bv_pipe_config import BVPipeConfigNode
|
|
|
|
self.assertEqual(
|
|
BVPipeNode.CATEGORY,
|
|
"🌀 BV Node Pack/pipe/Deprecated Legacy Nodes",
|
|
)
|
|
self.assertEqual(
|
|
BVPipeConfigNode.CATEGORY,
|
|
"🌀 BV Node Pack/pipe/Deprecated Legacy Nodes",
|
|
)
|
|
self.assertEqual(
|
|
BVLatentRandomAspectRatio.CATEGORY,
|
|
"🌀 BV Node Pack/latent/Deprecated Legacy Nodes",
|
|
)
|
|
|
|
def test_optional_manual_chains_live_below_their_product_families(self):
|
|
categories: dict[str, str] = {}
|
|
for path in (ROOT / "py" / "nodes").glob("*.py"):
|
|
categories.update(collect_declared_node_categories(path.name))
|
|
|
|
expected_categories = {
|
|
node_name: category
|
|
for category, node_names in EXPECTED_NODE_NAMES_BY_CATEGORY.items()
|
|
for node_name in node_names
|
|
}
|
|
self.assertEqual(categories, expected_categories)
|
|
|
|
manual_chain_names = {
|
|
name
|
|
for name, category in categories.items()
|
|
if category.endswith("/Manual Chains (Optional)")
|
|
}
|
|
self.assertEqual(
|
|
manual_chain_names,
|
|
{
|
|
"BV Named LoRA Stack",
|
|
"BV LoRA Stack Collector",
|
|
"BV LUT Job",
|
|
"BV LUT Loader",
|
|
},
|
|
)
|
|
|
|
def test_m0_feasibility_nodes_are_not_publicly_registered(self):
|
|
from fixtures.bv_regional_v3_m0 import (
|
|
NODE_CLASS_MAPPINGS,
|
|
NODE_DISPLAY_NAME_MAPPINGS,
|
|
)
|
|
|
|
self.assertEqual(NODE_CLASS_MAPPINGS, {})
|
|
self.assertEqual(NODE_DISPLAY_NAME_MAPPINGS, {})
|
|
node_list = json.loads((ROOT / "node_list.json").read_text(encoding="utf-8"))
|
|
self.assertFalse(any(name.startswith("BV M0 Fake ") for name in node_list))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|