- --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
155 lines
4.8 KiB
Python
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])
|