When the picked option carried no tag on a declared accepts axis, the run warned "$outfit has no 'SHOES' axis — is that tag group marked 'accepts'?", sending users to fix a group that was fine. Wildcards now record which accepts axes they declare (`__wp_axis_decl__`, carried across nodes and held seeds like `__wp_axes__`), so the resolver can tell the cases apart: an untagged pick becomes an info note, and a genuinely unknown axis names the axes that do exist. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BxeNnfTMTyaKQwCYWA6frg
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
"""`$outfit.SHOES` reads the tag rolled at pick time — it never re-rolls.
|
|
|
|
That is the whole consistency guarantee: two modules, or two chained nodes,
|
|
read the same stored value rather than each drawing their own.
|
|
"""
|
|
import random
|
|
|
|
from engine.modules import build_resolve_ctx
|
|
from engine.syntax import resolve_text
|
|
|
|
|
|
def _ctx(axes, decl=None):
|
|
c = {
|
|
"__wp_rng__": random.Random(0),
|
|
"__wp_warnings__": [],
|
|
"__wp_axes__": axes,
|
|
"outfit": "white t-shirt",
|
|
}
|
|
if decl is not None:
|
|
c["__wp_axis_decl__"] = decl
|
|
return c
|
|
|
|
|
|
def test_single_pick_axis_reads_the_rolled_tag():
|
|
rc = build_resolve_ctx(_ctx({"outfit": {"SHOES": "sandals"}}), surface="combine")
|
|
assert resolve_text("wearing $outfit.SHOES", rc) == "wearing sandals"
|
|
|
|
|
|
def test_multi_pick_bare_read_joins():
|
|
rc = build_resolve_ctx(
|
|
_ctx({"outfit": [{"SHOES": "sandals"}, {"SHOES": "sneakers"}]}),
|
|
surface="combine",
|
|
)
|
|
assert resolve_text("$outfit.SHOES", rc) == "sandals, sneakers"
|
|
|
|
|
|
def test_multi_pick_index_selects_one_in_either_order():
|
|
rc = build_resolve_ctx(
|
|
_ctx({"outfit": [{"SHOES": "sandals"}, {"SHOES": "sneakers"}]}),
|
|
surface="combine",
|
|
)
|
|
assert resolve_text("$outfit.1.SHOES", rc) == "sneakers"
|
|
assert resolve_text("$outfit.SHOES.1", rc) == "sneakers"
|
|
|
|
|
|
def test_multi_pick_index_keeps_its_slot_when_a_pick_has_no_axis_tag():
|
|
# Pick 0 ("robe") carries no SHOES tag. `.1` must still mean pick 1, the
|
|
# same pick `$outfit.1` names, not slide down onto pick 0's empty slot.
|
|
c = _ctx({"outfit": [{}, {"SHOES": "boots"}]})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.0.SHOES", rc) == ""
|
|
assert resolve_text("$outfit.1.SHOES", rc) == "boots"
|
|
assert resolve_text("$outfit.SHOES", rc) == "boots"
|
|
assert not any(w["type"] == "unknown_tag_axis" for w in c["__wp_warnings__"])
|
|
|
|
|
|
def test_multi_pick_index_out_of_range_is_empty():
|
|
rc = build_resolve_ctx(
|
|
_ctx({"outfit": [{"SHOES": "sandals"}]}), surface="combine",
|
|
)
|
|
assert resolve_text("$outfit.4.SHOES", rc) == ""
|
|
|
|
|
|
def test_single_pick_behaves_as_a_one_element_list():
|
|
# Mirrors deref_var_value's contract so the two accessors agree.
|
|
rc = build_resolve_ctx(_ctx({"outfit": {"SHOES": "sandals"}}), surface="combine")
|
|
assert resolve_text("$outfit.0.SHOES", rc) == "sandals"
|
|
assert resolve_text("$outfit.9.SHOES", rc) == ""
|
|
|
|
|
|
def test_reading_the_same_axis_twice_gives_one_answer():
|
|
# The point of rolling at pick time. A lazy roll would make these differ.
|
|
rc = build_resolve_ctx(_ctx({"outfit": {"SHOES": "sandals"}}), surface="combine")
|
|
assert resolve_text("$outfit.SHOES / $outfit.SHOES", rc) == "sandals / sandals"
|
|
|
|
|
|
def test_unknown_axis_renders_empty_and_warns():
|
|
c = _ctx({"outfit": {"SHOES": "sandals"}})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.BELTS", rc) == ""
|
|
assert any(w["type"] == "unknown_tag_axis" for w in c["__wp_warnings__"])
|
|
|
|
|
|
def test_axis_on_a_binding_that_rolled_nothing_warns():
|
|
c = _ctx({})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.SHOES", rc) == ""
|
|
assert any(w["type"] == "unknown_tag_axis" for w in c["__wp_warnings__"])
|
|
|
|
|
|
def test_declared_axis_the_pick_has_no_tag_on_is_an_info_note():
|
|
# The reported confusion: the pick ("robe") simply carries no SHOES tag,
|
|
# and the warning asked whether SHOES was marked accepts, which it was.
|
|
c = _ctx({"outfit": {}}, decl={"outfit": ["SHOES"]})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.SHOES", rc) == ""
|
|
[w] = c["__wp_warnings__"]
|
|
assert w["type"] == "axis_untagged_pick"
|
|
assert w["severity"] == "info"
|
|
assert "has no 'SHOES' tag" in w["message"]
|
|
assert "accepts" not in w["message"]
|
|
|
|
|
|
def test_multi_pick_with_no_tag_on_any_pick_is_an_info_note():
|
|
c = _ctx({"outfit": [{}, {}]}, decl={"outfit": ["SHOES"]})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.SHOES", rc) == ""
|
|
assert [w["type"] for w in c["__wp_warnings__"]] == ["axis_untagged_pick"]
|
|
|
|
|
|
def test_undeclared_axis_names_the_axes_that_do_exist():
|
|
c = _ctx({"outfit": {"SHOES": "sandals"}}, decl={"outfit": ["SHOES"]})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.BELTS", rc) == ""
|
|
[w] = c["__wp_warnings__"]
|
|
assert w["type"] == "unknown_tag_axis"
|
|
assert w["severity"] == "warn"
|
|
assert "SHOES" in w["message"]
|
|
|
|
|
|
def test_wildcard_with_no_accepts_groups_asks_about_accepts():
|
|
c = _ctx({"outfit": {}}, decl={"outfit": []})
|
|
rc = build_resolve_ctx(c, surface="combine")
|
|
assert resolve_text("$outfit.SHOES", rc) == ""
|
|
[w] = c["__wp_warnings__"]
|
|
assert w["type"] == "unknown_tag_axis"
|
|
assert "no accepts groups" in w["message"]
|
|
|
|
|
|
def test_plain_var_read_is_untouched():
|
|
rc = build_resolve_ctx(_ctx({"outfit": {"SHOES": "sandals"}}), surface="combine")
|
|
assert resolve_text("$outfit", rc) == "white t-shirt"
|