""" Shared fixtures for Cortex test suite. Key design choices: - All file I/O goes to a tmp_path, never touching personas/inara/ or real sessions. - LLM calls are mocked by default — tests are fast and deterministic. - The 'app' fixture patches settings before importing main, so all modules see the temp directory. """ import json import pytest import pytest_asyncio from pathlib import Path from unittest.mock import AsyncMock, patch import httpx from httpx import ASGITransport # --------------------------------------------------------------------------- # Temp persona directory # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def personas_root(tmp_path_factory) -> Path: """A temp personas/ dir with a minimal 'inara' persona for testing.""" root = tmp_path_factory.mktemp("personas") _make_persona(root, "inara", "Inara", "Scott") return root def _make_persona(root: Path, name: str, agent: str, user: str) -> Path: p = root / name p.mkdir(parents=True, exist_ok=True) (p / "IDENTITY.md").write_text(f"# {agent}\nTest identity for {agent}.") (p / "SOUL.md").write_text(f"# Soul\nTest soul for {agent}.") (p / "PROTOCOLS.md").write_text("# Protocols\nBe helpful.") (p / "USER.md").write_text(f"# {user}\nTest user profile.") (p / "HELP.md").write_text("# Help\nTest help content.") (p / "MEMORY_LONG.md").write_text("Not yet populated.") (p / "MEMORY_MID.md").write_text("Not yet populated.") (p / "MEMORY_SHORT.md").write_text("Not yet populated.") (p / "TASKS.json").write_text("[]") (p / "CRONS.json").write_text("[]") (p / "SCRATCH.md").write_text("") (p / "REMINDERS.md").write_text("") (p / "sessions").mkdir() return p # --------------------------------------------------------------------------- # App fixture — patches settings before the ASGI app is started # --------------------------------------------------------------------------- @pytest_asyncio.fixture async def client(personas_root, tmp_path): """HTTPX async test client against the live ASGI app with patched paths.""" import config import persona as persona_mod sessions_dir = tmp_path / "sessions" sessions_dir.mkdir() with ( patch.object(config.settings, "personas_dir", personas_root), patch.object(config.settings, "sessions_dir", sessions_dir), patch("scheduler.start"), # don't run APScheduler in tests patch("scheduler.stop"), ): # Force persona module to re-read patched settings persona_mod._current.set("inara") from main import app async with httpx.AsyncClient( transport=ASGITransport(app=app), base_url="http://test", ) as c: yield c # --------------------------------------------------------------------------- # LLM mock # --------------------------------------------------------------------------- @pytest.fixture def mock_llm(): """ Patch complete() at every import site so no real LLM calls are made. Each router does `from llm_client import complete`, creating a local reference. Patching llm_client.complete alone won't intercept those — patch each site. """ ret = ("Hello, I am a test response.", "claude") with ( patch("routers.chat.complete", new_callable=AsyncMock, return_value=ret), patch("routers.nextcloud_talk.complete", new_callable=AsyncMock, return_value=ret), patch("routers.google_chat.complete", new_callable=AsyncMock, return_value=ret), patch("llm_client.complete", new_callable=AsyncMock, return_value=ret), ): yield