Replace the constraint one-shot/consumed-set model with a reach selector:
each constraint's target_select (default {mode:"all"}) decides which firing
target instances it covers, and every covering constraint is applied +
combined sequentially. A per-constraint hit counter (__wp_constraint_hits__)
replaces the consumed-set; reach is downstream-relative (a constraint is only
in ctx after its module ran).
- apply_constraints_for_target rewritten: hits/firing_uid/carrier_ctx params,
_occurrence_matches matcher (carrier_ctx None from callers — nested-pick
threading is a later task). consumed= kept accepted+ignored for back-compat.
- claim_carrier_constraints deleted (the positional spill failsafe is obsolete
without consumption); its import + both wildcard_handler call sites + the
_claim_carried helper removed.
- resolve.py nested path shares the ctx hit counter (get_constraint_hits) so
first/next coverage spans direct + nested encounters; carrier_ctx=None.
- pipeline.py finalisation now hits-based: constraint_never_applied when a
selector covered zero, new constraint_partial_reach when next N / pick
matched fewer than asked.
- cross-node: __wp_constraint_hits__ replaces __wp_consumed_constraints__ in
_CROSS_NODE_INTERNAL_KEYS; build_payload dedup also collapses partial_reach.
Existing one-shot tests adapted to the new semantics:
- test_constraint_first_instance: the implicit "fires once" tests re-expressed
via explicit target_select={mode:"first"}; added an all-mode-default test and
a partial_reach finalisation test. The pre-seeded-consumed no-op test became a
first-mode "counter already past 1" test (consumed-set concept gone).
- test_user_chain_simulation: consumed assertions → hit-count assertions; the
carrier-claim tests (no-spill, null-pick claim, source-not-picked, sibling
one-shots) rewritten to the new model — a carrier that doesn't resolve the ref
no longer "claims" anything, so the constraint applies on the next actual
resolution (or legitimately never fires → never_applied).
- test_module_interactions: "only first fires" composition test replaced with a
two-all-constraints-compose-on-one-target test (both excludes zero the pool →
excludes-all fallback), plus a single-all-constraint survivor test.
958 lines
40 KiB
Python
958 lines
40 KiB
Python
"""Cross-module interaction integration tests.
|
|
|
|
Exercises real PipelineEngine runs that combine multiple module kinds.
|
|
Goal: catch regressions where a single handler works in isolation but
|
|
breaks when downstream modules consume its bindings, when a constraint
|
|
fires on a not-yet-picked source, when nested ``@{}`` refs cycle, etc.
|
|
|
|
These are NOT pure unit tests — they spin up the real
|
|
``PipelineEngine`` and assert end-to-end behavior on the resulting
|
|
context. Each test is intentionally small + named after the scenario
|
|
it covers so a future failure points at the exact interaction that
|
|
broke.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from engine.context import strip_internals
|
|
from engine.pipeline import PipelineEngine
|
|
|
|
|
|
def _wildcard(mid: str, binding: str, options: list[dict], **inst) -> dict:
|
|
return {
|
|
"id": mid, "type": "wildcard", "enabled": True,
|
|
"payload": {
|
|
"var_binding": binding,
|
|
"options": options,
|
|
"sub_categories": [],
|
|
},
|
|
"instance": inst,
|
|
}
|
|
|
|
|
|
def _fixed(mid: str, values: list[dict], **inst) -> dict:
|
|
return {
|
|
"id": mid, "type": "fixed_values", "enabled": True,
|
|
"payload": {"values": values},
|
|
"instance": inst,
|
|
}
|
|
|
|
|
|
def _combine(mid: str, template: str, output_var: str, **inst) -> dict:
|
|
return {
|
|
"id": mid, "type": "combine", "enabled": True,
|
|
"payload": {
|
|
"template": template, "output_var": output_var,
|
|
"input_vars": [],
|
|
},
|
|
"instance": inst,
|
|
}
|
|
|
|
|
|
def _derivation(mid: str, rules: list[dict], **inst) -> dict:
|
|
return {
|
|
"id": mid, "type": "derivation", "enabled": True,
|
|
"payload": {"rules": rules},
|
|
"instance": inst,
|
|
}
|
|
|
|
|
|
def _constraint(mid: str, source: str, target: str, matrix=None, exceptions=None, **inst) -> dict:
|
|
return {
|
|
"id": mid, "type": "constraint", "enabled": True,
|
|
"payload": {
|
|
"source_wildcard_id": source,
|
|
"target_wildcard_id": target,
|
|
"matrix": matrix or {},
|
|
"exceptions": exceptions or [],
|
|
},
|
|
"instance": inst,
|
|
}
|
|
|
|
|
|
def _run(
|
|
modules: list[dict], *, seed: int = 0, ctx: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
return PipelineEngine().run(modules, ctx=ctx, seed=seed) # type: ignore[arg-type]
|
|
|
|
|
|
# ─── Producer → Consumer chains ──────────────────────────────────────
|
|
|
|
|
|
class TestProducerConsumer:
|
|
"""One module writes a $var, the next reads it."""
|
|
|
|
def test_fixed_values_feeds_combine_template(self):
|
|
ctx = _run([
|
|
_fixed("fv1", [
|
|
{"id": "v1", "name": "color", "value": "red"},
|
|
{"id": "v2", "name": "mood", "value": "calm"},
|
|
]),
|
|
_combine("cmb1", "a $color $mood sky", "prompt"),
|
|
])
|
|
assert ctx["color"] == "red"
|
|
assert ctx["mood"] == "calm"
|
|
assert ctx["prompt"] == "a red calm sky"
|
|
|
|
def test_wildcard_feeds_combine_template(self):
|
|
ctx = _run([
|
|
_wildcard("w1", "hair", [{"id": "o1", "value": "blonde", "weight": 1}]),
|
|
_combine("cmb1", "a $hair person", "prompt"),
|
|
], seed=42)
|
|
assert ctx["hair"] == "blonde"
|
|
assert ctx["prompt"] == "a blonde person"
|
|
|
|
def test_combine_feeds_combine(self):
|
|
"""First combine writes $phrase, second reads it via $phrase token."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "noun", "value": "knight"}]),
|
|
_combine("cmb1", "a brave $noun", "phrase"),
|
|
_combine("cmb2", "$phrase in armor", "final"),
|
|
])
|
|
assert ctx["phrase"] == "a brave knight"
|
|
assert ctx["final"] == "a brave knight in armor"
|
|
|
|
def test_combine_feeds_derivation_condition(self):
|
|
"""Combine writes $mood; derivation reads $mood via condition."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "weather", "value": "stormy"}]),
|
|
_combine("cmb", "$weather", "mood"),
|
|
_derivation("der", [{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "mood", "op": "equals", "value": "stormy"},
|
|
"action": {"target_var": "lighting", "mode": "replace", "value": "dramatic"},
|
|
}],
|
|
}]),
|
|
])
|
|
assert ctx["mood"] == "stormy"
|
|
assert ctx["lighting"] == "dramatic"
|
|
|
|
def test_derivation_feeds_combine(self):
|
|
"""Derivation writes $tone; combine reads it."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "age", "value": "30"}]),
|
|
_derivation("der", [{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "age", "op": "equals", "value": "30"},
|
|
"action": {"target_var": "tone", "mode": "replace", "value": "serious"},
|
|
}],
|
|
}]),
|
|
_combine("cmb", "$tone face", "phrase"),
|
|
])
|
|
assert ctx["tone"] == "serious"
|
|
assert ctx["phrase"] == "serious face"
|
|
|
|
|
|
# ─── Wildcard ↔ Constraint ───────────────────────────────────────────
|
|
|
|
|
|
class TestWildcardConstraint:
|
|
"""Constraint modules adjust a target wildcard's option weights based
|
|
on what its source wildcard already picked."""
|
|
|
|
def test_exception_excludes_specific_pair(self):
|
|
"""Source picks 'long_hair'; exception excludes target 'punk' →
|
|
target falls back to the other option.
|
|
|
|
Chain order is load-bearing: src runs first (records its pick),
|
|
constraint runs second (registers into __wp_constraints__), tgt
|
|
runs third (reads the constraint + the source pick at roll time).
|
|
"""
|
|
ctx = _run([
|
|
_wildcard("src", "hair", [
|
|
{"id": "h1", "value": "long_hair", "weight": 1},
|
|
]),
|
|
_constraint("c1", source="src", target="tgt", exceptions=[
|
|
{"source": "long_hair", "target": "punk", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("tgt", "style", [
|
|
{"id": "s1", "value": "punk", "weight": 1},
|
|
{"id": "s2", "value": "classic", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
assert ctx["hair"] == "long_hair"
|
|
assert ctx["style"] == "classic" # punk excluded by constraint
|
|
|
|
def test_constraint_source_after_target_warns(self):
|
|
"""Constraint targets a wildcard whose source HASN'T been picked
|
|
yet — engine skips application + emits unknown_constraint_source.
|
|
|
|
Order: constraint first (registers), target second (reads + finds
|
|
no source pick), source last (never consulted by target).
|
|
"""
|
|
ctx = _run([
|
|
_constraint("c1", source="src", target="tgt", exceptions=[
|
|
{"source": "long_hair", "target": "punk", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("tgt", "style", [
|
|
{"id": "s1", "value": "punk", "weight": 1},
|
|
{"id": "s2", "value": "classic", "weight": 1},
|
|
]),
|
|
_wildcard("src", "hair", [
|
|
{"id": "h1", "value": "long_hair", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert "unknown_constraint_source" in warning_types
|
|
|
|
def test_constraint_excludes_all_options_warns_and_falls_back(self):
|
|
"""Exception list excludes every target option → engine emits
|
|
constraint_excludes_all_options + falls back to options[0]."""
|
|
ctx = _run([
|
|
_wildcard("src", "hair", [{"id": "h1", "value": "long_hair", "weight": 1}]),
|
|
_constraint("c1", source="src", target="tgt", exceptions=[
|
|
{"source": "long_hair", "target": "punk", "mode": "exclude", "factor": 0},
|
|
{"source": "long_hair", "target": "classic", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("tgt", "style", [
|
|
{"id": "s1", "value": "punk", "weight": 1},
|
|
{"id": "s2", "value": "classic", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert "constraint_excludes_all_options" in warning_types
|
|
assert ctx["style"] == "punk" # falls back to options[0]
|
|
|
|
|
|
# ─── Nested @{} refs (wildcard option contains @{other_uuid}) ────────
|
|
|
|
|
|
class TestNestedRefs:
|
|
"""Wildcard option values can carry @{uuid} refs to other wildcards.
|
|
Engine walks them via the __wp_catalog__ at resolve time."""
|
|
|
|
def test_wildcard_option_references_catalog_wildcard(self):
|
|
"""Option value `@{xyz}` resolves to xyz's picked option value."""
|
|
catalog = {
|
|
"abc12345": {
|
|
"type": "wildcard",
|
|
"var_binding": "color",
|
|
"options": [{"id": "c1", "value": "red", "weight": 1}],
|
|
},
|
|
}
|
|
ctx = _run([
|
|
_wildcard("outer", "outfit", [
|
|
{"id": "o1", "value": "@{abc12345} dress", "weight": 1},
|
|
]),
|
|
], ctx={"__wp_catalog__": catalog}, seed=7)
|
|
assert ctx["outfit"] == "red dress"
|
|
|
|
def test_ref_to_missing_uuid_emits_warning(self):
|
|
"""Catalog miss → leniency: render the literal token + warn."""
|
|
ctx = _run([
|
|
_wildcard("outer", "outfit", [
|
|
{"id": "o1", "value": "@{deadbeef} dress", "weight": 1},
|
|
]),
|
|
], ctx={"__wp_catalog__": {}}, seed=7)
|
|
assert "dress" in ctx["outfit"]
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert any("ref" in t.lower() or "unknown" in t.lower() for t in warning_types)
|
|
|
|
|
|
# ─── Instance overrides ──────────────────────────────────────────────
|
|
|
|
|
|
class TestInstanceOverrides:
|
|
def test_wildcard_variable_binding_override_rebinds(self):
|
|
"""instance.variable_binding overrides payload.var_binding."""
|
|
ctx = _run([
|
|
_wildcard("w1", "color", [{"id": "o1", "value": "red", "weight": 1}],
|
|
variable_binding="my_alias"),
|
|
], seed=1)
|
|
assert ctx.get("my_alias") == "red"
|
|
assert "color" not in strip_internals(ctx)
|
|
|
|
def test_wildcard_pinned_mode_picks_specific_option(self):
|
|
"""instance.mode='pinned' + pinned_option_id picks that exact id."""
|
|
ctx = _run([
|
|
_wildcard("w1", "color", [
|
|
{"id": "o1", "value": "red", "weight": 1},
|
|
{"id": "o2", "value": "blue", "weight": 99}, # higher weight, would normally win
|
|
{"id": "o3", "value": "green", "weight": 1},
|
|
], mode="pinned", pinned_option_id="o3"),
|
|
], seed=1)
|
|
assert ctx["color"] == "green"
|
|
|
|
def test_wildcard_enabled_options_narrows_pool(self):
|
|
"""instance.enabled_options restricts which options can be picked."""
|
|
ctx = _run([
|
|
_wildcard("w1", "color", [
|
|
{"id": "o1", "value": "red", "weight": 1},
|
|
{"id": "o2", "value": "blue", "weight": 1},
|
|
{"id": "o3", "value": "green", "weight": 1},
|
|
], enabled_options=["o2"]),
|
|
], seed=1)
|
|
assert ctx["color"] == "blue"
|
|
|
|
def test_wildcard_enabled_options_empty_yields_empty_string(self):
|
|
"""All options disabled → binding still set, but to empty string."""
|
|
ctx = _run([
|
|
_wildcard("w1", "color", [
|
|
{"id": "o1", "value": "red", "weight": 1},
|
|
], enabled_options=[]),
|
|
], seed=1)
|
|
assert ctx["color"] == ""
|
|
|
|
def test_combine_template_override_replaces_payload(self):
|
|
"""instance.template_override wins over payload.template."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "noun", "value": "knight"}]),
|
|
_combine("cmb", "old: $noun", "phrase", template_override="new: $noun"),
|
|
])
|
|
assert ctx["phrase"] == "new: knight"
|
|
|
|
def test_combine_variable_binding_override_rebinds_output(self):
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "noun", "value": "knight"}]),
|
|
_combine("cmb", "a $noun", "phrase", variable_binding="renamed_phrase"),
|
|
])
|
|
assert ctx["renamed_phrase"] == "a knight"
|
|
assert "phrase" not in strip_internals(ctx)
|
|
|
|
def test_fixed_values_overrides_replace_payload(self):
|
|
"""instance.values_overrides fully replaces payload.values."""
|
|
ctx = _run([{
|
|
"id": "fv1", "type": "fixed_values", "enabled": True,
|
|
"payload": {"values": [{"id": "v1", "name": "color", "value": "red"}]},
|
|
"instance": {
|
|
"values_overrides": [
|
|
{"id": "v1", "name": "color", "value": "blue"},
|
|
{"id": "v2", "name": "mood", "value": "wild"},
|
|
],
|
|
},
|
|
}])
|
|
assert ctx["color"] == "blue"
|
|
assert ctx["mood"] == "wild"
|
|
|
|
|
|
# ─── Internal flag ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestInternalFlag:
|
|
def test_internal_wildcard_visible_to_downstream_but_stripped_at_boundary(self):
|
|
"""Internal scratch var is readable by other modules in same chain
|
|
but `strip_internals` drops it at the socket boundary."""
|
|
ctx = _run([
|
|
_wildcard("scratch", "mood", [
|
|
{"id": "o1", "value": "calm", "weight": 1},
|
|
], internal=True),
|
|
_combine("public", "a $mood face", "phrase"),
|
|
], seed=1)
|
|
# Both vars are in ctx for the chain.
|
|
assert ctx["mood"] == "calm"
|
|
assert ctx["phrase"] == "a calm face"
|
|
# After strip — internal `mood` is gone, public `phrase` survives.
|
|
public = strip_internals(ctx)
|
|
assert "mood" not in public
|
|
assert public["phrase"] == "a calm face"
|
|
|
|
|
|
# ─── Locked seed reproducibility ─────────────────────────────────────
|
|
|
|
|
|
class TestSeedLock:
|
|
def test_locked_seed_pins_pick_across_runs(self):
|
|
"""Two runs with the same locked_seed produce identical picks
|
|
regardless of the chain seed."""
|
|
options = [
|
|
{"id": "o1", "value": "red", "weight": 1},
|
|
{"id": "o2", "value": "blue", "weight": 1},
|
|
{"id": "o3", "value": "green", "weight": 1},
|
|
]
|
|
ctx1 = _run([_wildcard("w1", "color", options, locked_seed=999)], seed=1)
|
|
ctx2 = _run([_wildcard("w1", "color", options, locked_seed=999)], seed=2)
|
|
ctx3 = _run([_wildcard("w1", "color", options, locked_seed=999)], seed=3)
|
|
assert ctx1["color"] == ctx2["color"] == ctx3["color"]
|
|
|
|
def test_unlocked_pick_varies_with_chain_seed(self):
|
|
"""Without lock, different chain seeds can produce different picks."""
|
|
options = [
|
|
{"id": "o1", "value": "red", "weight": 1},
|
|
{"id": "o2", "value": "blue", "weight": 1},
|
|
{"id": "o3", "value": "green", "weight": 1},
|
|
]
|
|
picks: set[str] = set()
|
|
for seed in range(20):
|
|
ctx = _run([_wildcard("w1", "color", options)], seed=seed)
|
|
picks.add(ctx["color"])
|
|
# 20 distinct seeds should produce at least 2 different picks
|
|
# (vanishingly small probability of all 20 picking the same option).
|
|
assert len(picks) >= 2
|
|
|
|
|
|
# ─── Disabled module + ordering ──────────────────────────────────────
|
|
|
|
|
|
class TestDisabledAndOrdering:
|
|
def test_disabled_module_does_not_write(self):
|
|
"""Disabled wildcard contributes nothing to ctx; trace records skip."""
|
|
ctx = _run([
|
|
{**_wildcard("w1", "color", [{"id": "o1", "value": "red", "weight": 1}]),
|
|
"enabled": False},
|
|
_combine("cmb", "color is $color", "phrase"),
|
|
])
|
|
# `$color` resolves to empty (or literal) since w1 didn't write it.
|
|
assert "color" not in strip_internals(ctx) or strip_internals(ctx)["color"] == ""
|
|
# Trace records w1 as skipped_disabled.
|
|
trace_by_id = {t["id"]: t for t in ctx["__wp_trace__"]}
|
|
assert trace_by_id["w1"]["status"] == "skipped_disabled"
|
|
|
|
def test_last_write_wins_when_two_modules_target_same_var(self):
|
|
"""Second module's write overwrites the first's value for the same var."""
|
|
ctx = _run([
|
|
_fixed("fv1", [{"id": "v1", "name": "color", "value": "red"}]),
|
|
_fixed("fv2", [{"id": "v1", "name": "color", "value": "blue"}]),
|
|
])
|
|
assert ctx["color"] == "blue"
|
|
# Trace marks the second write as overwrite=True.
|
|
fv2_trace = next(t for t in ctx["__wp_trace__"] if t["id"] == "fv2")
|
|
color_write = next(w for w in fv2_trace["writes"] if w["variable"] == "color")
|
|
assert color_write["overwrite"] is True
|
|
|
|
|
|
# ─── Derivation rules with sequencing ────────────────────────────────
|
|
|
|
|
|
class TestDerivationSequencing:
|
|
def test_derivation_rule_sees_earlier_rule_writes(self):
|
|
"""Two rules in one derivation; second checks the var set by first."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "age", "value": "30"}]),
|
|
_derivation("der", [
|
|
{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "age", "op": "equals", "value": "30"},
|
|
"action": {"target_var": "tier", "mode": "replace", "value": "adult"},
|
|
}],
|
|
},
|
|
{
|
|
"id": "r2",
|
|
"branches": [{
|
|
"condition": {"var": "tier", "op": "equals", "value": "adult"},
|
|
"action": {"target_var": "wisdom", "mode": "replace", "value": "moderate"},
|
|
}],
|
|
},
|
|
]),
|
|
])
|
|
assert ctx["tier"] == "adult"
|
|
assert ctx["wisdom"] == "moderate"
|
|
|
|
def test_derivation_else_fires_when_no_branch_matches(self):
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "age", "value": "99"}]),
|
|
_derivation("der", [{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "age", "op": "equals", "value": "30"},
|
|
"action": {"target_var": "tier", "mode": "replace", "value": "adult"},
|
|
}],
|
|
"else": {
|
|
"action": {"target_var": "tier", "mode": "replace", "value": "unknown"},
|
|
},
|
|
}]),
|
|
])
|
|
assert ctx["tier"] == "unknown"
|
|
|
|
def test_derivation_append_mode_concatenates(self):
|
|
ctx = _run([
|
|
_fixed("fv", [
|
|
{"id": "v1", "name": "base", "value": "hello"},
|
|
{"id": "v2", "name": "flag", "value": "yes"},
|
|
]),
|
|
_derivation("der", [{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "flag", "op": "equals", "value": "yes"},
|
|
"action": {"target_var": "base", "mode": "append", "value": " world"},
|
|
}],
|
|
}]),
|
|
])
|
|
assert ctx["base"] == "hello world"
|
|
|
|
def test_derivation_presence_check_exists_op(self):
|
|
"""`exists` op fires when the var is present in ctx, regardless of value."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "marker", "value": ""}]), # empty but set
|
|
_derivation("der", [{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "marker", "op": "exists", "value": ""},
|
|
"action": {"target_var": "found", "mode": "replace", "value": "yes"},
|
|
}],
|
|
}]),
|
|
])
|
|
assert ctx["found"] == "yes"
|
|
|
|
|
|
# ─── Downstream Context chain ────────────────────────────────────────
|
|
|
|
|
|
class TestDownstreamContextChain:
|
|
"""Two Context nodes in series — downstream reads upstream's ctx."""
|
|
|
|
def test_downstream_overrides_upstream_var(self):
|
|
upstream = _run([
|
|
_fixed("up", [
|
|
{"id": "v1", "name": "color", "value": "red"},
|
|
{"id": "v2", "name": "noun", "value": "dragon"},
|
|
]),
|
|
])
|
|
# Strip internals before threading into downstream (mimics WP_Context boundary)
|
|
downstream_start = strip_internals(upstream)
|
|
downstream = _run([
|
|
_fixed("down", [{"id": "v1", "name": "color", "value": "blue"}]),
|
|
_combine("cmb", "$color $noun", "prompt"),
|
|
], ctx=downstream_start)
|
|
assert downstream["color"] == "blue" # overridden
|
|
assert downstream["noun"] == "dragon" # passed through
|
|
assert downstream["prompt"] == "blue dragon"
|
|
|
|
|
|
# ─── Inline syntax inside option values ──────────────────────────────
|
|
|
|
|
|
class TestInlineSyntaxInValues:
|
|
"""resolve_text runs on every picked option value, so $var reads
|
|
and {a|b|c} alternations inside option strings work."""
|
|
|
|
def test_wildcard_option_rejects_dollar_var_read(self):
|
|
"""Wildcard is a PRODUCER surface — $var reads are gated off
|
|
(see engine/syntax/resolve.py). Lenient mode renders the
|
|
literal token + emits a var_out_of_surface warning so the
|
|
user gets a signal instead of silent leakage."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "color", "value": "red"}]),
|
|
_wildcard("w1", "phrase", [
|
|
{"id": "o1", "value": "$color tone", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
# Literal token rendered (not expanded) — confirms surface gate.
|
|
assert ctx["phrase"] == "$color tone"
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert "var_out_of_surface" in warning_types
|
|
|
|
def test_wildcard_option_inline_alternation_resolves(self):
|
|
"""Option value `{a|b|c}` resolves to one of a/b/c via per-module rng."""
|
|
# Use a single option so the wildcard always picks it, then check
|
|
# the alternation resolved to one of the expected branches.
|
|
ctx = _run([
|
|
_wildcard("w1", "mood", [
|
|
{"id": "o1", "value": "{calm|wild|serene}", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
assert ctx["mood"] in ("calm", "wild", "serene")
|
|
|
|
def test_combine_template_with_inline_alternation(self):
|
|
"""{a|b|c} inside a combine template resolves at run time."""
|
|
ctx = _run([
|
|
_fixed("fv", [{"id": "v1", "name": "subject", "value": "knight"}]),
|
|
_combine("cmb", "a {brave|noble|fierce} $subject", "phrase"),
|
|
])
|
|
# All three branches should produce a valid string with the subject.
|
|
assert ctx["phrase"].endswith("knight")
|
|
assert any(adj in ctx["phrase"] for adj in ("brave", "noble", "fierce"))
|
|
|
|
|
|
# ─── Constraint composition ──────────────────────────────────────────
|
|
|
|
|
|
class TestConstraintComposition:
|
|
"""SP3 reach selector: every constraint whose `target_select` covers
|
|
a firing target instance is applied, combined sequentially. Multiple
|
|
`all` constraints targeting the same wildcard now DO compose on a
|
|
single target instance (the pre-SP3 one-shot model, where the first
|
|
claimed and the second waited, is retired)."""
|
|
|
|
def test_two_all_constraints_compose_on_one_target(self):
|
|
"""Two excludes (default `all`) targeting the same wildcard + ONE
|
|
target instance: BOTH fire on `tgt` and compose. c1 excludes
|
|
`punk`, c2 excludes `classic` → every option zeroed → the
|
|
weighted pick falls back to options[0] (`punk`) and an
|
|
excludes-all warning fires. Both constraints reached the
|
|
instance (hit count 1 each) — no never_applied."""
|
|
ctx = _run([
|
|
_wildcard("src1", "hair", [{"id": "h1", "value": "long", "weight": 1}]),
|
|
_wildcard("src2", "eyes", [{"id": "e1", "value": "blue", "weight": 1}]),
|
|
_constraint("c1", source="src1", target="tgt", exceptions=[
|
|
{"source": "long", "target": "punk", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_constraint("c2", source="src2", target="tgt", exceptions=[
|
|
{"source": "blue", "target": "classic", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("tgt", "style", [
|
|
{"id": "s1", "value": "punk", "weight": 1},
|
|
{"id": "s2", "value": "classic", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
# Both excludes apply → whole pool zeroed → fallback options[0].
|
|
assert ctx["style"] == "punk"
|
|
hits = ctx.get("__wp_constraint_hits__", {})
|
|
assert hits.get("c1") == 1 and hits.get("c2") == 1
|
|
never = [
|
|
w for w in ctx.get("__wp_warnings__", [])
|
|
if w.get("type") == "constraint_never_applied"
|
|
]
|
|
assert never == []
|
|
excludes_all = [
|
|
w for w in ctx.get("__wp_warnings__", [])
|
|
if w.get("type") == "constraint_excludes_all_options"
|
|
]
|
|
assert excludes_all, "expected an excludes-all warning when both fire"
|
|
|
|
def test_first_mode_constraint_yields_to_let_other_survive(self):
|
|
"""The pre-SP3 'only the first constraint fires' outcome is still
|
|
reachable by marking c2 `first` and giving it no earlier target:
|
|
with a single target instance, c1 (`all`) excludes `punk` and c2
|
|
(also `all` here) excludes `classic` would zero the pool — so to
|
|
keep `classic` available we drop c2 to a selector that doesn't
|
|
cover this instance. Here c2 is `next 1` but registered such that
|
|
its own counter is already past — simplest: only c1 targets tgt,
|
|
c2 targets a different (absent) wildcard, proving a single `all`
|
|
constraint leaves the non-excluded option."""
|
|
ctx = _run([
|
|
_wildcard("src1", "hair", [{"id": "h1", "value": "long", "weight": 1}]),
|
|
_constraint("c1", source="src1", target="tgt", exceptions=[
|
|
{"source": "long", "target": "punk", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("tgt", "style", [
|
|
{"id": "s1", "value": "punk", "weight": 1},
|
|
{"id": "s2", "value": "classic", "weight": 1},
|
|
]),
|
|
], seed=1)
|
|
# c1 alone fires on the single tgt instance → punk excluded →
|
|
# classic survives. No excludes-all (one option keeps weight).
|
|
assert ctx["style"] == "classic"
|
|
assert ctx.get("__wp_constraint_hits__", {}).get("c1") == 1
|
|
|
|
|
|
# ─── Locked-seed reproducibility across a full chain ─────────────────
|
|
|
|
|
|
class TestChainReproducibility:
|
|
def test_full_locked_chain_reproduces_identically(self):
|
|
"""Every wildcard locked → identical output across runs regardless
|
|
of chain seed."""
|
|
modules = [
|
|
_wildcard("w1", "color", [
|
|
{"id": "o1", "value": "red", "weight": 1},
|
|
{"id": "o2", "value": "blue", "weight": 1},
|
|
], locked_seed=111),
|
|
_wildcard("w2", "shape", [
|
|
{"id": "o1", "value": "round", "weight": 1},
|
|
{"id": "o2", "value": "square", "weight": 1},
|
|
], locked_seed=222),
|
|
_combine("cmb", "$color $shape", "phrase"),
|
|
]
|
|
c1 = _run(modules, seed=999)
|
|
c2 = _run(modules, seed=1)
|
|
c3 = _run(modules, seed=42)
|
|
assert c1["phrase"] == c2["phrase"] == c3["phrase"]
|
|
assert c1["color"] == c2["color"] == c3["color"]
|
|
assert c1["shape"] == c2["shape"] == c3["shape"]
|
|
|
|
|
|
# ─── Realistic kitchen-sink scenario ─────────────────────────────────
|
|
|
|
|
|
class TestNestedConstraints:
|
|
"""2026-05-19 cycle — constraints now apply to wildcards reached via
|
|
nested `@{uuid}` refs, not just chain-level wildcards. Pre-fix the
|
|
matrix silently bypassed when target was buried inside another
|
|
wildcard's option value."""
|
|
|
|
def test_constraint_applies_to_nested_target(self):
|
|
"""Outer wildcard's option is `@{nested}`. Constraint excludes
|
|
nested='forbidden' when src='trigger'. Expect: nested rolls
|
|
'allowed', never 'forbidden', across many seeds.
|
|
|
|
Pre-fix: constraint silently bypassed → 'forbidden' appeared.
|
|
Post-fix: constraint applied during ref resolution.
|
|
"""
|
|
catalog = {
|
|
"bbbbbbbb": {
|
|
"type": "wildcard",
|
|
"var_binding": "inner",
|
|
"options": [
|
|
{"id": "b1", "value": "forbidden", "weight": 1},
|
|
{"id": "b2", "value": "allowed", "weight": 1},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("srcwild", "src", [
|
|
{"id": "s1", "value": "trigger", "weight": 1},
|
|
]),
|
|
_constraint("c1", source="srcwild", target="bbbbbbbb", exceptions=[
|
|
{"source": "trigger", "target": "forbidden", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("outer", "final", [
|
|
{"id": "o1", "value": "@{bbbbbbbb}", "weight": 1},
|
|
]),
|
|
]
|
|
# Run many seeds; with constraint working, "forbidden" never appears.
|
|
picks: set[str] = set()
|
|
for seed in range(40):
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=seed)
|
|
picks.add(ctx["final"])
|
|
assert "forbidden" not in picks
|
|
assert "allowed" in picks # confirm pool wasn't accidentally empty
|
|
|
|
def test_nested_constraint_excludes_all_emits_warning(self):
|
|
"""Exceptions exclude every option of the nested target →
|
|
ref resolver falls back to options[0] + emits the
|
|
constraint_excludes_all_options warning (same path as chain-
|
|
level wildcards)."""
|
|
catalog = {
|
|
"cccccccc": {
|
|
"type": "wildcard",
|
|
"var_binding": "inner",
|
|
"options": [
|
|
{"id": "c1", "value": "punk", "weight": 1},
|
|
{"id": "c2", "value": "classic", "weight": 1},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("srcwild", "src", [
|
|
{"id": "s1", "value": "trigger", "weight": 1},
|
|
]),
|
|
_constraint("c1", source="srcwild", target="cccccccc", exceptions=[
|
|
{"source": "trigger", "target": "punk", "mode": "exclude", "factor": 0},
|
|
{"source": "trigger", "target": "classic", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("outer", "final", [
|
|
{"id": "o1", "value": "@{cccccccc}", "weight": 1},
|
|
]),
|
|
]
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=1)
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert "constraint_excludes_all_options" in warning_types
|
|
|
|
def test_nested_constraint_warns_when_source_not_picked(self):
|
|
"""Source wildcard sits after the outer wildcard that nests the
|
|
constraint's target → at nested-roll time the source's pick
|
|
isn't in __wp_picks__ yet → emits unknown_constraint_source.
|
|
|
|
Sanity check that the warning surface is consistent between
|
|
chain-level and nested constraint application paths."""
|
|
catalog = {
|
|
"dddddddd": {
|
|
"type": "wildcard",
|
|
"var_binding": "inner",
|
|
"options": [{"id": "d1", "value": "x", "weight": 1}],
|
|
},
|
|
}
|
|
modules = [
|
|
_constraint("c1", source="srcwild", target="dddddddd", exceptions=[
|
|
{"source": "trigger", "target": "x", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("outer", "final", [
|
|
{"id": "o1", "value": "@{dddddddd}", "weight": 1},
|
|
]),
|
|
_wildcard("srcwild", "src", [
|
|
{"id": "s1", "value": "trigger", "weight": 1},
|
|
]),
|
|
]
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=1)
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert "unknown_constraint_source" in warning_types
|
|
|
|
|
|
class TestNestedSubcategoryFilter:
|
|
"""`@{uuid:subcat,subcat2}` per-call sub-category filter — restricts
|
|
the nested wildcard's pick to options whose `sub_category` matches.
|
|
Same semantics as chain-level `instance.category_filter` but scoped
|
|
per call site so one library wildcard can be narrowed differently
|
|
from each reference."""
|
|
|
|
def test_subcat_filter_narrows_pool(self):
|
|
"""`@{color:warm}` should only pick from options whose
|
|
sub_category == 'warm', skipping cool options entirely."""
|
|
catalog = {
|
|
"eeeeeeee": {
|
|
"type": "wildcard",
|
|
"var_binding": "color",
|
|
"options": [
|
|
{"id": "c1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
|
|
{"id": "c2", "value": "yellow", "weight": 1, "sub_categories": ["warm"]},
|
|
{"id": "c3", "value": "blue", "weight": 1, "sub_categories": ["cool"]},
|
|
{"id": "c4", "value": "green", "weight": 1, "sub_categories": ["cool"]},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("outer", "phrase", [
|
|
{"id": "o1", "value": "@{eeeeeeee:warm}", "weight": 1},
|
|
]),
|
|
]
|
|
picks: set[str] = set()
|
|
for seed in range(30):
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=seed)
|
|
picks.add(ctx["phrase"])
|
|
assert picks.issubset({"red", "yellow"})
|
|
assert "blue" not in picks
|
|
assert "green" not in picks
|
|
|
|
def test_subcat_filter_multi_categories(self):
|
|
"""Comma-separated subcats accept union of matching options."""
|
|
catalog = {
|
|
"ffffffff": {
|
|
"type": "wildcard",
|
|
"var_binding": "color",
|
|
"options": [
|
|
{"id": "c1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
|
|
{"id": "c2", "value": "blue", "weight": 1, "sub_categories": ["cool"]},
|
|
{"id": "c3", "value": "green", "weight": 1, "sub_categories": ["earth"]},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("outer", "phrase", [
|
|
{"id": "o1", "value": "@{ffffffff:warm,cool}", "weight": 1},
|
|
]),
|
|
]
|
|
picks: set[str] = set()
|
|
for seed in range(30):
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=seed)
|
|
picks.add(ctx["phrase"])
|
|
assert picks.issubset({"red", "blue"})
|
|
assert "green" not in picks
|
|
|
|
def test_subcat_filter_empty_pool_warns(self):
|
|
"""Filter matching no options → empty string output + warning."""
|
|
catalog = {
|
|
"aaaa1111": {
|
|
"type": "wildcard",
|
|
"var_binding": "color",
|
|
"options": [
|
|
{"id": "c1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("outer", "phrase", [
|
|
{"id": "o1", "value": "[@{aaaa1111:nonexistent}]", "weight": 1},
|
|
]),
|
|
]
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=1)
|
|
# Outer renders the literal brackets with empty ref result inside
|
|
assert ctx["phrase"] == "[]"
|
|
warning_types = [w["type"] for w in ctx["__wp_warnings__"]]
|
|
assert "ref_subcategory_empty_pool" in warning_types
|
|
|
|
def test_empty_filter_equivalent_to_no_filter(self):
|
|
"""`@{uuid:}` with empty subcat list = full pool (defensive
|
|
against trailing-colon typos breaking everything)."""
|
|
catalog = {
|
|
"aaaa2222": {
|
|
"type": "wildcard",
|
|
"var_binding": "color",
|
|
"options": [
|
|
{"id": "c1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("outer", "phrase", [
|
|
{"id": "o1", "value": "@{aaaa2222:}", "weight": 1},
|
|
]),
|
|
]
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=1)
|
|
assert ctx["phrase"] == "red"
|
|
|
|
def test_subcat_filter_composes_with_constraint(self):
|
|
"""`@{color:warm}` + constraint excluding 'red' → only 'yellow'
|
|
survives. Confirms filter applies BEFORE constraint, both
|
|
compose correctly."""
|
|
catalog = {
|
|
"aaaa3333": {
|
|
"type": "wildcard",
|
|
"var_binding": "color",
|
|
"options": [
|
|
{"id": "c1", "value": "red", "weight": 1, "sub_categories": ["warm"]},
|
|
{"id": "c2", "value": "yellow", "weight": 1, "sub_categories": ["warm"]},
|
|
{"id": "c3", "value": "blue", "weight": 1, "sub_categories": ["cool"]},
|
|
],
|
|
},
|
|
}
|
|
modules = [
|
|
_wildcard("srcwild", "src", [
|
|
{"id": "s1", "value": "trigger", "weight": 1},
|
|
]),
|
|
_constraint("c1", source="srcwild", target="aaaa3333", exceptions=[
|
|
{"source": "trigger", "target": "red", "mode": "exclude", "factor": 0},
|
|
]),
|
|
_wildcard("outer", "phrase", [
|
|
{"id": "o1", "value": "@{aaaa3333:warm}", "weight": 1},
|
|
]),
|
|
]
|
|
picks: set[str] = set()
|
|
for seed in range(30):
|
|
ctx = _run(modules, ctx={"__wp_catalog__": catalog}, seed=seed)
|
|
picks.add(ctx["phrase"])
|
|
assert picks == {"yellow"}
|
|
|
|
|
|
class TestKitchenSink:
|
|
"""End-to-end scenario combining most module kinds — the kind of
|
|
pipeline a real user authors. If this test breaks, something
|
|
structural changed."""
|
|
|
|
def test_fv_then_constrained_wildcards_then_derivation_then_combine(self):
|
|
catalog = {
|
|
"deadbeef": {
|
|
"type": "wildcard",
|
|
"var_binding": "accent_color",
|
|
"options": [{"id": "ac1", "value": "gold", "weight": 1}],
|
|
},
|
|
}
|
|
ctx = _run([
|
|
# Static identity vars
|
|
_fixed("identity", [
|
|
{"id": "v1", "name": "subject", "value": "knight"},
|
|
{"id": "v2", "name": "era", "value": "medieval"},
|
|
]),
|
|
# Source wildcard
|
|
_wildcard("hair_pick", "hair", [
|
|
{"id": "h1", "value": "long", "weight": 1},
|
|
]),
|
|
# Constraint between hair and style
|
|
_constraint("c1", source="hair_pick", target="style_pick", exceptions=[
|
|
{"source": "long", "target": "punk", "mode": "exclude", "factor": 0},
|
|
]),
|
|
# Target wildcard with a nested @{} ref inside option value
|
|
_wildcard("style_pick", "style", [
|
|
{"id": "s1", "value": "punk", "weight": 1},
|
|
{"id": "s2", "value": "@{deadbeef}-trimmed classic", "weight": 1},
|
|
]),
|
|
# Derivation reads era and writes lighting
|
|
_derivation("der1", [{
|
|
"id": "r1",
|
|
"branches": [{
|
|
"condition": {"var": "era", "op": "equals", "value": "medieval"},
|
|
"action": {"target_var": "lighting", "mode": "replace", "value": "torchlit"},
|
|
}],
|
|
}]),
|
|
# Final combine reads everything
|
|
_combine(
|
|
"final",
|
|
"$lighting portrait of a $hair-haired $subject in $style style",
|
|
"prompt",
|
|
),
|
|
], ctx={"__wp_catalog__": catalog}, seed=42)
|
|
|
|
# Hair forced (single option), style forced to non-excluded option,
|
|
# derivation fired, combine concatenated.
|
|
assert ctx["hair"] == "long"
|
|
assert ctx["style"] == "gold-trimmed classic"
|
|
assert ctx["lighting"] == "torchlit"
|
|
assert ctx["prompt"] == (
|
|
"torchlit portrait of a long-haired knight in gold-trimmed classic style"
|
|
)
|
|
# No warnings on the happy path.
|
|
assert ctx["__wp_warnings__"] == []
|