"""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