Files
DumiFlex-ComfyUI-Wildcard-P…/tests/engine/cascade/test_scan.py
T
DumiFlex 4cda98f227 fix(cascade): discover v2 boolean sub-category filter refs
Sub-category rename/delete discovery keyed on the whole filter body, so
it missed any ref using an SP1 boolean filter. `@{mood:warm or intense}`
indexed as `mood:warm or intense` (TS) / matched by `sub == "warm"` exact
equality (Python), so a rename or delete of `warm` neither updated the
SPA badge nor counted the ref in the server-side preview — even though
the rewrite (engine/cascade/fixers.py) already token-matches and DID
rewrite it on apply. Preview undercounted what apply changed.

Both discovery layers now collect the expression's tags via the shared
parser and index/match per-tag (negated tags count; the `!null` marker is
not a tag):
- TS: new collectTags in subcatExprCascade.ts; reverse-dep-index builds
  one toSubcat entry per tag.
- Python: new collect_tags in fixers.py; scan._scan_subcat tests tag
  membership instead of raw-string equality.
2026-06-08 06:41:15 +03:00

218 lines
8.0 KiB
Python

"""Reverse-ref scan tests for cascade-edit mutations."""
from engine.cascade.scan import scan_affected
from engine.db.repositories import BundleRepository, CategoryRepository, ModuleRepository
def test_scan_wildcard_delete_returns_bundles_and_constraints_and_text_refs(wp_db):
mod = ModuleRepository(wp_db)
wc = mod.create(
type="wildcard", name="palette", description="", category_id=None, tags=[],
payload={"options": [{"id": "o1", "value": "red", "weight": 1}]},
)
other_wc = mod.create(
type="wildcard", name="other", description="", category_id=None, tags=[],
payload={"options": [{"id": "o2", "value": "see @{" + wc["id"] + "}", "weight": 1}]},
)
constraint = mod.create(
type="constraint", name="c1", description="", category_id=None, tags=[],
payload={
"source_wildcard_id": wc["id"],
"target_wildcard_id": other_wc["id"],
"matrix": {},
"exceptions": [],
},
)
bundle = BundleRepository(wp_db).create(
name="b1",
children=[{"id": wc["id"], "type": "module"}],
)
affected = scan_affected(wp_db, kind="wildcard", id=wc["id"], action="delete")
affected_ids = {a["id"] for a in affected}
assert constraint["id"] in affected_ids
assert other_wc["id"] in affected_ids
# Bundles intentionally NOT in the impact set — children are frozen
# snapshots independent of the source wildcard, so a delete leaves
# them untouched. See engine/cascade/fixers.py:fix_wildcard_delete.
assert bundle["id"] not in affected_ids
def test_scan_subcat_delete_returns_constraint_with_matrix_key(wp_db):
mod = ModuleRepository(wp_db)
wc = mod.create(
type="wildcard", name="palette", description="", category_id=None, tags=[],
payload={"options": [
{"id": "o1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
{"id": "o2", "value": "blue", "weight": 1, "sub_categories": ["cool"]},
]},
)
other = mod.create(
type="wildcard", name="other", description="", category_id=None, tags=[],
payload={"options": [{"id": "x", "value": "x", "weight": 1, "sub_categories": ["a"]}]},
)
c1 = mod.create(
type="constraint", name="c1", description="", category_id=None, tags=[],
payload={
"source_wildcard_id": wc["id"],
"target_wildcard_id": other["id"],
"matrix": {"warm": {"a": {"mode": "block"}}},
"exceptions": [],
},
)
affected = scan_affected(
wp_db, kind="subcategory", id=wc["id"], action="delete",
extra={"subcat_name": "warm"},
)
assert {a["id"] for a in affected} == {c1["id"]}
def test_scan_subcat_delete_also_catches_text_refs(wp_db):
mod = ModuleRepository(wp_db)
target = mod.create(
type="wildcard", name="palette", description="", category_id=None, tags=[],
payload={"options": [
{"id": "o1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
]},
)
referrer = mod.create(
type="wildcard", name="user", description="", category_id=None, tags=[],
payload={"options": [
{"id": "u1", "value": "see @{" + target["id"] + ":warm} now", "weight": 1},
]},
)
affected = scan_affected(
wp_db, kind="subcategory", id=target["id"], action="delete",
extra={"subcat_name": "warm"},
)
assert referrer["id"] in {a["id"] for a in affected}
def test_scan_subcat_delete_catches_boolean_filter_text_refs(wp_db):
# SP1: a boolean filter `@{target:warm or cool!null}` references BOTH
# tags. Deleting/renaming "warm" must surface this referrer in the
# preview — the fixer WILL rewrite it (engine/cascade/fixers.py token-
# matches), so the discovery scan must agree or the affected-count lies.
mod = ModuleRepository(wp_db)
target = mod.create(
type="wildcard", name="palette", description="", category_id=None, tags=[],
payload={"options": [
{"id": "o1", "value": "red", "weight": 1, "sub_categories": ["warm", "cool"]},
]},
)
referrer = mod.create(
type="wildcard", name="user", description="", category_id=None, tags=[],
payload={"options": [
{"id": "u1", "value": "see @{" + target["id"] + ":warm or cool!null} now", "weight": 1},
]},
)
affected = scan_affected(
wp_db, kind="subcategory", id=target["id"], action="delete",
extra={"subcat_name": "warm"},
)
assert referrer["id"] in {a["id"] for a in affected}
def test_scan_subcat_rename_catches_negated_filter_text_refs(wp_db):
# The tag inside a `not` is still a dependency: renaming "warm" must
# rewrite `@{target:not warm}`, so the scan must surface the referrer.
mod = ModuleRepository(wp_db)
target = mod.create(
type="wildcard", name="palette", description="", category_id=None, tags=[],
payload={"options": [
{"id": "o1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
]},
)
referrer = mod.create(
type="wildcard", name="user", description="", category_id=None, tags=[],
payload={"options": [
{"id": "u1", "value": "x @{" + target["id"] + ":not warm} y", "weight": 1},
]},
)
affected = scan_affected(
wp_db, kind="subcategory", id=target["id"], action="rename",
extra={"subcat_name": "warm"},
)
assert referrer["id"] in {a["id"] for a in affected}
def test_scan_combine_output_var_rename_returns_derivations_and_wildcards(wp_db):
mod = ModuleRepository(wp_db)
combine = mod.create(
type="combine", name="c1", description="", category_id=None, tags=[],
payload={"template": "$a", "output_var": "mood"},
)
deriv = mod.create(
type="derivation", name="d1", description="", category_id=None, tags=[],
payload={"rules": [{"id": "r1", "branches": [{"key": "default", "actions": [
{"set_var": "mood", "value": "x"},
]}]}]},
)
wildcard = mod.create(
type="wildcard", name="w1", description="", category_id=None, tags=[],
payload={"options": [{"id": "o1", "value": "uses $mood here", "weight": 1}]},
)
affected = scan_affected(
wp_db, kind="combine_output_var", id=combine["id"], action="rename",
extra={"old_name": "mood", "new_name": "tone"},
)
affected_ids = {a["id"] for a in affected}
assert deriv["id"] in affected_ids
assert wildcard["id"] in affected_ids
def test_scan_category_delete_returns_modules_with_that_category_id(wp_db):
cat = CategoryRepository(wp_db).create(name="Style", color="#ff0000", icon="palette")
mod = ModuleRepository(wp_db)
m1 = mod.create(
type="wildcard", name="w1", description="", category_id=cat["id"], tags=[],
payload={"options": []},
)
m2 = mod.create(
type="combine", name="c1", description="", category_id=cat["id"], tags=[],
payload={"template": "$a", "output_var": "x"},
)
affected = scan_affected(wp_db, kind="category", id=cat["id"], action="delete")
assert {a["id"] for a in affected} == {m1["id"], m2["id"]}
def test_scan_returns_empty_when_no_refs(wp_db):
mod = ModuleRepository(wp_db)
isolated = mod.create(
type="wildcard", name="iso", description="", category_id=None, tags=[],
payload={"options": []},
)
affected = scan_affected(wp_db, kind="wildcard", id=isolated["id"], action="delete")
assert affected == []
def test_scan_returns_empty_for_unsupported_kind_action_pair(wp_db):
"""v1 stub: unrecognized (kind, action) pairs return empty list."""
mod = ModuleRepository(wp_db)
m = mod.create(
type="wildcard", name="x", description="", category_id=None, tags=[],
payload={"options": []},
)
# Not yet implemented for v1.
affected = scan_affected(
wp_db, kind="option_value", id=m["id"], action="delete",
extra={"option_id": "o1"},
)
assert affected == []