File size: 3,988 Bytes
16ff49b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""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