Spaces:
Running
Running
| """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 | |