# #region Test.Api.AgentConversations [C:3] [TYPE Module] [SEMANTICS test,agent,conversations,routes] # @BRIEF Tests for agent conversation API routes — list, save, history, delete, active, llm-config. # @RELATION BINDS_TO -> [AgentChat.Api.Conversations] import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") os.environ.setdefault("DEV_MODE", "true") import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) def _make_client(overrides: dict | None = None) -> TestClient: from src.api.routes.agent_conversations import router, agent_router from src.core.database import get_db from src.dependencies import get_current_user, get_config_manager from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router) app.include_router(agent_router) mock_user = User( id="user-1", username="testuser", email="test@x.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="r1", name="User", description="", permissions=[])], ) app.dependency_overrides[get_db] = lambda: MagicMock() app.dependency_overrides[get_current_user] = lambda: mock_user app.dependency_overrides[get_config_manager] = lambda: MagicMock() if overrides: for dep, fn in overrides.items(): app.dependency_overrides[dep] = fn return TestClient(app) # ── list_conversations ── class TestListConversations: """GET /api/assistant/conversations""" def test_success(self): mock_db = MagicMock() mock_query = MagicMock() mock_db.query.return_value = mock_query mock_query.filter.return_value = mock_query mock_query.count.return_value = 2 mock_conv = MagicMock() mock_conv.id = "conv-1" mock_conv.title = "Test Conversation" mock_conv.updated_at = None mock_conv.messages = [MagicMock()] mock_query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [mock_conv, mock_conv] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/assistant/conversations") assert resp.status_code == 200 data = resp.json() assert len(data["items"]) == 2 assert data["items"][0]["id"] == "conv-1" def test_with_search(self): mock_db = MagicMock() mock_query = MagicMock() mock_db.query.return_value = mock_query mock_query.filter.return_value = mock_query mock_query.count.return_value = 0 mock_query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/assistant/conversations?search=test&include_archived=true") assert resp.status_code == 200 def test_empty(self): mock_db = MagicMock() mock_query = MagicMock() mock_db.query.return_value = mock_query mock_query.filter.return_value = mock_query mock_query.count.return_value = 0 mock_query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/assistant/conversations") assert resp.status_code == 200 assert resp.json()["items"] == [] # ── save_conversation ── class TestSaveConversation: """POST /api/agent/conversations/save""" SAVE_PAYLOAD = { "conversation_id": "conv-1", "title": "New Chat", "messages": [ {"id": "msg-1", "role": "user", "text": "Hello"}, {"id": "msg-2", "role": "assistant", "text": "Hi there", "tool_calls": [{"name": "search"}]}, ], } def test_create_new(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/agent/conversations/save", json=self.SAVE_PAYLOAD) assert resp.status_code == 200 data = resp.json() assert data["saved"] is True assert data["conversation_id"] == "conv-1" def test_update_existing(self): existing_conv = MagicMock() mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [existing_conv, None] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/agent/conversations/save", json=self.SAVE_PAYLOAD) assert resp.status_code == 200 def test_skip_empty_message_id(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/agent/conversations/save", json={ "conversation_id": "conv-1", "messages": [{"id": "", "role": "user", "text": "skip me"}], }) assert resp.status_code == 200 def test_skip_duplicate_message(self): existing_msg = MagicMock() mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [None, existing_msg] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/agent/conversations/save", json=self.SAVE_PAYLOAD) assert resp.status_code == 200 # ── get_history ── class TestGetHistory: """GET /api/assistant/history""" def test_success(self): mock_db = MagicMock() mock_conv = MagicMock() mock_conv.id = "conv-1" mock_msg = MagicMock() mock_msg.id = "msg-1" mock_msg.conversation_id = "conv-1" mock_msg.role = "user" mock_msg.text = "Hello" mock_msg.tool_calls = None mock_msg.attachments = None mock_msg.created_at = None mock_conv.messages = [mock_msg] mock_db.query.return_value.filter.return_value.first.return_value = mock_conv from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/assistant/history?conversation_id=conv-1") assert resp.status_code == 200 data = resp.json() assert len(data["items"]) == 1 assert data["items"][0]["text"] == "Hello" def test_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/assistant/history?conversation_id=unknown") assert resp.status_code == 404 # ── delete_conversation ── class TestDeleteConversation: """DELETE /api/assistant/conversations/{conversation_id}""" def test_success(self): mock_db = MagicMock() mock_conv = MagicMock() mock_conv.is_archived = False mock_db.query.return_value.filter.return_value.first.return_value = mock_conv from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.delete("/api/assistant/conversations/conv-1") assert resp.status_code == 200 assert resp.json()["deleted"] is True assert mock_conv.is_archived is True def test_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.delete("/api/assistant/conversations/unknown") assert resp.status_code == 404 # ── check_active_session ── class TestCheckActiveSession: """GET /api/agent/conversations/active""" def test_returns_false(self): client = _make_client() resp = client.get("/api/agent/conversations/active") assert resp.status_code == 200 assert resp.json()["active"] is False # ── get_agent_llm_config ── class TestGetAgentLlmConfig: """GET /api/agent/llm-config""" def test_success_with_preferred(self): mock_db = MagicMock() mock_config_mgr = MagicMock() mock_config = MagicMock() mock_config.settings.llm = {"assistant_planner_provider": "prov-1"} mock_config_mgr.get_config.return_value = mock_config mock_svc = MagicMock() mock_provider = MagicMock() mock_provider.id = "prov-1" mock_provider.provider_type = "openai" mock_provider.base_url = "https://api.openai.com" mock_provider.default_model = "gpt-4" mock_provider.name = "GPT-4 Provider" mock_provider.is_active = True mock_svc.get_all_providers.return_value = [mock_provider] mock_svc.get_decrypted_api_key.return_value = "sk-real-key" from src.core.database import get_db from src.dependencies import get_config_manager with patch("src.api.routes.agent_conversations.LLMProviderService", return_value=mock_svc): client = _make_client({ get_db: lambda: mock_db, get_config_manager: lambda: mock_config_mgr, }) resp = client.get("/api/agent/llm-config") assert resp.status_code == 200 data = resp.json() assert data["configured"] is True assert data["provider_type"] == "openai" def test_fallback_to_active(self): mock_db = MagicMock() mock_config_mgr = MagicMock() mock_config = MagicMock() mock_config.settings.llm = {} mock_config_mgr.get_config.return_value = mock_config mock_svc = MagicMock() mock_provider = MagicMock() mock_provider.id = "prov-2" mock_provider.provider_type = "anthropic" mock_provider.base_url = "" mock_provider.default_model = "claude-3" mock_provider.name = "Claude" mock_provider.is_active = True mock_svc.get_all_providers.return_value = [mock_provider] mock_svc.get_decrypted_api_key.return_value = "sk-anthropic" from src.core.database import get_db from src.dependencies import get_config_manager with patch("src.api.routes.agent_conversations.LLMProviderService", return_value=mock_svc): client = _make_client({ get_db: lambda: mock_db, get_config_manager: lambda: mock_config_mgr, }) resp = client.get("/api/agent/llm-config") assert resp.status_code == 200 assert resp.json()["configured"] is True def test_no_active_provider(self): mock_db = MagicMock() mock_config_mgr = MagicMock() mock_config = MagicMock() mock_config.settings.llm = {} mock_config_mgr.get_config.return_value = mock_config mock_svc = MagicMock() mock_svc.get_all_providers.return_value = [] from src.core.database import get_db from src.dependencies import get_config_manager with patch("src.api.routes.agent_conversations.LLMProviderService", return_value=mock_svc): client = _make_client({ get_db: lambda: mock_db, get_config_manager: lambda: mock_config_mgr, }) resp = client.get("/api/agent/llm-config") assert resp.status_code == 200 assert resp.json()["configured"] is False assert resp.json()["reason"] == "no_active_provider" def test_invalid_api_key(self): mock_db = MagicMock() mock_config_mgr = MagicMock() mock_config = MagicMock() mock_config.settings.llm = {} mock_config_mgr.get_config.return_value = mock_config mock_svc = MagicMock() mock_provider = MagicMock() mock_provider.id = "prov-1" mock_provider.is_active = True mock_svc.get_all_providers.return_value = [mock_provider] mock_svc.get_decrypted_api_key.return_value = "" from src.core.database import get_db from src.dependencies import get_config_manager with patch("src.api.routes.agent_conversations.LLMProviderService", return_value=mock_svc): client = _make_client({ get_db: lambda: mock_db, get_config_manager: lambda: mock_config_mgr, }) resp = client.get("/api/agent/llm-config") assert resp.status_code == 200 assert resp.json()["configured"] is False assert resp.json()["reason"] == "invalid_api_key" # #endregion Test.Api.AgentConversations