""" Tests for the initialization status API endpoints. These tests verify the loading screen backend functionality: - /api/init-status endpoint - /api/game/new endpoint - /api/control/reinit endpoint - Initialization phases and progress tracking """ import pytest import asyncio import time from unittest.mock import patch, MagicMock, AsyncMock from fastapi.testclient import TestClient from src.server import main from src.server.main import app, game_instance, update_init_progress, INIT_PHASE_NAMES @pytest.fixture def client(): """Create a test client for the FastAPI app.""" return TestClient(app) @pytest.fixture def reset_game_instance(): """Reset game_instance to initial state before each test.""" original_state = dict(game_instance) game_instance.clear() game_instance.update({ "world": None, "sim": None, "is_paused": True, "init_status": "idle", "init_phase": 0, "init_phase_name": "", "init_progress": 0, "init_start_time": None, "init_error": None, "llm_check_failed": False, "llm_error_message": "", }) yield game_instance.clear() game_instance.update(original_state) class TestInitStatusEndpoint: """Tests for /api/init-status endpoint.""" def test_init_status_idle(self, client, reset_game_instance): """Test init-status returns idle state correctly.""" response = client.get("/api/init-status") assert response.status_code == 200 data = response.json() assert data["status"] == "idle" assert data["phase"] == 0 assert data["phase_name"] == "" assert data["progress"] == 0 assert data["error"] is None assert data["llm_check_failed"] is False assert data["llm_error_message"] == "" def test_init_status_in_progress(self, client, reset_game_instance): """Test init-status during initialization.""" game_instance["init_status"] = "in_progress" game_instance["init_phase"] = 2 game_instance["init_phase_name"] = "initializing_sects" game_instance["init_progress"] = 33 game_instance["init_start_time"] = time.time() - 5 # 5 seconds ago response = client.get("/api/init-status") assert response.status_code == 200 data = response.json() assert data["status"] == "in_progress" assert data["phase"] == 2 assert data["phase_name"] == "initializing_sects" assert data["progress"] == 33 assert data["elapsed_seconds"] >= 5 def test_init_status_ready(self, client, reset_game_instance): """Test init-status when initialization is complete.""" game_instance["init_status"] = "ready" game_instance["init_phase"] = 5 game_instance["init_phase_name"] = "generating_initial_events" game_instance["init_progress"] = 100 response = client.get("/api/init-status") assert response.status_code == 200 data = response.json() assert data["status"] == "ready" assert data["progress"] == 100 def test_init_status_error(self, client, reset_game_instance): """Test init-status when initialization failed.""" game_instance["init_status"] = "error" game_instance["init_error"] = "LLM connection failed" response = client.get("/api/init-status") assert response.status_code == 200 data = response.json() assert data["status"] == "error" assert data["error"] == "LLM connection failed" def test_init_status_llm_check_failed(self, client, reset_game_instance): """Test init-status includes LLM check status.""" game_instance["init_status"] = "ready" game_instance["llm_check_failed"] = True game_instance["llm_error_message"] = "API key invalid" response = client.get("/api/init-status") assert response.status_code == 200 data = response.json() assert data["llm_check_failed"] is True assert data["llm_error_message"] == "API key invalid" class TestUpdateInitProgress: """Tests for update_init_progress function.""" def test_update_progress_with_phase_name(self, reset_game_instance): """Test updating progress with explicit phase name.""" update_init_progress(2, "initializing_sects") assert game_instance["init_phase"] == 2 assert game_instance["init_phase_name"] == "initializing_sects" assert game_instance["init_progress"] == 33 def test_update_progress_without_phase_name(self, reset_game_instance): """Test updating progress uses default phase name from mapping.""" update_init_progress(3) assert game_instance["init_phase"] == 3 assert game_instance["init_phase_name"] == "generating_avatars" assert game_instance["init_progress"] == 50 def test_all_phase_names_mapped(self): """Test all phases have corresponding names.""" expected_phases = { 0: "scanning_assets", 1: "loading_map", 2: "initializing_sects", 3: "generating_avatars", 4: "checking_llm", 5: "generating_initial_events", } assert INIT_PHASE_NAMES == expected_phases def test_progress_percentages(self, reset_game_instance): """Test progress percentages for each phase.""" expected_progress = {0: 0, 1: 17, 2: 33, 3: 50, 4: 67, 5: 83} for phase, expected in expected_progress.items(): update_init_progress(phase) assert game_instance["init_progress"] == expected, f"Phase {phase} should have progress {expected}" class TestNewGameEndpoint: """Tests for /api/game/new endpoint.""" def test_new_game_starts_initialization(self, client, reset_game_instance): """Test /api/game/new starts initialization process.""" with patch.object(main, 'init_game_async', new_callable=AsyncMock) as mock_init: response = client.post("/api/game/new") assert response.status_code == 200 data = response.json() assert data["status"] == "ok" assert "started" in data["message"].lower() assert game_instance["init_status"] == "pending" def test_new_game_rejects_when_in_progress(self, client, reset_game_instance): """Test /api/game/new rejects request when already initializing.""" game_instance["init_status"] = "in_progress" response = client.post("/api/game/new") assert response.status_code == 400 assert "already initializing" in response.json()["detail"].lower() def test_new_game_clears_existing_state(self, client, reset_game_instance): """Test /api/game/new clears existing game state when ready.""" mock_world = MagicMock() mock_sim = MagicMock() game_instance["world"] = mock_world game_instance["sim"] = mock_sim game_instance["init_status"] = "ready" with patch.object(main, 'init_game_async', new_callable=AsyncMock): response = client.post("/api/game/new") assert response.status_code == 200 assert game_instance["world"] is None assert game_instance["sim"] is None class TestReinitEndpoint: """Tests for /api/control/reinit endpoint.""" def test_reinit_clears_state(self, client, reset_game_instance): """Test /api/control/reinit clears all game state.""" game_instance["world"] = MagicMock() game_instance["sim"] = MagicMock() game_instance["init_status"] = "error" game_instance["init_error"] = "Some error" game_instance["init_phase"] = 3 game_instance["init_progress"] = 50 with patch.object(main, 'init_game_async', new_callable=AsyncMock): response = client.post("/api/control/reinit") assert response.status_code == 200 assert game_instance["world"] is None assert game_instance["sim"] is None assert game_instance["init_status"] == "pending" assert game_instance["init_phase"] == 0 assert game_instance["init_progress"] == 0 assert game_instance["init_error"] is None def test_reinit_starts_new_initialization(self, client, reset_game_instance): """Test /api/control/reinit starts new initialization task.""" with patch.object(main, 'init_game_async', new_callable=AsyncMock) as mock_init: response = client.post("/api/control/reinit") assert response.status_code == 200 data = response.json() assert data["status"] == "ok" assert "reinitialization" in data["message"].lower() class TestMapAndStateAPIDuringInit: """Tests to verify /api/map and /api/state availability during initialization phases.""" def test_map_available_during_checking_llm(self, client, reset_game_instance): """Test /api/map is available when world exists (checking_llm phase).""" # Simulate world being created but LLM check in progress. mock_world = MagicMock() mock_map = MagicMock() mock_map.width = 100 mock_map.height = 100 mock_map.tiles = {} mock_map.regions = {} mock_world.map = mock_map game_instance["world"] = mock_world game_instance["init_status"] = "in_progress" game_instance["init_phase"] = 4 game_instance["init_phase_name"] = "checking_llm" # The /api/map endpoint should work. response = client.get("/api/map") # It may return data or empty, but should not error with 503. assert response.status_code == 200 def test_state_available_during_generating_events(self, client, reset_game_instance): """Test /api/state is available during generating_initial_events phase.""" mock_world = MagicMock() mock_world.month_stamp.get_year.return_value = 100 mock_world.month_stamp.get_month.return_value = MagicMock(value=1) mock_world.avatar_manager.avatars = {} mock_world.event_manager = None game_instance["world"] = mock_world game_instance["init_status"] = "in_progress" game_instance["init_phase"] = 5 game_instance["init_phase_name"] = "generating_initial_events" response = client.get("/api/state") assert response.status_code == 200 class TestInitGameAsync: """Tests for the async initialization flow.""" @pytest.mark.asyncio async def test_init_sets_status_to_in_progress(self, reset_game_instance): """Test initialization sets status to in_progress immediately.""" with patch.object(main, 'scan_avatar_assets'), \ patch.object(main, 'load_cultivation_world_map') as mock_load_map, \ patch.object(main, 'check_llm_connectivity', return_value=(True, "")), \ patch('src.server.main.World') as mock_world_class, \ patch('src.server.main.Simulator') as mock_sim_class: mock_map = MagicMock() mock_load_map.return_value = mock_map mock_world = MagicMock() mock_world.avatar_manager.avatars = {} mock_world_class.return_value = mock_world mock_sim = MagicMock() mock_sim.step = AsyncMock() mock_sim_class.return_value = mock_sim # Start init but check status immediately. task = asyncio.create_task(main.init_game_async()) await asyncio.sleep(0.01) # Let it start. assert game_instance["init_status"] in ["in_progress", "ready"] await task # Let it complete. @pytest.mark.asyncio async def test_init_error_sets_error_status(self, reset_game_instance): """Test initialization error sets status to error.""" with patch.object(main, 'scan_avatar_assets', side_effect=Exception("Test error")): await main.init_game_async() assert game_instance["init_status"] == "error" assert "Test error" in game_instance["init_error"] @pytest.mark.asyncio async def test_init_completes_with_ready_status(self, reset_game_instance): """Test successful initialization sets status to ready.""" with patch.object(main, 'scan_avatar_assets'), \ patch.object(main, 'load_cultivation_world_map') as mock_load_map, \ patch.object(main, 'check_llm_connectivity', return_value=(True, "")), \ patch('src.server.main.World') as mock_world_class, \ patch('src.server.main.Simulator') as mock_sim_class, \ patch('src.server.main.sects_by_id', {}), \ patch('src.server.main.CONFIG') as mock_config: mock_config.game.sect_num = 0 mock_config.game.init_npc_num = 0 mock_config.avatar.protagonist = "none" mock_map = MagicMock() mock_load_map.return_value = mock_map mock_world = MagicMock() mock_world.avatar_manager.avatars = {} mock_world_class.return_value = mock_world mock_sim = MagicMock() mock_sim.step = AsyncMock() mock_sim_class.return_value = mock_sim await main.init_game_async() assert game_instance["init_status"] == "ready" assert game_instance["init_progress"] == 100 @pytest.mark.asyncio async def test_init_records_llm_failure(self, reset_game_instance): """Test LLM check failure is recorded but doesn't stop initialization.""" with patch.object(main, 'scan_avatar_assets'), \ patch.object(main, 'load_cultivation_world_map') as mock_load_map, \ patch.object(main, 'check_llm_connectivity', return_value=(False, "API key invalid")), \ patch('src.server.main.World') as mock_world_class, \ patch('src.server.main.Simulator') as mock_sim_class, \ patch('src.server.main.sects_by_id', {}), \ patch('src.server.main.CONFIG') as mock_config: mock_config.game.sect_num = 0 mock_config.game.init_npc_num = 0 mock_config.avatar.protagonist = "none" mock_map = MagicMock() mock_load_map.return_value = mock_map mock_world = MagicMock() mock_world.avatar_manager.avatars = {} mock_world_class.return_value = mock_world mock_sim = MagicMock() mock_sim.step = AsyncMock() mock_sim_class.return_value = mock_sim await main.init_game_async() # Should still complete successfully. assert game_instance["init_status"] == "ready" # But LLM failure should be recorded. assert game_instance["llm_check_failed"] is True assert game_instance["llm_error_message"] == "API key invalid" @pytest.mark.asyncio async def test_init_pauses_after_initial_events(self, reset_game_instance): """Test game is paused after generating initial events.""" with patch.object(main, 'scan_avatar_assets'), \ patch.object(main, 'load_cultivation_world_map') as mock_load_map, \ patch.object(main, 'check_llm_connectivity', return_value=(True, "")), \ patch('src.server.main.World') as mock_world_class, \ patch('src.server.main.Simulator') as mock_sim_class, \ patch('src.server.main.sects_by_id', {}), \ patch('src.server.main.CONFIG') as mock_config: mock_config.game.sect_num = 0 mock_config.game.init_npc_num = 0 mock_config.avatar.protagonist = "none" mock_map = MagicMock() mock_load_map.return_value = mock_map mock_world = MagicMock() mock_world.avatar_manager.avatars = {} mock_world_class.return_value = mock_world mock_sim = MagicMock() mock_sim.step = AsyncMock() mock_sim_class.return_value = mock_sim await main.init_game_async() # Game should be paused after initialization. assert game_instance["is_paused"] is True