Stamp the source/target wildcard display names onto the constraint payload (source_wildcard_name / target_wildcard_name) when the source/target is set, so the broken-reference banner can show "Source wildcard 'Starter subject' (54693e08)" instead of just the uuid after the wildcard is deleted. Display-only and additive: the engine resolves by uuid and never reads these names (diagnostic, like producer_engine_version), so there is no engine resolution change, no Py=TS parity, and no community schema bump. Legacy constraints lack the fields and gracefully fall back to uuid-only. SPA ConstraintEditor stamps on pick + reattach + load and persists when known; the canvas ConstraintInstanceModal mirrors the reattach stamp and falls back to the cached name in the banner when the live lookup misses. Spec: docs/superpowers/specs/2026-06-12-constraint-cached-ref-names-design.md
599 lines
22 KiB
Python
599 lines
22 KiB
Python
"""Tests for ConstraintHandler — pass-through stub that records metadata."""
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from engine.modules.constraint_handler import ConstraintHandler
|
|
|
|
|
|
def _payload():
|
|
return {
|
|
"source_wildcard_id": "wc_outfit",
|
|
"target_wildcard_id": "wc_pose",
|
|
"matrix": {
|
|
"kimono": {"casual": {"mode": "exclude", "factor": 1.0}},
|
|
},
|
|
"exceptions": [],
|
|
}
|
|
|
|
|
|
def test_handler_type_id_is_constraint():
|
|
assert ConstraintHandler.type_id == "constraint"
|
|
|
|
|
|
def test_validate_payload_accepts_well_formed():
|
|
ConstraintHandler.validate_payload(_payload())
|
|
|
|
|
|
def test_validate_payload_rejects_blank_source_id():
|
|
payload = _payload()
|
|
payload["source_wildcard_id"] = ""
|
|
with pytest.raises(ValueError, match="source_wildcard_id"):
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_validate_payload_rejects_unknown_mode():
|
|
payload = _payload()
|
|
payload["matrix"]["kimono"]["casual"]["mode"] = "weird"
|
|
with pytest.raises(ValueError, match="mode"):
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_validate_payload_accepts_zero_factor():
|
|
"""factor=0 is canonical for "exclude this pair" — both `mode: exclude`
|
|
and a zero factor encode the same intent. Strict > 0 was rejecting
|
|
legitimate SPA-saved payloads where the editor surfaced an explicit
|
|
zero weight (a real QA-reported regression). Negative factors stay
|
|
rejected — they have no defined semantics in the weighted-pick
|
|
model."""
|
|
payload = _payload()
|
|
payload["matrix"]["kimono"]["casual"]["factor"] = 0.0
|
|
ConstraintHandler.validate_payload(payload) # must not raise
|
|
|
|
|
|
def test_validate_payload_rejects_negative_factor():
|
|
payload = _payload()
|
|
payload["matrix"]["kimono"]["casual"]["factor"] = -1.0
|
|
with pytest.raises(ValueError, match="factor"):
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_validate_payload_rejects_non_numeric_factor():
|
|
payload = _payload()
|
|
payload["matrix"]["kimono"]["casual"]["factor"] = "1.0"
|
|
with pytest.raises(ValueError, match="factor"):
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_validate_payload_rejects_bool_factor():
|
|
"""``True`` is technically an int, but a bool factor is meaningless here."""
|
|
payload = _payload()
|
|
payload["matrix"]["kimono"]["casual"]["factor"] = True
|
|
with pytest.raises(ValueError, match="factor"):
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_validate_payload_accepts_exceptions():
|
|
payload = _payload()
|
|
payload["exceptions"] = [
|
|
{"source": "kimono", "target": "casual", "mode": "boost", "factor": 1.5},
|
|
]
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_validate_payload_rejects_exception_with_bad_mode():
|
|
payload = _payload()
|
|
payload["exceptions"] = [
|
|
{"source": "kimono", "target": "casual", "mode": "huh", "factor": 1.0},
|
|
]
|
|
with pytest.raises(ValueError, match="mode"):
|
|
ConstraintHandler.validate_payload(payload)
|
|
|
|
|
|
def test_resolve_records_metadata_into_ctx_constraints_list():
|
|
ctx: dict = {}
|
|
out = ConstraintHandler.resolve(_payload(), instance={}, ctx=ctx)
|
|
# Pass-through stub returns no bindings; mutates ctx instead.
|
|
assert out == {}
|
|
assert isinstance(ctx["__wp_constraints__"], list)
|
|
assert len(ctx["__wp_constraints__"]) == 1
|
|
meta = ctx["__wp_constraints__"][0]
|
|
assert meta["source_wildcard_id"] == "wc_outfit"
|
|
assert meta["target_wildcard_id"] == "wc_pose"
|
|
assert meta["matrix"] == {"kimono": {"casual": {"mode": "exclude", "factor": 1.0}}}
|
|
assert meta["exceptions"] == []
|
|
|
|
|
|
def test_resolve_appends_to_existing_constraints():
|
|
ctx: dict = {"__wp_constraints__": [{"source_wildcard_id": "earlier"}]}
|
|
ConstraintHandler.resolve(_payload(), instance={}, ctx=ctx)
|
|
assert len(ctx["__wp_constraints__"]) == 2
|
|
assert ctx["__wp_constraints__"][0]["source_wildcard_id"] == "earlier"
|
|
assert ctx["__wp_constraints__"][1]["source_wildcard_id"] == "wc_outfit"
|
|
|
|
|
|
def test_resolve_rejects_malformed_payload():
|
|
with pytest.raises(ValueError):
|
|
ConstraintHandler.resolve(
|
|
{"source_wildcard_id": "", "target_wildcard_id": "x", "matrix": {}},
|
|
instance={},
|
|
ctx={},
|
|
)
|
|
|
|
|
|
def test_resolve_via_dispatcher_after_import():
|
|
from engine.modules import resolve_module
|
|
ctx: dict = {}
|
|
snap = {"type": "constraint", "payload": _payload(), "instance": {}}
|
|
out = resolve_module(snap, ctx=ctx)
|
|
assert out == {}
|
|
assert len(ctx["__wp_constraints__"]) == 1
|
|
|
|
|
|
def test_constraint_bucket_does_not_leak_to_public_socket():
|
|
"""`strip_internals` must drop the `__wp_constraints__` bucket so the
|
|
assembler / downstream user-facing payload never sees it as a
|
|
spurious `_constraints` variable. Regression: pre-rename the bucket
|
|
used a single-underscore key that bypassed the strip filter, so
|
|
autocomplete in the assembler showed `$_constraints` as a pickable
|
|
var even though no module had bound it."""
|
|
from engine.context import strip_internals
|
|
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(_payload(), instance={}, ctx=ctx)
|
|
# Internal bucket lives in ctx during the run.
|
|
assert "__wp_constraints__" in ctx
|
|
# Public socket strip drops it — no `_constraints`, no
|
|
# `__wp_constraints__`, nothing constraint-flavoured surfaces.
|
|
public = strip_internals(ctx)
|
|
assert "_constraints" not in public
|
|
assert "__wp_constraints__" not in public
|
|
|
|
|
|
def _instance(**kwargs):
|
|
"""Fresh instance dict with explicit overrides."""
|
|
base = {
|
|
"disabled_rule_ids": None,
|
|
"disabled_exception_keys": None,
|
|
"disabled_matrix_cells": None,
|
|
}
|
|
base.update(kwargs)
|
|
return base
|
|
|
|
|
|
def test_constraint_resolve_disabled_matrix_cells_excluded_from_ctx():
|
|
payload = {
|
|
"source_wildcard_id": "src-uuid",
|
|
"target_wildcard_id": "tgt-uuid",
|
|
"matrix": {
|
|
"s1": {"t1": {"mode": "allow", "factor": 1}, "t2": {"mode": "exclude", "factor": 0}},
|
|
},
|
|
"exceptions": [],
|
|
}
|
|
instance = _instance(disabled_matrix_cells=[json.dumps(["s1", "t1"], separators=(",", ":"))])
|
|
ctx: dict = {"__wp_constraints__": []}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
matrix = ctx["__wp_constraints__"][0]["matrix"]
|
|
assert "t1" not in matrix.get("s1", {})
|
|
assert "t2" in matrix.get("s1", {})
|
|
|
|
|
|
def test_constraint_resolve_disabled_exception_keys_excluded():
|
|
payload = {
|
|
"source_wildcard_id": "src-uuid",
|
|
"target_wildcard_id": "tgt-uuid",
|
|
"matrix": {},
|
|
"exceptions": [
|
|
{"source_value": "red", "target_value": "blue", "mode": "allow", "factor": 1},
|
|
{"source_value": "x", "target_value": "y", "mode": "allow", "factor": 1},
|
|
],
|
|
}
|
|
instance = _instance(
|
|
disabled_exception_keys=[json.dumps(["red", "blue"], separators=(",", ":"))]
|
|
)
|
|
ctx: dict = {"__wp_constraints__": []}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
excs = ctx["__wp_constraints__"][0]["exceptions"]
|
|
assert len(excs) == 1
|
|
assert excs[0]["source_value"] == "x"
|
|
|
|
|
|
def test_constraint_resolve_empty_disable_lists_unchanged_output():
|
|
payload = {
|
|
"source_wildcard_id": "src",
|
|
"target_wildcard_id": "tgt",
|
|
"matrix": {"s": {"t": {"mode": "allow", "factor": 1}}},
|
|
"exceptions": [{"source_value": "a", "target_value": "b", "mode": "allow", "factor": 1}],
|
|
}
|
|
ctx: dict = {"__wp_constraints__": []}
|
|
ConstraintHandler.resolve(payload, _instance(), ctx)
|
|
meta = ctx["__wp_constraints__"][0]
|
|
assert meta["matrix"] == payload["matrix"]
|
|
assert meta["exceptions"] == payload["exceptions"]
|
|
|
|
|
|
# ── Tier-D override fields (2026-05-10 modal expansion) ─────────────
|
|
|
|
|
|
def _payload_with_two_cells():
|
|
return {
|
|
"source_wildcard_id": "wc_outfit",
|
|
"target_wildcard_id": "wc_pose",
|
|
"matrix": {
|
|
"kimono": {
|
|
"casual": {"mode": "boost", "factor": 2.0},
|
|
"formal": {"mode": "allow", "factor": 1.0},
|
|
},
|
|
},
|
|
"exceptions": [
|
|
{"source_value": "red", "target_value": "black", "mode": "exclude", "factor": 1.0},
|
|
],
|
|
}
|
|
|
|
|
|
def test_resolve_applies_cell_mode_override():
|
|
payload = _payload_with_two_cells()
|
|
instance = {"cell_mode_overrides": {'["kimono","casual"]': "exclude"}}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
bucket = ctx["__wp_constraints__"]
|
|
assert bucket[0]["matrix"]["kimono"]["casual"]["mode"] == "exclude"
|
|
# Factor untouched.
|
|
assert bucket[0]["matrix"]["kimono"]["casual"]["factor"] == 2.0
|
|
|
|
|
|
def test_resolve_applies_cell_factor_override():
|
|
payload = _payload_with_two_cells()
|
|
instance = {"cell_factor_overrides": {'["kimono","casual"]': 5.0}}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
bucket = ctx["__wp_constraints__"]
|
|
assert bucket[0]["matrix"]["kimono"]["casual"]["factor"] == 5.0
|
|
assert bucket[0]["matrix"]["kimono"]["casual"]["mode"] == "boost"
|
|
|
|
|
|
def test_resolve_cell_overrides_skipped_when_cell_disabled():
|
|
payload = _payload_with_two_cells()
|
|
instance = {
|
|
"disabled_matrix_cells": ['["kimono","casual"]'],
|
|
"cell_mode_overrides": {'["kimono","casual"]': "exclude"},
|
|
}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
bucket = ctx["__wp_constraints__"]
|
|
assert "casual" not in bucket[0]["matrix"]["kimono"]
|
|
|
|
|
|
def test_resolve_cell_override_for_unknown_key_creates_new_matrix_entry():
|
|
"""Phase B: per-instance authoring — overrides for keys absent from
|
|
the library matrix populate a new entry. Implicit baseline is mode
|
|
"allow" + factor 1.0; the override fills whichever side(s) the user
|
|
set. Keeps original library cells untouched."""
|
|
payload = _payload_with_two_cells()
|
|
instance = {"cell_mode_overrides": {'["ghost","spectre"]': "exclude"}}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
bucket = ctx["__wp_constraints__"]
|
|
matrix = bucket[0]["matrix"]
|
|
# New entry created from override; default factor 1.0.
|
|
assert matrix["ghost"]["spectre"]["mode"] == "exclude"
|
|
assert matrix["ghost"]["spectre"]["factor"] == 1.0
|
|
# Original cells unchanged.
|
|
assert matrix["kimono"]["casual"]["mode"] == "boost"
|
|
|
|
|
|
def test_resolve_factor_override_alone_for_empty_cell_uses_allow_default():
|
|
"""Factor-only override on an empty cell defaults mode to 'allow'."""
|
|
payload = _payload_with_two_cells()
|
|
instance = {"cell_factor_overrides": {'["ghost","spectre"]': 2.5}}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
matrix = ctx["__wp_constraints__"][0]["matrix"]
|
|
assert matrix["ghost"]["spectre"]["mode"] == "allow"
|
|
assert matrix["ghost"]["spectre"]["factor"] == 2.5
|
|
|
|
|
|
def test_resolve_disabled_set_skips_empty_cell_overrides():
|
|
"""Disabled set wins over instance-only overrides — the cell is
|
|
omitted from the resolved matrix entirely."""
|
|
payload = _payload_with_two_cells()
|
|
instance = {
|
|
"disabled_matrix_cells": ['["ghost","spectre"]'],
|
|
"cell_mode_overrides": {'["ghost","spectre"]': "exclude"},
|
|
}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
matrix = ctx["__wp_constraints__"][0]["matrix"]
|
|
assert "ghost" not in matrix
|
|
|
|
|
|
def test_resolve_malformed_key_in_cell_overrides_skipped_silently():
|
|
"""Defensive guard: bogus key (not a 2-string-array JSON) is
|
|
skipped, not raised. Library cells continue resolving."""
|
|
payload = _payload_with_two_cells()
|
|
instance = {"cell_mode_overrides": {"not-a-json-key": "exclude"}}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
matrix = ctx["__wp_constraints__"][0]["matrix"]
|
|
assert matrix["kimono"]["casual"]["mode"] == "boost"
|
|
|
|
|
|
def test_resolve_applies_exception_overrides():
|
|
payload = _payload_with_two_cells()
|
|
instance = {
|
|
"exception_mode_overrides": {'["red","black"]': "boost"},
|
|
"exception_factor_overrides": {'["red","black"]': 3.0},
|
|
}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
exc = ctx["__wp_constraints__"][0]["exceptions"][0]
|
|
assert exc["mode"] == "boost"
|
|
assert exc["factor"] == 3.0
|
|
|
|
|
|
def test_resolve_appends_extra_exceptions():
|
|
payload = _payload_with_two_cells()
|
|
instance = {
|
|
"extra_exceptions": [
|
|
{"source_value": "blue", "target_value": "green", "mode": "boost", "factor": 2.5},
|
|
],
|
|
}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
exceptions = ctx["__wp_constraints__"][0]["exceptions"]
|
|
assert len(exceptions) == 2
|
|
extra = exceptions[-1]
|
|
assert extra["source_value"] == "blue"
|
|
assert extra["mode"] == "boost"
|
|
|
|
|
|
def test_resolve_extra_exception_with_invalid_mode_rejected():
|
|
payload = _payload_with_two_cells()
|
|
instance = {
|
|
"extra_exceptions": [
|
|
{"source_value": "blue", "target_value": "green", "mode": "weird", "factor": 1.0},
|
|
],
|
|
}
|
|
with pytest.raises(ValueError, match="mode"):
|
|
ConstraintHandler.resolve(payload, instance, {})
|
|
|
|
|
|
def test_resolve_extra_exception_with_negative_factor_rejected():
|
|
payload = _payload_with_two_cells()
|
|
instance = {
|
|
"extra_exceptions": [
|
|
{"source_value": "blue", "target_value": "green", "mode": "boost", "factor": -1.0},
|
|
],
|
|
}
|
|
with pytest.raises(ValueError, match="factor"):
|
|
ConstraintHandler.resolve(payload, instance, {})
|
|
|
|
|
|
def test_resolve_cell_factor_zero_accepted():
|
|
payload = _payload_with_two_cells()
|
|
instance = {"cell_factor_overrides": {'["kimono","casual"]': 0.0}}
|
|
ctx: dict = {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
assert ctx["__wp_constraints__"][0]["matrix"]["kimono"]["casual"]["factor"] == 0.0
|
|
|
|
|
|
# ── SP3: target_select reach selector (shape + plumbing only) ───────
|
|
#
|
|
# {mode: "first"|"next"|"all"|"pick", count?, picks?}, default {mode:"all"}.
|
|
# These cover validation + registration into ctx metadata; reach
|
|
# *behaviour* lands in a later task and is NOT exercised here.
|
|
|
|
|
|
def _ts_payload(**extra):
|
|
"""Minimal valid constraint payload, optionally carrying target_select."""
|
|
base = {
|
|
"source_wildcard_id": "aaaaaaaa",
|
|
"target_wildcard_id": "bbbbbbbb",
|
|
"matrix": {},
|
|
"exceptions": [],
|
|
}
|
|
base.update(extra)
|
|
return base
|
|
|
|
|
|
def test_target_select_defaults_all_when_absent():
|
|
# Absent target_select validates fine (defaults applied at resolve).
|
|
ConstraintHandler.validate_payload(_ts_payload()) # no raise
|
|
|
|
|
|
def test_target_select_none_is_allowed():
|
|
ConstraintHandler.validate_payload(_ts_payload(target_select=None)) # no raise
|
|
|
|
|
|
def test_target_select_explicit_all_accepted():
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "all"})
|
|
) # no raise
|
|
|
|
|
|
def test_target_select_rejects_non_object():
|
|
with pytest.raises(ValueError, match="target_select"):
|
|
ConstraintHandler.validate_payload(_ts_payload(target_select="all"))
|
|
|
|
|
|
def test_target_select_rejects_bad_mode():
|
|
with pytest.raises(ValueError, match="target_select.mode"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "sometimes"})
|
|
)
|
|
|
|
|
|
def test_target_select_next_requires_positive_count():
|
|
with pytest.raises(ValueError, match="count"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "next", "count": 0})
|
|
)
|
|
|
|
|
|
def test_target_select_next_rejects_bool_count():
|
|
# bool is an int subclass; reject it explicitly.
|
|
with pytest.raises(ValueError, match="count"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "next", "count": True})
|
|
)
|
|
|
|
|
|
def test_target_select_next_accepts_positive_count():
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "next", "count": 3})
|
|
) # no raise
|
|
|
|
|
|
def test_target_select_pick_requires_list():
|
|
with pytest.raises(ValueError, match="picks"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "pick", "picks": "nope"})
|
|
)
|
|
|
|
|
|
def test_target_select_pick_rejects_bad_kind():
|
|
with pytest.raises(ValueError, match=r"picks\[0\].kind"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "pick", "picks": [{"kind": "weird"}]})
|
|
)
|
|
|
|
|
|
def test_target_select_pick_direct_needs_string_uid():
|
|
with pytest.raises(ValueError, match=r"picks\[0\].uid"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(target_select={"mode": "pick", "picks": [{"kind": "direct"}]})
|
|
)
|
|
|
|
|
|
def test_target_select_pick_nested_needs_carrier_and_option():
|
|
with pytest.raises(ValueError, match=r"picks\[0\]"):
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(
|
|
target_select={"mode": "pick", "picks": [{"kind": "nested"}]}
|
|
)
|
|
)
|
|
|
|
|
|
def test_target_select_pick_accepts_valid_entries():
|
|
ConstraintHandler.validate_payload(
|
|
_ts_payload(
|
|
target_select={
|
|
"mode": "pick",
|
|
"picks": [
|
|
{"kind": "direct", "uid": "u1"},
|
|
{"kind": "nested", "carrier_uid": "c1", "option_id": "o1"},
|
|
],
|
|
}
|
|
)
|
|
) # no raise
|
|
|
|
|
|
def test_resolve_registers_target_select_in_meta():
|
|
ctx = {"__wp_current_module_uid__": "u1", "__wp_current_module_id__": "lib1"}
|
|
ConstraintHandler.resolve(
|
|
_ts_payload(target_select={"mode": "next", "count": 2}), {}, ctx
|
|
)
|
|
assert ctx["__wp_constraints__"][-1]["target_select"] == {"mode": "next", "count": 2}
|
|
|
|
|
|
def test_resolve_defaults_target_select_to_all_when_absent():
|
|
ctx = {"__wp_current_module_uid__": "u1", "__wp_current_module_id__": "lib1"}
|
|
ConstraintHandler.resolve(_ts_payload(), {}, ctx)
|
|
assert ctx["__wp_constraints__"][-1]["target_select"] == {"mode": "all"}
|
|
|
|
|
|
def test_resolve_instance_target_select_overrides_payload():
|
|
ctx = {"__wp_current_module_uid__": "u1", "__wp_current_module_id__": "lib1"}
|
|
ConstraintHandler.resolve(
|
|
_ts_payload(target_select={"mode": "all"}),
|
|
{"target_select": {"mode": "first"}},
|
|
ctx,
|
|
)
|
|
assert ctx["__wp_constraints__"][-1]["target_select"] == {"mode": "first"}
|
|
|
|
|
|
# ── SP3: per-instance reach override beats the library default for EVERY
|
|
# mode (first / next / all / pick). The canvas modal writes the placement's
|
|
# reach to `instance.target_select`; the engine MUST apply that, not the
|
|
# published library `payload.target_select`. `apply_constraints_for_target`
|
|
# reads `meta["target_select"]`, so these lock the value the apply pass sees.
|
|
|
|
|
|
def _recorded_target_select(payload_ts, instance_ts):
|
|
"""Resolve with the given library (payload) + per-instance reach and
|
|
return the `target_select` recorded into ctx — exactly what
|
|
`apply_constraints_for_target` reads to decide coverage."""
|
|
ctx: dict = {"__wp_current_module_uid__": "u1", "__wp_current_module_id__": "lib1"}
|
|
payload = _ts_payload(
|
|
**({"target_select": payload_ts} if payload_ts is not None else {})
|
|
)
|
|
instance = {"target_select": instance_ts} if instance_ts is not None else {}
|
|
ConstraintHandler.resolve(payload, instance, ctx)
|
|
return ctx["__wp_constraints__"][-1]["target_select"]
|
|
|
|
|
|
def test_instance_reach_next_overrides_library():
|
|
# Library default `all`; instance overrides to `next 3` → engine uses next 3.
|
|
assert _recorded_target_select(
|
|
{"mode": "all"}, {"mode": "next", "count": 3}
|
|
) == {"mode": "next", "count": 3}
|
|
|
|
|
|
def test_instance_reach_pick_overrides_library():
|
|
# Library default `first`; instance overrides to an explicit pick list.
|
|
picks = [{"kind": "direct", "uid": "abc123def456"}]
|
|
assert _recorded_target_select(
|
|
{"mode": "first"}, {"mode": "pick", "picks": picks}
|
|
) == {"mode": "pick", "picks": picks}
|
|
|
|
|
|
def test_instance_reach_explicit_all_overrides_nonall_library():
|
|
# The critical "all" case: library default `first`, instance explicitly
|
|
# widens to `all`. The engine honors the PRESENT instance value (its
|
|
# `instance or payload or default` chain), so an explicit `{mode:"all"}`
|
|
# override wins over a non-all library default. (The canvas modal must
|
|
# actually SEND `{mode:"all"}` here rather than collapsing it to null —
|
|
# see ConstraintInstanceModal.onTargetSelect.)
|
|
assert _recorded_target_select(
|
|
{"mode": "first"}, {"mode": "all"}
|
|
) == {"mode": "all"}
|
|
|
|
|
|
def test_library_reach_used_when_instance_absent():
|
|
# No per-instance override → fall back to the published library value.
|
|
assert _recorded_target_select(
|
|
{"mode": "next", "count": 2}, None
|
|
) == {"mode": "next", "count": 2}
|
|
|
|
|
|
# ── Cached source/target ref NAMES (display-only, additive) ─────────
|
|
#
|
|
# The SPA + canvas constraint editors stamp the source/target wildcard
|
|
# display name onto the payload (`source_wildcard_name` /
|
|
# `target_wildcard_name`) so the broken-reference banner can show
|
|
# "Source wildcard 'Starter subject' (54693e08)" after the wildcard is
|
|
# deleted. The engine NEVER reads these names — they're additive,
|
|
# non-load-bearing display state (like `producer_engine_version`). These
|
|
# lock that `validate_payload` tolerates the extra keys (it checks only
|
|
# specific fields, with no extra-key rejection); if this ever starts
|
|
# raising, the additive assumption is broken.
|
|
|
|
|
|
def test_validate_payload_tolerates_cached_ref_names():
|
|
payload = _payload()
|
|
payload["source_wildcard_name"] = "Starter subject"
|
|
payload["target_wildcard_name"] = "Outfit"
|
|
ConstraintHandler.validate_payload(payload) # must not raise
|
|
|
|
|
|
def test_resolve_tolerates_cached_ref_names():
|
|
"""resolve() runs validate_payload first, then records meta — the
|
|
extra display-name keys must pass straight through without error."""
|
|
payload = _payload()
|
|
payload["source_wildcard_name"] = "Starter subject"
|
|
payload["target_wildcard_name"] = "Outfit"
|
|
ctx: dict = {}
|
|
out = ConstraintHandler.resolve(payload, instance={}, ctx=ctx)
|
|
assert out == {}
|
|
assert len(ctx["__wp_constraints__"]) == 1
|