fix(models): reject combatant ids that misparse the transcript format
This commit is contained in:
@@ -410,6 +410,20 @@ class Combatant(_FrozenModel):
|
|||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@field_validator("id")
|
||||||
|
@classmethod
|
||||||
|
def _validate_id(cls, value: str) -> str:
|
||||||
|
# The transcript line format is "round N <id>: <message>"; an id
|
||||||
|
# containing ": " would be mis-split by cli._parse_transcript_line,
|
||||||
|
# and leading/trailing whitespace breaks the "id: " join.
|
||||||
|
if not value or value != value.strip():
|
||||||
|
msg = f"combatant id must be non-empty without surrounding whitespace, got {value!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
if ": " in value:
|
||||||
|
msg = f"combatant id must not contain ': ' (breaks transcript parsing), got {value!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
return value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_lawful(self) -> bool:
|
def is_lawful(self) -> bool:
|
||||||
"""Law axis: LG/LN/LE."""
|
"""Law axis: LG/LN/LE."""
|
||||||
|
|||||||
@@ -189,6 +189,22 @@ class TestCombatant:
|
|||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
Combatant.model_validate(_combatant_dict(hit_dice="1d8"))
|
Combatant.model_validate(_combatant_dict(hit_dice="1d8"))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad_id", ["a: b", " goblin", "goblin ", ""])
|
||||||
|
def test_rejects_transcript_hostile_id(self, bad_id: str) -> None:
|
||||||
|
# Given an id that would misparse the "round N id: message" transcript
|
||||||
|
# (contains ": ") or is empty / padded with whitespace
|
||||||
|
# When/Then validation rejects it
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
Combatant.model_validate(_combatant_dict(id=bad_id))
|
||||||
|
|
||||||
|
def test_accepts_id_with_colon_without_space(self) -> None:
|
||||||
|
# Given an id using ":" as a separator but never ": " (weapon-style
|
||||||
|
# ids are safe for the transcript parser)
|
||||||
|
# When validated
|
||||||
|
combatant = Combatant.model_validate(_combatant_dict(id="goblin:claw"))
|
||||||
|
# Then it is accepted
|
||||||
|
assert combatant.id == "goblin:claw"
|
||||||
|
|
||||||
def test_frozen(self) -> None:
|
def test_frozen(self) -> None:
|
||||||
# Given a validated combatant
|
# Given a validated combatant
|
||||||
combatant = Combatant.model_validate(_combatant_dict())
|
combatant = Combatant.model_validate(_combatant_dict())
|
||||||
|
|||||||
Reference in New Issue
Block a user