"""Unit tests for case↔room binding (no LLM required). Exercises `_assemble_case` directly with stub drafts to prove the binding never emits an invalid room id, binds to the scene manifest's floorplan, and carries human occupations — even when the (simulated) model returns garbage zones. """ from backend.services import case_generator as cg from backend.models.scene_map import SceneManifest, SceneZone def _manifest(): """A small synthetic scene manifest (the static-map replacement for the old procedural house generator).""" return SceneManifest( scene_id="test_scene", background="assets/maps/test/background.png", tile_px=32, grid_w=10, grid_h=10, zones=[ SceneZone(id="kitchen", label="Kitchen", rect=[0, 0, 5, 5], spawn_points=[[1, 1]], clue_anchors=[[2, 2]]), SceneZone(id="study", label="Study", rect=[5, 0, 5, 5], spawn_points=[[6, 1]], clue_anchors=[[7, 2]]), SceneZone(id="hall", label="Hall", rect=[0, 5, 5, 5], spawn_points=[[1, 6]], clue_anchors=[[2, 7]]), ], doors=[], collision=[[0] * 10 for _ in range(10)], ) def _story_data(): return { "method": "Poisoned tea", "motive": "Greed", "victim_raw": "Gerald Hoffmann, 52", "opening_narration": "Something was wrong.", "culprit_lying_skill": 8, "timeline": [{"time": "21:00", "event": "tea"}], } def _drafts(n): roles = ["culprit"] + ["witness"] * (n - 1) return [ {"soul": f"## YOUR ALIBI\nI was home.\n", "role": roles[i], "name": f"NPC{i}", "idx": i, "occupation": "butler"} for i in range(n) ] def test_assemble_binds_to_floorplan_and_roster(): manifest = _manifest() fp = manifest.to_floorplan() room_ids = manifest.zone_ids n = 4 spawn_by_idx = [room_ids[i % len(room_ids)] for i in range(n)] # Clues with deliberately invalid zones — must be remapped to real room ids. clues_raw = [ {"id": "clue_01", "name": "Cup", "description": "x", "zone": "not_a_room", "tile_coords": [3, 3], "implicates": ["npc_00"], "is_red_herring": False}, {"id": "clue_02", "name": "Bottle", "description": "y", "zone": room_ids[2], "tile_coords": [4, 4], "implicates": ["npc_00"], "is_red_herring": False}, ] case = cg._assemble_case(_story_data(), _drafts(n), clues_raw, {}, "", "medium", fp, spawn_by_idx, scene="test_scene", victim_name="Gerald Hoffmann", victim_description="The master of the house.") valid = set(room_ids) for npc in case.npcs: assert npc.spawn_zone in valid, npc.spawn_zone assert npc.occupation, "occupation must be set" # human occupation carried for clue in case.clues: assert clue.zone in valid, clue.zone # invalid zone repaired assert case.floorplan is fp assert case.scene == "test_scene" assert case.victim.name == "Gerald Hoffmann" # player-supplied victim assert case.zones == room_ids # zones synced to floorplan def test_seed_case_is_internally_consistent(): sc = cg.SEED_CASE assert sc.floorplan is not None ids = {r.id for r in sc.floorplan.rooms} assert sc.zones == [r.id for r in sc.floorplan.rooms] for npc in sc.npcs: assert npc.spawn_zone in ids for clue in sc.clues: assert clue.zone in ids if __name__ == "__main__": for fn in [test_assemble_binds_to_floorplan_and_roster, test_seed_case_is_internally_consistent]: fn() print(f"PASS {fn.__name__}")