Option sub_category -> sub_categories[] (default []). Both pool filters (instance category_filter, nested @{} ref) become boolean expressions (and/or/not/parens; comma=or) matched against an option's tag set via the shared engine/syntax/subcat_filter matcher, applied BEFORE constraints. Exclude-null is a separate flag: instance exclude_null + the trailing !null ref marker, replacing the old 'null' keyword.
4-segment ref grammar @{uuid#name:expr!null} single-sourced across tokenize.py/scan.py/pipeline.py/_constraints.py (! excluded from name/expr captures); tokenizer REF meta now carries filter_expr + exclude_null instead of a comma-split list. scan.py findall unpack sites updated for the 4th group.
validate_payload: per-option sub_categories[] must be in the registry; optional tag_groups (UI-only axes, members in-registry, <=1 group); registry names validated via subcat_filter.validate_subcat_name (rejects whitespace, boolean/ref chars, reserved words incl. null). wp_api/_validators delegates to the same function.
Constraints stay alive via a primary-tag stopgap: _apply_constraint_to_options + _record_pick use sub_categories[0] as the matrix key; true multi-tag matrix matching is SP3. Cascade fix_subcat_delete/rename + orchestrator scrub the per-option list; instance_schemas category_filter -> string. Existing engine tests migrated to the v2 shape; added tests for the ref regex, payload validation, and multi-tag pool filtering.
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
"""Real-shape fixture tests for subcategory rename + delete fixers.
|
|
|
|
These tests construct wildcards via `ModuleRepository.create` so the
|
|
data matches what the server actually stores under the SP1 multi-tag
|
|
model: each option carries a `sub_categories` membership list plus a
|
|
top-level `payload.sub_categories` declared list. A subcat rename
|
|
rewrites the matching entry inside each option's list; a delete strips
|
|
it from the list (the option itself is preserved).
|
|
|
|
Regression: 2026-05-24 live QA on cascade-edit-indicators v1.
|
|
"""
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from engine.cascade.orchestrator import apply_cascade
|
|
from engine.db.migrations import migrate
|
|
from engine.db.repositories import ModuleRepository
|
|
|
|
|
|
@pytest.fixture
|
|
def conn():
|
|
c = sqlite3.connect(":memory:")
|
|
c.row_factory = sqlite3.Row
|
|
migrate(c)
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
def _create_wildcard(conn, name, sub_categories, options):
|
|
repo = ModuleRepository(conn)
|
|
return repo.create(
|
|
type="wildcard",
|
|
name=name,
|
|
description="",
|
|
category_id=None,
|
|
tags=[],
|
|
payload={"sub_categories": sub_categories, "options": options},
|
|
)
|
|
|
|
|
|
def test_subcat_rename_updates_top_level_list(conn):
|
|
w = _create_wildcard(
|
|
conn,
|
|
"mood",
|
|
sub_categories=["positive", "negative"],
|
|
options=[
|
|
{"value": "serene", "weight": 1, "sub_categories": ["positive"], "probability": 1.0},
|
|
{"value": "tense", "weight": 1, "sub_categories": ["negative"], "probability": 1.0},
|
|
],
|
|
)
|
|
res = apply_cascade(conn, {
|
|
"kind": "subcategory", "id": w["id"], "action": "rename",
|
|
"cascade_refs": True, "new_name": "good",
|
|
"extra": {"subcat_name": "positive"},
|
|
})
|
|
assert res["ok"], res
|
|
|
|
updated = ModuleRepository(conn).get(w["id"])
|
|
assert "good" in updated["payload"]["sub_categories"]
|
|
assert "positive" not in updated["payload"]["sub_categories"]
|
|
|
|
|
|
def test_subcat_rename_updates_option_assignments(conn):
|
|
w = _create_wildcard(
|
|
conn,
|
|
"mood",
|
|
sub_categories=["positive", "negative"],
|
|
options=[
|
|
{"value": "serene", "weight": 1, "sub_categories": ["positive"], "probability": 1.0},
|
|
{"value": "tense", "weight": 1, "sub_categories": ["negative"], "probability": 1.0},
|
|
],
|
|
)
|
|
apply_cascade(conn, {
|
|
"kind": "subcategory", "id": w["id"], "action": "rename",
|
|
"cascade_refs": True, "new_name": "good",
|
|
"extra": {"subcat_name": "positive"},
|
|
})
|
|
|
|
updated = ModuleRepository(conn).get(w["id"])
|
|
assignments = [o["sub_categories"] for o in updated["payload"]["options"]]
|
|
assert assignments == [["good"], ["negative"]]
|
|
|
|
|
|
def test_subcat_delete_removes_from_top_level_and_options(conn):
|
|
w = _create_wildcard(
|
|
conn,
|
|
"mood",
|
|
sub_categories=["positive", "negative"],
|
|
options=[
|
|
{"value": "serene", "weight": 1, "sub_categories": ["positive"], "probability": 1.0},
|
|
{"value": "tense", "weight": 1, "sub_categories": ["negative"], "probability": 1.0},
|
|
],
|
|
)
|
|
res = apply_cascade(conn, {
|
|
"kind": "subcategory", "id": w["id"], "action": "delete",
|
|
"cascade_refs": True, "extra": {"subcat_name": "positive"},
|
|
})
|
|
assert res["ok"]
|
|
|
|
updated = ModuleRepository(conn).get(w["id"])
|
|
assert updated["payload"]["sub_categories"] == ["negative"]
|
|
assignments = [o["sub_categories"] for o in updated["payload"]["options"]]
|
|
assert assignments == [[], ["negative"]]
|
|
|
|
|
|
def test_subcat_rename_optout_updates_source_only(conn):
|
|
w = _create_wildcard(
|
|
conn,
|
|
"mood",
|
|
sub_categories=["positive", "negative"],
|
|
options=[
|
|
{"value": "serene", "weight": 1, "sub_categories": ["positive"], "probability": 1.0},
|
|
],
|
|
)
|
|
res = apply_cascade(conn, {
|
|
"kind": "subcategory", "id": w["id"], "action": "rename",
|
|
"cascade_refs": False, "new_name": "good",
|
|
"extra": {"subcat_name": "positive"},
|
|
})
|
|
assert res["ok"]
|
|
|
|
updated = ModuleRepository(conn).get(w["id"])
|
|
assert updated["payload"]["sub_categories"] == ["good", "negative"]
|
|
assert updated["payload"]["options"][0]["sub_categories"] == ["good"]
|