| """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_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" |
| for clue in case.clues: |
| assert clue.zone in valid, clue.zone |
| assert case.floorplan is fp |
| assert case.scene == "test_scene" |
| assert case.victim.name == "Gerald Hoffmann" |
| assert case.zones == room_ids |
|
|
|
|
| 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__}") |
|
|