Files
code_of_conquest_dnd/api/tests/test_origin_schema.py
Phillip Tarrant 5149e817f6 fix(contract): repair 3-arg LogPlayer callers + digest article bug
Three callers (main_window_shell.gd, narrate_harness.gd, npc_harness.gd)
still built LogPlayer with the OLD 3-arg shape (name, class_id,
luck_descriptor) after LogPlayer.new() was migrated to the 4-arg
(name, race_id, calling_id, luck_descriptor) shape. This was invisible
to both grep and the test suite: every ctor param carries a default, so
GDScript compiles a 3-arg call against a 4-arg signature without error —
it just silently mis-binds positionally. "sellsword" landed in the
race_id slot (rejected — not a race), the luck descriptor landed in the
calling_id slot (rejected — not a calling), and luck_descriptor itself
fell back to its default "". Every field in the emitted dict came out
empty, which 422s against the canon-log schema's minLength/enum
constraints. Grepping for class_id can't see this, because there is no
class_id token left anywhere — the bug is purely positional.

Also fixes prompts.py's _article(""), which returned "an" because
Python's "" in "aeiou" is True, producing a doubled-space/wrong-article
digest line when race_id is empty. _describe_player now builds its
descriptor from whichever of race/calling are present and omits the
clause entirely when both are absent.

Adds a schema-parity guard for origin.schema.json's inlined
allowed_callings enum, which duplicated the calling roster with no
runtime check tying it to Callings.IDS. Renames leftover "class"
vocabulary in test names/comments to "calling".

Regression coverage:
- client/tests/unit/test_entities.gd: ctor populates both race_id and
  calling_id; a calling passed into the race_id slot (the exact shape
  of the three broken call sites) yields an all-empty row.
- api/tests/test_prompts.py: empty-race and empty-race-and-calling
  digest rendering, asserting no doubled space and no dangling article.
- client/tests/unit/test_schema_parity.gd: origin.schema.json's
  allowed_callings enum matches Callings.IDS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:34:05 -05:00

55 lines
1.6 KiB
Python

import copy
from app.canon_log import validate_origin
VALID_ORIGIN = {
"schema_version": 1,
"id": "deserter",
"display_name": "The Deserter",
"description": "You walked away from a company that doesn't allow walking away.",
"start_location_id": "greywater_docks",
"situation": ["Arrived at Greywater by barge before dawn, hood up"],
"opening_facts": ["the player deserted the Iron Kettle mercenary company"],
"disposition_overrides": {"brannoc_thane": 40, "cadwyn_vell": 15},
"inventory_grants": [{"item_id": "worn_shortsword", "qty": 1}],
"start_quest_id": "find_the_ledger",
"build_constraints": {
"allowed_callings": ["sellsword", "cutpurse", "bonesetter"],
"luck_modifier": 0,
},
}
def test_valid_origin_passes():
assert validate_origin(VALID_ORIGIN) == []
def test_null_start_quest_is_allowed():
doc = copy.deepcopy(VALID_ORIGIN)
doc["start_quest_id"] = None
assert validate_origin(doc) == []
def test_unknown_calling_is_rejected():
doc = copy.deepcopy(VALID_ORIGIN)
doc["build_constraints"]["allowed_callings"] = ["bard"]
assert validate_origin(doc) != []
def test_missing_required_field_is_rejected():
doc = copy.deepcopy(VALID_ORIGIN)
del doc["start_location_id"]
assert validate_origin(doc) != []
def test_extra_field_is_rejected():
doc = copy.deepcopy(VALID_ORIGIN)
doc["surprise"] = True
assert validate_origin(doc) != []
def test_disposition_override_key_must_match_id_pattern():
doc = copy.deepcopy(VALID_ORIGIN)
doc["disposition_overrides"] = {"Brannoc Thane": 40}
assert validate_origin(doc) != []