case0 / tests /test_run.py
HusseinEid's picture
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55
"""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