Files
code_of_conquest_dnd/tools/content_build/tests/test_resolve.py
Phillip Tarrant f84e55ab4b fix(content-build): --check orphan detection; canon secrecy-0 + value-type guards
- --check now detects orphaned/stale build artifacts in build-owned dirs
  (client canon/ + topics/ fully owned; client npcs/ shared with legacy
  hand-authored files, only npc.* ids are treated as build-owned)
- resolve() enforces the schema rule that canon entities must be secrecy 0
- resolve() and ladder_rungs() guard against malformed value TYPES (non-int
  secrecy, non-list related/rungs) raising BuildError instead of crashing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
2026-07-11 19:01:28 -05:00

155 lines
4.8 KiB
Python

import pytest
from content_build.errors import BuildError
from content_build.model import Entry, KnowsLink
from content_build.resolve import resolve
def ladder():
return Entry(id="rule.disposition-ladder", type="rule", status="canon",
secrecy=0, related=[], body="x", source="t:1",
rungs=["hostile", "cold", "neutral", "warm", "trusted"])
def town():
return Entry(id="town.a", type="town", status="canon", secrecy=0,
related=[], body="A town.", source="t:2")
def rumor():
return Entry(id="rumor.x", type="rumor", status="canon", secrecy=1,
related=["town.a"], body="A rumor.", source="t:3")
def npc(**over):
base = dict(id="npc.t", type="person", status="canon", secrecy=0,
related=["town.a"], body="Persona.", source="t:4",
start_disposition="cold",
knows=[KnowsLink("rumor.x", "neutral")])
base.update(over)
return Entry(**base)
def test_valid_world_resolves():
world = resolve([ladder(), town(), rumor(), npc()])
assert world.by_id["npc.t"].id == "npc.t"
def test_duplicate_id():
with pytest.raises(BuildError):
resolve([ladder(), town(), town()])
def test_illegal_namespace():
bad = Entry(id="shrine.s", type="place", status="canon", secrecy=0,
related=[], body="x", source="t:9")
with pytest.raises(BuildError):
resolve([ladder(), bad])
def test_id_type_mismatch():
bad = Entry(id="place.s", type="town", status="canon", secrecy=0,
related=[], body="x", source="t:9")
with pytest.raises(BuildError):
resolve([ladder(), bad])
def test_npc_type_must_be_person():
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(), npc(type="town")])
def test_dangling_related():
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(),
npc(related=["town.nowhere"])])
def test_person_record_with_knows_rejected():
rec = Entry(id="person.p", type="person", status="canon", secrecy=0,
related=[], body="x", source="t:9",
knows=[KnowsLink("rumor.x", "warm")])
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(), rec])
def test_npc_missing_start_disposition():
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(), npc(start_disposition=None)])
def test_knows_target_must_be_knowledge_entry():
# points at the town (a canon entity), not a knowledge entry
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(),
npc(knows=[KnowsLink("town.a", "cold")])])
def test_illegal_gate():
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(),
npc(knows=[KnowsLink("rumor.x", "besties")])])
def test_never_is_a_legal_gate():
world = resolve([ladder(), town(), rumor(),
npc(knows=[KnowsLink("rumor.x", "never")])])
assert world.by_id["npc.t"].knows[0].gate == "never"
def test_missing_ladder():
with pytest.raises(BuildError):
resolve([town()])
def test_illegal_start_disposition():
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(), npc(start_disposition="besties")])
def test_illegal_status():
bad = Entry(id="town.a", type="town", status="draft", secrecy=0,
related=[], body="x", source="t:9")
with pytest.raises(BuildError):
resolve([ladder(), bad])
def test_npc_empty_knows_rejected():
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(), npc(knows=[])])
def test_non_npc_with_start_disposition_rejected():
rec = Entry(id="person.p", type="person", status="canon", secrecy=0,
related=[], body="x", source="t:9", start_disposition="cold")
with pytest.raises(BuildError):
resolve([ladder(), town(), rumor(), rec])
def test_empty_rungs_rejected():
empty = Entry(id="rule.disposition-ladder", type="rule", status="canon",
secrecy=0, related=[], body="x", source="t:1", rungs=[])
with pytest.raises(BuildError):
resolve([empty])
def test_canon_entity_nonzero_secrecy_rejected():
bad = Entry(id="town.a", type="town", status="canon", secrecy=2,
related=[], body="x", source="t:9")
with pytest.raises(BuildError):
resolve([ladder(), bad])
def test_non_int_secrecy_raises():
bad = Entry(id="town.a", type="town", status="canon", secrecy="high",
related=[], body="x", source="t:9")
with pytest.raises(BuildError):
resolve([ladder(), bad])
def test_non_list_rungs_raises():
bad_ladder = Entry(id="rule.disposition-ladder", type="rule", status="canon",
secrecy=0, related=[], body="x", source="t:1", rungs="trusted")
with pytest.raises(BuildError):
resolve([bad_ladder])