case0 / tests /test_case_adapter.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
"""casefile_to_public: a generated CaseFile projects to the PUBLIC view without leaking
the sealed solution, and conforms to the same wire contract the client expects.
"""
from __future__ import annotations
import json
from case_zero.api.case_adapter import casefile_to_public
from case_zero.api.public_view import PublicCase
from case_zero.generator.assemble import assemble_case
from case_zero.generator.stages import GenSuspect, MysteryOut, WorldCastOut
from case_zero.schemas.case import GenerationKnobs
from case_zero.schemas.enums import MotiveCategory
from case_zero.schemas.timeline import TimeWindow
def _casefile():
world = WorldCastOut(
title="THE LANTERN ROOM",
briefing="A bookbinder is found dead in a locked study during a winter salon.",
setting_name="Ashgrove House",
locations=["The Study", "The Parlor", "The Conservatory", "The Cellar"],
victim_name="Edmund Crane",
victim_role="Master Bookbinder",
cause_of_death="Struck from behind with a heavy press",
found_at_index=0,
weapon_name="bookbinding press",
weapon_kind="blunt",
suspects=[
GenSuspect(name="Cora Vale", role="Apprentice", persona_summary="A quiet apprentice with ink-stained hands.", secret="She forged a signature once.", cover_story="I was cataloguing in the parlor.", evidence_name="ink bottle", evidence_reveal="A spilled bottle of rare ink.", gender="female"),
GenSuspect(name="Miles Ardent", role="Patron", persona_summary="A wealthy patron with debts.", secret="He owes the victim money.", cover_story="I never left the salon.", evidence_name="promissory note", evidence_reveal="A note for a large sum, unpaid.", gender="male"),
GenSuspect(name="Greta Holt", role="Housekeeper", persona_summary="The long-serving housekeeper.", secret="She skims the household accounts.", cover_story="I was in the cellar fetching wine.", evidence_name="ledger", evidence_reveal="A ledger with altered figures.", gender="female"),
GenSuspect(name="Felix Crane", role="Nephew", persona_summary="The restless nephew and heir.", secret="He gambles in secret.", cover_story="I stepped out to the conservatory for air.", evidence_name="card", evidence_reveal="A gambling marker from a city den.", gender="male"),
],
)
mystery = MysteryOut(
motive_category=MotiveCategory.GREED,
motive_summary="To erase a debt that would have ruined him.",
method_narrative="He waited until the study emptied, then struck.",
alibi_claim="I was in the parlor with the others all evening.",
alibi_claimed_loc_index=1,
breaker_one_name="thread",
breaker_one_reveal="A torn thread of dark cloth on the study latch.",
breaker_two_name="footprint",
breaker_two_reveal="A damp footprint by the press, fresh at the hour.",
deduction_chain=["The study was locked from inside.", "Only one route stayed open.", "The thread matches the patron's coat."],
)
return assemble_case(
case_id="gen-000042", seed=42, knobs=GenerationKnobs(), world=world, mystery=mystery,
window=TimeWindow(start_min=21 * 60, end_min=22 * 60),
tod=TimeWindow(start_min=21 * 60 + 20, end_min=21 * 60 + 50),
culprit_idx=1, crime_idx=0, claimed_idx=1,
)
def test_adapter_conforms_to_public_contract():
pub = casefile_to_public(_casefile())
assert isinstance(pub, PublicCase)
assert len(pub.suspects) == 4
assert len(pub.evidence) >= 1
assert len(pub.motives) == 4
assert any(m.id == "M1" for m in pub.motives) # the true motive is option M1
assert pub.timeline and pub.flashback.title
for s in pub.suspects:
assert len(s.suggested_questions) == 5
def test_adapter_does_not_leak_culprit():
case = _casefile()
pub = casefile_to_public(case)
blob = json.dumps(pub.model_dump(by_alias=True))
# the sealed culprit is S2 (culprit_idx=1); nothing must mark who it is
for token in ('"isCulprit"', '"is_culprit"', '"secret"', '"breaksOn"', '"correctMotive"', '"killer"'):
assert token not in blob
# the true motive text appears (as one option) but is not flagged correct
assert case.culprit.true_motive.summary in blob