""" Test the repair pass logic for Phase 2, Task 8. Tests that repair_game() can: 1. Fix missing fields and structure 2. Add safety constraints 3. Repair task content issues 4. Handle multiple failures simultaneously 5. Preserve valid content when fixing """ from app.services.validator import validate_game, repair_game def print_result(label: str, valid: bool, failures: list[str]): status = "✓ PASS" if valid else "✗ FAIL" print(f" {status}: {label}") if failures: for f in failures[:3]: print(f" → {f}") if len(failures) > 3: print(f" → ... and {len(failures) - 3} more") def test_repair_minimal_game(): """Repair a game with almost nothing filled in.""" print("\n" + "=" * 80) print("TEST 1: Repair minimal (empty) game") print("=" * 80) bad_game = { "game_id": "", "title": "", "theme": "", "setup": {}, "rules": [], "tasks": [], "global_hints": [], "score_rules": [], "tie_breaker": "", "safety": {}, "story_seed": {} } config = { "game_type": "scavenger_hunt", "city": "Paris", "area": "Le Marais", "duration_minutes": 60, "num_players": 4, "difficulty": "medium", "age_group": "adults" } # Show validation before repair is_valid, failures = validate_game(bad_game, config) print_result("Before repair", is_valid, failures) # Apply repair repaired = repair_game(bad_game, failures, config) # Validate repaired game is_valid, failures = validate_game(repaired, config) print_result("After repair", is_valid, failures) # Show what was fixed print(f"\n Repaired game ID: {repaired['game_id']}") print(f" Title: {repaired['title']}") print(f" Theme: {repaired['theme']}") print(f" Tasks: {len(repaired['tasks'])}") print(f" Rules: {len(repaired['rules'])}") print(f" Score rules: {len(repaired['score_rules'])}") print(f" Safety zone: {repaired['safety']['allowed_zone'][:50]}...") print(f" Supervision: {repaired['safety']['adult_supervision']}") print(f" Forbidden behaviors: {len(repaired['safety']['forbidden_behaviors'])}") print(f" Stop conditions: {len(repaired['safety']['stop_conditions'])}") return is_valid def test_repair_missing_task_fields(): """Repair tasks with missing critical fields.""" print("\n" + "=" * 80) print("TEST 2: Repair tasks with missing fields") print("=" * 80) bad_game = { "game_id": "game-001", "title": "Test Game", "theme": "discovery", "setup": { "city": "Paris", "area": "Montmartre", "meeting_point": "Sacré-Cœur steps", "duration_minutes": 45, "num_players": 4 }, "rules": ["Rule 1", "Rule 2"], "tasks": [ { "task_id": "t1", "title": "Find the vineyard", "description": "Locate the vineyard sign", "location_hint": "North slope near Lapin Agile", "points": 20, "time_limit_minutes": 15, # missing proof_type # missing hint # missing safety_note } ], "global_hints": [], "score_rules": [], "tie_breaker": "", "safety": { "allowed_zone": "Montmartre public areas", "forbidden_behaviors": ["No entering buildings"], # missing adult_supervision # missing stop_conditions }, "story_seed": { "tone": "playful", "motifs": [], "recap_style": "episode_recap" } } config = { "game_type": "scavenger_hunt", "city": "Paris", "area": "Montmartre", "duration_minutes": 45, "num_players": 4, "difficulty": "hard", "age_group": "adults" } # Before repair is_valid, failures = validate_game(bad_game, config) print_result("Before repair", is_valid, failures) # Repair repaired = repair_game(bad_game, failures, config) # After repair is_valid, failures = validate_game(repaired, config) print_result("After repair", is_valid, failures) # Show fixed task task = repaired['tasks'][0] print(f"\n Fixed task fields:") print(f" proof_type: {task.get('proof_type')}") print(f" hint: {task.get('hint')[:40]}...") print(f" safety_note: {task.get('safety_note')[:40]}...") print(f" Fixed safety:") print(f" adult_supervision: {repaired['safety'].get('adult_supervision')}") print(f" stop_conditions: {len(repaired['safety'].get('stop_conditions', []))}") return is_valid def test_repair_kids_game_no_supervision(): """Repair a kids game missing adult supervision.""" print("\n" + "=" * 80) print("TEST 3: Repair kids game missing adult supervision") print("=" * 80) bad_game = { "game_id": "game-kids", "title": "Kids Hunt", "theme": "play", "setup": { "city": "Paris", "area": "Parc des Buttes-Chaumont", "meeting_point": "Main entrance", "duration_minutes": 30, "num_players": 6 }, "rules": ["Stay in park"], "tasks": [ { "task_id": "t1", "title": "Find the temple", "description": "Find the temple viewpoint", "location_hint": "At the top of the hill", "points": 15, "time_limit_minutes": 10, "proof_type": "observation", "hint": "Look up", "safety_note": "Stay safe" } ], "global_hints": [], "score_rules": [], "tie_breaker": "", "safety": { "allowed_zone": "Park", "forbidden_behaviors": ["No climbing"], "adult_supervision": False, # Should be True for kids! "stop_conditions": [] }, "story_seed": { "tone": "playful", "motifs": [], "recap_style": "episode_recap" } } config = { "game_type": "hide_and_seek", "city": "Paris", "area": "Parc des Buttes-Chaumont", "duration_minutes": 30, "num_players": 6, "difficulty": "easy", "age_group": "kids" } # Before repair is_valid, failures = validate_game(bad_game, config) print_result("Before repair", is_valid, failures) # Repair repaired = repair_game(bad_game, failures, config) # After repair is_valid, failures = validate_game(repaired, config) print_result("After repair", is_valid, failures) print(f"\n Adult supervision: {repaired['safety']['adult_supervision']} (was False)") print(f" Rules: {len(repaired['rules'])} (was 1)") print(f" Safety note: {repaired['tasks'][0]['safety_note'][:50]}...") return is_valid def test_repair_content_with_forbidden_keywords(): """Repair tasks mentioning forbidden locations.""" print("\n" + "=" * 80) print("TEST 4: Repair content with forbidden keyword mentions") print("=" * 80) bad_game = { "game_id": "game-004", "title": "Building Hunt", "theme": "urban", "setup": { "city": "Paris", "area": "Centre", "meeting_point": "Plaza", "duration_minutes": 45, "num_players": 3, "time_limit_minutes": 40 }, "rules": ["Rule 1", "Rule 2", "Rule 3"], "tasks": [ { "task_id": "t1", "title": "Enter the shop", "description": "Go inside the shop on Rue de Rivoli and ask staff for directions", "location_hint": "Inside the building entrance", "points": 20, "time_limit_minutes": 10, "proof_type": "photo", "hint": "Check behind the counter", "safety_note": "Be careful" } ], "global_hints": [], "score_rules": ["Complete tasks"], "tie_breaker": "First to finish", "safety": { "allowed_zone": "City center", "forbidden_behaviors": [], "adult_supervision": False, "stop_conditions": [] }, "story_seed": { "tone": "playful", "motifs": [], "recap_style": "episode_recap" } } config = { "game_type": "scavenger_hunt", "city": "Paris", "area": "Centre", "duration_minutes": 45, "num_players": 3, "difficulty": "medium", "age_group": "adults" } # Before repair is_valid, failures = validate_game(bad_game, config) print_result("Before repair", is_valid, failures) # Repair repaired = repair_game(bad_game, failures, config) # After repair is_valid, failures = validate_game(repaired, config) print_result("After repair", is_valid, failures) print(f"\n Task safety note: {repaired['tasks'][0]['safety_note'][:70]}...") print(f" Forbidden behaviors: {len(repaired['safety']['forbidden_behaviors'])} items") return is_valid def test_repair_wrong_task_count(): """Repair games with too few or too many tasks for duration.""" print("\n" + "=" * 80) print("TEST 5: Repair unrealistic task count for duration") print("=" * 80) # 90 min game with only 1 task bad_game = { "game_id": "game-005", "title": "Long Game", "theme": "epic", "setup": { "city": "Paris", "area": "Le Marais", "meeting_point": "Centre", "duration_minutes": 90, "num_players": 4 }, "rules": ["Rule 1", "Rule 2", "Rule 3", "Rule 4", "Rule 5"], "tasks": [ { "task_id": "t1", "title": "One task", "description": "Walk around", "location_hint": "Around", "points": 30, "time_limit_minutes": None, "proof_type": "photo", "hint": "Look around", "safety_note": "Stay in public areas" } ], "global_hints": [], "score_rules": ["Complete all"], "tie_breaker": "First", "safety": { "allowed_zone": "Le Marais", "forbidden_behaviors": ["No buildings", "No roads"], "adult_supervision": False, "stop_conditions": ["Injury", "Weather", "Emergency"] }, "story_seed": {"tone": "playful", "motifs": [], "recap_style": "episode_recap"} } config = { "game_type": "scavenger_hunt", "city": "Paris", "area": "Le Marais", "duration_minutes": 90, "num_players": 4, "difficulty": "medium", "age_group": "adults" } # Before repair is_valid, failures = validate_game(bad_game, config) print_result("Before repair", is_valid, failures) # Repair repaired = repair_game(bad_game, failures, config) # After repair is_valid, failures = validate_game(repaired, config) print_result("After repair", is_valid, failures) print(f"\n Tasks: {len(repaired['tasks'])} (was 1)") print(f" Task IDs: {', '.join(t['task_id'] for t in repaired['tasks'])}") print(f" Task points: {[t['points'] for t in repaired['tasks']]}") return is_valid def test_full_pipeline_repair(): """Test full pipeline: generate → validate → repair → re-validate.""" print("\n" + "=" * 80) print("TEST 6: Full pipeline - generate, validate, repair, re-validate") print("=" * 80) from app.services.generator import generate_game from app.services.retrieval import load_games_dataset, normalize_game_record, retrieve_examples # Load dataset raw = load_games_dataset("app/data/games_dataset.json") normalized = [normalize_game_record(r) for r in raw] config = { "game_type": "scavenger_hunt", "city": "Paris", "area": "Le Marais", "location_type": "mixed", "duration_minutes": 60, "num_players": 4, "difficulty": "medium", "age_group": "adults", "photo_enabled": True } # Generate retrieved = retrieve_examples(config, normalized, k=3) game = generate_game(config, retrieved) print(f" Generated: {game['game_id']} ({len(game['tasks'])} tasks)") # Validate is_valid, failures = validate_game(game, config) print_result("Initial validation", is_valid, failures) # Repair if needed if not is_valid: repaired = repair_game(game, failures, config) is_valid, failures = validate_game(repaired, config) print_result("After repair", is_valid, failures) print(f"\n Originally: {len(game['tasks'])} tasks, {len(game['rules'])} rules") print(f" Repaired: {len(repaired['tasks'])} tasks, {len(repaired['rules'])} rules") else: print(" No repair needed (game already valid)") return is_valid def main(): print("\n" + "=" * 80) print("PHASE 2, TASK 8: REPAIR PASS LOGIC TESTS") print("=" * 80) results = [] # Run all tests results.append(("Test 1: Minimal game repair", test_repair_minimal_game())) results.append(("Test 2: Missing task fields", test_repair_missing_task_fields())) results.append(("Test 3: Kids game supervision", test_repair_kids_game_no_supervision())) results.append(("Test 4: Forbidden content", test_repair_content_with_forbidden_keywords())) results.append(("Test 5: Task count realism", test_repair_wrong_task_count())) results.append(("Test 6: Full pipeline", test_full_pipeline_repair())) # Summary print("\n" + "=" * 80) print("REPAIR PASS TEST SUMMARY") print("=" * 80) passed = sum(1 for _, ok in results if ok) for name, ok in results: status = "✓ PASS" if ok else "✗ FAIL" print(f" {status}: {name}") print(f"\n Total: {passed}/{len(results)} tests passed") print("\n" + "=" * 80) if __name__ == "__main__": main()