Spaces:
Running on Zero
Running on Zero
| """ | |
| 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() |