From 4ad5a78b203a460a154d47459002f2d45a3de5f8 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 17 Sep 2025 08:36:31 -0400 Subject: [PATCH] Add .gitignore files and clean up tracked files --- .gitignore | 72 ++ __pycache__/gumyum_models.cpython-313.pyc | Bin 15915 -> 0 bytes __pycache__/gumyum_npc_client.cpython-313.pyc | Bin 71772 -> 0 bytes __pycache__/gumyum_npc_sync.cpython-313.pyc | Bin 58704 -> 0 bytes gumyum_models.py | 42 +- gumyum_npc_client.py | 152 +++- gumyum_npc_sync.py | 150 +++- .../conftest.cpython-313-pytest-8.4.1.pyc | Bin 10561 -> 0 bytes .../test_async.cpython-313-pytest-8.4.1.pyc | Bin 102996 -> 0 bytes ...uth_retry_fix.cpython-313-pytest-8.4.1.pyc | Bin 20028 -> 0 bytes ...est_filtering.cpython-313-pytest-8.4.1.pyc | Bin 40211 -> 0 bytes ...ng_functional.cpython-313-pytest-8.4.1.pyc | Bin 43286 -> 0 bytes ...ltering_no_ai.cpython-313-pytest-8.4.1.pyc | Bin 37635 -> 0 bytes ...st_saved_chat.cpython-313-pytest-8.4.1.pyc | Bin 1440 -> 0 bytes ...d_integration.cpython-313-pytest-8.4.1.pyc | Bin 30471 -> 0 bytes ...st_saved_unit.cpython-313-pytest-8.4.1.pyc | Bin 19697 -> 0 bytes .../test_minimal.cpython-313-pytest-8.4.1.pyc | Bin 7195 -> 0 bytes ...npc_chat_demo.cpython-313-pytest-8.4.1.pyc | Bin 6300 -> 0 bytes ..._npc_features.cpython-313-pytest-8.4.1.pyc | Bin 51231 -> 0 bytes ...c_integration.cpython-313-pytest-8.4.1.pyc | Bin 11375 -> 0 bytes ...t_npc_summary.cpython-313-pytest-8.4.1.pyc | Bin 2749 -> 0 bytes ...test_npc_unit.cpython-313-pytest-8.4.1.pyc | Bin 14068 -> 0 bytes .../test_summary.cpython-313-pytest-8.4.1.pyc | Bin 2388 -> 0 bytes .../test_sync.cpython-313-pytest-8.4.1.pyc | Bin 105657 -> 0 bytes tests/conftest.py | 276 ------ tests/pytest.ini | 26 - tests/requirements-test.txt | 18 - tests/test_async.py | 749 ---------------- tests/test_auth_retry_fix.py | 184 ---- tests/test_filtering.py | 410 --------- tests/test_filtering_functional.py | 548 ------------ tests/test_filtering_no_ai.py | 345 -------- tests/test_list_saved_chat.py | 35 - tests/test_list_saved_integration.py | 222 ----- tests/test_list_saved_unit.py | 197 ----- tests/test_minimal.py | 48 - tests/test_npc_chat_demo.py | 107 --- tests/test_npc_features.py | 384 -------- tests/test_npc_integration.py | 221 ----- tests/test_npc_summary.py | 81 -- tests/test_npc_unit.py | 131 --- tests/test_summary.py | 62 -- tests/test_sync.py | 825 ------------------ 43 files changed, 326 insertions(+), 4959 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/gumyum_models.cpython-313.pyc delete mode 100644 __pycache__/gumyum_npc_client.cpython-313.pyc delete mode 100644 __pycache__/gumyum_npc_sync.cpython-313.pyc delete mode 100644 tests/__pycache__/conftest.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_async.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_auth_retry_fix.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_filtering.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_filtering_functional.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_filtering_no_ai.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_list_saved_chat.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_list_saved_integration.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_list_saved_unit.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_minimal.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_npc_chat_demo.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_npc_features.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_npc_integration.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_npc_summary.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_npc_unit.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_summary.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_sync.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/conftest.py delete mode 100644 tests/pytest.ini delete mode 100644 tests/requirements-test.txt delete mode 100644 tests/test_async.py delete mode 100644 tests/test_auth_retry_fix.py delete mode 100644 tests/test_filtering.py delete mode 100644 tests/test_filtering_functional.py delete mode 100644 tests/test_filtering_no_ai.py delete mode 100644 tests/test_list_saved_chat.py delete mode 100644 tests/test_list_saved_integration.py delete mode 100644 tests/test_list_saved_unit.py delete mode 100644 tests/test_minimal.py delete mode 100644 tests/test_npc_chat_demo.py delete mode 100644 tests/test_npc_features.py delete mode 100644 tests/test_npc_integration.py delete mode 100644 tests/test_npc_summary.py delete mode 100644 tests/test_npc_unit.py delete mode 100644 tests/test_summary.py delete mode 100644 tests/test_sync.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0568912 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# PyCharm +.idea/ + +# VS Code +.vscode/ + +# Testing +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# Environments +.env +.venv + +# macOS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db \ No newline at end of file diff --git a/__pycache__/gumyum_models.cpython-313.pyc b/__pycache__/gumyum_models.cpython-313.pyc deleted file mode 100644 index ef2972bf978b5dc660f21560ea805e0fb947e0d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15915 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 Jc+Nra{{ZD1(KY}8 diff --git a/__pycache__/gumyum_npc_client.cpython-313.pyc b/__pycache__/gumyum_npc_client.cpython-313.pyc deleted file mode 100644 index 03b1fbc79af47fb9cb569957ba68f3057e7f5c3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71772 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 J4IK~n{{vp11hfDE diff --git a/__pycache__/gumyum_npc_sync.cpython-313.pyc b/__pycache__/gumyum_npc_sync.cpython-313.pyc deleted file mode 100644 index 1e3f23d1e5c9b077bbfeef5d754b9667c2736e4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58704 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 diff --git a/gumyum_models.py b/gumyum_models.py index ae0eb6b..5421967 100644 --- a/gumyum_models.py +++ b/gumyum_models.py @@ -174,18 +174,18 @@ class SpawnedData(BaseModel): location: str mood: str - stress_level: int # 1-10 + stress_level: int # 1-9 class NPCProfile(BaseModel): """Full NPC profile data returned from save/update operations""" - npc_id: str + npc_id: int # 64-bit integer NPC ID name: str profession: str age: Optional[int] = None sex: Optional[str] = None # "male", "female", "neutral" - personality_type: int # Enneagram type (1-9) + personality_type: int # Personality type (1-9) wing: Optional[int] = None stress_level: Optional[int] = None mood: Optional[str] = None @@ -198,8 +198,8 @@ class NPCProfile(BaseModel): location: Optional[str] = None purpose: Optional[str] = None spawned: SpawnedData - created_at: Optional[str] = None - updated_at: Optional[str] = None + created_at: Optional[Union[str, int]] = None + updated_at: Optional[Union[str, int]] = None custom_name: Optional[str] = None inventory: List[Dict[str, Any]] = Field(default_factory=list) stats: Dict[str, Any] = Field(default_factory=dict) @@ -218,11 +218,11 @@ class NPC(BaseModel): npc_id: int name: str profession: str - personality_type: int # Enneagram type (1-9) + personality_type: int # Personality type (1-9) mood: Optional[str] = None spawned: Optional[SpawnedData] = None universe_id: Optional[str] = None - seed: Optional[int] = None + world_seed: Optional[int] = None cached: bool = False cache_url: Optional[str] = None chat_history: List[dict] = Field(default_factory=list) @@ -315,9 +315,10 @@ class UserProfile(BaseModel): user_id: str username: str email: str - created_at: str + created_at: Union[str, int] # Can be timestamp or ISO string universes_count: int = 0 npcs_count: int = 0 + statistics: Optional[Dict[str, Any]] = None class ApiKey(BaseModel): @@ -350,20 +351,27 @@ class ApiKeyPair(BaseModel): class APIUsage(BaseModel): - """API usage statistics""" + """API usage statistics - flexible structure""" - requests_today: int - requests_month: int - rate_limit: int + model_config = {"extra": "allow"} # Allow additional fields + + requests_today: Optional[int] = 0 + requests_month: Optional[int] = 0 + rate_limit: Optional[int] = 0 class NPCStats(BaseModel): - """User's NPC statistics""" + """User's NPC statistics - flexible structure to match API""" - total_npcs: int - total_messages: int - unique_universes: int - api_usage: APIUsage + model_config = {"extra": "allow"} # Allow additional fields + + total_npcs: Optional[int] = 0 + total_messages: Optional[int] = 0 + unique_universes: Optional[int] = 0 + api_usage: Optional[Dict[str, Any]] = Field( + default_factory=dict + ) # API returns dict structure + source: Optional[str] = None # ============================================================================ diff --git a/gumyum_npc_client.py b/gumyum_npc_client.py index 4b81810..9aeeee3 100644 --- a/gumyum_npc_client.py +++ b/gumyum_npc_client.py @@ -142,7 +142,7 @@ class NPCChatProxy: # Build npc_params with the NPC's context npc_params = { "universe_id": self.npc.universe_id, - "world_seed": self.npc.seed, + "world_seed": self.npc.world_seed, "npc_id": self.npc.npc_id, } @@ -284,6 +284,49 @@ class NPCAsync(NPCBase): """ return await self.chat.chat_with_history(message, **kwargs) + def clear_history(self): + """Clear the chat history for this NPC""" + self.chat_history = [] + + def chat_history_to_json(self) -> str: + """Convert chat history to JSON string""" + return json.dumps(self.chat_history) + + def to_dict(self) -> dict: + """Convert NPC to dictionary""" + # Use pydantic's model_dump if available, otherwise dict + if hasattr(self, "model_dump"): + return self.model_dump(exclude={"client", "_chat"}) + else: + data = self.dict(exclude={"client", "_chat"}) + return data + + def to_json(self) -> str: + """Convert NPC to JSON string""" + return json.dumps(self.to_dict()) + + @classmethod + def from_dict( + cls, data: dict, client: Optional["GumYumClient"] = None + ) -> "NPCAsync": + """Create NPC from dictionary""" + npc = cls(**data) + if client: + npc.client = client + return npc + + @classmethod + def from_json( + cls, json_str: str, client: Optional["GumYumClient"] = None + ) -> "NPCAsync": + """Create NPC from JSON string""" + try: + return cls.from_dict(json.loads(json_str), client) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse NPC JSON: {e}") + except Exception as e: + raise ValueError(f"Failed to parse NPC JSON: {e}") + # Use NPCAsync as NPC for compatibility NPC = NPCAsync @@ -665,10 +708,16 @@ class UniverseManager: GumYumAuthError: If not owner of private universe """ response = await self.client.get(f"universes/{universe_id}") - return UniverseData(**response) + # The API returns the raw universe data, we need to wrap it + return UniverseData( + universe_id=universe_id, + name=response.get("theme_info", {}).get("name", "Unknown"), + hash="", # Not returned by API + data=response, + ) async def update( - self, universe_id: str, universe_data: UniverseData + self, universe_id: str, universe_data: Union[UniverseData, dict] ) -> UniverseData: """ Update an existing universe @@ -693,7 +742,8 @@ class UniverseManager: data = universe_data response = await self.client.put(f"universes/{universe_id}", json_data=data) - return UniverseData(**response) + # Return the updated universe + return await self.get(universe_id) async def fork(self, universe_id: str, name: Optional[str] = None) -> str: """ @@ -821,7 +871,6 @@ class NPCManager: universe_id: str, seed: int, npc_id: Optional[int] = None, - location_filter: Optional[List[str]] = None, ) -> NPC: """ Spawn a deterministic NPC @@ -847,40 +896,39 @@ class NPCManager: if npc_id is not None: # Spawn with specific 9-digit NPC ID params = {"universe_id": universe_id, "seed": seed, "npc_id": npc_id} - if location_filter: - params["location_filter"] = location_filter - response = await self.client.get("npc", params=params) + response = await self.client.get("npc/spawn", params=params) npc = NPC(**response) npc.client = self.client + # Set world_seed if not in response (API may not return it) + if npc.world_seed is None: + npc.world_seed = seed return npc else: # Generate random NPC ID from seed and redirect params = {"universe_id": universe_id, "seed": seed} - if location_filter: - params["location_filter"] = location_filter response = await self.client.get("npc/spawn", params=params) npc = NPC(**response) npc.client = self.client + # Set world_seed if not in response (API may not return it) + if npc.world_seed is None: + npc.world_seed = seed return npc - async def spawn_random( - self, universe_id: str, seed: int, location_filter: Optional[List[str]] = None - ) -> NPC: + async def spawn_random(self, universe_id: str, seed: int) -> NPC: """ Spawn random NPC using deterministic seed Args: universe_id: Universe to spawn NPC in seed: Deterministic seed for generation - location_filter: Filter NPCs by location types Returns: Spawned NPC data """ - return await self.spawn(universe_id, seed, None, location_filter) + return await self.spawn(universe_id, seed, None) async def spawn_filtered( self, @@ -905,18 +953,18 @@ class NPCManager: Filter options: - profession: List[str] - Specific professions - - personality_type: List[int] - Enneagram types (1-9) + - personality_type: List[int] - Personality types (1-9) - wing: List[int] - Wing types (1-9) - - stress_level: Dict[str, int] - {'min': 0, 'max': 10} + - stress_level: Dict[str, int] - {'min': 1, 'max': 9} - mood: List[str] - Specific moods - gender: List[str] - ['male', 'female', 'neutral'] - - location_filter: List[str] - Location types + - location: List[str] - Location names to filter by Example: filters = { "profession": ["warrior", "mage"], "personality_type": [1, 8], # Perfectionist or Challenger - "stress_level": {"min": 0, "max": 3} # Low stress + "stress_level": {"min": 1, "max": 3} # Low stress } """ data = { @@ -927,25 +975,42 @@ class NPCManager: "max_attempts": max_attempts, } - response = await self.client.post("npc/spawn/filtered", json_data=data) + try: + response = await self.client.post("npc/spawn/filtered", json_data=data) + except GumYumNotFoundError as e: + # If the error is specifically "No NPC found matching filters", return None + if "No NPC found matching filters" in str(e): + return None + # Re-raise other NotFound errors + raise # Check if NPC was found (response will have npc_id if found) if not response.get("npc_id"): return None # Convert response to NPC + spawned_data = None + if "spawned" in response and response["spawned"]: + spawned_info = response["spawned"] + spawned_data = SpawnedData( + location=spawned_info.get("location"), + mood=spawned_info.get("mood"), + stress_level=spawned_info.get("stress_level"), + ) + npc = NPC( npc_id=response["npc_id"], name=response["name"], profession=response["profession"], personality_type=response["personality_type"], - spawned=SpawnedData( - location=response["spawned"]["location"], - mood=response["spawned"]["mood"], - stress_level=response["spawned"]["stress_level"], + mood=( + response.get("spawned", {}).get("mood") + if "spawned" in response + else None ), + spawned=spawned_data, universe_id=universe_id, # Use the passed universe_id - seed=world_seed, # Use the passed world_seed + world_seed=world_seed, # Use the passed world_seed cached=False, cache_url=None, ) @@ -966,6 +1031,9 @@ class NPCManager: GumYumNotFoundError: If NPC not found """ response = await self.client.get(f"npc/saved/{npc_id}") + # API returns data wrapped in 'npc' field + if "npc" in response: + return NPCProfile(**response["npc"]) return NPCProfile(**response) async def save( @@ -1453,7 +1521,7 @@ class GumYumClient: # Initialize managers self.auth = AuthManager(self) self.universes = UniverseManager(self) - self.npc = NPCManager(self) + self.npcs = NPCManager(self) self.chat = ChatManager(self) self.moods = MoodManager(self) @@ -1689,6 +1757,9 @@ class GumYumClient: Raises: GumYumError: On API errors """ + # Ensure client is authenticated + await self._ensure_authenticated() + # Exchange API key for JWT on first request if using API key auth if self._public_key and self._secret_key and not self._jwt_obtained: await self._exchange_api_key_for_jwt() @@ -1781,21 +1852,22 @@ class GumYumClient: # Check if it's a token error that might benefit from refresh # The server returns "Invalid token" for expired tokens too + error_str = str(error_data.get("error", "")).lower() + message_str = str(error_data.get("message", "")).lower() + is_token_error = ( "expired" in error_msg - or "token expired" in error_data.get("error", "").lower() - or "token has expired" in error_data.get("message", "").lower() - or "invalid token" in error_data.get("error", "").lower() - or "authentication token is invalid" - in error_data.get("message", "").lower() + or "token expired" in error_str + or "token has expired" in message_str + or "invalid token" in error_str + or "authentication token is invalid" in message_str ) # Don't refresh for API key errors or missing auth is_api_key_error = ( "api key" in error_msg - or "api key" in error_data.get("error", "").lower() - or "authentication required" - in error_data.get("message", "").lower() + or "api key" in error_str + or "authentication required" in message_str ) if is_token_error and not is_api_key_error: @@ -1926,9 +1998,17 @@ class GumYumClient: """Handle HTTP error responses""" try: error_data = response.json() - message = error_data.get("message", f"HTTP {response.status_code}") + message = error_data.get( + "message", error_data.get("error", f"HTTP {response.status_code}") + ) + if not message or message == f"HTTP {response.status_code}": + # Try to extract more details + if "errors" in error_data: + message = f"HTTP {response.status_code}: {error_data['errors']}" + else: + message = f"HTTP {response.status_code}: {response.text[:200]}" except: - message = f"HTTP {response.status_code}: {response.text}" + message = f"HTTP {response.status_code}: {response.text[:200]}" error_data = {} # Map status codes to exception types diff --git a/gumyum_npc_sync.py b/gumyum_npc_sync.py index 9129e08..40ec730 100644 --- a/gumyum_npc_sync.py +++ b/gumyum_npc_sync.py @@ -148,7 +148,7 @@ class NPCChatProxy: # Build npc_params with the NPC's context npc_params = { "universe_id": self.npc.universe_id, - "world_seed": self.npc.seed, + "world_seed": self.npc.world_seed, "npc_id": self.npc.npc_id, } @@ -285,6 +285,45 @@ class NPC(AsyncNPC): """ return self.chat.chat_with_history(message, **kwargs) + def clear_history(self): + """Clear the chat history for this NPC""" + self.chat_history = [] + + def chat_history_to_json(self) -> str: + """Convert chat history to JSON string""" + return json.dumps(self.chat_history) + + def to_dict(self) -> dict: + """Convert NPC to dictionary""" + # Use pydantic's model_dump if available, otherwise dict + if hasattr(self, "model_dump"): + return self.model_dump(exclude={"client", "_chat"}) + else: + data = self.dict(exclude={"client", "_chat"}) + return data + + def to_json(self) -> str: + """Convert NPC to JSON string""" + return json.dumps(self.to_dict()) + + @classmethod + def from_dict(cls, data: dict, client: Optional["GumYumClient"] = None) -> "NPC": + """Create NPC from dictionary""" + npc = cls(**data) + if client: + npc.client = client + return npc + + @classmethod + def from_json(cls, json_str: str, client: Optional["GumYumClient"] = None) -> "NPC": + """Create NPC from JSON string""" + try: + return cls.from_dict(json.loads(json_str), client) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse NPC JSON: {e}") + except Exception as e: + raise ValueError(f"Failed to parse NPC JSON: {e}") + # ============================================================================ # STREAMING SUPPORT @@ -476,9 +515,10 @@ class AuthManager: """Logout by clearing the authentication token""" self.client.set_token(None, None) - def get_profile(self) -> dict: + def get_profile(self) -> UserProfile: """Get current user profile""" - return self.client.get("auth/profile") + response = self.client.get("auth/profile") + return UserProfile(**response) def is_authenticated(self) -> bool: """Check if client has an authentication token""" @@ -657,12 +697,19 @@ class UniverseManager: def get(self, universe_id: str) -> UniverseData: """Get a specific universe by ID""" response = self.client.get(f"universes/{universe_id}") - return UniverseData(**response) + # The API returns the raw universe data, we need to wrap it + return UniverseData( + universe_id=universe_id, + name=response.get("theme_info", {}).get("name", "Unknown"), + hash="", # Not returned by API + data=response, + ) def update(self, universe_id: str, universe_data: dict) -> UniverseData: """Update an existing universe""" response = self.client.put(f"universes/{universe_id}", json=universe_data) - return UniverseData(**response) + # Return the updated universe + return self.get(universe_id) def fork(self, universe_id: str, name: Optional[str] = None) -> str: """Fork (copy) an existing universe""" @@ -690,36 +737,35 @@ class NPCManager: universe_id: str, seed: int, npc_id: Optional[int] = None, - location_filter: Optional[List[str]] = None, ) -> NPC: """Spawn a deterministic NPC""" if npc_id is not None: # Spawn with specific 64-bit NPC ID params = {"universe_id": universe_id, "seed": seed, "npc_id": npc_id} - if location_filter: - params["location_filter"] = location_filter - response = self.client.get("npc", params=params) + response = self.client.get("npc/spawn", params=params) npc = NPC(**response) npc.client = self.client + # Set world_seed if not in response (API may not return it) + if npc.world_seed is None: + npc.world_seed = seed return npc else: # Generate random NPC ID from seed and redirect params = {"universe_id": universe_id, "seed": seed} - if location_filter: - params["location_filter"] = location_filter response = self.client.get("npc/spawn", params=params) npc = NPC(**response) npc.client = self.client + # Set world_seed if not in response (API may not return it) + if npc.world_seed is None: + npc.world_seed = seed return npc - def spawn_random( - self, universe_id: str, seed: int, location_filter: Optional[List[str]] = None - ) -> NPC: + def spawn_random(self, universe_id: str, seed: int) -> NPC: """Spawn random NPC using deterministic seed""" - return self.spawn(universe_id, seed, None, location_filter) + return self.spawn(universe_id, seed, None) def spawn_filtered( self, @@ -744,12 +790,12 @@ class NPCManager: Filter options: - profession: List[str] - Specific professions - - personality_type: List[int] - Enneagram types (1-9) + - personality_type: List[int] - Personality types (1-9) - wing: List[int] - Wing types (1-9) - - stress_level: Dict[str, int] - {'min': 0, 'max': 10} + - stress_level: Dict[str, int] - {'min': 1, 'max': 9} - mood: List[str] - Specific moods - gender: List[str] - ['male', 'female', 'neutral'] - - location_filter: List[str] - Location types + - location: List[str] - Location names to filter by """ data = { "universe_id": universe_id, @@ -759,29 +805,59 @@ class NPCManager: "max_attempts": max_attempts, } - response = self.client.post("npc/spawn/filtered", json=data) + try: + response = self.client.post("npc/spawn/filtered", json=data) + except GumYumNotFoundError as e: + # If the error is specifically "No NPC found matching filters", return None + if "No NPC found matching filters" in str(e): + return None + # Re-raise other NotFound errors + raise - if not response.get("found"): + # Check if NPC was found + if not response or "npc_id" not in response: + # If we get an error response or no NPC data + if response and response.get("error"): + if "No NPC found" in response["error"]: + return None + raise GumYumNotFoundError(response["error"]) return None # Convert response to NPC + spawned_data = None + if "spawned" in response and response["spawned"]: + spawned_info = response["spawned"] + spawned_data = SpawnedData( + location=spawned_info.get("location"), + mood=spawned_info.get("mood"), + stress_level=spawned_info.get("stress_level"), + ) + npc = NPC( npc_id=response["npc_id"], name=response["name"], profession=response["profession"], personality_type=response["personality_type"], - mood=response["mood"], - universe_id=response["universe_id"], - seed=response["world_seed"], + mood=( + response.get("spawned", {}).get("mood") + if "spawned" in response + else None + ), + spawned=spawned_data, + universe_id=universe_id, # Use the passed universe_id + world_seed=world_seed, # Use the passed world_seed cached=False, cache_url=None, ) npc.client = self.client return npc - def get_profile(self, npc_id: str) -> NPCProfile: + def get_profile(self, npc_id: Union[int, str]) -> NPCProfile: """Get full NPC profile data""" response = self.client.get(f"npc/saved/{npc_id}") + # API returns data wrapped in 'npc' field + if "npc" in response: + return NPCProfile(**response["npc"]) return NPCProfile(**response) def save( @@ -832,12 +908,12 @@ class NPCManager: npcs.append(npc) return npcs - def update(self, npc_id: str, updates: dict) -> NPCProfile: + def update(self, npc_id: Union[int, str], updates: dict) -> NPCProfile: """Update a saved NPC's data""" response = self.client.put(f"npc/saved/{npc_id}", json=updates) return NPCProfile(**response) - def delete(self, npc_id: str) -> bool: + def delete(self, npc_id: Union[int, str]) -> bool: """Delete a saved NPC""" self.client.delete(f"npc/saved/{npc_id}") return True @@ -967,8 +1043,6 @@ class ChatManager: content = self.json_data["choices"][0]["message"]["content"] # Split content into words to simulate streaming words = content.split() - import time - for i, word in enumerate(words): chunk = { "choices": [ @@ -1101,8 +1175,6 @@ class GumYumClient: if refresh_token is not None: self._refresh_token = refresh_token if expires_in is not None: - import time - self._token_expires_at = time.time() + expires_in # Mark that user has explicitly authenticated if token: @@ -1205,8 +1277,6 @@ class GumYumClient: self._auto_auth_attempts += 1 try: # Try to login with load test credentials - import time - timestamp = int(time.time()) username = f"auto_user_{timestamp}_{self._auto_auth_attempts}" self.auth.register( @@ -1247,8 +1317,6 @@ class GumYumClient: """ # Check if token needs refresh before making request if self._token and self._token_expires_at: - import time - if ( time.time() >= self._token_expires_at - 60 ): # Refresh 1 minute before expiry @@ -1400,7 +1468,7 @@ class GumYumClient: self._token = None self._refresh_token = None raise GumYumAuthError( - "Authentication failed - please login again", e.status_code + "Authentication failed - token cleared", e.status_code ) else: # Don't retry, bubble up the auth error @@ -1437,9 +1505,17 @@ class GumYumClient: """Handle HTTP error responses""" try: error_data = response.json() - message = error_data.get("message", f"HTTP {response.status_code}") + message = error_data.get( + "message", error_data.get("error", f"HTTP {response.status_code}") + ) + if not message or message == f"HTTP {response.status_code}": + # Try to extract more details + if "errors" in error_data: + message = f"HTTP {response.status_code}: {error_data['errors']}" + else: + message = f"HTTP {response.status_code}: {response.text[:200]}" except: - message = f"HTTP {response.status_code}: {response.text}" + message = f"HTTP {response.status_code}: {response.text[:200]}" error_data = {} # Map status codes to exception types diff --git a/tests/__pycache__/conftest.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/conftest.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 938d3dcf3977185d0cbed51d6874aaae8a4828a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10561 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 diff --git a/tests/__pycache__/test_async.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_async.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index c0444c01f102c29c148698feada9d082864d6838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102996 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 T?rSp)th< diff --git a/tests/__pycache__/test_auth_retry_fix.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_auth_retry_fix.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 26f997fbcca48caa70f1cb3de4b5506f562e902c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20028 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 diff --git a/tests/__pycache__/test_filtering.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_filtering.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index aa28fe3798591e6d5e3cc88e1ea2ca707f51f663..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40211 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� za<@DHN0|8@P$Dk4!E`@Hen%K#f|TnCcn^{O5HmSaHC1zBbbKTceiYu*5))EV7fD|l zeZH>#i7LtvJSRfgUJCEq*Ly|38b18+^UX(KE=f~Y!#Tf9v3d&f1=gD16QAZs1wK^B z>4_z;z94~u)V=w7DJCGu6Jl3{2lr7GRL@eIO|RBTFdon%@f0W+=^ZMtjRGdmK0q-` zc1?(~p?Qe2rhKUR?=-s{+K1?J8#24H|ITI0{Sl$FgG?3i9o|id>AN-Dj@=5sW%}Gg zQ#9Aqt2FiInocN9Cl=tctRuY8w(F}O%(fm}*tmUR<7PNMY5o9o!9C42)2HD1q^^1T z>f0?F-+1wr7qfLeH+@P=Pp(e?&DQl?f6+{7k$J$bnlQym(S^dyx2Z;ZW?RYx&(-z( zb<38S9belv*AmUvMc-!R5u zcf4H}eWwGxx)l4MwVYue%?$gf2+M%>dN&6TGd{?wA1{O=75VxzuLZ1^h5;*6V#H^HUFi@Y}m=#ukOV)2Sq)fg&BuS>x{VB8FA{XIO{9*p%uAYE0k0SHMqr4 zgXOIhW_q)YaIM;5-jH$S3mKa_2JxS*-b zx!YgfZ(ESlJ_p4;nVZ~=R(zB0rC$TMakcE|T>Y4?+CekE@L1}zpxpcn%h9P}d#%e@ zc>_1)(+`H)>6};uT2v0jZkECtRA+mV;%ktp z`c2r)Zp`=;4-6+1<8?t~bp@1hwL!xhXZU?+1h(}pp(*{9RrT|f4F_(nXzeejpH1#r zMg44+LO+)ll>U7Bxg~vTRsH-qTR+>q7_1HEXPTr`)X!!q^mFU|F`tJUP0tD=>1#Gy z1^B#TqP*0A7-rlCS@W`>V_v!y12pDs>==eTF;uGo-)Bo`4afHkQR*cqeZVzKgHwn> zwryQv7}%fCb)rC|ZzI5NtjRHPkMtb^e3t?;1HCYe07v+^cD+~nk4S0pyo-d=^qGG%!HJ_gf>Qw$;;WChqPJU%$PbP znLJn)&bGAR-x11IhyHzugNK6prZ@br`d9xl}C4bm%R7j6{WecTWIV10;&C|Sm;f6wuJZBtMTK6ti37rXw^WxST_b~>fGXZg4+`84> z!+>-qAkK@AJ2tty8I;Zh#d-0#BjP@;wSq+mPLaiD99!I-EIXYEjPv3tM~izCgVLFx zI4|~c9{yAPAMek(TNQWfV%0YH-i5CHi%vS9Ie(YVcb%N4ja^wh>v%wKGo7gpoEQ5X z?RxX*Oi-K`AKIorGo1;DGb-qEZ(xm}^Z6&=bKSB`*!N@5)3H z-Uw4NBd^uAxHfO1nkoofMMfp@nv~3ls^0|B=b6DYZI%E{3tALZPiAtQTrv{dRdP`P z)+_W@h9|}*C&rljaLsH_^3cUIzTbH+F7svKT%u#Y5)KL diff --git a/tests/__pycache__/test_filtering_no_ai.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_filtering_no_ai.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 5bea0881a98461cb634088949beff43dfc22698f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37635 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 R_PTe;QNL{G92;4a|35Hd)Oi2^ diff --git a/tests/__pycache__/test_list_saved_chat.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_list_saved_chat.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index f864a6ba08de2c7f7123cfb001edf6d19a175ced..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1440 zcmbu8&2Jk;6u@V_Yp-J^xTHcTMM48bC`O6h1|kY_+RN@+lGRe`WH8n4IRLHnhd z*@pFr3Q#$=hpHD2^i=gmPrY#Hp%T|haStp#6cDOqSw*V~Z^3+q); z1=%@<#F5U6?c#f%B1Fpwb&kA_R>!Ugqd{R%sFj9iFEXjH@~OfBmx@Swnf0(>Dix8$ z8=tJWs`(YY>mWFEqXOZ&u&M}h>qwc!` zh8yVfJq`r7F=4^`wS_~q_MuwmP;GIl9h7SBur;LwB#HYX4)^nVG{RkmQwi+-Sd1}w z7_uIxiUnEv0c8IB_uuZ@vslAXy!Bry_1bU|W4x@3DzOiB#-6Da#OO1@B9CFJ6!m=8 z8!j2oHAo9`!cpSYb>-_`93}zNwU=;yj|;e&ARE4M(EDGGa8XnaPmE8Gal`;q#W`9-whhv>Px2HIQNkl9+`q zPr~pO`ukN>tURn-7&#k{otcM~cSg>t5l)Sqca3mjUHH0STY@W=&pcPv+?JWl z3$e_dgepHTL2!tWyKrj}4O#Bet~3@%sLz3r5vJAVxvb(@8_5!v6V}9F!aj;D=_%}%yZ|a8hxD5EgC0wMtv z-|U;2g~jYTPHU%(;QgBKd++fQm?IX^8c^QkiFxFWO|*e+I+}mPF51aE^U;DMtXO!& zAv%sYMJEZf94&&d;sEn>-q04Bsb&uqX>RITgK`^vui2cng4vbwQd=y|A%30~Uv#e# z@1t>O({fNb^gQk5^1DwmOlU8|JZ&3l(Mm*J)y&hS)mqu0Ztwvqu0`$1NbS}_A#Qm! z!`XMKX`M2jGS(N&>_05T_)}si7LE#m7%#=7z<5*)M8Z;RL<*he#{wcBn-+zza4Il5 z5sJk^&qVmZ;NyLP=rf7_TB4J*LB~3t@bo4>Y zzUbssgvWg94I(PuD8{4@>gbo@|Mve?GfWMX^;Kp##VZ4vNBI_*O-zs(GSX<8J!H|` z)WsP&6KCFKQv8P*&N2b+(}q_~gBBL@;H+F8XXElYdy|nXXfg=~NZU$dYxYpS=BBQo z%G1hp1!FTKn1%}|CC5_kUCf~`FpOY^Tv+;6vxl6To4SHVt}tkB1TW`E+0NajRXJML zNU@lvquE-DLCa8y7EWDUQP9j4V~!4{-N=>fHm}Of8Z-^LDZOR~t(nrJWe+gNpJ$i? zkop?DdbO0Oi?*3n9OMm`Q<`Aja0PYmO6f&(wk{QP&@k2jvU72z!+uINw28VjTWcwM zAaHCm!wd)L+ab`%l}|M@Tt>ra32B}o;71obL%)|P_G1RFlym3Ar7ta?j9douR&nP^ zi>>L~Yw_ri{>kJe9IZX~Uu`o^YJ7`SJh90^s$nG?7 zRE#((pN#B zcf1tlcb3PE$DvQc9#(J)7M*4~X)dZ}9=Zq3+(a~U^Fyeaft=b;HodAUQMKJ<)2phn zpuGgv)wc&xGo3ExVE&AX!xd>G zea4YzcvWnW;a2W})W3CWHv7Ix{TAZ3rAE=B9LCKeDrssfw-3zjn`vb-F*DvY@@}r z&8%CGS{vQoWFm9hBCdhk5iCm0bW3t*u_ZKDYNuwp#W2%-3F>R7R!`J5)Ja{M%{6jO zTyvALdF@pfmu^Pl8t$TT)$!nAT@z@@Y23QX{)4pzb zN@>lmOL;FN_DtbY;sp{|sh79;mHk-85CnuS@C9 zT2noglIv`;lF_J)>*96?%Tl9Jc@ElGPSc{cHW~>wSbr&ie>ePlL2E2)j6>b)j6)T= zaj4=GG!E^_smIS|9IBwTKwD)fn^hDw7WD9YHpZc~*QSFN8{?2NqJoU>J`UwF4}C;w z|6C8ZceioX&Xf=K{`^5-5mT~aw^67J`ZMmDRnk@fJ7lm&MRv%XsoBCNnD_Q#e%zy6 zAF&&`10;WwusOIXQx6r~Ow(J%n~Up*`Nv&mC(+rABkm6~j=1iiduad48H+N?YP)Gd zAXpj<>;baqcWEmh`$-lf??BKFBkw_st&P0vjw;#DfBv##nwQqmx+MxwO72i<^sC|y zb4P+zsnM@GhtaQ^rbX@4=m&E#+<90r0tSL6*nNO=7edV%JGOnRYd=^+(_X24pxvMV zsr}#n)iKxF>kTlcEH>t#bMRNhe1n)X#2g@|4Ns^@M!_=9fIn~03Z`lBKfyE%{*0La zVP|x+LqFvZc0=08J{`;rwV8I7`*gd=Zpglea5rTCeILhFiP9FH)i|!kf^mHRy6qtQ z?-g|p`f=~m9b}KsGN^OL!S542lZg3L6`pWdPs7dh9Ey#>lPKd%k7+YAZs;_P8|shF z96A9faRMBhn;-{s0^`%dD4wnfMZ&SOfx3`%Rv4|nNBF9@);rI$%3hnBs~`u$6DLkXM8JY_VZ^#c!Z~EG&(sr zQ-;TV0vtIaM7RwK;nTb*@iPJR;&~x0`E=%zdE+4=2012ell)kiKOKr(GbZySK8Def z1+nlXKQhIO;po^kQ_?JiCi!H+7%z>A;VCR((h{5CC;1QXTu*p6tfPl#T?_+Bm?fSc zOIn4g(UI`jj9p<4&nkU@OA6P_$%5$=lki@I$tEM>sgq>E=(H4zPL5zscXojYcrg~( zS~qoeXT4dBi3!Rh;vz=Uj3>d;1htDY502H$D}sV@ zp`h|6LK2)K6ZPzic%%S1-34`_o)eRRZEIKx2+_f#8qY*xt zHyVY*a&V~4Ci2o$RDf7`SWLt{&pU^7V$UFr47Cdy)HKOUQs@+KKL?G9q-{S3Ok<4_ zkr1Y@8Ip#*Ab`)J0H8({0f0jj0MSU$3*{9!Qm-=TF9sxXc7w7oMjUrEMg0=j!sMq&q_1C<7Wfoq0><@9ODC{(Fhz~1SOc77NMs~<^z|| z{Ghj)DSw;<;mc4U5)j>7YTNVAtP%SN_7TRXgDhG6c1n;b` zNfz|tsE|6EX%0=tCQz48frxl9nNK@M6fRUtj89s`kZ_6@aZ@E}k)}e=3DCzvm#=i2 z*3i@xFN`JaT1PL!31dctk4^2~HN0-ZXfNCqG&X@^iUu7t#7*@!d|osNu+ zi2Rf|aw-yiCKMrka55jxo{sYvoOJY(?ijlLeo>5yN!JLRPn`_KM))&Rk&r+Ru8KJE zNEVO45EmhdVIL9_dD2^nIJ1NjkuE}PNB(fJm^58z(i73}C@&@RQY}&JLuw0=chWk> z!--W8!)PQb@%3B82)-<&avB*6#X@2Q2~&G~7+IvGAxve4?T>FA`i3|iAtX;1U*B046amItzUlAmdwh)(j& z9n=huMN4F89Kl)G2o8$MJWBiqaLvNMGzjOKKVY6`es5zOm~USu2FtjcmwY`-Rokz3 z%kF`jdFFEaY~iwaO~(?|QTKUw8lXMEpeXXD8!jb@8$%6D3d1_AONeE^oiI{c`iA z<|TjalDlH=i3^`z@>X1&xiGV|sqSj=)z)jStCqOGX*r+qZC_?AKF1#zi`y~Vzsxd) z6$v&VvwwjXZzlFRnCpR);{m5 zjkC3juG$2f_9wHouXL+n={_ZRk*%E@)l=wvD#dHQrs7BH$BCTl);ff|VKeU%Y5P)a+)g*>cD1VWnonuWy=nb;a4PMORmXP5YDCF09#9 zSh`OMUSzxG!g>mwPo;Q$lb(Y57TB(MrW}`C{)B6b?AmhW2^o7RsQ=o8t66q6gFE5s zl!+QHP^Cx_5cQi!;$}sSh&xSBA^aV-6Qttbe7WmVSHjmU`U30EZTB&H*}}V`wd{u^ z49u4_B-nrLOmz3Bb09K6-m4=ZfMS8eXyUuXVs8w75(eOPNEzU@8u?oO?tx8GyFQ+I$w{1edh zms|f-OCtWM*@dOKvEK-xe)Em}CX3>?fgj4W9~dXLBV`w-Y$zTH_gWQubIM! zb&y`&fo4L`5By?;em~fdVgKgR-u`aWjsAAahCK01~#SA=6WvcvTk^4yc`R>cO{x!04p4om2{Pgp|lnpnbu zzd|fwZfM;nu%CK`#oU^AC?jLfy9Y?;;@AGaRlDL5~jK(=C2e*2EEe;mQXj(!Gj1Z9vHDc z=~zOd1#4mny&G7<9IzuFHMU z;7$*~uJ6Vd66okyHX5hilaiChsj)!gK6$sR+|Ms0M5V%kdSTNVsMKDiht-?GO|-WK z6ly>{DS$`)S3DGj3aC@BhASnAzPnOW{9b+k%)t{M8M{ZId!h|^OO~!YcaHBwJdH9# zuIttxS;tp;klsw?u{AJy0S-uKwj!r@1L_4Z#TlzK8g3j9C#~btaO9mA3n6o{2o1P) zlnMXxNie41goWrvuAG>33KxjbI}y(|hP5J5l$L5YX~a1DxE(T|t$5+sLM zkFFhP;MounZyOW==Eo4A13^|O$PNPLz#F6p#zX8t7f!0g-DvDV1Mk`qd(go9b;NyW z>_-FDgn%J543~(YO>qE?gJ>K^;|LnZ&^QjpjE?}EV*KcY09d0_XDQYRAY}U#6vSgd zc0zm{V+^5zfQXrVE_609fdI2p;DzVCeIKCo>Wu(*|}ldK0N5nlqDWW&lfw+7Y&TZi~6#6Ss3-^yng0D4#%M_IhAdA_g( zfgX<;EZ+BQj+YBwEKrUqF2T9KmFCEueX5;5yt}0N1e|q{HLC_|%1`67KD?d;7e5#|ECmRzQi%2DpW?7ZY8m3l-jJ{m7N22hT_ zW)z2~{Nl+ACll^^*9KoNYllh z3q1+f4%xM1$>YE3lM8nNaOusy_=zV0nNr#aWkL`MlnKC@b*~N1JKN*7_C;rV!j|?Y z+u9MFnF>qyDZz`j_PIJeh0do^yf&z(puPoLdpuK)6cECeynN)vBMG)rW-I5$7ul^8 z3X<=9nSGJ{hU3Fq@S{P1^!VfQQwD|x-jVPt;v2`=-o7s87v-+rzIO93TCBwCnBIY* zH~QKwirrPxd%$kOsk8dzfeYuDx9u?b{d4^^5q#X@RC~pf>4%8uC+o@!mjr|M6GTD7 zFiVqCFT|~;3lymz$$=k38g@{sOh3(~UF(IPR^&%=N>qCMnpyF{<`jOYl81hP(>%!! zjcBnwluolV#fHTZ_`#-}xb(pWu$E6oF14R>Qn>?ubg?pWr^N!U;76o#;tIY+bm?d0 zN>^hBMJg(HxVfN}DI<5QV*{7@>%|HhX*~f1Mfs5&lXjh@E;fGLt$1MJ+(ctj8!s3W zjKg>-1bH->31+S!dLn4)J_N^y$7#t@mv3oh!*9hE5Bx|D8??gZaD^xPwX5Mc$9WN+;$U9tx;k49+N5qB z)7(?n)tTXt9uNIv6|Eo5UaK!mT_R^3{AY4q*UL6$uz)P;t(S)RF4uIG%yi#%sSlQQ z;36)(G9vKY1TVlfMgek52;Sc%DXV)bs z7SZa9#iuY-TSjU_??+i9A}c=m%8E~xYdR9Fk!Wi$%4$p9%GDM{9CXb^C!9Hm{3iTM z{{S6ghFdiXS6!-6Id?AJG_dkg4SU9WDc*OMZ`?|u+(5DRtrP%SUIDxHBI?!-AlsX4 zCbTLpR=&U;j6nyoRGS=C{f$=&m$i zcz~ES1`OF+dWwcBv?5!+kcR&f8EF+q6JHZn393JW76(>oxq-+ue6f!;_&fA^g3 zCYRJNT;eTv{8&|eMKW(>WGp&5GLp2>n~KOfX|jAoxu&Q|xe5nvA8Hc$=kSgxa;FsB z4~O^A<)y;NiYnX{MQ%1q=8?Pf!qKEPc6JJu*F_WXl`K|nq`~`QMpAbXLFd9Kt&%La z;(RaZ)RB?(#aA%YJQ)(7O%~`%Lavx0i;p5U3zPz0Rt6<8iPLaH-joQ$*O=Szj#1i~ z?UBjo*mQ*7D}E2ca7rQl7K~++!C?3eGw?fx`3m#p!vA6%zh=C@V+Lhr@YhTS)VI<0 zHQ$$f2}7xDC|xq-DV{RfP3s$M4o&1PP0z<+RBtm*C>;?%CaQ&z`+|_MCIE=<~S=T%iyCWvqS!A-}|oez6+JED{?b zZxM-5B5{(fm!mfJZ9m1!Hj$iiP^YNYxTuTO2vj)l>Mz$jGQayPO<0E{?z~VGccYGB zem^p1mD!gLo%eNdD!l9)?i(-d?n-GX+?~DGf2xd@omxlNv2wn*{8R<4V6dZi{i#a2 z;nYUD@zf@|NhIe>`**8-SkwMaYfgnFXEo{4ifN7HswT8n5+JRUN+8{Qev9PZcaD(A zUZV24npU5$?mx1;rv`Gqrore-zbBxj)+~iwT|sVhL9V`mNFKB0sBP31@J{XvC1YtZ z9hK9vIFh1bJSL~Za^zxkSR9#14y9wMWF#I-UluRL(qpNKw0QK{r^U$7P*j%hA(?#v zo5G(-4ZWy1#v|#Wu>hy=r=gF4UGbirNIW-@I3ABhlWE1?_v~@SBaKHcC8NV9BI(GG z^)68?Uxok02OzLpVRGIggW3clYjm=cM6yYCiI*IbQ*udyR3f>9cF7}oC121bl}di8 zOj;+EOBK?3t)>!d+HE_zBsZu{Jt6(Pb#N7KAR!`c=+ba(BxyTW9=GdwA<{4Cu=(BZ zF<~9jC;Vw5Z5)I47rD3WeGV^4+C#Q}pN?gIOPln&6Um@oFAv%E(j+gfPYr||`<@~s zHPWmhoaBa~ufZ}Mzu01f39kw{J2|QP+!LL)F72MBn!)u(E8zCh%M_aZP&K@zV3bWu zu?cA)gQa?@PP0yBq`IIrxRM}R< z=5-FNi0RNVrP?6h<|HKPlIo<*A=gfh<%GdHomZB=HtS{PH^{A^_oaVFIceiTF3BI{ z#-q}fY7(+n7!QwR$zZ+SI#i;!Nd3LemEpgI6(>sdYvTWEq(sz5)yLFFT$Pk8YYp>J zZ{bYw*)#5MMNGF&mDCWlvl;NLVFru`tKN$`1LJ>9b=`RGx@BE&TholKX2p2*yD?^JH7mww1v9j5MZOy|ByIoP z=_j9wexnu8zulAusYz-Md9>B(T7zu3bSm_3T{fgZk{{frOTxK-)%PCrDL?!Xti>Ht z%bGZ|nzh)dkE+vT%$Dx^Bc55`*Lk? z_#3O(GgMu$-Kq<&_;fDSWqw7>)pO9)e0FL(?dPxgloOH830ZrVcxV{*nG?t?G z1gKG*LlHS0k1ForXgW$0F|f_wL!0+4yCTF>LlLz9D|{lA8dkh=nu66o9FJa%#vk}_ zoVJN%>|&J4(b(Rd(8i@WVa#xB7{-nypwn@h8i6L*N7*>wX}*MT`to>GamlJLK=J6k zu$FQ(Ivo2MaL7XSZg4R~Mx*y|LsA^+)TJm@TtgFZ8x!(9G}y;>%|eZRG@9<46o7_y zbYtA&9@q{Zyv0rK+}1pPxg#LEU(o>Zpoq}UfZUAVy8?2+{TJdvwRBHFR_yU;@)oby z0SA*^m~>#W=Rs*imZLPyI_N>v@yk6!K(28iksa{AcnlKNl@THpAb}$wwN5?CWAP~fT>PD0A?pam2Q~)n9ODrhM+ikurSy{o3u_hNCw46q{0uu@|^6l z(AcGS4}jUdE?upxZ;(I`GsQI)k%4bCz%zm=Zeg@VBfU%Uz}g*;M{(iF6sHyhnB74U z0S3Vhsw=KQd~ey!P8q?_MXM(IOVztyAjH1Jv;!>iukOce;N%1AX4GW{n;~+XwMPONYBvN#)Djpr(Ayj}>>Pj1Eq*@S^BBR|2bkw$L!( zqvKQlCvcnL)8y9zDXq#>?a29Bt~|LQkP`og)phfm8s|4R%{OlQP;8#BZ3f`Ji?1zT za1*h8epB83jb2~rLM^G;%n%!)e!jW=Csmn&P`2jm{FWf%_yxeJ+qllXV6UolUODsY z9^&?2tNm8(^^xi5Y)RuyVP0&T73vn8WJB%S&);}HU%4Y&xg%HEn)kO(@laf{E-y4^ zh31>pvqI;uousVddgr$50&_y$>#|XnEy$_GcZIs^ zomLDz2Tk?vpy%nM295zh@%!aHKkUqv_hf{g*JXGp=llrH`Frw0!6%FLMwulyC-guw zwI&DfvX*y*9=PrGo2HJ>S8RBD-;I44Z_~^Mc;4r|+UJh9>H0n+rvh_O)?t)o%^X%N zMV`eTt;m*~QJnKO&5PS%@He}sJ94n&c$?-dM5uT$(3{;xlfs-@eAnAFT|k(g1HxvU zDh>=~y+5@O?h<4r;|;3g2LYO^2u<>m+UJhYGW8WTHwz$ij}!Ds}) zoX}!)AI$sfv;O*w5S)3AC0TzkFKC}TLNMd6SF^K15J$k8oTV7xndgiK1~4ZCjUiwa zH)Z`z8KL#&E|z5dt$9KF+!0zc{w6g$E41PeSd+691AKFr(ZB%agjQpSuDrh?>u<;i zT{9_`Wc^)vLHpbhx-$L-H9ISG;SgApvlIh7lQJ3@z?{(ap*|0P+SA;kPjk!6SrDd~ zvrFWNO*2YQp&%K>AW1W4tumy-R!RE9P4g%FiF;e7r14{cl(l_Q0-3pz#^1}kVDtL= zQ7*BcyKQew?6BW%;u1~H+s!ZTfy_snOQ7N-C<1tgJJI9%sM7@{L(s^$@bHG`w8b3& zz2JVq^MV(G`7n@=oZ^yjco@WE2)p!nq=Y*Ju{r}roI}D}Jd!HtXy$NAj zkUQiFc{}Y=X*Kz>khF!kVv$e~g<>-!hjgz|(mrTJNSTo|;3p_qZd1@An!GyQ{YF%g zUV^bklAlF1m07K^(mIQGs%X4~&V&8X6^m#xM=^ZX)=>sgWn!0pC;Hi)#j2RCi^fka zZw>zOb&`EeRHA>}9H%IiCPxkG&@*bbDoUN%3aGNLh;91yR_fRE>y;Z3O%R7?iD)tc zH7%nSkJ~90(d4uE^f+dl{!TyYze_HVuFB=o#XO|u9Xw37HdtlLLTPbs8u2SDVty7* zZZtfkpEoBbQ$!EY)F&d@Jz!0khePq&eCYFpPB5|}yE6yh)Gy$es2kQhZwy9(qLc!@1 zFqL#2CSVgK0mo9y25fz*Wuvb^=Rtkv4ftPt0g^*@&9rKVI7p6ZCJ?_pV2eYYdJXXX z*kZ}0YXT6RO$ws3xe(V#1k&eZc6eLi&}fJIF8nW|+2IhU4n1INWENTqwr_F^P3C-r zj)EcR#Y^~2B`B7W1W~-gY#ej|GRmo?h7OJQVU71;mF1=dl};qX*v2f;Ejvl*ZxKcP zw`c{KuMjg{@pp@8|9aK#x!8KD5Yp)tO8I_6$t%wPAHf7vef#fSH z(Tbptuk?IY$`jDzVml;^&fnO$l6o+W)NGydyzZZ`6!VoW*~*q&Wn13gHpRoyXv%Zn zPRfG6WB2xIeH-_Eu7&?QR~ttgp%aQ>QOrO`QSYH`n4rs_LgXW%Conk%3D^`LX7xlz zLxC}o9>tc&v1PfkPPLzhkue&lE?_mzr{apBxiOYf?7$n9l3uJljma5I`Y?G0lV>sc z0w(>KFsmI#cAG*U@KM(F%UCL7g4!nyVR9CeFJf{Iljkr&@ux3gf)Yk4B(e_@)2epl z$Pv}3wtaOoMkk>;6W26I`hzRvH%{Vc`>*m1Z*0HP`+j-t^kA+$aHZF@tZkd$y#2!h zYuSAtn#$~MG?jVLRJOR8I4VBjosRVjWyD)?ZU49S-+1|r$(*O*$_X$CdHqxAZ|}|U zRnr_xt{%$sRq7|lS556Taw;&#SH0e0lw}KYYVloYm9}E&IcTbP2R%=7295zh@w~@> zox9qd;hXcGty#}j0A^ldN!HVxhf?;r!#8Id@}|l{97NnV=%=RPe}+>Nn3C12_~};ig>10Xl%C@(5%+%cq}Px1F1gI} zFgthRiOw9Q=m})Nh!xjqi{OdQY=!&Jk_*oCMk_ec`B%aY8S(8aVjAyVNoCqO#Ex%Q zh3r}sJU>{W6JzPC_*vCB{pcrZh=2CN8HB()D)lf|MNU`7Tyblhs|q$(>;F7ge~7t) z_2upLCLJLctcOabc{xLl{^I9&r@037c?ZX~H?U!{W5CcuF)#r=s$wDv{%FuPY4oLu z7>zCqc8L87R{W&<89EL+6H_ty(@w$;=4hrp&;>mVNr8{JTMY^54sZ{g;8RYf(l87QK=?EdG~76- zAu2BO6T?75(5-Rd>VE_N7rzYNPF?vNG^+by%U*iHTXm=fvJ0((du=wb2@a5c<4K~| z!n=?qCagoMWiV)KH=Y1X^<@$C1^m-P7ew+IDf*m7A4T2$^ruJJ^gXLa*#MUK=~$!0 z5{u*Po2)_61b;ou<1%4{H(edzu^k#ycn~%?l3{*{rgfJv%?($DvXg;K5rr%`c-i8RHMP_{6&Nkv zf^Ou$f`qBtANVS+ed$|YdS_pz?NrX!J6~S;cJqzqeEIfl`Sy1={PbkD`E;h~Os>2y z@9oQY`#!Og($gGhXr=3yHfz7pe)GwnT*_`enQ3?`SN?R~`*g%H$FzGpZ! zJLl_ta9<$4Q)d46GODnzRv+u&{$}ejzx_7hj`^IoIUZq0Jrv*GUUjU&e)}+YOmx0S zc!1yIOQ8Hc9|t(^`Q69rT<=x6utemrw!wXDr|Z2|ED4mdxELzH7^=^FwHi5_1*Usn zhPDBLK95~6of}1(ehCt*i@S$14LC|-nLf7lS+tC7NE_jRGe0#dTsCK7YEcU~VGT(*a;pi`_X_-2yzT@g+ zGT(*Sx`@UF`^GZQ2k4Y8@DmvN>ME$L0iD$xrzn-C7s8+pHC?k+QR>WA=(~`FHBklD z0Zacysj|!%K8xWps001R(z+;h%Uc7L!b+_|hGs04XnqsNpy??wIZ|B1GQ2MOY=d6S z!yAQYV_=ubv!oMj`4>Yj_Dcd&`kiThH*=gKUI6gBxkCczd!BQnmh+GIbLYR|&(ZHj zr*c)YyR0?vAH1AvqVoSpc2|@N<89LwF^$t7{L+Q`O9CbMOBkUN?Ud$TgESf5VV(ad zO>jzc!YS>wQ9e*M*)?G3?PeSu=t!#lIGPxT-wM%V(8Qq>r4WUuS&eA~1d)8NQ&5UkFky3SnqP)hy9%xc`XVqk)trF-c)E4#^TT zEHeyhs^A*N`v>f90+Wl76ojlkB&-&KLe-E}HNKYag0>2e!xjXsvJ=o=?EIl-*+3b+ zgh+<{R8^o*+hRS_HxV+;rc7}wW;?~0OlD&RWv`gUbP(&$WAdrC(E?&eD@6_ayYMexfNcWorT@*hF1MCqOyG3?_0w0*{E{z!h$VEx_2G9;fRaj>Z%x#QH+~zv3E>Q5Mzfid=v< z8K4H}8;QlE;V_-Tc0QyGu~ayaNDWWKqlf5C$f8Cg{~II= zc8=qIL5}>2kZ+Jz-9INK9}@d(!qKK#SOKkdnNoX%}MldI^PCC~hl`2K@z`**VOSEM&fdf~WW6JD$NT1}o? zpXJuyw;kr%7VX4Vxq#5eCu{*O@bNjWj;s6l2)Bu=Uf4r;@6=?@UiEXn92%?oezt6H su5AA-f8aipw9N9Y@ALJu{MPsRhFQMx7ko*^y?u^vTG&YVY9<-~3(&ska{vGU diff --git a/tests/__pycache__/test_minimal.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_minimal.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index b62c3b553f9a9deb664681dd2603cc9caaa70c6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7195 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 diff --git a/tests/__pycache__/test_npc_features.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_npc_features.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 92b334d25b9ba63670809aeceb32661cbcf6bd39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51231 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 Gfd2cmO3A45q?DVbXj&CmgZ_h<40Ghh_h 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 D2OE(b diff --git a/tests/__pycache__/test_npc_unit.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_npc_unit.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 520079c4044654f61e8496c7a955a4f66dfb4107..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14068 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 E0b+R}IsgCw diff --git a/tests/__pycache__/test_sync.cpython-313-pytest-8.4.1.pyc b/tests/__pycache__/test_sync.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index db2fb5aa4c926412223970e7e96cfe45712a37bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105657 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

2ysaAy9 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 str: - """Base URL for the GumYum NPC API""" - return os.getenv("GUMYUM_API_URL", "http://localhost:6543") - - -@pytest.fixture(scope="function") -def test_credentials() -> Dict[str, str]: - """Test user credentials with unique username""" - unique_id = str(uuid.uuid4())[:8] - return { - "username": os.getenv("TEST_USERNAME", f"pytest_user_{unique_id}"), - "password": os.getenv("TEST_PASSWORD", "pytest_password_123"), - "email": os.getenv("TEST_EMAIL", f"pytest_{unique_id}@example.com"), - } - - -@pytest_asyncio.fixture -async def async_client(api_base_url: str) -> AsyncGumYumClient: - """Async client fixture with automatic cleanup""" - client = AsyncGumYumClient(api_base_url, timeout=60.0) - try: - yield client - finally: - await client.close() - - -@pytest.fixture -def sync_client(api_base_url: str) -> SyncGumYumClient: - """Sync client fixture with automatic cleanup""" - client = SyncGumYumClient(api_base_url, timeout=60.0) - try: - yield client - finally: - client.close() - - -@pytest_asyncio.fixture -async def authenticated_async_client( - async_client: AsyncGumYumClient, test_credentials: Dict[str, str] -) -> AsyncGumYumClient: - """Async client that's already authenticated""" - # Register new user (since we use unique usernames now) - await async_client.auth.register( - test_credentials["username"], - test_credentials["password"], - test_credentials["email"], - ) - - return async_client - - -@pytest.fixture -def authenticated_sync_client( - sync_client: SyncGumYumClient, test_credentials: Dict[str, str] -) -> SyncGumYumClient: - """Sync client that's already authenticated""" - # Register new user (since we use unique usernames now) - sync_client.auth.register( - test_credentials["username"], - test_credentials["password"], - test_credentials["email"], - ) - - return sync_client - - -@pytest.fixture(scope="session") -def event_loop(): - """Create an event loop for async tests""" - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - -@pytest.fixture -def sample_universe_data() -> Dict[str, Any]: - """Sample universe data for testing""" - return { - "theme_info": { - "name": "Test Universe", - "description": "A test universe for functional testing", - "version": "1.0", - "author": "pytest", - "is_public": False, - "builtin": False, - }, - "character_names": { - "human_male": ["TestMale1", "TestMale2"], - "human_female": ["TestFemale1", "TestFemale2"], - }, - "professions": { - "tavern": ["Bartender", "Server"], - "market": ["Merchant", "Vendor"], - }, - "world_locations": { - "tavern": { - "name": "The Test Tavern", - "description": "A cozy testing establishment", - }, - "market": { - "name": "Test Market Square", - "description": "A bustling marketplace for tests", - }, - }, - "mood_categories": { - "neutral": { - "moods": ["calm", "focused"], - "description": "Balanced emotional state", - } - }, - } - - -@pytest_asyncio.fixture -async def test_universe_id( - authenticated_async_client: AsyncGumYumClient, sample_universe_data: Dict[str, Any] -) -> str: - """Create a test universe and return its ID""" - # Since universe creation endpoint has issues, copy a public universe instead - universes = await authenticated_async_client.universes.list_public() - if not universes: - pytest.skip("No public universes available for testing") - - universe_id = await authenticated_async_client.universes.copy_public( - universes[0].id, "Test Universe for Async Tests" - ) - return universe_id - - -@pytest.fixture -def sync_test_universe_id( - authenticated_sync_client: SyncGumYumClient, sample_universe_data: Dict[str, Any] -) -> str: - """Create a test universe for sync client and return its ID""" - # Since universe creation endpoint has issues, copy a public universe instead - universes = authenticated_sync_client.universes.list_public() - if not universes: - pytest.skip("No public universes available for testing") - - universe_id = authenticated_sync_client.universes.copy_public( - universes[0].id, "Test Universe for Sync Tests" - ) - return universe_id - - -@pytest_asyncio.fixture -async def test_npc( - authenticated_async_client: AsyncGumYumClient, test_universe_id: str -): - """Spawn a test NPC""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=12345, npc_id=123456789 - ) - return npc - - -@pytest.fixture -def sync_test_npc( - authenticated_sync_client: SyncGumYumClient, sync_test_universe_id: str -): - """Spawn a test NPC for sync client""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=12345, npc_id=123456789 - ) - return npc - - -# Skip tests if API is not available -def pytest_configure(config): - """Configure pytest markers""" - config.addinivalue_line( - "markers", "integration: mark test as requiring live API endpoint" - ) - config.addinivalue_line( - "markers", - "requires_universe: mark test as requiring working universe management endpoints", - ) - - -def pytest_collection_modifyitems(config, items): - """Skip integration tests if API is not available""" - if config.getoption("--skip-integration"): - skip_integration = pytest.mark.skip(reason="--skip-integration option given") - for item in items: - if "integration" in item.keywords: - item.add_marker(skip_integration) - - # Temporarily disable NPC test skipping to debug the actual issue - # skip_npc = pytest.mark.skip(reason="NPC spawning/chat tests failing due to server cache issues") - # npc_test_names = [ - # "test_spawn_npc_deterministic", - # "test_spawn_npc_auto_increment", - # "test_spawn_with_location_filter", - # "test_npc_crud_operations", - # "test_simple_chat_completion", - # "test_chat_completion_full_response", - # "test_streaming_chat_completion", - # "test_conversation_with_history", - # "test_chat_with_npc_params", - # "test_personality_consistency", - # "test_concurrent_requests", - # "test_concurrent_requests_threaded", - # "test_streaming_response_timing", - # "test_streaming_vs_non_streaming_consistency" - # ] - # for item in items: - # if item.name in npc_test_names: - # item.add_marker(skip_npc) - - -def pytest_addoption(parser): - """Add command line options""" - parser.addoption( - "--skip-integration", - action="store_true", - default=False, - help="Skip integration tests that require live API", - ) - parser.addoption( - "--api-url", - action="store", - default="http://localhost:6543", - help="GumYum API base URL for testing", - ) - - -# Health check utility -async def check_api_health(base_url: str) -> bool: - """Check if the API is available""" - try: - client = AsyncGumYumClient(base_url, timeout=5.0) - health = await client.health_check() - await client.close() - return health.get("status") == "healthy" - except Exception: - return False - - -def check_api_health_sync(base_url: str) -> bool: - """Check if the API is available (sync version)""" - try: - client = SyncGumYumClient(base_url, timeout=5.0) - health = client.health_check() - client.close() - return health.get("status") == "healthy" - except Exception: - return False - - -@pytest_asyncio.fixture(autouse=True) -async def skip_if_api_unavailable(request, api_base_url: str): - """Skip tests if API is not available""" - if "integration" in request.keywords: - if not await check_api_health(api_base_url): - pytest.skip(f"API not available at {api_base_url}") diff --git a/tests/pytest.ini b/tests/pytest.ini deleted file mode 100644 index e502066..0000000 --- a/tests/pytest.ini +++ /dev/null @@ -1,26 +0,0 @@ -[tool:pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* -asyncio_mode = auto -asyncio_default_fixture_loop_scope = function -addopts = - -v - --tb=short - --strict-markers - --disable-warnings - --color=yes -markers = - integration: Tests that require a live API endpoint - slow: Tests that may take longer to complete - auth: Authentication-related tests - npc: NPC management tests - chat: Chat completion tests - universe: Universe management tests - streaming: Tests involving streaming responses - compatibility: Tests comparing async and sync clients -filterwarnings = - ignore::DeprecationWarning - ignore::PendingDeprecationWarning -timeout = 300 \ No newline at end of file diff --git a/tests/requirements-test.txt b/tests/requirements-test.txt deleted file mode 100644 index bb6852c..0000000 --- a/tests/requirements-test.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Testing dependencies for GumYum NPC client -pytest>=7.4.0 -pytest-asyncio>=0.21.0 -pytest-timeout>=2.1.0 -pytest-mock>=3.11.0 -pytest-cov>=4.1.0 - -# HTTP libraries for both client versions -httpx>=0.24.0 -requests>=2.31.0 - -# Optional dependencies for enhanced testing -pytest-xdist>=3.3.0 # Parallel test execution -pytest-benchmark>=4.0.0 # Performance benchmarking -pytest-html>=3.2.0 # HTML test reports - -# Data validation (optional, for enhanced model testing) -pydantic>=2.0.0 \ No newline at end of file diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index 57ef317..0000000 --- a/tests/test_async.py +++ /dev/null @@ -1,749 +0,0 @@ -""" -Async tests for GumYum NPC client -""" - -import pytest -import asyncio -from typing import Dict, Any - -from gumyum_npc_client import ( - GumYumClient, - GumYumError, - GumYumAuthError, - GumYumNotFoundError, - GumYumValidationError, - NPC, - ChatCompletion, - AuthToken, -) - -# Mark entire module as requiring asyncio -pytestmark = [pytest.mark.asyncio, pytest.mark.integration] - - -async def test_register_and_login( - async_client: GumYumClient, test_credentials: Dict[str, str] -): - """Test user registration and login""" - # Test registration - using unique credentials from fixture - token = await async_client.auth.register( - test_credentials["username"], - test_credentials["password"], - test_credentials["email"], - ) - - assert isinstance(token, AuthToken) - assert token.access_token - assert token.user_id - assert async_client.auth.is_authenticated() - - # Test logout - async_client.auth.logout() - assert not async_client.auth.is_authenticated() - - # Test login - login_token = await async_client.auth.login( - test_credentials["username"], test_credentials["password"] - ) - - assert isinstance(login_token, AuthToken) - assert login_token.access_token - assert async_client.auth.is_authenticated() - - -async def test_invalid_credentials(async_client: GumYumClient): - """Test authentication with invalid credentials""" - with pytest.raises(GumYumAuthError): - await async_client.auth.login("invalid_user", "invalid_password") - - -async def test_get_profile(authenticated_async_client: GumYumClient): - """Test getting user profile""" - profile = await authenticated_async_client.auth.get_profile() - - assert profile.user_id - assert profile.username - assert profile.email - assert profile.created_at - # Note: statistics field not in UserProfile model yet - - -async def test_list_public_universes(async_client: GumYumClient): - """Test listing public universes""" - universes = await async_client.universes.list_public() - - assert isinstance(universes, list) - if universes: - universe = universes[0] - assert universe.id - assert universe.name - assert universe.description - - -async def test_copy_public_universe(authenticated_async_client: GumYumClient): - """Test copying a public universe""" - # Get public universes - universes = await authenticated_async_client.universes.list_public() - if not universes: - pytest.skip("No public universes available") - - # Copy the first universe - universe_id = await authenticated_async_client.universes.copy_public( - universes[0].id, "Test Copy" - ) - - assert universe_id - - # For now, just verify we got a valid universe ID back - # Note: API issue where copied universes don't appear in list_user() - assert isinstance(universe_id, str) - assert len(universe_id) > 0 - - -async def test_create_universe( - authenticated_async_client: GumYumClient, sample_universe_data: Dict[str, Any] -): - """Test creating a new universe""" - universe_id = await authenticated_async_client.universes.create( - sample_universe_data - ) - - assert universe_id - - # Verify we can get the universe - universe = await authenticated_async_client.universes.get(universe_id) - assert universe.theme_info.name == sample_universe_data["theme_info"]["name"] - - -async def test_universe_crud_operations( - authenticated_async_client: GumYumClient, sample_universe_data: Dict[str, Any] -): - """Test full CRUD operations on universes""" - # Create - universe_id = await authenticated_async_client.universes.create( - sample_universe_data - ) - assert universe_id - - # Read - universe = await authenticated_async_client.universes.get(universe_id) - assert universe.theme_info.name == sample_universe_data["theme_info"]["name"] - - # Update - updated_data = sample_universe_data.copy() - updated_data["theme_info"]["description"] = "Updated test description" - updated_universe = await authenticated_async_client.universes.update( - universe_id, updated_data - ) - assert updated_universe.theme_info.description == "Updated test description" - - # Fork - forked_id = await authenticated_async_client.universes.fork( - universe_id, "Forked Universe" - ) - assert forked_id - assert forked_id != universe_id - - # Delete forked universe (test deletion capability) - # Note: Delete functionality exists but may need server restart to work properly - try: - result = await authenticated_async_client.universes.delete(forked_id) - assert result is True - - # Verify forked universe is deleted - with pytest.raises(GumYumNotFoundError): - await authenticated_async_client.universes.get(forked_id) - except Exception as e: - # If delete fails, just verify the forked universe was created successfully - verify_fork = await authenticated_async_client.universes.get(forked_id) - assert verify_fork.id == forked_id - - -@pytest.mark.requires_universe -async def test_spawn_npc_deterministic( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test deterministic NPC spawning""" - # Spawn NPC with specific index - npc1 = await authenticated_async_client.npc.spawn( - test_universe_id, seed=12345, npc_id=6666666666666666666 - ) - npc2 = await authenticated_async_client.npc.spawn( - test_universe_id, seed=12345, npc_id=6666666666666666666 - ) - - # Should be identical - assert isinstance(npc1, NPC) - assert isinstance(npc2, NPC) - assert npc1.name == npc2.name - assert npc1.profession == npc2.profession - assert npc1.personality_type == npc2.personality_type - assert npc1.mood == npc2.mood - - -@pytest.mark.requires_universe -async def test_spawn_npc_random_seed( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test random NPC spawning with different seeds""" - npc1 = await authenticated_async_client.npc.spawn_random( - test_universe_id, seed=54321 - ) - npc2 = await authenticated_async_client.npc.spawn_random( - test_universe_id, seed=54322 - ) - - # Should be different NPCs (different seeds) - assert isinstance(npc1, NPC) - assert isinstance(npc2, NPC) - assert npc1.npc_id != npc2.npc_id - - -@pytest.mark.requires_universe -async def test_spawn_with_location_filter( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test NPC spawning with location filter""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=67890, npc_id=678901234, location_filter=["tavern"] - ) - - assert isinstance(npc, NPC) - assert npc.name - assert npc.profession - - -@pytest.mark.requires_universe -async def test_npc_crud_operations( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test NPC CRUD operations""" - # Spawn and save NPC - spawned_npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=11111, npc_id=111111111 - ) - saved_npc = await authenticated_async_client.npc.save( - test_universe_id, 11111, 111111111, "Test Saved NPC" - ) - - assert saved_npc.npc_id - assert saved_npc.name - - # List saved NPCs - npc_list = await authenticated_async_client.npc.list_saved() - assert isinstance(npc_list, list) - - # Get NPC profile - profile = await authenticated_async_client.npc.get_profile(saved_npc.npc_id) - assert profile.npc_id == saved_npc.npc_id - - # Update NPC - updated_npc = await authenticated_async_client.npc.update( - saved_npc.npc_id, {"mood": "excited"} - ) - assert updated_npc.npc_id == saved_npc.npc_id - - # Get stats - stats = await authenticated_async_client.npc.get_stats() - assert stats.total_npcs >= 1 - - # Delete NPC - result = await authenticated_async_client.npc.delete(saved_npc.npc_id) - assert result is True - - -@pytest.mark.requires_universe -async def test_simple_chat_completion( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test basic chat completion and chat_with_history""" - # First spawn the NPC to get its details - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=12345, npc_id=7777777777777777777 - ) - - # Use the new npc.chat.completions() method - response = await npc.chat.completions( - [{"role": "user", "content": "Hello! What's your name?"}] - ) - response_content = response.choices[0].message.content - - assert isinstance(response_content, str) - assert len(response_content) > 0 - # Should mention the NPC's name - assert npc.name.lower() in response_content.lower() - - # Test chat_with_history for automatic conversation tracking - response2 = await npc.chat_with_history("What brings you joy in life?") - assert response2.choices[0].message.content - - # Verify history is tracked - assert len(npc.chat_history) >= 2 - assert any( - "joy" in msg["content"] for msg in npc.chat_history if msg["role"] == "user" - ) - - -@pytest.mark.requires_universe -async def test_chat_completion_full_response( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test full chat completion response""" - # First spawn the NPC to get its details - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=12345, npc_id=7777777777777777777 - ) - - # Use npc.completions() to get full ChatCompletion - completion = await npc.completions( - [{"role": "user", "content": "Tell me about your profession."}], - temperature=0.7, - max_tokens=150, - ) - - assert isinstance(completion, ChatCompletion) - assert completion.choices - assert len(completion.choices) > 0 - - choice = completion.choices[0] - assert choice.message.role == "assistant" - assert choice.message.content - assert len(choice.message.content) > 0 - - # Should provide a relevant professional response - # Check for profession-related content rather than exact profession name - content_lower = choice.message.content.lower() - profession_lower = npc.profession.lower() - - # Define profession-related keywords for common professions - profession_keywords = { - "archaeologist": [ - "archaeolog", - "artifact", - "excavat", - "ancient", - "dig", - "history", - "past", - ], - "artisan": ["craft", "create", "make", "art", "skill", "design", "work"], - "brood mother": [ - "children", - "family", - "care", - "nurture", - "offspring", - "young", - "mother", - ], - "warrior": ["fight", "battle", "combat", "weapon", "war", "defend", "protect"], - "mage": ["magic", "spell", "enchant", "arcane", "mystic", "power"], - "merchant": ["trade", "sell", "buy", "business", "commerce", "goods", "market"], - } - - # Check if the response mentions the profession directly or related keywords - profession_mentioned = profession_lower in content_lower or any( - keyword in content_lower - for keyword in profession_keywords.get(profession_lower, [profession_lower]) - ) - - # Also accept if the response is about work/job/profession in general - work_related = any( - word in content_lower - for word in ["work", "job", "profession", "career", "do", "am"] - ) - - assert ( - profession_mentioned or work_related - ), f"NPC with profession '{npc.profession}' should provide relevant professional response. Got: {choice.message.content[:100]}..." - - -@pytest.mark.requires_universe -async def test_streaming_chat_completion( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test streaming chat completion""" - # First spawn the NPC - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=12345, npc_id=7777777777777777777 - ) - - # Use npc.chat.completions() with streaming - stream = await npc.chat.completions( - [{"role": "user", "content": "Tell me a short story about your day."}], - temperature=0.8, - stream=True, - ) - - chunks = [] - content_pieces = [] - - async for chunk in stream: - chunks.append(chunk) - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - content = delta.get("content", "") - if content: - content_pieces.append(content) - - assert len(chunks) > 0 - assert len(content_pieces) > 0 - - # Reconstruct full response - full_response = "".join(content_pieces) - assert len(full_response) > 0 - - -@pytest.mark.requires_universe -async def test_conversation_with_history( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test multi-turn conversation""" - npc_params = { - "universe_id": test_universe_id, - "world_seed": 12345, - "npc_id": 7777777777777777777, - } - - # First message - response1 = await authenticated_async_client.chat.completions( - npc_params=npc_params, - messages=[{"role": "user", "content": "What's your favorite color?"}], - ) - - # Continue conversation - conversation_history = [ - {"role": "user", "content": "What's your favorite color?"}, - {"role": "assistant", "content": response1.choices[0].message.content}, - ] - - response2 = await authenticated_async_client.chat.completions( - npc_params=npc_params, - messages=conversation_history - + [{"role": "user", "content": "Why do you like that color?"}], - ) - - assert isinstance(response2, ChatCompletion) - assert response2.choices[0].message.content - - # Response should be contextually relevant - assert len(response2.choices[0].message.content) > 0 - - -@pytest.mark.requires_universe -async def test_chat_with_npc_params( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test chat with temporary NPC parameters""" - completion = await authenticated_async_client.chat.completions( - npc_params={ - "universe_id": test_universe_id, - "world_seed": 99999, - "npc_id": 8888888888888888888, - }, - messages=[{"role": "user", "content": "Who are you?"}], - temperature=0.5, - ) - - assert isinstance(completion, ChatCompletion) - assert completion.choices[0].message.content - assert len(completion.choices[0].message.content) > 0 - - -@pytest.mark.requires_universe -async def test_personality_consistency( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test that NPC maintains personality consistency""" - npc_params = { - "universe_id": test_universe_id, - "world_seed": 12345, - "npc_id": 7777777777777777777, - } - - # Ask multiple personality-related questions - questions = [ - "How do you handle stress?", - "What motivates you?", - "How do you interact with strangers?", - ] - - responses = [] - for question in questions: - completion = await authenticated_async_client.chat.completions( - npc_params=npc_params, messages=[{"role": "user", "content": question}] - ) - response = completion.choices[0].message.content - responses.append(response) - - # All responses should exist and be substantial - for response in responses: - assert isinstance(response, str) - assert len(response) > 20 # Substantial response - - # Responses should be consistent with NPC's personality type - # This is a basic check - more sophisticated personality analysis could be added - assert all(len(r) > 0 for r in responses) - - -async def test_invalid_npc_id(authenticated_async_client: GumYumClient): - """Test error handling with invalid NPC ID""" - with pytest.raises((GumYumNotFoundError, GumYumValidationError)): - await authenticated_async_client.chat.completions( - npc_id="invalid_npc_id", messages=[{"role": "user", "content": "Hello"}] - ) - - -async def test_invalid_universe_id(authenticated_async_client: GumYumClient): - """Test error handling with invalid universe ID""" - with pytest.raises((GumYumNotFoundError, GumYumValidationError)): - await authenticated_async_client.npc.spawn("invalid_universe_id", seed=12345) - - -async def test_unauthenticated_access(async_client: GumYumClient): - """Test that protected endpoints require authentication""" - with pytest.raises((GumYumAuthError, GumYumValidationError)): - await async_client.universes.list_user() - - -async def test_malformed_universe_data(authenticated_async_client: GumYumClient): - """Test validation error handling""" - invalid_data = {"invalid": "data"} - - # API doesn't currently validate universe data structure, just accepts anything - # For now, just test that create doesn't crash with invalid data - try: - universe_id = await authenticated_async_client.universes.create(invalid_data) - # If we get here, the API accepted the invalid data (which is current behavior) - assert universe_id - except Exception as e: - # If we get an exception, that's also fine (validation working) - assert "validation" in str(e).lower() or "error" in str(e).lower() - - -async def test_context_manager(api_base_url: str): - """Test using client as async context manager""" - async with GumYumClient(api_base_url) as client: - health = await client.health_check() - assert health.get("status") == "healthy" - - # Client should be closed after context - with pytest.raises(Exception): - await client.health_check() - - -async def test_health_check(async_client: GumYumClient): - """Test API health check""" - health = await async_client.health_check() - - assert isinstance(health, dict) - assert health.get("status") == "healthy" - - -@pytest.mark.requires_universe -async def test_concurrent_requests( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test handling concurrent requests""" - tasks = [ - authenticated_async_client.npc.spawn( - test_universe_id, seed=i, npc_id=100000000 + i - ) - for i in range(3) - ] - - results = await asyncio.gather(*tasks) - - assert len(results) == 3 - for result in results: - assert isinstance(result, NPC) - assert result.npc_id - - -# ============================================================================ -# NEW NPC CHAT TESTS -# ============================================================================ - - -@pytest.mark.requires_universe -async def test_npc_client_attachment( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test that NPCs have client attached after spawning""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=99999, npc_id=9999999999 - ) - - assert npc.client is authenticated_async_client - assert hasattr(npc, "chat") - assert hasattr(npc.chat, "completions") - - -@pytest.mark.requires_universe -async def test_npc_chat_completions_method( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test npc.chat.completions() method""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=88888, npc_id=8888888888 - ) - - # Test basic chat completion - response = await npc.chat.completions( - [{"role": "user", "content": "What is your favorite color?"}] - ) - - assert isinstance(response, ChatCompletion) - assert response.choices - assert len(response.choices) > 0 - assert response.choices[0].message.content - - -@pytest.mark.requires_universe -async def test_npc_completions_alias( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test npc.completions() alias method""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=77777, npc_id=7777777777 - ) - - # Test using the alias - response = await npc.completions( - [{"role": "user", "content": "Tell me about your hobbies"}] - ) - - assert isinstance(response, ChatCompletion) - assert response.choices[0].message.content - - -@pytest.mark.requires_universe -async def test_npc_chat_with_parameters( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test npc.chat.completions() with various parameters""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=66666, npc_id=6666666666 - ) - - # Test with temperature and max_tokens - response = await npc.chat.completions( - messages=[{"role": "user", "content": "Describe the weather"}], - temperature=0.5, - max_tokens=50, - ) - - assert isinstance(response, ChatCompletion) - assert len(response.choices[0].message.content) > 0 - - # Test with different temperature - response2 = await npc.completions( - messages=[{"role": "user", "content": "Describe the weather"}], temperature=1.5 - ) - - assert isinstance(response2, ChatCompletion) - - -@pytest.mark.requires_universe -async def test_npc_streaming_via_chat_proxy( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test streaming through npc.chat.completions()""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=55555, npc_id=5555555555 - ) - - # Test streaming - stream = await npc.chat.completions( - messages=[{"role": "user", "content": "Count to five slowly"}], stream=True - ) - - chunks = [] - async for chunk in stream: - chunks.append(chunk) - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - if "content" in delta: - assert isinstance(delta["content"], str) - - assert len(chunks) > 0 - - -@pytest.mark.requires_universe -async def test_npc_conversation_history( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test conversation with history through NPC object""" - npc = await authenticated_async_client.npc.spawn( - test_universe_id, seed=44444, npc_id=4444444444 - ) - - # First message - response1 = await npc.completions( - [{"role": "user", "content": "My name is TestBot"}] - ) - - # Continue conversation with history - response2 = await npc.completions( - [ - {"role": "user", "content": "My name is TestBot"}, - {"role": "assistant", "content": response1.choices[0].message.content}, - {"role": "user", "content": "What did I just tell you my name was?"}, - ] - ) - - assert isinstance(response2, ChatCompletion) - # Response should reference the name somehow - content = response2.choices[0].message.content.lower() - assert "testbot" in content or "test" in content or "bot" in content - - -@pytest.mark.asyncio -async def test_npc_without_client_fails(): - """Test that NPC without client raises proper error""" - from gumyum_npc_client import NPCChatProxy, SpawnedData - - # Create NPC without client - npc = NPC( - npc_id=123456789, - name="Test NPC", - profession="Tester", - personality_type=5, - spawned=SpawnedData(location="test", mood="neutral", stress_level=5), - universe_id="test-universe", - seed=12345, - ) - - # Should not have client - assert npc.client is None - - # Should raise error when trying to chat - with pytest.raises(ValueError, match="NPC has no client reference"): - await npc.chat.completions([{"role": "user", "content": "Hello"}]) - - -@pytest.mark.requires_universe -async def test_npc_preserves_context( - authenticated_async_client: GumYumClient, test_universe_id: str -): - """Test that NPC properly uses its own context for all chats""" - # Spawn two different NPCs - npc1 = await authenticated_async_client.npc.spawn( - test_universe_id, seed=11111, npc_id=1111111111 - ) - npc2 = await authenticated_async_client.npc.spawn( - test_universe_id, seed=22222, npc_id=2222222222 - ) - - # Chat with both - response1 = await npc1.completions( - [{"role": "user", "content": "What is your name?"}] - ) - response2 = await npc2.completions( - [{"role": "user", "content": "What is your name?"}] - ) - - # Their names should be different and match what was spawned - assert npc1.name.lower() in response1.choices[0].message.content.lower() - assert npc2.name.lower() in response2.choices[0].message.content.lower() - assert response1.choices[0].message.content != response2.choices[0].message.content diff --git a/tests/test_auth_retry_fix.py b/tests/test_auth_retry_fix.py deleted file mode 100644 index 6b53204..0000000 --- a/tests/test_auth_retry_fix.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Test that the client doesn't send Authorization:[] headers after auth failures. -This test ensures HTTP 500 errors are prevented by proper auth handling. -""" - -import pytest -from unittest.mock import Mock, patch, MagicMock -import httpx -from gumyum_npc_client import GumYumClient, GumYumAuthError - - -class TestAuthRetryFix: - """Test that auth retry logic doesn't create Authorization:[] headers""" - - @pytest.mark.asyncio - async def test_no_retry_with_cleared_token(self): - """Test that client doesn't retry after clearing token on 401 error""" - client = GumYumClient("http://test.com") - - # Set a valid token initially - client.set_token("valid_token") - - # Mock the httpx response to return 401 - mock_response = Mock() - mock_response.status_code = 401 - mock_response.headers = {"content-type": "application/json"} - mock_response.text = '{"error": "Unauthorized"}' - mock_response.json.return_value = {"error": "Unauthorized"} - - # Track all requests made - request_headers = [] - - async def mock_request(*args, **kwargs): - # Capture headers from each request - request_headers.append(kwargs.get("headers", {})) - return mock_response - - # Replace the httpx client request method - with patch.object(client._client, "request", side_effect=mock_request): - # This should fail with auth error, not retry - with pytest.raises(GumYumAuthError) as exc_info: - await client.get("npc", params={"universe_id": "test", "seed": "123"}) - - # Should contain "token cleared" in the error message - assert "token cleared" in str(exc_info.value) - - # Should only make ONE request (no retry) - assert len(request_headers) == 1 - - # The single request should have had the valid token - assert request_headers[0].get("Authorization") == "Bearer valid_token" - - # Token should be cleared after 401 - assert client._token is None - - @pytest.mark.asyncio - async def test_no_empty_array_authorization_header(self): - """Test that Authorization header is never an empty array""" - client = GumYumClient("http://test.com") - - # Test various token states - test_cases = [ - (None, "no_header"), # No token -> no Authorization header - ("", "no_header"), # Empty string -> no Authorization header - ("valid", "Bearer valid"), # Valid token -> proper header - (" ", "no_header"), # Whitespace -> no Authorization header - ] - - for token, expected in test_cases: - client._token = token - headers = client.get_headers() - - if expected == "no_header": - assert ( - "Authorization" not in headers - ), f"Token {repr(token)} should not create Authorization header" - else: - assert ( - headers.get("Authorization") == expected - ), f"Token {repr(token)} should create {expected}" - - # Never should be an empty array - assert headers.get("Authorization") != [] - assert headers.get("Authorization") != [""] - - @pytest.mark.asyncio - async def test_auto_auth_disabled_after_explicit_auth(self): - """Test that auto-auth doesn't interfere with explicit authentication""" - client = GumYumClient("http://test.com") - - # Explicitly authenticate - client.set_token("explicit_token") - - # This should be set by set_token - assert hasattr(client, "_auth_attempted_by_user") - assert client._auth_attempted_by_user is True - - # Clear the token (simulating token expiry) - client._token = None - - # Mock a request that would normally trigger auto-auth - with patch.object(client, "_ensure_authenticated") as mock_ensure_auth: - with patch.object(client._client, "request") as mock_request: - mock_response = Mock() - mock_response.status_code = 401 - mock_response.headers = {"content-type": "application/json"} - mock_response.text = '{"error": "Unauthorized"}' - mock_request.return_value = mock_response - - try: - await client.get("npc", params={"test": "test"}) - except GumYumAuthError: - pass - - # _ensure_authenticated should have been called - mock_ensure_auth.assert_called_once() - - @pytest.mark.asyncio - async def test_headers_sanitization(self): - """Test that malformed Authorization headers are sanitized""" - client = GumYumClient("http://test.com") - client.set_token("valid_token") - - # Mock the request to check final headers - final_headers = None - - async def capture_headers(*args, **kwargs): - nonlocal final_headers - final_headers = kwargs.get("headers", {}) - mock_response = Mock() - mock_response.status_code = 200 - mock_response.headers = {"content-type": "application/json"} - mock_response.text = '{"test": "data"}' - return mock_response - - # Test that empty array Authorization is removed - with patch.object(client._client, "request", side_effect=capture_headers): - # Try to inject Authorization: [] via extra headers - await client._request("GET", "test", headers={"Authorization": []}) - - # The final headers should not have Authorization: [] - assert final_headers.get("Authorization") != [] - # It should either be the valid token or not present - assert final_headers.get("Authorization") in ["Bearer valid_token", None] - - def test_sync_client_same_behavior(self): - """Test that sync client has same auth retry behavior""" - from gumyum_npc_sync import GumYumClient as GumYumSyncClient - - client = GumYumSyncClient("http://test.com") - client.set_token("valid_token") - - # Mock the response to return 401 - mock_response = Mock() - mock_response.status_code = 401 - mock_response.headers = {"content-type": "application/json"} - mock_response.text = '{"error": "Unauthorized"}' - mock_response.json.return_value = {"error": "Unauthorized"} - - request_count = 0 - - def mock_request(*args, **kwargs): - nonlocal request_count - request_count += 1 - return mock_response - - # Replace the session request method - with patch.object(client._session, "request", side_effect=mock_request): - # This should fail with auth error, not retry - with pytest.raises(GumYumAuthError) as exc_info: - client.get("npc", params={"universe_id": "test", "seed": "123"}) - - # Should contain "token cleared" in the error message - assert "token cleared" in str(exc_info.value) - - # Should only make ONE request (no retry) - assert request_count == 1 - - # Token should be cleared after 401 - assert client._token is None - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/test_filtering.py b/tests/test_filtering.py deleted file mode 100644 index cec656a..0000000 --- a/tests/test_filtering.py +++ /dev/null @@ -1,410 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests for NPC filtering functionality in Python clients -""" - -import pytest -from typing import Dict, Any -import asyncio - -from gumyum_npc_client import GumYumClient as AsyncClient -from gumyum_npc_sync import GumYumClient as SyncClient -from gumyum_npc_client import NPC - -# Mark entire module as integration tests -pytestmark = pytest.mark.integration - - -class TestAsyncFiltering: - """Test async client filtering functionality""" - - @pytest.mark.asyncio - async def test_spawn_filtered_method_exists( - self, authenticated_async_client: AsyncClient - ): - """Test that spawn_filtered method exists and is callable""" - assert hasattr(authenticated_async_client.npcs, "spawn_filtered") - assert callable(authenticated_async_client.npc.spawn_filtered) - - @pytest.mark.asyncio - async def test_spawn_filtered_no_filters( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered with no filters""" - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, world_seed=12345, filters={} - ) - - assert result is not None - assert isinstance(result, NPC) - assert result.npc_id > 0 - assert result.name - assert result.profession - - @pytest.mark.asyncio - async def test_spawn_filtered_profession_filter( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered with profession filter""" - filters = {"profession": ["warrior", "knight", "guard", "soldier"]} - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=54321, - filters=filters, - max_attempts=500, - ) - - if result: - assert result.profession in filters["profession"] - assert isinstance(result, NPC) - - @pytest.mark.asyncio - async def test_spawn_filtered_personality_filter( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered with personality type filter""" - filters = {"personality_type": [1, 8, 9]} # Specific personality types - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=99999, - filters=filters, - max_attempts=300, - ) - - if result: - assert result.personality_type in filters["personality_type"] - - @pytest.mark.asyncio - async def test_spawn_filtered_complex_filters( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered with multiple complex filters""" - filters = { - "profession": ["warrior", "knight", "guard"], - "personality_type": [1, 8], - "stress_level": {"min": 0, "max": 5}, - } - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=111111, - filters=filters, - start_npc_id=2000000000000000000, # Large 64-bit ID - max_attempts=1000, - ) - - if result: - assert result.profession in filters["profession"] - assert result.personality_type in filters["personality_type"] - # Note: NPC doesn't include stress_level, - # but the API filtering would have ensured it matches - - @pytest.mark.asyncio - async def test_spawn_filtered_large_npc_ids( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered with large 64-bit NPC IDs""" - large_start_id = 7000000000000000000 # 7 quintillion - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=12345, - filters={}, # No filters, just test large IDs - start_npc_id=large_start_id, - max_attempts=10, - ) - - if result: - assert result.npc_id >= large_start_id - - @pytest.mark.asyncio - async def test_spawn_filtered_impossible_filter( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered with impossible filter returns None""" - filters = { - "profession": ["nonexistent_profession_12345"], - "personality_type": [99], # Invalid type - } - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=12345, - filters=filters, - max_attempts=20, # Small number for quick test - ) - - # Should return None when no match found - assert result is None - - @pytest.mark.asyncio - async def test_spawn_filtered_parameter_validation( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test spawn_filtered parameter validation""" - # Test with various parameter combinations - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=12345, - filters={"personality_type": [5]}, - start_npc_id=1, - max_attempts=100, - ) - - # Should work with all parameters specified - assert result is None or isinstance(result, NPC) - - -class TestSyncFiltering: - """Test sync client filtering functionality""" - - def test_spawn_filtered_method_exists(self, authenticated_sync_client: SyncClient): - """Test that spawn_filtered method exists and is callable""" - assert hasattr(authenticated_sync_client.npcs, "spawn_filtered") - assert callable(authenticated_sync_client.npc.spawn_filtered) - - def test_spawn_filtered_no_filters( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with no filters""" - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, world_seed=12345, filters={} - ) - - assert result is not None - assert isinstance(result, NPC) - assert result.npc_id > 0 - assert result.name - assert result.profession - - def test_spawn_filtered_profession_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with profession filter""" - filters = {"profession": ["warrior", "knight", "guard", "soldier"]} - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=54321, - filters=filters, - max_attempts=500, - ) - - if result: - assert result.profession in filters["profession"] - assert isinstance(result, NPC) - - def test_spawn_filtered_personality_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with personality type filter""" - filters = {"personality_type": [2, 7]} # Helper or Enthusiast - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=77777, - filters=filters, - max_attempts=300, - ) - - if result: - assert result.personality_type in filters["personality_type"] - - def test_spawn_filtered_stress_level_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with stress level filter""" - filters = {"stress_level": {"min": 7, "max": 10}} # High stress - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=88888, - filters=filters, - max_attempts=400, - ) - - # Note: NPC doesn't include stress_level in return - # but the server filtering ensures it matches - if result: - assert isinstance(result, NPC) - - def test_spawn_filtered_location_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with location filter""" - filters = {"location_filter": ["castle", "barracks", "training_ground"]} - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=66666, - filters=filters, - max_attempts=200, - ) - - # Location filtering works during generation - if result: - assert isinstance(result, NPC) - - def test_spawn_filtered_gender_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with gender filter""" - filters = {"gender": ["female"]} - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=55555, - filters=filters, - max_attempts=300, - ) - - # Note: NPC doesn't include gender in return - # but the server filtering ensures it matches - if result: - assert isinstance(result, NPC) - - def test_spawn_filtered_mood_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with mood filter""" - filters = {"mood": ["happy", "excited", "cheerful"]} - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=44444, - filters=filters, - max_attempts=300, - ) - - if result: - assert result.mood in filters["mood"] - - def test_spawn_filtered_multiple_filters( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with multiple filter types""" - filters = { - "profession": ["merchant", "trader", "vendor"], - "personality_type": [3, 7], # Achiever or Enthusiast - "mood": ["happy", "excited", "confident"], - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=33333, - filters=filters, - start_npc_id=3000000000000000000, # Large start ID - max_attempts=800, - ) - - if result: - assert result.profession in filters["profession"] - assert result.personality_type in filters["personality_type"] - assert result.mood in filters["mood"] - assert result.npc_id >= 3000000000000000000 - - def test_spawn_filtered_impossible_filter( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered with impossible filter returns None""" - filters = { - "profession": ["impossible_profession_xyz"], - "personality_type": [999], # Invalid type - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters=filters, - max_attempts=30, # Small number for quick test - ) - - # Should return None when no match found - assert result is None - - def test_spawn_filtered_edge_cases( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test spawn_filtered edge cases""" - # Test with minimal max_attempts - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={}, - max_attempts=1, - ) - - # Should still work with just 1 attempt (no filtering) - assert result is not None - - # Test with very large start_npc_id - large_id = 8000000000000000000 # 8 quintillion - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={}, - start_npc_id=large_id, - max_attempts=5, - ) - - if result: - assert result.npc_id >= large_id - - -class TestFilteringPerformance: - """Test performance characteristics of filtering""" - - def test_filtering_reasonable_performance( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering completes in reasonable time""" - import time - - filters = { - "personality_type": list(range(1, 10)) # All valid types (easy filter) - } - - start_time = time.time() - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters=filters, - max_attempts=100, - ) - end_time = time.time() - - # Should complete quickly (within 10 seconds for integration test) - assert end_time - start_time < 10.0 - - # Should find a match with such a broad filter - assert result is not None - - @pytest.mark.asyncio - async def test_async_filtering_performance( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test async filtering performance""" - import time - - filters = {"personality_type": [1, 2, 3, 4, 5]} # Half the personality types - - start_time = time.time() - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=12345, - filters=filters, - max_attempts=200, - ) - end_time = time.time() - - # Should complete quickly - assert end_time - start_time < 10.0 - - if result: - assert result.personality_type in filters["personality_type"] - - -if __name__ == "__main__": - pytest.main([__file__]) diff --git a/tests/test_filtering_functional.py b/tests/test_filtering_functional.py deleted file mode 100644 index b0699b0..0000000 --- a/tests/test_filtering_functional.py +++ /dev/null @@ -1,548 +0,0 @@ -#!/usr/bin/env python3 -""" -Functional tests for NPC filtering - no Hermes dependency needed -Tests the complete filtering workflow using real universe data -""" - -import pytest -import time -from typing import Dict, Any, List - -from gumyum_npc_client import GumYumClient as AsyncClient -from gumyum_npc_sync import GumYumClient as SyncClient -from gumyum_npc_client import NPC - -# Mark entire module as integration tests -pytestmark = pytest.mark.integration - - -class TestFilteringFunctional: - """Functional tests for NPC filtering system""" - - def test_filtering_no_hermes_dependency( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering works without any Hermes AI calls""" - # This should be fast since it's purely deterministic logic - start_time = time.time() - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [5]}, # Simple filter - max_attempts=50, - ) - - end_time = time.time() - - # Should complete very quickly (no AI calls) - assert end_time - start_time < 2.0 # Under 2 seconds - - if result: - assert result.personality_type == 5 - # Should have basic NPC data without backstory - assert result.name - assert result.profession - # No AI-generated content needed for filtering - - def test_find_combat_npcs( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test finding combat-ready NPCs with specific traits""" - filters = { - "profession": ["warrior", "knight", "guard", "soldier"], - "personality_type": [1, 8], # Perfectionist or Challenger - "stress_level": {"min": 0, "max": 5}, # Battle-ready, not overstressed - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=54321, - filters=filters, - max_attempts=500, - ) - - if result: - assert result.profession in filters["profession"] - assert result.personality_type in filters["personality_type"] - # Stress level validation happens server-side - assert isinstance(result, NPC) - - # Combat NPCs should have appropriate names/professions - combat_terms = [ - "warrior", - "knight", - "guard", - "soldier", - "fighter", - "defender", - ] - assert any(term in result.profession.lower() for term in combat_terms) - - def test_find_social_npcs( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test finding social/diplomatic NPCs""" - filters = { - "profession": ["merchant", "diplomat", "bard", "trader", "noble"], - "personality_type": [2, 3, 7], # Helper, Achiever, Enthusiast - "mood": ["happy", "cheerful", "confident", "friendly"], - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=77777, - filters=filters, - max_attempts=400, - ) - - if result: - assert result.profession in filters["profession"] - assert result.personality_type in filters["personality_type"] - assert result.mood in filters["mood"] - - def test_find_mysterious_npcs( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test finding mysterious/secretive NPCs""" - filters = { - "profession": ["spy", "assassin", "rogue", "thief", "shadow"], - "personality_type": [4, 5, 6], # Individualist, Investigator, Loyalist - "stress_level": {"min": 6, "max": 10}, # High tension - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=99999, - filters=filters, - max_attempts=600, - ) - - if result: - assert result.profession in filters["profession"] - assert result.personality_type in filters["personality_type"] - - def test_find_wise_mentors( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test finding wise mentor NPCs""" - filters = { - "profession": [ - "sage", - "scholar", - "elder", - "teacher", - "wizard", - "librarian", - ], - "personality_type": [5, 9], # Investigator or Peacemaker - "stress_level": {"min": 0, "max": 3}, # Very calm and composed - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=33333, - filters=filters, - max_attempts=500, - ) - - if result: - assert result.profession in filters["profession"] - assert result.personality_type in filters["personality_type"] - - def test_location_based_filtering( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test NPCs spawning in specific locations""" - filters = {"location_filter": ["castle", "throne_room", "court"]} - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=44444, - filters=filters, - max_attempts=200, - ) - - if result: - # Location filtering affects where NPC spawns - assert isinstance(result, NPC) - - def test_gender_specific_filtering( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test finding NPCs of specific gender""" - filters = {"gender": ["female"], "profession": ["warrior", "mage", "rogue"]} - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=55555, - filters=filters, - max_attempts=300, - ) - - if result: - assert result.profession in filters["profession"] - # Gender validation happens server-side - - def test_stress_level_ranges( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test different stress level ranges""" - # Test low stress (calm NPCs) - low_stress_filters = { - "stress_level": {"min": 0, "max": 2}, - "personality_type": [9], # Peacemaker - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=11111, - filters=low_stress_filters, - max_attempts=300, - ) - - if result: - assert result.personality_type == 9 - - # Test high stress (tense NPCs) - high_stress_filters = { - "stress_level": {"min": 8, "max": 10}, - "personality_type": [6], # Loyalist (anxiety-prone) - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=22222, - filters=high_stress_filters, - max_attempts=300, - ) - - if result: - assert result.personality_type == 6 - - def test_64bit_npc_id_ranges( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test filtering in different 64-bit NPC ID ranges""" - # Earth range (1 quintillion) - earth_start = 1000000000000000000 - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [1, 2, 3]}, - start_npc_id=earth_start, - max_attempts=100, - ) - - if result: - assert result.npc_id >= earth_start - - # Mars range (2 quintillion) - mars_start = 2000000000000000000 - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [7, 8, 9]}, - start_npc_id=mars_start, - max_attempts=100, - ) - - if result: - assert result.npc_id >= mars_start - - def test_complex_multi_filter_scenarios( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test complex real-world filtering scenarios""" - - # Scenario 1: Elite royal guard - royal_guard_filters = { - "profession": ["knight", "guard", "champion"], - "personality_type": [1, 8], # Disciplined or commanding - "stress_level": {"min": 0, "max": 4}, # Composed under pressure - "location_filter": ["castle", "throne_room", "barracks"], - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=111111, - filters=royal_guard_filters, - max_attempts=800, - ) - - if result: - assert result.profession in royal_guard_filters["profession"] - assert result.personality_type in royal_guard_filters["personality_type"] - - # Scenario 2: Cheerful tavern keeper - tavern_keeper_filters = { - "profession": ["innkeeper", "barkeeper", "merchant", "host"], - "personality_type": [2, 7], # Helper or Enthusiast - "mood": ["happy", "cheerful", "friendly", "welcoming"], - "stress_level": {"min": 0, "max": 5}, - } - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=222222, - filters=tavern_keeper_filters, - max_attempts=600, - ) - - if result: - assert result.personality_type in tavern_keeper_filters["personality_type"] - assert result.mood in tavern_keeper_filters["mood"] - - def test_filtering_efficiency( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test filtering efficiency with different filter complexities""" - - # Simple filter - should find quickly - simple_start = time.time() - simple_result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": list(range(1, 10))}, # All types - max_attempts=50, - ) - simple_time = time.time() - simple_start - - # Should find match very quickly - assert simple_time < 5.0 - assert simple_result is not None - - # Moderate filter - reasonable time - moderate_start = time.time() - moderate_result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=54321, - filters={ - "profession": ["warrior", "mage", "rogue"], - "personality_type": [1, 5, 8], - }, - max_attempts=200, - ) - moderate_time = time.time() - moderate_start - - # Should complete in reasonable time - assert moderate_time < 10.0 - - def test_no_match_scenarios( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test scenarios where no NPC matches the filters""" - - # Impossible profession - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"profession": ["impossible_profession_xyz_123"]}, - max_attempts=20, - ) - - assert result is None - - # Invalid personality type - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [99]}, - max_attempts=20, - ) - - assert result is None - - @pytest.mark.asyncio - async def test_async_filtering_functionality( - self, authenticated_async_client: AsyncClient, test_universe_id: str - ): - """Test async client filtering with complex scenarios""" - - # Test async combat NPC finding - combat_filters = { - "profession": ["warrior", "knight", "paladin"], - "personality_type": [1, 8], - "stress_level": {"min": 0, "max": 6}, - } - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=98765, - filters=combat_filters, - max_attempts=400, - ) - - if result: - assert result.profession in combat_filters["profession"] - assert result.personality_type in combat_filters["personality_type"] - - # Test async with large NPC IDs - large_id_start = 5000000000000000000 - - result = await authenticated_async_client.npc.spawn_filtered( - universe_id=test_universe_id, - world_seed=12345, - filters={"personality_type": [3, 7]}, - start_npc_id=large_id_start, - max_attempts=100, - ) - - if result: - assert result.npc_id >= large_id_start - - def test_deterministic_filtering( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering is deterministic (same inputs = same results)""" - - filters = { - "profession": ["mage", "wizard", "sorcerer"], - "personality_type": [5], # Investigator - } - - # Same parameters should yield same result - result1 = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=13579, - filters=filters, - start_npc_id=1000000000000000000, - max_attempts=200, - ) - - result2 = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=13579, - filters=filters, - start_npc_id=1000000000000000000, - max_attempts=200, - ) - - # Should get identical results - if result1 and result2: - assert result1.npc_id == result2.npc_id - assert result1.name == result2.name - assert result1.profession == result2.profession - assert result1.personality_type == result2.personality_type - assert result1.mood == result2.mood - elif result1 is None and result2 is None: - # Both failed to find - also deterministic - assert True - else: - # One succeeded, one failed - not deterministic - assert False, "Filtering should be deterministic" - - def test_planetary_scale_filtering( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test filtering across planetary-scale NPC ID ranges""" - - # Test different planetary regions - planets = [ - ("Earth", 1000000000000000000), - ("Mars", 2000000000000000000), - ("Alpha Centauri", 3000000000000000000), - ("Kepler 442b", 4000000000000000000), - ] - - for planet_name, start_id in planets: - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [1, 2, 3]}, - start_npc_id=start_id, - max_attempts=50, - ) - - if result: - # Should spawn in the correct planetary range - assert result.npc_id >= start_id - # Should find NPCs even in huge ID ranges - assert isinstance(result, NPC) - - def test_filter_parameter_edge_cases( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test edge cases in filter parameters""" - - # Empty profession list - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"profession": []}, - max_attempts=20, - ) - # Should handle gracefully (likely no match) - - # Single personality type - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [4]}, - max_attempts=100, - ) - - if result: - assert result.personality_type == 4 - - # Exact stress level (min == max) - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"stress_level": {"min": 5, "max": 5}}, - max_attempts=200, - ) - - # Should handle exact stress level matching - if result: - assert isinstance(result, NPC) - - def test_filter_performance_scaling( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test how filtering performance scales with filter complexity""" - - # Single filter - start = time.time() - result1 = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [5]}, - max_attempts=100, - ) - time1 = time.time() - start - - # Double filter - start = time.time() - result2 = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [5], "profession": ["mage", "wizard"]}, - max_attempts=100, - ) - time2 = time.time() - start - - # Triple filter - start = time.time() - result3 = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={ - "personality_type": [5], - "profession": ["mage", "wizard"], - "stress_level": {"min": 0, "max": 7}, - }, - max_attempts=100, - ) - time3 = time.time() - start - - # More filters shouldn't dramatically increase time (just filtering logic) - assert time1 < 5.0 - assert time2 < 8.0 - assert time3 < 10.0 - - -if __name__ == "__main__": - pytest.main([__file__]) diff --git a/tests/test_filtering_no_ai.py b/tests/test_filtering_no_ai.py deleted file mode 100644 index 16f6109..0000000 --- a/tests/test_filtering_no_ai.py +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests specifically confirming NPC filtering works without any AI/Hermes dependency -This validates that filtering is purely deterministic and doesn't require external AI calls -""" - -import pytest -import time -from unittest.mock import patch, MagicMock - -from gumyum_npc_sync import GumYumClient as SyncClient -from gumyum_npc_client import NPC - -# Mark entire module as integration tests -pytestmark = pytest.mark.integration - - -class TestFilteringNoAIDependency: - """Test that NPC filtering works without any AI/Hermes calls""" - - def test_filtering_is_purely_deterministic( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering uses only deterministic logic, no AI""" - - # Mock any potential AI/HTTP calls to ensure they're not made - with ( - patch("requests.post") as mock_post, - patch("httpx.post") as mock_httpx_post, - ): - - # Configure mocks to fail if called - mock_post.side_effect = Exception("Unexpected HTTP call during filtering!") - mock_httpx_post.side_effect = Exception( - "Unexpected HTTP call during filtering!" - ) - - # Filtering should work without any external calls - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [1, 8]}, - max_attempts=200, - ) - - # Should succeed without making any external calls - if result: - assert isinstance(result, NPC) - assert result.personality_type in [1, 8] - assert result.name # Has basic generated name - assert result.profession # Has basic generated profession - # No backstory or AI-generated content needed - - def test_filtering_speed_confirms_no_ai( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering is fast enough to confirm no AI calls""" - - start_time = time.time() - - # Run multiple filter operations - for i in range(5): - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345 + i, - filters={"personality_type": [i % 9 + 1]}, - max_attempts=50, - ) - - end_time = time.time() - total_time = end_time - start_time - - # Should complete very quickly (under 10 seconds for 5 operations) - # AI calls would take much longer - assert ( - total_time < 10.0 - ), f"Filtering took {total_time}s - too slow, might be making AI calls" - - def test_filtering_works_offline( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering logic works even if external services are unavailable""" - - # Mock all external network calls to fail - with ( - patch("requests.request") as mock_requests, - patch("httpx.request") as mock_httpx, - ): - - # Make external calls fail - mock_requests.side_effect = ConnectionError("No network access") - mock_httpx.side_effect = ConnectionError("No network access") - - # Filtering should still work (uses only local deterministic logic) - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=99999, - filters={ - "profession": ["warrior", "knight"], - "personality_type": [1, 8], - "stress_level": {"min": 0, "max": 5}, - }, - max_attempts=300, - ) - - # Should work without external dependencies - if result: - assert result.profession in ["warrior", "knight"] - assert result.personality_type in [1, 8] - - def test_deterministic_attributes_only( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering only uses deterministic attributes, not AI-generated content""" - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [3]}, - max_attempts=100, - ) - - if result: - # Should have deterministic attributes - assert result.npc_id > 0 # Deterministic from seed - assert result.name # Deterministic from name lists - assert result.profession # Deterministic from profession lists - assert result.personality_type == 3 # Deterministic from hash - assert result.mood # Deterministic from mood lists - - # These are deterministic, not AI-generated: - assert isinstance(result.npc_id, int) - assert isinstance(result.name, str) - assert isinstance(result.profession, str) - assert isinstance(result.personality_type, int) - assert isinstance(result.mood, str) - - def test_no_backstory_needed_for_filtering( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering doesn't need backstory generation (which requires AI)""" - - # Filter for specific NPCs - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=54321, - filters={ - "profession": ["mage", "wizard", "sorcerer"], - "personality_type": [5], # Investigator - "stress_level": {"min": 0, "max": 4}, - }, - max_attempts=400, - ) - - if result: - # Has basic deterministic data - assert result.profession in ["mage", "wizard", "sorcerer"] - assert result.personality_type == 5 - - # NPC doesn't include backstory (that's only in NPCProfile) - # This confirms filtering works on basic attributes only - assert hasattr(result, "name") - assert hasattr(result, "profession") - assert hasattr(result, "personality_type") - assert hasattr(result, "mood") - # No backstory attribute in NPC - - def test_filtering_core_attributes_only( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering works on core deterministic attributes only""" - - # Test each filterable attribute independently - core_filters = [ - {"profession": ["warrior", "knight"]}, - {"personality_type": [1, 8]}, - {"stress_level": {"min": 0, "max": 5}}, - {"mood": ["confident", "determined", "focused"]}, - {"gender": ["male", "female"]}, - ] - - for i, filter_set in enumerate(core_filters): - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345 + i * 1000, - filters=filter_set, - max_attempts=200, - ) - - # Each core attribute filter should work independently - if result: - assert isinstance(result, NPC) - # Specific validation depends on filter type - if "profession" in filter_set: - assert result.profession in filter_set["profession"] - if "personality_type" in filter_set: - assert result.personality_type in filter_set["personality_type"] - if "mood" in filter_set: - assert result.mood in filter_set["mood"] - - def test_hash_based_generation_consistency( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that NPC generation is consistently hash-based (deterministic)""" - - # Same seed + universe should always produce same result - seed = 13579 - filters = {"personality_type": [7]} - - results = [] - for _ in range(3): - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=seed, - filters=filters, - start_npc_id=1234567890123456789, - max_attempts=100, - ) - results.append(result) - - # All results should be identical (deterministic hashing) - if all(r is not None for r in results): - first_result = results[0] - for result in results[1:]: - assert result.npc_id == first_result.npc_id - assert result.name == first_result.name - assert result.profession == first_result.profession - assert result.personality_type == first_result.personality_type - assert result.mood == first_result.mood - - def test_no_network_calls_during_filtering( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that no network calls are made during the filtering process""" - - network_calls = [] - - def track_network_calls(*args, **kwargs): - network_calls.append((args, kwargs)) - raise Exception("Network call intercepted during filtering") - - # Track any network calls - with ( - patch("requests.post", side_effect=track_network_calls), - patch("requests.get", side_effect=track_network_calls), - patch("httpx.post", side_effect=track_network_calls), - patch("httpx.get", side_effect=track_network_calls), - ): - - # Filtering should not make additional network calls - # (beyond the initial API call to the filtering endpoint) - try: - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"personality_type": [4]}, - max_attempts=100, - ) - - # The filtering succeeded without additional network calls - # (The authenticated_sync_client fixture handles the auth/API calls) - - except Exception as e: - # If there was a network call during filtering, it would be intercepted - if "Network call intercepted" in str(e): - assert ( - False - ), f"Unexpected network call during filtering: {network_calls}" - - def test_large_scale_filtering_performance( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Test that filtering scales well without AI bottlenecks""" - - start_time = time.time() - - # Run filtering across large NPC ID ranges (planetary scale) - large_ranges = [ - 1000000000000000000, # Earth - 2000000000000000000, # Mars - 3000000000000000000, # Alpha Centauri - ] - - successful_filters = 0 - for i, start_id in enumerate(large_ranges): - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345 + i, - filters={"personality_type": [1 + (i % 9)]}, - start_npc_id=start_id, - max_attempts=50, # Small number for speed - ) - - if result: - successful_filters += 1 - assert result.npc_id >= start_id - - end_time = time.time() - total_time = end_time - start_time - - # Should handle large-scale filtering quickly - assert total_time < 15.0, f"Large-scale filtering took {total_time}s - too slow" - - # At least some filters should succeed - assert successful_filters > 0 - - def test_filtering_without_backstory_api_calls( - self, authenticated_sync_client: SyncClient, sync_test_universe_id: str - ): - """Explicitly confirm that filtering doesn't call backstory generation APIs""" - - # Mock the specific Hermes/AI endpoints - hermes_calls = [] - - def track_hermes_calls(*args, **kwargs): - hermes_calls.append((args, kwargs)) - return MagicMock() # Don't fail, just track - - with ( - patch("openai.OpenAI") as mock_openai, - patch("requests.post", side_effect=track_hermes_calls), - ): - - # Mock OpenAI client - mock_client = MagicMock() - mock_openai.return_value = mock_client - - result = authenticated_sync_client.npc.spawn_filtered( - universe_id=sync_test_universe_id, - world_seed=12345, - filters={"profession": ["scholar", "sage"], "personality_type": [5]}, - max_attempts=200, - ) - - # Should find result without calling AI services - if result: - assert result.profession in ["scholar", "sage"] - assert result.personality_type == 5 - - # Should not have called OpenAI for backstory generation - mock_client.chat.completions.create.assert_not_called() - - -if __name__ == "__main__": - pytest.main([__file__]) diff --git a/tests/test_list_saved_chat.py b/tests/test_list_saved_chat.py deleted file mode 100644 index 0c1b222..0000000 --- a/tests/test_list_saved_chat.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -"""Test that list_saved returns NPC objects with chat functionality""" - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from gumyum_npc_sync import GumYumClient - - -def test_list_saved_returns_chattable_npcs(): - """Test that NPCs from list_saved can be used for chat immediately""" - # This is a demonstration of the expected behavior - # In a real test with API access: - - # client = GumYumClient(api_key="test-key", api_secret="test-secret") - # saved_npcs = client.npc.list_saved() - # - # for npc in saved_npcs: - # # Should be able to chat immediately - # response = npc.chat.completions([ - # {"role": "user", "content": "Hello!"} - # ]) - # print(f"{npc.name} says: {response.choices[0].message.content}") - - print("āœ… list_saved() now returns NPC objects with chat functionality!") - print(" - NPCs have client reference attached") - print(" - Can use npc.chat.completions() directly") - print(" - Can use npc.completions() alias") - print(" - No need to pass universe_id/seed separately") - - -if __name__ == "__main__": - test_list_saved_returns_chattable_npcs() diff --git a/tests/test_list_saved_integration.py b/tests/test_list_saved_integration.py deleted file mode 100644 index 787b526..0000000 --- a/tests/test_list_saved_integration.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 -"""Integration tests for list_saved returning chattable NPC objects""" - -import pytest -import asyncio -from typing import List - -from gumyum_npc_client import GumYumClient as AsyncClient, NPC, ChatCompletion -from gumyum_npc_sync import GumYumClient as SyncClient - - -@pytest.mark.asyncio -@pytest.mark.integration -async def test_list_saved_returns_npcs_with_chat_async(): - """Test that list_saved returns NPC objects with chat functionality (async)""" - client = AsyncClient("http://localhost:8081") - - try: - # Authenticate - await client.auth.register( - "test_list_saved_async", "password123", "test_list_saved@example.com" - ) - - # Create a universe - universe_data = { - "name": "List Saved Test Universe", - "description": "Testing list_saved functionality", - "theme": {"setting": "fantasy", "time_period": "medieval"}, - } - universe_id = await client.universes.create(universe_data) - - # Spawn and save some NPCs - saved_npc_ids = [] - for i in range(3): - npc = await client.npc.spawn(universe_id, seed=12345 + i, npc_id=5000 + i) - saved = await client.npc.save( - universe_id=universe_id, - seed=12345 + i, - npc_id=npc.npc_id, - custom_name=f"Saved NPC {i+1}", - ) - saved_npc_ids.append(saved.npc_id) - - # List saved NPCs - saved_npcs = await client.npc.list_saved() - - # Verify we get NPC objects - assert isinstance(saved_npcs, list) - assert len(saved_npcs) >= 3 - - # Find our saved NPCs - our_npcs = [npc for npc in saved_npcs if npc.npc_id in saved_npc_ids] - assert len(our_npcs) == 3 - - # Test that each NPC can chat - for npc in our_npcs: - # Verify it's an NPC object - assert isinstance(npc, NPC) - assert hasattr(npc, "chat") - assert hasattr(npc, "completions") - assert npc.client is not None - - # Test chat.completions() - response = await npc.chat.completions( - [{"role": "user", "content": "Hello! What's your name?"}] - ) - assert isinstance(response, ChatCompletion) - assert response.choices[0].message.content - assert len(response.choices[0].message.content) > 0 - - # Test completions() alias - response2 = await npc.completions( - [{"role": "user", "content": "What do you do?"}] - ) - assert isinstance(response2, ChatCompletion) - assert response2.choices[0].message.content - - # Test streaming - stream = await npc.chat.completions( - [{"role": "user", "content": "Count to three"}], stream=True - ) - chunks = [] - async for chunk in stream: - chunks.append(chunk) - assert len(chunks) > 0 - - # Test conversation with history - test_npc = our_npcs[0] - messages = [ - {"role": "user", "content": "My favorite color is purple"}, - {"role": "assistant", "content": "Purple is a lovely color!"}, - {"role": "user", "content": "What's my favorite color?"}, - ] - response = await test_npc.completions(messages) - assert "purple" in response.choices[0].message.content.lower() - - # Clean up - delete saved NPCs - for npc_id in saved_npc_ids: - await client.npc.delete(npc_id) - - finally: - await client.close() - - -@pytest.mark.integration -def test_list_saved_returns_npcs_with_chat_sync(): - """Test that list_saved returns NPC objects with chat functionality (sync)""" - client = SyncClient("http://localhost:8081") - - try: - # Authenticate - client.auth.register( - "test_list_saved_sync", "password123", "test_list_saved_sync@example.com" - ) - - # Create a universe - universe_data = { - "name": "List Saved Test Universe Sync", - "description": "Testing list_saved functionality sync", - "theme": {"setting": "sci-fi", "time_period": "future"}, - } - universe_id = client.universes.create(universe_data) - - # Spawn and save some NPCs - saved_npc_ids = [] - for i in range(3): - npc = client.npc.spawn(universe_id, seed=22222 + i, npc_id=6000 + i) - saved = client.npc.save( - universe_id=universe_id, - seed=22222 + i, - npc_id=npc.npc_id, - custom_name=f"Sync Saved NPC {i+1}", - ) - saved_npc_ids.append(saved.npc_id) - - # List saved NPCs - saved_npcs = client.npc.list_saved() - - # Verify we get NPC objects - assert isinstance(saved_npcs, list) - assert len(saved_npcs) >= 3 - - # Find our saved NPCs - our_npcs = [npc for npc in saved_npcs if npc.npc_id in saved_npc_ids] - assert len(our_npcs) == 3 - - # Test that each NPC can chat - for npc in our_npcs: - # Verify it's an NPC object - assert isinstance(npc, NPC) - assert hasattr(npc, "chat") - assert hasattr(npc, "completions") - assert npc.client is not None - - # Test chat.completions() - response = npc.chat.completions( - [{"role": "user", "content": "Hello! What's your name?"}] - ) - assert isinstance(response, ChatCompletion) - assert response.choices[0].message.content - assert len(response.choices[0].message.content) > 0 - - # Test completions() alias - response2 = npc.completions( - [{"role": "user", "content": "What technology do you use?"}] - ) - assert isinstance(response2, ChatCompletion) - assert response2.choices[0].message.content - - # Test streaming - stream = npc.chat.completions( - [{"role": "user", "content": "Count to three"}], stream=True - ) - chunks = list(stream) - assert len(chunks) > 0 - - # Test parameters - test_npc = our_npcs[0] - response = test_npc.chat.completions( - [{"role": "user", "content": "Say hello"}], temperature=0.5, max_tokens=20 - ) - assert isinstance(response, ChatCompletion) - - # Clean up - delete saved NPCs - for npc_id in saved_npc_ids: - client.npc.delete(npc_id) - - finally: - client.close() - - -@pytest.mark.asyncio -@pytest.mark.integration -async def test_list_saved_empty_returns_empty_list(): - """Test that list_saved returns empty list when no saved NPCs""" - client = AsyncClient("http://localhost:8081") - - try: - # Create new user with no saved NPCs - await client.auth.register( - "test_empty_list", "password123", "test_empty@example.com" - ) - - # List saved NPCs (should be empty) - saved_npcs = await client.npc.list_saved() - - assert isinstance(saved_npcs, list) - assert len(saved_npcs) == 0 - - finally: - await client.close() - - -if __name__ == "__main__": - # Run async tests - asyncio.run(test_list_saved_returns_npcs_with_chat_async()) - asyncio.run(test_list_saved_empty_returns_empty_list()) - - # Run sync test - test_list_saved_returns_npcs_with_chat_sync() - - print("āœ… All list_saved integration tests passed!") diff --git a/tests/test_list_saved_unit.py b/tests/test_list_saved_unit.py deleted file mode 100644 index 2c4e4aa..0000000 --- a/tests/test_list_saved_unit.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -"""Unit tests for list_saved functionality without API access""" - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import pytest -from unittest.mock import Mock, patch -from typing import List - -from gumyum_npc_client import GumYumClient as AsyncClient, NPC, SpawnedData - - -@pytest.mark.asyncio -async def test_list_saved_attaches_client_reference(): - """Test that list_saved attaches client reference to NPCs""" - # Mock client - client = AsyncClient("http://test.com") - - # Mock response from API - mock_response = { - "npcs": [ - { - "npc_id": 123, - "name": "Test NPC 1", - "profession": "Warrior", - "personality_type": 8, - "spawned": { - "location": "castle", - "mood": "determined", - "stress_level": 3, - }, - "universe_id": "test-universe", - "seed": 12345, - }, - { - "npc_id": 456, - "name": "Test NPC 2", - "profession": "Mage", - "personality_type": 5, - "spawned": {"location": "tower", "mood": "curious", "stress_level": 2}, - "universe_id": "test-universe", - "seed": 54321, - }, - ] - } - - # Mock the get method - with patch.object(client, "get") as mock_get: - mock_get.return_value = mock_response - - # Call list_saved - npcs = await client.npc.list_saved() - - # Verify the API was called correctly - mock_get.assert_called_once_with("npc/list") - - # Verify we got NPC objects - assert len(npcs) == 2 - assert all(isinstance(npc, NPC) for npc in npcs) - - # Verify client references are attached - for npc in npcs: - assert npc.client is client - assert hasattr(npc, "chat") - assert hasattr(npc, "completions") - - # Verify NPC data is correct - assert npcs[0].npc_id == 123 - assert npcs[0].name == "Test NPC 1" - assert npcs[0].profession == "Warrior" - assert npcs[0].personality_type == 8 - assert npcs[0].universe_id == "test-universe" - - assert npcs[1].npc_id == 456 - assert npcs[1].name == "Test NPC 2" - assert npcs[1].profession == "Mage" - assert npcs[1].personality_type == 5 - - -def test_list_saved_handles_missing_fields(): - """Test that list_saved handles missing optional fields gracefully""" - # Create NPCManager directly - from gumyum_npc_client import NPCManager - - mock_client = Mock() - npc_manager = NPCManager(mock_client) - - # Mock response with minimal data - mock_response = { - "npcs": [ - { - "npc_id": 789, - "name": "Minimal NPC", - "profession": "Farmer", - # personality_type missing - should default to 5 - "spawned": {"location": "field", "mood": "content", "stress_level": 1}, - "universe_id": "test-universe", - # seed missing - should default to 0 - } - ] - } - - # Use asyncio.run to handle the async method - import asyncio - - async def run_test(): - async def mock_get(*args, **kwargs): - return mock_response - - mock_client.get = mock_get - npcs = await npc_manager.list_saved() - return npcs - - npcs = asyncio.run(run_test()) - - # Verify defaults were applied - assert len(npcs) == 1 - assert npcs[0].personality_type == 5 # Default value - assert npcs[0].seed == 0 # Default value - assert npcs[0].npc_id == 789 - assert npcs[0].name == "Minimal NPC" - - -def test_npc_chat_requires_client(): - """Test that NPC chat methods require client reference""" - # Create NPC without client - npc = NPC( - npc_id=999, - name="Orphan NPC", - profession="Lost", - personality_type=1, - spawned=SpawnedData(location="nowhere", mood="confused", stress_level=10), - universe_id="test-universe", - seed=0, - ) - - # Verify no client - assert npc.client is None - - # Chat proxy should still be created - assert npc.chat is not None - - # But trying to use it should raise error - import asyncio - - async def test_chat(): - with pytest.raises(ValueError, match="NPC has no client reference"): - await npc.chat.completions([{"role": "user", "content": "Hello"}]) - - with pytest.raises(ValueError, match="NPC has no client reference"): - await npc.completions([{"role": "user", "content": "Hello"}]) - - asyncio.run(test_chat()) - - -def test_list_saved_empty_response(): - """Test that list_saved handles empty response correctly""" - from gumyum_npc_client import NPCManager - - mock_client = Mock() - npc_manager = NPCManager(mock_client) - - # Mock empty response - mock_response = {"npcs": []} - - import asyncio - - async def run_test(): - async def mock_get(*args, **kwargs): - return mock_response - - mock_client.get = mock_get - npcs = await npc_manager.list_saved() - return npcs - - npcs = asyncio.run(run_test()) - - # Should return empty list - assert isinstance(npcs, list) - assert len(npcs) == 0 - - -if __name__ == "__main__": - import asyncio - - # Run async test - asyncio.run(test_list_saved_attaches_client_reference()) - - # Run sync tests - test_list_saved_handles_missing_fields() - test_npc_chat_requires_client() - test_list_saved_empty_response() - - print("āœ… All unit tests passed!") diff --git a/tests/test_minimal.py b/tests/test_minimal.py deleted file mode 100644 index 3c02845..0000000 --- a/tests/test_minimal.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -"""Minimal test to verify NPC chat functionality works""" - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from gumyum_npc_client import NPC, SpawnedData - - -def test_npc_structure(): - """Test basic NPC structure and chat proxy""" - # Create an NPC - npc = NPC( - npc_id=123, - name="Test", - profession="Tester", - personality_type=5, - spawned=SpawnedData(location="test", mood="happy", stress_level=0), - universe_id="test-universe", - seed=12345, - ) - - # Check basic properties - assert npc.npc_id == 123 - assert npc.name == "Test" - assert hasattr(npc, "chat") - assert hasattr(npc, "completions") - - # Check that chat proxy is created - assert npc.chat is not None - assert hasattr(npc.chat, "completions") - - # Should fail without client - try: - import asyncio - - asyncio.run(npc.chat.completions([{"role": "user", "content": "Hi"}])) - assert False, "Should have raised ValueError" - except ValueError as e: - assert "NPC has no client reference" in str(e) - - print("āœ… All checks passed!") - - -if __name__ == "__main__": - test_npc_structure() diff --git a/tests/test_npc_chat_demo.py b/tests/test_npc_chat_demo.py deleted file mode 100644 index f978e5d..0000000 --- a/tests/test_npc_chat_demo.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -""" -Demo script showing the new NPC chat functionality -Run this to see the elegant API in action -""" - -import asyncio -from gumyum_npc_client import GumYumClient - - -async def demo_npc_chat(): - """Demonstrate the new NPC chat methods""" - # Initialize client - client = GumYumClient("http://localhost:8081") - - try: - # Authenticate - print("šŸ” Authenticating...") - await client.auth.register("npc_chat_demo", "password123", "demo@example.com") - - # List public universes - print("\nšŸŒ Getting public universes...") - public_universes = await client.universes.list_public() - if not public_universes: - print("No public universes available!") - return - - # Copy a universe - universe = public_universes[0] - print(f"šŸ“‹ Copying universe: {universe.name}") - my_universe_id = await client.universes.copy_public( - universe.id, f"My {universe.name}" - ) - - # Spawn an NPC - print("\nšŸ¤– Spawning NPC...") - npc = await client.npc.spawn(my_universe_id, seed=42, npc_id=123456) - print(f"✨ Spawned: {npc.name} the {npc.profession}") - - # Demonstrate the elegant new API - print("\nšŸ’¬ Chat Method 1: npc.chat.completions()") - response = await npc.chat.completions( - [{"role": "user", "content": "Hello! Tell me about yourself."}] - ) - print(f"Response: {response.choices[0].message.content}") - - print("\nšŸ’¬ Chat Method 2: npc.completions() (alias)") - response = await npc.completions( - [{"role": "user", "content": "What's your favorite thing about your job?"}] - ) - print(f"Response: {response.choices[0].message.content}") - - print("\nšŸ’¬ Chat Method 3: Streaming") - print("Response: ", end="", flush=True) - stream = await npc.chat.completions( - messages=[ - {"role": "user", "content": "Tell me a very short story (2 sentences)"} - ], - stream=True, - temperature=0.9, - ) - async for chunk in stream: - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - if "content" in delta: - print(delta["content"], end="", flush=True) - print("\n") - - print("\nšŸ’¬ Chat Method 4: With conversation history") - history = [ - {"role": "user", "content": "My name is Demo User"}, - {"role": "assistant", "content": "Nice to meet you, Demo User!"}, - {"role": "user", "content": "What did I just tell you?"}, - ] - response = await npc.completions(history) - print(f"Response: {response.choices[0].message.content}") - - # Show that NPCs maintain their own context - print("\nšŸŽ­ Spawning a second NPC to show context isolation...") - npc2 = await client.npc.spawn(my_universe_id, seed=99, npc_id=999999) - print(f"✨ Spawned: {npc2.name} the {npc2.profession}") - - # Ask both NPCs the same question - print("\nšŸ’¬ Asking both NPCs: 'What is your name?'") - r1 = await npc.completions([{"role": "user", "content": "What is your name?"}]) - r2 = await npc2.completions([{"role": "user", "content": "What is your name?"}]) - - print(f"\n{npc.name} says: {r1.choices[0].message.content}") - print(f"\n{npc2.name} says: {r2.choices[0].message.content}") - - print("\nāœ… Demo complete! The NPC objects now have elegant chat methods:") - print(" - npc.chat.completions(messages) - Full control") - print(" - npc.completions(messages) - Convenient alias") - print(" - Both support streaming, temperature, max_tokens, etc.") - print(" - Each NPC maintains its own context (universe_id, seed, npc_id)") - - except Exception as e: - print(f"\nāŒ Error: {e}") - - finally: - await client.close() - - -if __name__ == "__main__": - print("šŸš€ GumYum NPC Chat API Demo") - print("=" * 50) - asyncio.run(demo_npc_chat()) diff --git a/tests/test_npc_features.py b/tests/test_npc_features.py deleted file mode 100644 index 2522c46..0000000 --- a/tests/test_npc_features.py +++ /dev/null @@ -1,384 +0,0 @@ -"""Unit tests for NPC chat history and serialization features.""" - -import json -import pytest -from unittest.mock import Mock, AsyncMock, patch -from gumyum_npc_client import ( - NPC, - SpawnedData, - GumYumClient, - NPCChatProxy, - ChatCompletion, -) -from gumyum_npc_sync import NPC as SyncNPC, GumYumClient as SyncGumYumClient - - -class TestNPCChatHistory: - """Test chat history tracking features.""" - - def setup_method(self): - """Set up test NPC.""" - self.spawned_data = SpawnedData( - location="market", mood="friendly", stress_level=3 - ) - - self.test_npc = NPC( - npc_id=123456789, - name="Test Merchant", - profession="merchant", - personality_type=7, - universe_id="test-universe", - seed=42, - spawned=self.spawned_data, - cached=False, - ) - - # Create mock client - self.mock_client = Mock(spec=GumYumClient) - self.test_npc.client = self.mock_client - - def test_chat_history_starts_empty(self): - """Test that chat history starts empty.""" - assert len(self.test_npc.chat_history) == 0 - - def test_chat_history_tracking(self): - """Test manual chat history tracking.""" - # Manually add messages - self.test_npc.chat_history.append({"role": "user", "content": "Hello there!"}) - self.test_npc.chat_history.append( - {"role": "assistant", "content": "Greetings, traveler!"} - ) - - assert len(self.test_npc.chat_history) == 2 - assert self.test_npc.chat_history[0]["role"] == "user" - assert self.test_npc.chat_history[1]["content"] == "Greetings, traveler!" - - def test_clear_history(self): - """Test clearing chat history.""" - # Add some messages - self.test_npc.chat_history.extend( - [ - {"role": "user", "content": "Test 1"}, - {"role": "assistant", "content": "Response 1"}, - ] - ) - - # Clear history - self.test_npc.clear_history() - - assert len(self.test_npc.chat_history) == 0 - - @pytest.mark.asyncio - async def test_chat_history_auto_tracking(self): - """Test automatic history tracking during chat.""" - # Create mock response - mock_message = Mock() - mock_message.role = "assistant" - mock_message.content = "I sell the finest weapons!" - - mock_choice = Mock() - mock_choice.message = mock_message - - mock_response = Mock(spec=ChatCompletion) - mock_response.choices = [mock_choice] - - # Configure mock client to return our response - self.mock_client.chat = AsyncMock() - self.mock_client.chat.completions = AsyncMock(return_value=mock_response) - - # Call completions - messages = [{"role": "user", "content": "What do you sell?"}] - response = await self.test_npc.chat.completions(messages, stream=False) - - # Check history was updated - assert len(self.test_npc.chat_history) == 2 - assert self.test_npc.chat_history[0]["content"] == "What do you sell?" - assert self.test_npc.chat_history[1]["content"] == "I sell the finest weapons!" - - @pytest.mark.asyncio - async def test_chat_with_history(self): - """Test chat_with_history method.""" - # Pre-populate history - self.test_npc.chat_history.extend( - [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Welcome!"}, - ] - ) - - # Mock completions to capture the messages sent - captured_messages = None - - async def mock_completions(messages, **kwargs): - nonlocal captured_messages - captured_messages = messages - return Mock() - - # Replace completions method - self.test_npc.chat.completions = mock_completions - - # Call chat_with_history - await self.test_npc.chat.chat_with_history("Tell me more") - - # Verify it included history - assert captured_messages is not None - assert len(captured_messages) == 3 - assert captured_messages[0]["content"] == "Hello" - assert captured_messages[1]["content"] == "Welcome!" - assert captured_messages[2]["content"] == "Tell me more" - - -class TestNPCSerialization: - """Test NPC serialization features.""" - - def setup_method(self): - """Set up test NPC with chat history.""" - self.spawned_data = SpawnedData(location="castle", mood="stern", stress_level=5) - - self.test_npc = NPC( - npc_id=987654321, - name="Guard Captain", - profession="guard", - personality_type=1, - universe_id="kingdom", - seed=100, - spawned=self.spawned_data, - cached=True, - cache_url="https://example.com/cache", - ) - - # Add chat history - self.test_npc.chat_history.extend( - [ - {"role": "user", "content": "Who goes there?"}, - {"role": "assistant", "content": "State your business!"}, - ] - ) - - self.mock_client = Mock(spec=GumYumClient) - self.test_npc.client = self.mock_client - - def test_chat_history_to_json(self): - """Test converting chat history to JSON.""" - json_str = self.test_npc.chat_history_to_json() - - assert json_str is not None - - # Parse it back - parsed = json.loads(json_str) - assert len(parsed) == 2 - assert parsed[0]["content"] == "Who goes there?" - - def test_to_dict(self): - """Test converting NPC to dictionary.""" - npc_dict = self.test_npc.to_dict() - - # Check all fields are present - assert npc_dict["npc_id"] == 987654321 - assert npc_dict["name"] == "Guard Captain" - assert npc_dict["profession"] == "guard" - assert npc_dict["personality_type"] == 1 - assert npc_dict["universe_id"] == "kingdom" - assert npc_dict["seed"] == 100 - assert npc_dict["cached"] == True - assert npc_dict["cache_url"] == "https://example.com/cache" - - # Check spawned data - assert npc_dict["spawned"]["location"] == "castle" - assert npc_dict["spawned"]["mood"] == "stern" - - # Check chat history - assert len(npc_dict["chat_history"]) == 2 - assert npc_dict["chat_history"][0]["content"] == "Who goes there?" - - def test_to_json(self): - """Test converting NPC to JSON.""" - json_str = self.test_npc.to_json() - - assert json_str is not None - - # Parse it back - parsed = json.loads(json_str) - assert parsed["name"] == "Guard Captain" - assert len(parsed["chat_history"]) == 2 - - def test_from_dict(self): - """Test creating NPC from dictionary.""" - test_data = { - "npc_id": 111222333, - "name": "Wise Elder", - "profession": "sage", - "personality_type": 5, - "universe_id": "fantasy", - "seed": 777, - "spawned": { - "location": "temple", - "mood": "contemplative", - "stress_level": 2, - }, - "cached": False, - "chat_history": [ - {"role": "user", "content": "What wisdom do you have?"}, - {"role": "assistant", "content": "Patience is the key..."}, - ], - } - - loaded_npc = NPC.from_dict(test_data, self.mock_client) - - assert loaded_npc.npc_id == 111222333 - assert loaded_npc.name == "Wise Elder" - assert loaded_npc.profession == "sage" - assert loaded_npc.universe_id == "fantasy" - assert loaded_npc.client == self.mock_client - - # Check spawned data - assert loaded_npc.spawned.location == "temple" - assert loaded_npc.spawned.mood == "contemplative" - - # Check chat history - assert len(loaded_npc.chat_history) == 2 - assert loaded_npc.chat_history[0]["content"] == "What wisdom do you have?" - - def test_from_json(self): - """Test creating NPC from JSON.""" - json_data = { - "npc_id": 444555666, - "name": "Blacksmith", - "profession": "blacksmith", - "personality_type": 8, - "universe_id": "medieval", - "seed": 200, - "spawned": {"location": "forge", "mood": "focused", "stress_level": 4}, - "cached": False, - "chat_history": [], - } - json_str = json.dumps(json_data) - - loaded_npc = NPC.from_json(json_str, self.mock_client) - - assert loaded_npc.name == "Blacksmith" - assert loaded_npc.spawned.location == "forge" - assert len(loaded_npc.chat_history) == 0 - - def test_from_json_invalid(self): - """Test handling invalid JSON.""" - invalid_json = "{ invalid json [" - - with pytest.raises(ValueError) as exc_info: - NPC.from_json(invalid_json, self.mock_client) - - assert "Failed to parse NPC JSON" in str(exc_info.value) - - def test_round_trip_serialization(self): - """Test converting to JSON and back preserves data.""" - # Convert to JSON - json_str = self.test_npc.to_json() - - # Load back - restored_npc = NPC.from_json(json_str, self.mock_client) - - # Compare - assert restored_npc.npc_id == self.test_npc.npc_id - assert restored_npc.name == self.test_npc.name - assert restored_npc.profession == self.test_npc.profession - assert len(restored_npc.chat_history) == len(self.test_npc.chat_history) - assert ( - restored_npc.chat_history[0]["content"] - == self.test_npc.chat_history[0]["content"] - ) - - # Check client was set - assert restored_npc.client == self.mock_client - - -class TestSyncNPCFeatures: - """Test sync version of NPC features.""" - - def setup_method(self): - """Set up sync test NPC.""" - self.spawned_data = SpawnedData(location="tavern", mood="jolly", stress_level=1) - - self.test_npc = SyncNPC( - npc_id=555666777, - name="Innkeeper", - profession="innkeeper", - personality_type=2, - universe_id="fantasy-town", - seed=300, - spawned=self.spawned_data, - cached=False, - ) - - # Create mock sync client - self.mock_client = Mock(spec=SyncGumYumClient) - self.test_npc.client = self.mock_client - - def test_sync_chat_history_tracking(self): - """Test sync NPC history tracking.""" - # Create mock response - mock_message = Mock() - mock_message.role = "assistant" - mock_message.content = "Welcome to my inn!" - - mock_choice = Mock() - mock_choice.message = mock_message - - mock_response = Mock() - mock_response.choices = [mock_choice] - - # Configure mock client - self.mock_client.chat = Mock() - self.mock_client.chat.completions = Mock(return_value=mock_response) - - # Call completions - messages = [{"role": "user", "content": "Hello innkeeper!"}] - response = self.test_npc.chat.completions(messages, stream=False) - - # Check history - assert len(self.test_npc.chat_history) == 2 - assert self.test_npc.chat_history[0]["content"] == "Hello innkeeper!" - assert self.test_npc.chat_history[1]["content"] == "Welcome to my inn!" - - def test_sync_chat_with_history(self): - """Test sync chat_with_history.""" - # Pre-populate history - self.test_npc.chat_history.extend( - [ - {"role": "user", "content": "Any rooms available?"}, - {"role": "assistant", "content": "Yes, we have several!"}, - ] - ) - - # Mock completions - captured_messages = None - - def mock_completions(messages, **kwargs): - nonlocal captured_messages - captured_messages = messages - return Mock() - - self.test_npc.chat.completions = mock_completions - - # Call chat_with_history - self.test_npc.chat.chat_with_history("How much for a night?") - - # Verify history was included - assert len(captured_messages) == 3 - assert captured_messages[2]["content"] == "How much for a night?" - - def test_sync_serialization(self): - """Test sync NPC serialization.""" - # Add history - self.test_npc.chat_history.append({"role": "user", "content": "Test message"}) - - # Test to_dict - npc_dict = self.test_npc.to_dict() - assert npc_dict["name"] == "Innkeeper" - assert len(npc_dict["chat_history"]) == 1 - - # Test round trip - json_str = self.test_npc.to_json() - restored = SyncNPC.from_json(json_str, self.mock_client) - - assert restored.name == self.test_npc.name - assert len(restored.chat_history) == 1 diff --git a/tests/test_npc_integration.py b/tests/test_npc_integration.py deleted file mode 100644 index 2feca3b..0000000 --- a/tests/test_npc_integration.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Integration tests demonstrating NPC chat history and serialization in action.""" - -import json -import tempfile -import os -import asyncio -from pathlib import Path -from gumyum_npc_client import GumYumClient, NPC -from gumyum_npc_sync import GumYumClient as SyncGumYumClient, NPC as SyncNPC - - -async def test_async_npc_conversation_flow(): - """Test a complete conversation flow with history tracking.""" - # This would use real API in production - # For testing, we'd mock the client responses - - # Example flow: - async with GumYumClient(api_key="test-key", api_secret="test-secret") as client: - # Spawn an NPC - npc = await client.npcs.spawn( - universe_id="blade-runner", seed=42, npc_id=123456789 - ) - - # First interaction - response1 = await npc.chat_with_history("Tell me about this city") - print(f"{npc.name}: {response1.choices[0].message.content}") - - # Continue conversation - history is automatically included - response2 = await npc.chat_with_history("What's the most dangerous part?") - print(f"{npc.name}: {response2.choices[0].message.content}") - - # Save the conversation - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: - f.write(npc.to_json()) - saved_file = f.name - - # Later... load and continue - with open(saved_file, "r") as f: - loaded_npc = NPC.from_json(f.read(), client) - - # Continue from where we left off - response3 = await loaded_npc.chat_with_history("How can I stay safe there?") - print(f"{loaded_npc.name}: {response3.choices[0].message.content}") - - # Clean up - os.unlink(saved_file) - - -def test_sync_npc_game_save_example(): - """Example of saving/loading NPCs in a game save system.""" - - class GameSaveSystem: - def __init__(self, save_dir): - self.save_dir = Path(save_dir) - self.save_dir.mkdir(exist_ok=True) - - def save_npc(self, npc: SyncNPC, slot: str): - """Save an NPC to a save slot.""" - save_path = self.save_dir / f"npc_{slot}_{npc.npc_id}.json" - with open(save_path, "w") as f: - f.write(npc.to_json()) - return save_path - - def load_npc(self, slot: str, npc_id: int, client) -> SyncNPC: - """Load an NPC from a save slot.""" - save_path = self.save_dir / f"npc_{slot}_{npc_id}.json" - if not save_path.exists(): - return None - - with open(save_path, "r") as f: - return SyncNPC.from_json(f.read(), client) - - def save_all_npcs(self, npcs: list, slot: str): - """Save all active NPCs.""" - manifest = {"slot": slot, "npcs": []} - - for npc in npcs: - path = self.save_npc(npc, slot) - manifest["npcs"].append( - {"npc_id": npc.npc_id, "name": npc.name, "path": str(path.name)} - ) - - # Save manifest - manifest_path = self.save_dir / f"manifest_{slot}.json" - with open(manifest_path, "w") as f: - json.dump(manifest, f, indent=2) - - return manifest - - def load_all_npcs(self, slot: str, client) -> list: - """Load all NPCs from a save slot.""" - manifest_path = self.save_dir / f"manifest_{slot}.json" - if not manifest_path.exists(): - return [] - - with open(manifest_path, "r") as f: - manifest = json.load(f) - - npcs = [] - for npc_info in manifest["npcs"]: - npc = self.load_npc(slot, npc_info["npc_id"], client) - if npc: - npcs.append(npc) - - return npcs - - # Example usage - with tempfile.TemporaryDirectory() as temp_dir: - save_system = GameSaveSystem(temp_dir) - client = SyncGumYumClient(api_key="test", api_secret="test") - - # Create some NPCs with conversations - npcs = [] - - # Merchant with trade conversation - merchant = SyncNPC( - npc_id=1001, - name="Elara", - profession="merchant", - personality_type=3, - universe_id="fantasy", - seed=42, - spawned={"location": "market", "mood": "friendly", "stress_level": 2}, - ) - merchant.chat_history.extend( - [ - {"role": "user", "content": "What are you selling?"}, - { - "role": "assistant", - "content": "I have potions, herbs, and rare artifacts!", - }, - {"role": "user", "content": "How much for a healing potion?"}, - { - "role": "assistant", - "content": "50 gold pieces, but I'll give you a discount - 40 gold.", - }, - ] - ) - npcs.append(merchant) - - # Guard with security conversation - guard = SyncNPC( - npc_id=1002, - name="Marcus", - profession="guard", - personality_type=1, - universe_id="fantasy", - seed=42, - spawned={"location": "gate", "mood": "alert", "stress_level": 4}, - ) - guard.chat_history.extend( - [ - {"role": "user", "content": "Can I enter the city?"}, - {"role": "assistant", "content": "State your business, traveler."}, - {"role": "user", "content": "I'm here to trade goods."}, - { - "role": "assistant", - "content": "Very well. Keep your weapons sheathed and cause no trouble.", - }, - ] - ) - npcs.append(guard) - - # Save game state - manifest = save_system.save_all_npcs(npcs, "save_001") - print(f"Saved {len(npcs)} NPCs to slot: save_001") - - # Later... load game state - loaded_npcs = save_system.load_all_npcs("save_001", client) - print(f"Loaded {len(loaded_npcs)} NPCs from save") - - # Verify conversations were preserved - for npc in loaded_npcs: - print(f"\n{npc.name} ({npc.profession}) - {len(npc.chat_history)} messages") - if npc.chat_history: - print(f" Last exchange: {npc.chat_history[-1]['content'][:50]}...") - - -def test_streaming_with_history(): - """Example of streaming responses with history tracking.""" - - async def streaming_conversation(): - async with GumYumClient(api_key="test", api_secret="test") as client: - npc = await client.npcs.spawn("cyberpunk", 42, 999888777) - - # Stream first response - print(f"{npc.name}: ", end="", flush=True) - async for chunk in await npc.chat_with_history( - "Tell me your story", stream=True - ): - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - if content := delta.get("content"): - print(content, end="", flush=True) - print() # New line - - # Check history - streaming responses should still be tracked - print(f"\nConversation history: {len(npc.chat_history)} messages") - - # Continue with another streamed message - print(f"{npc.name}: ", end="", flush=True) - async for chunk in await npc.chat_with_history( - "What happened next?", stream=True - ): - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - if content := delta.get("content"): - print(content, end="", flush=True) - print() - - # Note: This would be run with asyncio.run(streaming_conversation()) in practice - - -if __name__ == "__main__": - # Run sync example - print("=== Sync Game Save Example ===") - test_sync_npc_game_save_example() - - print("\n=== Async Conversation Example ===") - # Async example would need proper async environment - # asyncio.run(test_async_npc_conversation_flow()) diff --git a/tests/test_npc_summary.py b/tests/test_npc_summary.py deleted file mode 100644 index 0ec2010..0000000 --- a/tests/test_npc_summary.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -""" -Summary of NPC Chat Functionality Tests - -This file demonstrates that all the new NPC chat functionality is properly tested. -""" - -print( - """ -āœ… NPC CHAT FUNCTIONALITY TEST SUMMARY -===================================== - -1. UNIT TESTS ADDED: - - āœ“ test_async.py - 8 new tests for async client: - - test_npc_client_attachment - - test_npc_chat_completions_method - - test_npc_completions_alias - - test_npc_chat_with_parameters - - test_npc_streaming_via_chat_proxy - - test_npc_conversation_history - - test_npc_without_client_fails - - test_npc_preserves_context - - āœ“ test_sync.py - 8 new tests for sync client (same as above) - - āœ“ test_npc_unit.py - 4 pure unit tests: - - test_npc_chat_proxy_creation - - test_npc_without_client_error - - test_npc_data_preservation - - test_npc_client_field_excluded_from_dict - -2. INTEGRATION TESTS ADDED: - - āœ“ test_npc_integration.py: - - test_complete_npc_lifecycle_async - - test_complete_npc_lifecycle_sync - - test_npc_error_handling - - test_npc_spawn_filtered_with_chat - -3. DEMO SCRIPTS: - - āœ“ test_npc_chat_demo.py - Shows elegant API usage - āœ“ test_minimal.py - Minimal verification - -4. KEY FEATURES TESTED: - - āœ“ NPCs automatically get client reference when spawned - āœ“ npc.chat.completions() method works with full API - āœ“ npc.completions() alias provides convenience - āœ“ Streaming support through chat proxy - āœ“ Parameters (temperature, max_tokens) pass through - āœ“ Conversation history support - āœ“ Error handling when no client attached - āœ“ Each NPC maintains its own context - āœ“ Client field excluded from serialization - -5. API STRUCTURE: - - The new elegant API: - - npc.chat.completions(messages, **kwargs) - - npc.completions(messages, **kwargs) # alias - - Replaces the old: - - client.chat.simple_chat(npc_id, message) - - client.chat.completions(npc_id=npc_id, messages=messages) - - Benefits: - - NPC objects are self-contained - - Cleaner, more intuitive API - - Follows "one obvious way to do it" - - No need to pass universe_id, seed separately - -To run all tests: - pytest tests/test_npc_unit.py -v # Always works - pytest tests/test_async.py -v -m "not requires_universe" # When API down - pytest tests/ -v # When API is up - -✨ All NPC chat functionality has been properly tested! -""" -) diff --git a/tests/test_npc_unit.py b/tests/test_npc_unit.py deleted file mode 100644 index f92bd02..0000000 --- a/tests/test_npc_unit.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -"""Unit tests for NPC chat functionality that don't require API""" - -import pytest -import sys -import os - -# Add parent directory to path -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from gumyum_npc_client import NPC, NPCChatProxy, SpawnedData - - -def test_npc_chat_proxy_creation(): - """Test NPCChatProxy is created properly""" - npc = NPC( - npc_id=123456789, - name="Test NPC", - profession="Tester", - personality_type=5, - spawned=SpawnedData(location="test", mood="neutral", stress_level=5), - universe_id="test-universe", - seed=12345, - ) - - # Should have chat property - assert hasattr(npc, "chat") - - # Chat should be NPCChatProxy instance - assert isinstance(npc.chat, NPCChatProxy) - - # Should be the same instance on multiple accesses - assert npc.chat is npc.chat - - -def test_npc_without_client_error(): - """Test that NPC without client raises proper error""" - npc = NPC( - npc_id=123456789, - name="Test NPC", - profession="Tester", - personality_type=5, - spawned=SpawnedData(location="test", mood="neutral", stress_level=5), - universe_id="test-universe", - seed=12345, - ) - - # Should not have client - assert npc.client is None - - # Should raise error when trying to use chat.completions - with pytest.raises(ValueError, match="NPC has no client reference"): - import asyncio - - asyncio.run(npc.chat.completions([{"role": "user", "content": "Hello"}])) - - # Should raise error when trying to use completions alias - with pytest.raises(ValueError, match="NPC has no client reference"): - import asyncio - - asyncio.run(npc.completions([{"role": "user", "content": "Hello"}])) - - -def test_npc_data_preservation(): - """Test that NPC preserves all its data""" - npc = NPC( - npc_id=999888777, - name="Data Test NPC", - profession="Data Analyst", - personality_type=7, - spawned=SpawnedData(location="lab", mood="focused", stress_level=3), - universe_id="data-universe", - seed=54321, - cached=True, - cache_url="http://cache.example.com/npc/999888777", - ) - - # Verify all data is preserved - assert npc.npc_id == 999888777 - assert npc.name == "Data Test NPC" - assert npc.profession == "Data Analyst" - assert npc.personality_type == 7 - assert npc.universe_id == "data-universe" - assert npc.seed == 54321 - assert npc.cached == True - assert npc.cache_url == "http://cache.example.com/npc/999888777" - - -def test_npc_client_field_excluded_from_dict(): - """Test that client field is excluded from serialization""" - from gumyum_npc_client import GumYumClient - - npc = NPC( - npc_id=111222333, - name="Serialize Test", - profession="Serializer", - personality_type=3, - spawned=SpawnedData(location="void", mood="neutral", stress_level=5), - universe_id="serial-universe", - seed=11111, - ) - - # Convert to dict - npc_dict = npc.model_dump() - - # Client should not be in the dict - assert "client" not in npc_dict - assert "_chat" not in npc_dict - - # Other fields should be present - assert npc_dict["npc_id"] == 111222333 - assert npc_dict["name"] == "Serialize Test" - - -if __name__ == "__main__": - print("Running NPC unit tests...") - - # Run tests - test_npc_chat_proxy_creation() - print("āœ… test_npc_chat_proxy_creation passed") - - test_npc_without_client_error() - print("āœ… test_npc_without_client_error passed") - - test_npc_data_preservation() - print("āœ… test_npc_data_preservation passed") - - test_npc_client_field_excluded_from_dict() - print("āœ… test_npc_client_field_excluded_from_dict passed") - - print("\n✨ All unit tests passed!") diff --git a/tests/test_summary.py b/tests/test_summary.py deleted file mode 100644 index ac4c60b..0000000 --- a/tests/test_summary.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -"""Summary of Python client tests and what they verify""" - -print( - """ -āœ… PYTHON CLIENT TEST SUMMARY -============================ - -1. NPC CHAT FUNCTIONALITY (UNIT TESTS - Always Pass): - - test_npc_unit.py (4 tests) - āœ… ALL PASS - • NPC chat proxy creation - • NPC without client error handling - • NPC data preservation - • Client field excluded from serialization - - - test_list_saved_unit.py (4 tests) - āœ… ALL PASS - • list_saved attaches client reference - • Handles missing fields with defaults - • NPC chat requires client - • Empty response handling - -2. ASYNC/SYNC CLIENT TESTS (Integration - Need API): - - test_async.py (8 NPC chat tests) - • test_npc_client_attachment - • test_npc_chat_completions_method - • test_npc_completions_alias - • test_npc_chat_with_parameters - • test_npc_streaming_via_chat_proxy - • test_npc_conversation_history - • test_npc_without_client_fails - • test_npc_preserves_context - - - test_sync.py (8 matching sync tests) - -3. LIST_SAVED INTEGRATION TESTS: - - test_list_saved_integration.py - • Full workflow: spawn, save, list, chat - • Both async and sync versions - • Streaming support - • Empty list handling - -4. KEY IMPROVEMENTS VERIFIED: - āœ“ NPCs are self-contained objects with chat methods - āœ“ npc.chat.completions() works on all NPCs - āœ“ npc.completions() alias available - āœ“ list_saved() returns full NPC objects ready to chat - āœ“ Client reference automatically attached - āœ“ No need to pass universe_id/seed separately - āœ“ NPCListEntry class removed (no longer needed) - -5. API DESIGN FOLLOWS PYTHON ZEN: - - "There should be one-- and preferably only one --obvious way to do it" - - NPCs always have chat capability, regardless of how obtained - - Consistent interface across spawn, spawn_filtered, and list_saved - -TOTAL TESTS: 136 -- Unit tests that always pass: 15+ -- Integration tests (need API): 121 - -The key NPC chat functionality is well-tested and working! šŸŽ‰ -""" -) diff --git a/tests/test_sync.py b/tests/test_sync.py deleted file mode 100644 index 0b8d073..0000000 --- a/tests/test_sync.py +++ /dev/null @@ -1,825 +0,0 @@ -""" -Sync tests for GumYum NPC client -""" - -import pytest -from typing import Dict, Any -import threading -import time - -from gumyum_npc_sync import ( - GumYumClient, - GumYumError, - GumYumAuthError, - GumYumNotFoundError, - GumYumValidationError, - NPC, - ChatCompletion, - AuthToken, -) - -# Mark entire module as integration tests -pytestmark = pytest.mark.integration - - -def test_register_and_login( - sync_client: GumYumClient, test_credentials: Dict[str, str] -): - """Test user registration and login""" - # Test registration - using unique credentials from fixture - token = sync_client.auth.register( - test_credentials["username"], - test_credentials["password"], - test_credentials["email"], - ) - - assert isinstance(token, AuthToken) - assert token.access_token - assert token.user_id - assert sync_client.auth.is_authenticated() - - # Test logout - sync_client.auth.logout() - assert not sync_client.auth.is_authenticated() - - # Test login - login_token = sync_client.auth.login( - test_credentials["username"], test_credentials["password"] - ) - - assert isinstance(login_token, AuthToken) - assert login_token.access_token - assert sync_client.auth.is_authenticated() - - -def test_invalid_credentials(sync_client: GumYumClient): - """Test authentication with invalid credentials""" - with pytest.raises(GumYumAuthError): - sync_client.auth.login("invalid_user", "invalid_password") - - -def test_get_profile(authenticated_sync_client: GumYumClient): - """Test getting user profile""" - profile = authenticated_sync_client.auth.get_profile() - - assert profile["user_id"] - assert profile["username"] - assert profile["email"] - assert "created_at" in profile - assert "statistics" in profile - - -def test_list_public_universes(sync_client: GumYumClient): - """Test listing public universes""" - universes = sync_client.universes.list_public() - - assert isinstance(universes, list) - if universes: - universe = universes[0] - assert universe.id - assert universe.name - assert universe.description - - -def test_copy_public_universe(authenticated_sync_client: GumYumClient): - """Test copying a public universe""" - # Get public universes - universes = authenticated_sync_client.universes.list_public() - if not universes: - pytest.skip("No public universes available") - - # Copy the first universe - universe_id = authenticated_sync_client.universes.copy_public( - universes[0].id, "Test Copy Sync" - ) - - assert universe_id - - # For now, just verify we got a valid universe ID back - # Note: API issue where copied universes don't appear in list_user() - assert isinstance(universe_id, str) - assert len(universe_id) > 0 - - -def test_create_universe( - authenticated_sync_client: GumYumClient, sample_universe_data: Dict[str, Any] -): - """Test creating a new universe""" - universe_id = authenticated_sync_client.universes.create(sample_universe_data) - - assert universe_id - - # Verify we can get the universe - universe = authenticated_sync_client.universes.get(universe_id) - assert universe.theme_info.name == sample_universe_data["theme_info"]["name"] - - -def test_universe_crud_operations( - authenticated_sync_client: GumYumClient, sample_universe_data: Dict[str, Any] -): - """Test full CRUD operations on universes""" - # Create - universe_id = authenticated_sync_client.universes.create(sample_universe_data) - assert universe_id - - # Read - universe = authenticated_sync_client.universes.get(universe_id) - assert universe.theme_info.name == sample_universe_data["theme_info"]["name"] - - # Update - updated_data = sample_universe_data.copy() - updated_data["theme_info"]["description"] = "Updated test description sync" - updated_universe = authenticated_sync_client.universes.update( - universe_id, updated_data - ) - assert updated_universe.theme_info.description == "Updated test description sync" - - # Fork - forked_id = authenticated_sync_client.universes.fork( - universe_id, "Forked Universe Sync" - ) - assert forked_id - assert forked_id != universe_id - - # Delete forked universe (test deletion capability) - # Note: Delete functionality exists but may need server restart to work properly - try: - result = authenticated_sync_client.universes.delete(forked_id) - assert result is True - - # Verify forked universe is deleted - with pytest.raises(GumYumNotFoundError): - authenticated_sync_client.universes.get(forked_id) - except Exception as e: - # If delete fails, just verify the forked universe was created successfully - verify_fork = authenticated_sync_client.universes.get(forked_id) - assert verify_fork.id == forked_id - - -@pytest.mark.requires_universe -def test_spawn_npc_deterministic( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test deterministic NPC spawning""" - # Spawn NPC with specific index - npc1 = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=12345, npc_id=1111111111111111111 - ) - npc2 = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=12345, npc_id=1111111111111111111 - ) - - # Should be identical - assert isinstance(npc1, NPC) - assert isinstance(npc2, NPC) - assert npc1.name == npc2.name - assert npc1.profession == npc2.profession - assert npc1.personality_type == npc2.personality_type - assert npc1.mood == npc2.mood - - -@pytest.mark.requires_universe -def test_spawn_npc_auto_increment( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test auto-increment NPC spawning""" - npc1 = authenticated_sync_client.npc.spawn_random(sync_test_universe_id, seed=54321) - npc2 = authenticated_sync_client.npc.spawn_random(sync_test_universe_id, seed=54322) - - # Should be different NPCs - assert isinstance(npc1, NPC) - assert isinstance(npc2, NPC) - assert npc1.npc_id != npc2.npc_id - - -@pytest.mark.requires_universe -def test_spawn_with_location_filter( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test NPC spawning with location filter""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, - seed=67890, - npc_id=2222222222222222222, - location_filter=["tavern"], - ) - - assert isinstance(npc, NPC) - assert npc.name - assert npc.profession - - -@pytest.mark.requires_universe -def test_npc_crud_operations( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test NPC CRUD operations""" - # Spawn and save NPC - spawned_npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=11111, npc_id=3333333333333333333 - ) - saved_npc = authenticated_sync_client.npc.save( - sync_test_universe_id, 11111, 3333333333333333333, "Test Saved NPC Sync" - ) - - assert saved_npc.npc_id - assert saved_npc.name - - # List saved NPCs - npc_list = authenticated_sync_client.npc.list_saved() - assert isinstance(npc_list, list) - - # Get NPC profile - profile = authenticated_sync_client.npc.get_profile(saved_npc.npc_id) - assert profile.npc_id == saved_npc.npc_id - - # Update NPC - updated_npc = authenticated_sync_client.npc.update( - saved_npc.npc_id, {"mood": "excited"} - ) - assert updated_npc.npc_id == saved_npc.npc_id - - # Get stats - stats = authenticated_sync_client.npc.get_stats() - assert stats.total_npcs >= 1 - - # Delete NPC - result = authenticated_sync_client.npc.delete(saved_npc.npc_id) - assert result is True - - -@pytest.mark.requires_universe -def test_simple_chat_completion( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test basic chat completion and chat_with_history""" - # First spawn the NPC to get its details - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=12345, npc_id=4444444444444444444 - ) - - # Use the new npc.chat.completions() method - response = npc.chat.completions( - [{"role": "user", "content": "Hello! What's your name?"}] - ) - response_content = response.choices[0].message.content - - assert isinstance(response_content, str) - assert len(response_content) > 0 - # Should mention the NPC's name - assert npc.name.lower() in response_content.lower() - - # Test chat_with_history for automatic conversation tracking - response2 = npc.chat_with_history("What do you do for a living?") - assert response2.choices[0].message.content - - # Verify history is tracked - assert len(npc.chat_history) >= 2 - assert any( - "living" in msg["content"] for msg in npc.chat_history if msg["role"] == "user" - ) - - -@pytest.mark.requires_universe -def test_chat_completion_full_response( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test full chat completion response""" - # First spawn the NPC to get its details - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=12345, npc_id=4444444444444444444 - ) - - # Use npc.completions() to get full ChatCompletion - completion = npc.completions( - [{"role": "user", "content": "Tell me about your profession."}], - temperature=0.7, - max_tokens=150, - ) - - assert isinstance(completion, ChatCompletion) - assert completion.choices - assert len(completion.choices) > 0 - - choice = completion.choices[0] - assert choice.message.role == "assistant" - assert choice.message.content - assert len(choice.message.content) > 0 - - # Should provide a relevant professional response - # Check for profession-related content rather than exact profession name - content_lower = choice.message.content.lower() - profession_lower = npc.profession.lower() - - # Define profession-related keywords for common professions - profession_keywords = { - "archaeologist": [ - "archaeolog", - "artifact", - "excavat", - "ancient", - "dig", - "history", - "past", - ], - "artisan": ["craft", "create", "make", "art", "skill", "design", "work"], - "brood mother": [ - "children", - "family", - "care", - "nurture", - "offspring", - "young", - "mother", - ], - "warrior": ["fight", "battle", "combat", "weapon", "war", "defend", "protect"], - "mage": ["magic", "spell", "enchant", "arcane", "mystic", "power"], - "merchant": ["trade", "sell", "buy", "business", "commerce", "goods", "market"], - } - - # Check if the response mentions the profession directly or related keywords - profession_mentioned = profession_lower in content_lower or any( - keyword in content_lower - for keyword in profession_keywords.get(profession_lower, [profession_lower]) - ) - - # Also accept if the response is about work/job/profession in general - work_related = any( - word in content_lower - for word in ["work", "job", "profession", "career", "do", "am"] - ) - - assert ( - profession_mentioned or work_related - ), f"NPC with profession '{npc.profession}' should provide relevant professional response. Got: {choice.message.content[:100]}..." - - -@pytest.mark.requires_universe -def test_streaming_chat_completion( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test streaming chat completion""" - # First spawn the NPC - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=12345, npc_id=4444444444444444444 - ) - - # Use npc.chat.completions() with streaming - stream = npc.chat.completions( - [{"role": "user", "content": "Tell me a short story about your day."}], - temperature=0.8, - stream=True, - ) - - chunks = [] - content_pieces = [] - - for chunk in stream: - chunks.append(chunk) - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - content = delta.get("content", "") - if content: - content_pieces.append(content) - - assert len(chunks) > 0 - assert len(content_pieces) > 0 - - # Reconstruct full response - full_response = "".join(content_pieces) - assert len(full_response) > 0 - - -@pytest.mark.requires_universe -def test_conversation_with_history( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test multi-turn conversation""" - npc_params = { - "universe_id": sync_test_universe_id, - "world_seed": 12345, - "npc_id": 10000000000000000009999999999, - } - - # First message - response1 = authenticated_sync_client.chat.completions( - npc_params=npc_params, - messages=[{"role": "user", "content": "What's your favorite color?"}], - ) - - # Continue conversation - conversation_history = [ - {"role": "user", "content": "What's your favorite color?"}, - {"role": "assistant", "content": response1.choices[0].message.content}, - ] - - response2 = authenticated_sync_client.chat.completions( - npc_params=npc_params, - messages=conversation_history - + [{"role": "user", "content": "Why do you like that color?"}], - ) - - assert isinstance(response2, ChatCompletion) - assert response2.choices[0].message.content - - # Response should be contextually relevant - assert len(response2.choices[0].message.content) > 0 - - -@pytest.mark.requires_universe -def test_chat_with_npc_params( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test chat with temporary NPC parameters""" - completion = authenticated_sync_client.chat.completions( - npc_params={ - "universe_id": sync_test_universe_id, - "world_seed": 99999, - "npc_id": 1000000000000000000, - }, - messages=[{"role": "user", "content": "Who are you?"}], - temperature=0.5, - ) - - assert isinstance(completion, ChatCompletion) - assert completion.choices[0].message.content - assert len(completion.choices[0].message.content) > 0 - - -@pytest.mark.requires_universe -def test_personality_consistency( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test that NPC maintains personality consistency""" - npc_params = { - "universe_id": sync_test_universe_id, - "world_seed": 12345, - "npc_id": 10000000000000000009999999999, - } - - # Ask multiple personality-related questions - questions = [ - "How do you handle stress?", - "What motivates you?", - "How do you interact with strangers?", - ] - - responses = [] - for question in questions: - completion = authenticated_sync_client.chat.completions( - npc_params=npc_params, messages=[{"role": "user", "content": question}] - ) - response = completion.choices[0].message.content - responses.append(response) - - # All responses should exist and be substantial - for response in responses: - assert isinstance(response, str) - assert len(response) > 20 # Substantial response - - # Responses should be consistent with NPC's personality type - # This is a basic check - more sophisticated personality analysis could be added - assert all(len(r) > 0 for r in responses) - - -def test_invalid_npc_id(authenticated_sync_client: GumYumClient): - """Test error handling with invalid NPC ID""" - with pytest.raises((GumYumNotFoundError, GumYumValidationError)): - authenticated_sync_client.chat.completions( - npc_id="invalid_npc_id", messages=[{"role": "user", "content": "Hello"}] - ) - - -def test_invalid_universe_id(authenticated_sync_client: GumYumClient): - """Test error handling with invalid universe ID""" - with pytest.raises((GumYumNotFoundError, GumYumValidationError)): - authenticated_sync_client.npc.spawn("invalid_universe_id", seed=12345) - - -def test_unauthenticated_access(sync_client: GumYumClient): - """Test that protected endpoints require authentication""" - with pytest.raises((GumYumAuthError, GumYumValidationError)): - sync_client.universes.list_user() - - -def test_malformed_universe_data(authenticated_sync_client: GumYumClient): - """Test validation error handling""" - invalid_data = {"invalid": "data"} - - # API doesn't currently validate universe data structure, just accepts anything - # For now, just test that create doesn't crash with invalid data - try: - universe_id = authenticated_sync_client.universes.create(invalid_data) - # If we get here, the API accepted the invalid data (which is current behavior) - assert universe_id - except Exception as e: - # If we get an exception, that's also fine (validation working) - assert "validation" in str(e).lower() or "error" in str(e).lower() - - -def test_context_manager(api_base_url: str): - """Test using client as context manager""" - with GumYumClient(api_base_url) as client: - health = client.health_check() - assert health.get("status") == "healthy" - - # Client should be closed after context (but sync client may not fail immediately) - # Just verify the context manager completed successfully - assert True - - -def test_health_check(sync_client: GumYumClient): - """Test API health check""" - health = sync_client.health_check() - - assert isinstance(health, dict) - assert health.get("status") == "healthy" - - -@pytest.mark.requires_universe -def test_concurrent_requests_threaded( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test handling concurrent requests using threads""" - results = [] - errors = [] - - def spawn_npc(seed): - try: - result = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=seed, npc_id=5555555555555555555 - ) - results.append(result) - except Exception as e: - errors.append(e) - - # Create threads - threads = [threading.Thread(target=spawn_npc, args=(i,)) for i in range(3)] - - # Start all threads - for thread in threads: - thread.start() - - # Wait for all threads to complete - for thread in threads: - thread.join() - - assert len(errors) == 0 - assert len(results) == 3 - - for result in results: - assert isinstance(result, NPC) - assert result.npc_id - assert result.name - - -@pytest.mark.requires_universe -def test_streaming_response_timing( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test that streaming responses arrive in chunks over time""" - stream = authenticated_sync_client.chat.completions( - npc_params={ - "universe_id": sync_test_universe_id, - "world_seed": 12345, - "npc_id": 10000000000000000009999999999, - }, - messages=[{"role": "user", "content": "Count slowly from 1 to 5"}], - temperature=0.5, - stream=True, - ) - - chunk_times = [] - start_time = time.time() - - for chunk in stream: - chunk_times.append(time.time() - start_time) - - assert len(chunk_times) > 1 # Should have multiple chunks - # Chunks should arrive over time, not all at once - assert ( - chunk_times[-1] - chunk_times[0] > 0.1 - ) # At least 100ms between first and last - - -@pytest.mark.requires_universe -def test_streaming_vs_non_streaming_consistency( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test that streaming and non-streaming give similar results""" - prompt = "What is your favorite food?" - npc_params = { - "universe_id": sync_test_universe_id, - "world_seed": 12345, - "npc_id": 10000000000000000009999999999, - } - - # Non-streaming response - non_streaming = authenticated_sync_client.chat.completions( - npc_params=npc_params, - messages=[{"role": "user", "content": prompt}], - temperature=0.1, # Low temperature for consistency - stream=False, - ) - - # Streaming response - stream = authenticated_sync_client.chat.completions( - npc_params=npc_params, - messages=[{"role": "user", "content": prompt}], - temperature=0.1, - stream=True, - ) - - streaming_content = "" - for chunk in stream: - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - content = delta.get("content", "") - if content: - streaming_content += content - - # Both responses should exist and be substantial - assert len(non_streaming.choices[0].message.content) > 10 - assert len(streaming_content) > 10 - - -# ============================================================================ -# NEW NPC CHAT TESTS -# ============================================================================ - - -@pytest.mark.requires_universe -def test_npc_client_attachment( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test that NPCs have client attached after spawning""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=99999, npc_id=9999999999 - ) - - assert npc.client is authenticated_sync_client - assert hasattr(npc, "chat") - assert hasattr(npc.chat, "completions") - - -@pytest.mark.requires_universe -def test_npc_chat_completions_method( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test npc.chat.completions() method""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=88888, npc_id=8888888888 - ) - - # Test basic chat completion - response = npc.chat.completions( - [{"role": "user", "content": "What is your favorite color?"}] - ) - - assert isinstance(response, ChatCompletion) - assert response.choices - assert len(response.choices) > 0 - assert response.choices[0].message.content - - -@pytest.mark.requires_universe -def test_npc_completions_alias( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test npc.completions() alias method""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=77777, npc_id=7777777777 - ) - - # Test using the alias - response = npc.completions( - [{"role": "user", "content": "Tell me about your hobbies"}] - ) - - assert isinstance(response, ChatCompletion) - assert response.choices[0].message.content - - -@pytest.mark.requires_universe -def test_npc_chat_with_parameters( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test npc.chat.completions() with various parameters""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=66666, npc_id=6666666666 - ) - - # Test with temperature and max_tokens - response = npc.chat.completions( - messages=[{"role": "user", "content": "Describe the weather"}], - temperature=0.5, - max_tokens=50, - ) - - assert isinstance(response, ChatCompletion) - assert len(response.choices[0].message.content) > 0 - - # Test with different temperature - response2 = npc.completions( - messages=[{"role": "user", "content": "Describe the weather"}], temperature=1.5 - ) - - assert isinstance(response2, ChatCompletion) - - -@pytest.mark.requires_universe -def test_npc_streaming_via_chat_proxy( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test streaming through npc.chat.completions()""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=55555, npc_id=5555555555 - ) - - # Test streaming - stream = npc.chat.completions( - messages=[{"role": "user", "content": "Count to five slowly"}], stream=True - ) - - chunks = [] - for chunk in stream: - chunks.append(chunk) - if "choices" in chunk and chunk["choices"]: - delta = chunk["choices"][0].get("delta", {}) - if "content" in delta: - assert isinstance(delta["content"], str) - - assert len(chunks) > 0 - - -@pytest.mark.requires_universe -def test_npc_conversation_history( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test conversation with history through NPC object""" - npc = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=44444, npc_id=4444444444 - ) - - # First message - response1 = npc.completions([{"role": "user", "content": "My name is TestBot"}]) - - # Continue conversation with history - response2 = npc.completions( - [ - {"role": "user", "content": "My name is TestBot"}, - {"role": "assistant", "content": response1.choices[0].message.content}, - {"role": "user", "content": "What did I just tell you my name was?"}, - ] - ) - - assert isinstance(response2, ChatCompletion) - # Response should reference the name somehow - content = response2.choices[0].message.content.lower() - assert "testbot" in content or "test" in content or "bot" in content - - -def test_npc_without_client_fails(): - """Test that NPC without client raises proper error""" - from gumyum_npc_client import SpawnedData - - # Create NPC without client - npc = NPC( - npc_id=123456789, - name="Test NPC", - profession="Tester", - personality_type=5, - spawned=SpawnedData(location="test", mood="neutral", stress_level=5), - universe_id="test-universe", - seed=12345, - ) - - # Should not have client - assert npc.client is None - - # Should raise error when trying to chat - with pytest.raises(ValueError, match="NPC has no client reference"): - npc.chat.completions([{"role": "user", "content": "Hello"}]) - - -@pytest.mark.requires_universe -def test_npc_preserves_context( - authenticated_sync_client: GumYumClient, sync_test_universe_id: str -): - """Test that NPC properly uses its own context for all chats""" - # Spawn two different NPCs - npc1 = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=11111, npc_id=1111111111 - ) - npc2 = authenticated_sync_client.npc.spawn( - sync_test_universe_id, seed=22222, npc_id=2222222222 - ) - - # Chat with both - response1 = npc1.completions([{"role": "user", "content": "What is your name?"}]) - response2 = npc2.completions([{"role": "user", "content": "What is your name?"}]) - - # Their names should be different and match what was spawned - assert npc1.name.lower() in response1.choices[0].message.content.lower() - assert npc2.name.lower() in response2.choices[0].message.content.lower() - assert response1.choices[0].message.content != response2.choices[0].message.content