"""Unit tests for Memory API routes""" import pytest from unittest.mock import AsyncMock, MagicMock, patch from fastapi.testclient import TestClient from agentkit.llm.gateway import LLMGateway from agentkit.memory.retriever import MemoryRetriever from agentkit.memory.base import MemoryItem from agentkit.skills.registry import SkillRegistry from agentkit.tools.registry import ToolRegistry from agentkit.server.app import create_app @pytest.fixture def mock_llm_gateway(): gateway = LLMGateway() mock_provider = AsyncMock() from agentkit.llm.protocol import LLMResponse, TokenUsage mock_provider.chat.return_value = LLMResponse( content='{"result": "mocked"}', model="test-model", usage=TokenUsage(prompt_tokens=10, completion_tokens=20), ) gateway.register_provider("test", mock_provider) return gateway @pytest.fixture def mock_episodic(): episodic = AsyncMock() return episodic @pytest.fixture def mock_semantic(): semantic = AsyncMock() return semantic @pytest.fixture def memory_retriever(mock_episodic, mock_semantic): return MemoryRetriever( episodic_memory=mock_episodic, semantic_memory=mock_semantic, ) @pytest.fixture def app(mock_llm_gateway, memory_retriever): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = memory_retriever return app @pytest.fixture def client(app): return TestClient(app) class TestSearchEpisodicMemory: """GET /api/v1/memory/episodic""" def test_search_returns_results(self, client, mock_episodic): mock_episodic.search.return_value = [ MemoryItem( key="ep-1", value={"input_summary": "test input", "output_summary": "test output"}, score=0.85, metadata={"source": "episodic", "agent_name": "test_agent"}, ), ] response = client.get("/api/v1/memory/episodic?query=test") assert response.status_code == 200 data = response.json() assert data["query"] == "test" assert data["total"] == 1 assert data["results"][0]["key"] == "ep-1" assert data["results"][0]["score"] == 0.85 def test_search_with_agent_name_filter(self, client, mock_episodic): mock_episodic.search.return_value = [] response = client.get("/api/v1/memory/episodic?query=test&agent_name=my_agent") assert response.status_code == 200 mock_episodic.search.assert_called_once() call_kwargs = mock_episodic.search.call_args assert call_kwargs[1]["filters"] == {"agent_name": "my_agent"} or ( call_kwargs[0] and len(call_kwargs[0]) > 2 and call_kwargs[0][2] == {"agent_name": "my_agent"} ) def test_search_with_top_k(self, client, mock_episodic): mock_episodic.search.return_value = [] response = client.get("/api/v1/memory/episodic?query=test&top_k=10") assert response.status_code == 200 mock_episodic.search.assert_called_once() def test_search_returns_empty_results(self, client, mock_episodic): mock_episodic.search.return_value = [] response = client.get("/api/v1/memory/episodic?query=nonexistent") assert response.status_code == 200 data = response.json() assert data["total"] == 0 assert data["results"] == [] def test_returns_503_when_retriever_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = None client = TestClient(app) response = client.get("/api/v1/memory/episodic?query=test") assert response.status_code == 503 def test_returns_503_when_episodic_not_configured(self, mock_llm_gateway): retriever = MemoryRetriever(episodic_memory=None, semantic_memory=None) app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = retriever client = TestClient(app) response = client.get("/api/v1/memory/episodic?query=test") assert response.status_code == 503 class TestSearchSemanticMemory: """GET /api/v1/memory/semantic/search""" def test_search_returns_results(self, client, mock_semantic): mock_semantic.search.return_value = [ MemoryItem( key="doc-1", value="Relevant document content", score=0.92, metadata={"source": "rag", "knowledge_base_id": "kb-1"}, ), ] response = client.get("/api/v1/memory/semantic/search?query=hello") assert response.status_code == 200 data = response.json() assert data["query"] == "hello" assert data["total"] == 1 assert data["results"][0]["key"] == "doc-1" def test_search_with_knowledge_base_ids(self, client, mock_semantic): mock_semantic.search.return_value = [] response = client.get("/api/v1/memory/semantic/search?query=test&knowledge_base_ids=kb1,kb2") assert response.status_code == 200 mock_semantic.search.assert_called_once() call_args = mock_semantic.search.call_args # filters is passed as keyword arg filters = call_args.kwargs.get("filters") or call_args[1].get("filters") assert filters is not None assert "knowledge_base_ids" in filters assert filters["knowledge_base_ids"] == ["kb1", "kb2"] def test_search_returns_empty_results(self, client, mock_semantic): mock_semantic.search.return_value = [] response = client.get("/api/v1/memory/semantic/search?query=nonexistent") assert response.status_code == 200 data = response.json() assert data["total"] == 0 def test_returns_503_when_retriever_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = None client = TestClient(app) response = client.get("/api/v1/memory/semantic/search?query=test") assert response.status_code == 503 def test_returns_503_when_semantic_not_configured(self, mock_llm_gateway): retriever = MemoryRetriever(episodic_memory=None, semantic_memory=None) app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = retriever client = TestClient(app) response = client.get("/api/v1/memory/semantic/search?query=test") assert response.status_code == 503 class TestDeleteEpisodicMemory: """DELETE /api/v1/memory/episodic/{key}""" def test_delete_succeeds(self, client, mock_episodic): mock_episodic.delete.return_value = True response = client.delete("/api/v1/memory/episodic/ep-123") assert response.status_code == 200 data = response.json() assert data["key"] == "ep-123" assert data["deleted"] is True def test_delete_returns_404_when_not_found(self, client, mock_episodic): mock_episodic.delete.return_value = False response = client.delete("/api/v1/memory/episodic/nonexistent") assert response.status_code == 404 def test_returns_503_when_retriever_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = None client = TestClient(app) response = client.delete("/api/v1/memory/episodic/ep-1") assert response.status_code == 503 def test_returns_503_when_episodic_not_configured(self, mock_llm_gateway): retriever = MemoryRetriever(episodic_memory=None, semantic_memory=None) app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.memory_retriever = retriever client = TestClient(app) response = client.delete("/api/v1/memory/episodic/ep-1") assert response.status_code == 503