File size: 2,885 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""End-to-end run flow over the scripted golden engine: interrogate -> suspicion moves,
present the breaking exhibit -> the alibi cracks (without a confession), accuse -> graded
verdict. Plus anti-leak on the live (pre-verdict) endpoints.
"""

from __future__ import annotations

from starlette.testclient import TestClient

from case_zero.api import build_server


def _new_run() -> tuple[TestClient, str]:
    c = TestClient(build_server())
    r = c.post("/api/case", json={"caseId": "GRAYMOOR-3107"})
    return c, r.json()["runId"]


def test_interrogate_moves_suspicion() -> None:
    c, run = _new_run()
    r = c.post(f"/api/run/{run}/interrogate/iris", json={"questionId": "q3"})
    assert r.status_code == 200
    body = r.json()
    assert body["reply"]
    assert body["suspicionDelta"] == 18
    assert body["suspicion"] == 25 + 18  # iris baseline 25 + q3 delta


def test_breaking_evidence_cracks_without_confession() -> None:
    c, run = _new_run()
    r = c.post(f"/api/run/{run}/interrogate/iris", json={"presentEvidenceId": "keycard"}).json()
    assert r["flags"]["rattled"] and r["flags"]["cornered"]  # +26 is the strongest tell
    assert r["suspicion"] == 25 + 26
    low = r["reply"].lower()
    # cracks, but never confesses
    assert "i did it" not in low
    assert "i killed" not in low


def test_accuse_correct_is_case_closed() -> None:
    c, run = _new_run()
    v = c.post(
        f"/api/run/{run}/accuse",
        json={"suspectId": "iris", "motiveId": "m_credit", "evidenceIds": ["keycard", "voicemail", "cctv"]},
    ).json()
    assert v["correct"] is True
    assert v["verdict"]["stamp"] == "CASE CLOSED"
    assert v["verdict"]["killerName"] == "IRIS CALLOWAY"
    assert v["score"]["points"] == 100


def test_accuse_wrong_is_mistrial() -> None:
    c, run = _new_run()
    v = c.post(
        f"/api/run/{run}/accuse",
        json={"suspectId": "wexler", "motiveId": "m_money", "evidenceIds": ["receipt"]},
    ).json()
    assert v["correct"] is False
    assert v["verdict"]["stamp"] == "MISTRIAL"
    assert v["score"]["points"] < 100


def test_live_endpoints_do_not_leak_sealed() -> None:
    c, run = _new_run()
    turn = c.post(f"/api/run/{run}/interrogate/iris", json={"questionId": "q0"}).text
    hint = c.get(f"/api/run/{run}/hint", params={"screen": "interro"}).text
    for blob in (turn, hint):
        for token in ('"killer"', '"correctMotive"', '"keyEvidence"', '"d":'):
            assert token not in blob


def test_hint_is_screen_aware_and_spoiler_safe() -> None:
    c, run = _new_run()
    h = c.get(f"/api/run/{run}/hint", params={"screen": "accuse"}).json()["hint"]
    assert h
    assert "iris" not in h.lower()  # never names the killer


def test_unknown_run_404() -> None:
    c = TestClient(build_server())
    assert c.post("/api/run/nope/interrogate/iris", json={"questionId": "q0"}).status_code == 404