turnabout-bench / tests /test_schema.py
王致渊
Initial commit
57e0211
Raw
History Blame Contribute Delete
8.18 kB
"""Tests for the case data schema."""
import json
from pathlib import Path
import pytest
from pydantic import ValidationError
from turnabout.core.schema import (
CaseData,
Character,
Contradiction,
CourtProceedings,
CourtRound,
DialogueLine,
DialogueTree,
EvidenceAcquisition,
EvidenceItem,
EvidencePresentResponse,
Examinable,
InvestigationGate,
Location,
Statement,
Testimony,
WinCondition,
)
CASES_DIR = Path(__file__).parent.parent / "turnabout" / "cases"
def _make_minimal_case(**overrides) -> dict:
base = {
"id": "test_001",
"title": "Test Case",
"difficulty": "easy",
"description": "A test case.",
"characters": [
{"id": "defendant", "name": "D", "description": "d", "role": "defendant"},
{"id": "witness1", "name": "W", "description": "w", "role": "witness"},
],
"evidence": [
{
"id": "ev1",
"name": "E1",
"item_type": "document",
"description": "desc",
"detail": "detail",
"source": "pre_given",
}
],
"court": {
"penalty_limit": 5,
"rounds": [
{
"witness_id": "witness1",
"order": 0,
"initial_testimony_id": "t1",
"testimonies": [
{
"id": "t1",
"title": "Testimony",
"witness_id": "witness1",
"preamble": "The witness testifies.",
"statements": [
{
"id": "s1",
"text": "I saw something.",
"press_response": "Well...",
"contradiction": {
"evidence_id": "ev1",
"explanation": "This contradicts!",
"is_primary": True,
},
}
],
}
],
}
],
"win_condition": {"required_contradiction_ids": ["s1"]},
},
}
base.update(overrides)
return base
class TestMinimalCase:
def test_parse_minimal(self):
case = CaseData(**_make_minimal_case())
assert case.id == "test_001"
assert case.difficulty == "easy"
assert len(case.characters) == 2
assert len(case.evidence) == 1
assert case.court.penalty_limit == 5
def test_get_character(self):
case = CaseData(**_make_minimal_case())
assert case.get_character("defendant") is not None
assert case.get_character("nonexistent") is None
def test_get_evidence(self):
case = CaseData(**_make_minimal_case())
assert case.get_evidence("ev1") is not None
assert case.get_evidence("nonexistent") is None
def test_get_testimony(self):
case = CaseData(**_make_minimal_case())
assert case.get_testimony("t1") is not None
assert case.get_testimony("nonexistent") is None
def test_get_statement(self):
case = CaseData(**_make_minimal_case())
s = case.get_statement("s1")
assert s is not None
assert s.contradiction is not None
assert s.contradiction.evidence_id == "ev1"
def test_pre_given_evidence(self):
case = CaseData(**_make_minimal_case())
pre = case.get_pre_given_evidence()
assert len(pre) == 1
assert pre[0].id == "ev1"
def test_required_contradictions(self):
case = CaseData(**_make_minimal_case())
stmts = case.get_required_contradiction_statements()
assert len(stmts) == 1
assert stmts[0].id == "s1"
class TestValidation:
def test_invalid_difficulty(self):
with pytest.raises(ValidationError):
CaseData(**_make_minimal_case(difficulty="medium"))
def test_investigation_evidence_requires_acquisition(self):
data = _make_minimal_case()
data["evidence"].append(
{
"id": "ev2",
"name": "E2",
"item_type": "physical",
"description": "d",
"detail": "d",
"source": "investigation",
"acquisition": None,
}
)
with pytest.raises(ValidationError, match="investigation evidence must have acquisition"):
CaseData(**data)
def test_investigation_evidence_with_acquisition_ok(self):
data = _make_minimal_case()
data["locations"] = [
{
"id": "loc1",
"name": "L1",
"description": "d",
"examinables": [{"id": "obj1", "name": "O1", "description": "d"}],
}
]
data["evidence"].append(
{
"id": "ev2",
"name": "E2",
"item_type": "physical",
"description": "d",
"detail": "d",
"source": "investigation",
"acquisition": {
"method": "examine",
"location_id": "loc1",
"target_id": "obj1",
},
}
)
case = CaseData(**data)
assert len(case.evidence) == 2
class TestStolenPrototype:
@pytest.fixture
def case(self):
path = CASES_DIR / "stolen_prototype.json"
if not path.exists():
pytest.skip("stolen_prototype.json not yet created")
with open(path) as f:
data = json.load(f)
return CaseData(**data)
def test_loads(self, case):
assert case.id == "stolen_prototype"
assert case.title == "The Stolen Prototype"
def test_characters(self, case):
assert len(case.characters) == 5
roles = {c.role for c in case.characters}
assert "defendant" in roles
assert "witness" in roles
def test_evidence_count(self, case):
assert len(case.evidence) == 7
pre_given = case.get_pre_given_evidence()
assert len(pre_given) == 2
investigation = [e for e in case.evidence if e.source == "investigation"]
assert len(investigation) == 5
def test_locations(self, case):
assert len(case.locations) == 5
hale_office = case.get_location("hale_office")
assert hale_office is not None
assert not hale_office.available_from_start
assert "hale_suspicion" in hale_office.unlock_prerequisites
def test_court_structure(self, case):
assert case.court.penalty_limit == 5
assert len(case.court.rounds) == 1
rnd = case.court.rounds[0]
assert rnd.witness_id == "morrison"
assert len(rnd.testimonies) == 2
def test_win_condition(self, case):
wc = case.court.win_condition
assert len(wc.required_contradiction_ids) == 3
assert "patrol_1" in wc.required_contradiction_ids
assert "revised_2b" in wc.required_contradiction_ids
assert "revised_3" in wc.required_contradiction_ids
def test_investigation_gate(self, case):
gate = case.investigation_gate
assert gate is not None
assert len(gate.required_evidence) == 4
assert "cloning_device" in gate.required_evidence
def test_all_contradictions_reference_valid_evidence(self, case):
ev_ids = case.get_all_evidence_ids()
for rnd in case.court.rounds:
for t in rnd.testimonies:
for s in t.statements:
if s.contradiction:
assert s.contradiction.evidence_id in ev_ids, (
f"Statement {s.id} contradiction references unknown evidence "
f"{s.contradiction.evidence_id}"
)