The canon-log schema and NewGame code already migrated to race_id/ calling_id + allowed_callings, but two copies of the roster outside that schema were missed, and no parity test guards them: - docs/canon-log.md (the cross-boundary contract doc) still documented the OLD player shape and allowed_classes. Anyone building a player block from the doc would get a 422. - .claude/skills/world-building's schema reference still emitted allowed_classes with two dead calling ids (assassin, priest). Author a new origin with that skill and it fails origin.schema.json (additionalProperties: false, allowed_callings required) AND, if that ever loosened, silently allows zero callings at runtime. Also: - add schema tests rejecting a dead class_id field and an unknown calling_id (priest) - guard NewGame._validate's container types (spend/skills) so a malformed JSON round-trip (null spend, string skills) produces a front-loaded error list instead of a GDScript runtime crash - extend the no-mechanics-in-content test to db.races, not just db.callings - rewrite the roadmap's M4 bullet: the two contract migrations landed; only the creation screen and title->creation->shell flow remain client: 250/250. api: 74 passed, 2 skipped. content_build --check: clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
88 lines
2.4 KiB
Python
88 lines
2.4 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
from app.canon_log import validate_canon_log
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "canon_log_valid.json"
|
|
|
|
|
|
def _valid():
|
|
with open(FIXTURE) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def test_valid_canon_log_passes():
|
|
assert validate_canon_log(_valid()) == []
|
|
|
|
|
|
def test_numeric_luck_is_rejected():
|
|
# Charter §7: the AI must never be able to calculate Luck. A stray numeric
|
|
# luck field must fail validation, not slip through.
|
|
doc = _valid()
|
|
doc["player"]["luck"] = 5
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_recent_events_over_five_is_rejected():
|
|
doc = _valid()
|
|
doc["recent_events"] = [f"event {i}" for i in range(6)]
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_disposition_out_of_range_is_rejected():
|
|
doc = _valid()
|
|
doc["party"][0]["disposition"] = 200
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_unknown_quest_status_is_rejected():
|
|
doc = _valid()
|
|
doc["active_quests"][0]["status"] = "abandoned"
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_humiliation_weight_bounds_enforced():
|
|
doc = _valid()
|
|
doc["humiliations"][0]["weight"] = 11
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_empty_optional_arrays_are_allowed():
|
|
doc = _valid()
|
|
doc["party"] = []
|
|
doc["recent_events"] = []
|
|
doc["established_facts"] = []
|
|
doc["active_quests"] = []
|
|
doc["humiliations"] = []
|
|
assert validate_canon_log(doc) == []
|
|
|
|
|
|
def test_root_level_stray_field_is_rejected():
|
|
# §2/§7: stats/HP/inventory must never enter the log — additionalProperties:false at root.
|
|
doc = _valid()
|
|
doc["hp"] = 10
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_negative_turn_is_rejected():
|
|
doc = _valid()
|
|
doc["humiliations"][0]["turn"] = -1
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_dead_class_id_field_is_rejected():
|
|
# The player block migrated from {name, class_id, luck_descriptor} to
|
|
# {name, race_id, calling_id, luck_descriptor}. class_id must not slip
|
|
# back in via additionalProperties: false.
|
|
doc = _valid()
|
|
doc["player"]["class_id"] = "sellsword"
|
|
assert validate_canon_log(doc) != []
|
|
|
|
|
|
def test_unknown_calling_id_is_rejected():
|
|
# assassin/priest are dead ids from the pre-migration class roster and
|
|
# must not validate as calling_id values.
|
|
doc = _valid()
|
|
doc["player"]["calling_id"] = "priest"
|
|
assert validate_canon_log(doc) != []
|