Spaces:
Sleeping
Sleeping
| """Shared pytest fixtures. | |
| The mock LLM in this module is the workhorse for offline tests: it lets | |
| agents run end-to-end without paying for Gemini calls. Each test passes | |
| either typed Pydantic decisions (via ``call_typed``) or raw JSON strings | |
| (legacy paths) that the agent expects to receive. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from typing import Any, Dict, List | |
| import pytest | |
| from config import reset_settings, set_settings | |
| from utils import APIPoolManager, LLM | |
| class MockLLM(LLM): | |
| """LLM stub that returns canned responses in order. | |
| Tests construct it with a list of either: | |
| * a JSON-string (returned as-is for the untyped __call__ path), | |
| * a dict (JSON-serialised on push; validated against the requested schema | |
| on call_typed), | |
| * a Pydantic ``BaseModel`` instance (returned as-is from call_typed; its | |
| ``.model_dump_json()`` is used for __call__). | |
| Each call pops the next item. Out-of-script calls raise so missing | |
| fixtures are noisy rather than silent. | |
| """ | |
| def __init__(self, responses: List[Any]) -> None: | |
| self._responses: List[Any] = list(responses) | |
| self.calls: List[str] = [] | |
| self.typed_calls: List[tuple[str, type]] = [] | |
| def _next(self, prompt: str) -> Any: | |
| self.calls.append(prompt) | |
| if not self._responses: | |
| raise AssertionError( | |
| f"MockLLM ran out of canned responses (call #{len(self.calls)}). " | |
| f"Last prompt:\n{prompt[:300]}" | |
| ) | |
| return self._responses.pop(0) | |
| def __call__(self, prompt: str, **_: Any) -> List[str]: | |
| item = self._next(prompt) | |
| if hasattr(item, "model_dump_json"): | |
| return [item.model_dump_json()] | |
| if isinstance(item, dict): | |
| return [json.dumps(item)] | |
| return [str(item)] | |
| def call_typed(self, prompt: str, response_model: type, **_: Any): | |
| from pydantic import BaseModel | |
| self.typed_calls.append((prompt, response_model)) | |
| item = self._next(prompt) | |
| if isinstance(item, BaseModel): | |
| return item if isinstance(item, response_model) else None | |
| if isinstance(item, dict): | |
| try: | |
| return response_model.model_validate(item) | |
| except Exception: | |
| return None | |
| if isinstance(item, str): | |
| try: | |
| return response_model.model_validate_json(item) | |
| except Exception: | |
| return None | |
| return None | |
| def format_prompt(self, messages: List[Dict[str, str]]) -> str: | |
| return "\n".join(f"{m['role']}: {m['content']}" for m in messages) | |
| def mock_llm_factory(): | |
| """Factory to build a MockLLM from a list of canned responses.""" | |
| return MockLLM | |
| def fresh_settings(): | |
| """Reset the Settings singleton before/after each test for isolation.""" | |
| reset_settings() | |
| set_settings(debug_mode=False, log_dir=None, persistence_dir=None) | |
| yield | |
| reset_settings() | |
| def api_pool_no_limits(): | |
| """An APIPoolManager with rate limiting disabled — for unit tests that | |
| don't care about throttling.""" | |
| return APIPoolManager(["test-key-1", "test-key-2"], rate_limits=None) | |