case0 / tests /test_exhibits.py
HusseinEid's picture
Per-case visuals, incident vignettes, guided play, no-repeat dealing, four new crime kinds (#4)
16ff49b
Raw
History Blame Contribute Delete
3.99 kB
"""Rich-exhibit synthesis: every payload kind has the exact wire shape the client
renders, and synthesized content never leaks sealed material."""
from __future__ import annotations
import json
import re
from test_case_adapter import _casefile
from case_zero.api.case_adapter import casefile_to_public
from case_zero.api.exhibits import synthesize_payload
from case_zero.schemas.clue import Clue
from case_zero.schemas.enums import DiscoveryMethod
def _clue(case, name: str, reveal: str, method: DiscoveryMethod = DiscoveryMethod.SEARCH) -> Clue:
return Clue(
clue_id="C_t1",
name=name,
reveal_text=reveal,
discoverable_at_loc_id=case.setting.locations[0].loc_id,
discovery_method=method,
)
def test_phone_thread_shape():
case = _casefile()
clue = _clue(case, "burner phone", "You can't announce it tonight. Meet me first.")
out = synthesize_payload(case, clue, "phone", 0, 21 * 60 + 30, "9:30 PM")
assert out["type"] == "PHONE"
msgs = out["thread"]
assert 3 <= len(msgs) <= 5
for m in msgs:
assert m.from_ in ("me", "them")
assert re.match(r"^\d{1,2}:\d{2}$", m.t)
# the climactic lines are the clue's own reveal text, split at sentence bounds
joined = " ".join(m.m for m in msgs)
assert "Meet me first." in joined
# nobody is named unless the reveal names them
assert all(m.who in ("UNKNOWN", "CRANE") for m in msgs)
def test_phone_thread_names_only_verbatim_suspects():
case = _casefile()
clue = _clue(case, "message slip", "Miles Ardent never answered the last message.")
out = synthesize_payload(case, clue, "phone", 0, 21 * 60 + 30, "9:30 PM")
whos = {m.who for m in out["thread"]}
assert "ARDENT" in whos
def test_voicemail_shape():
case = _casefile()
clue = _clue(case, "wax cylinder recording", "I heard them argue about the press.")
out = synthesize_payload(case, clue, "voicemail", 0, 21 * 60, "9:00 PM")
assert out["type"] == "AUDIO"
assert out["transcript"].startswith("“")
assert re.match(r"^0:\d{2}$", out["dur"])
def test_keycard_rows_shape():
case = _casefile()
clue = _clue(case, "keycard log", "The service door opened twice after closing.")
out = synthesize_payload(case, clue, "keycard", 0, 21 * 60 + 41, "9:41 PM")
assert out["type"] == "DATA"
rows = out["rows"]
assert 2 <= len(rows) <= 6
assert all(len(r) == 4 for r in rows)
assert any(r[3] == "flag" for r in rows)
flag = next(r for r in rows if r[3] == "flag")
assert flag[2] == "UNREGISTERED" # reveal names nobody
assert [r[0] for r in rows] == sorted(r[0] for r in rows)
def test_cctv_and_photo_shapes():
case = _casefile()
cam = synthesize_payload(case, _clue(case, "cctv still", "A pale coat at the rail."), "cctv", 0, 0, "11:43 PM")
assert cam["type"] == "IMAGE" and cam["detail"].startswith("CAM —")
photo = synthesize_payload(case, _clue(case, "old photograph", "Two figures by the gate."), "photoEv", 2, 0, "9:00 PM")
assert photo["type"] == "IMAGE" and photo["icon"] == "photoEv" and "EVIDENCE MARKER 3" in photo["detail"]
def test_forensic_in_situ_keeps_icon():
case = _casefile()
clue = _clue(case, "partial fingerprint", "A partial print on the latch.", DiscoveryMethod.FORENSIC)
out = synthesize_payload(case, clue, "fingerprint", 0, 0, "9:00 PM")
assert out["type"] == "IMAGE"
assert "icon" not in out # the object icon stays
def test_paper_fallthrough():
case = _casefile()
out = synthesize_payload(case, _clue(case, "pawn ticket", "A stub from the city den."), "receipt", 0, 0, "9:00 PM")
assert out == {}
def test_synthesized_case_does_not_leak_sealed_material():
case = _casefile()
blob = json.dumps(casefile_to_public(case).model_dump(by_alias=True))
assert case.culprit.method_narrative not in blob
for s in case.suspects:
for secret in s.secrets:
assert secret not in blob