"""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