"""Unit tests for Evolution API routes""" import asyncio import pytest from fastapi.testclient import TestClient from agentkit.evolution.evolution_store import InMemoryEvolutionStore from agentkit.llm.gateway import LLMGateway from agentkit.llm.protocol import LLMResponse, TokenUsage from agentkit.skills.registry import SkillRegistry from agentkit.tools.registry import ToolRegistry from agentkit.server.app import create_app from unittest.mock import AsyncMock def _run_async(coro): """Run an async coroutine synchronously (works on Python 3.14+).""" try: loop = asyncio.get_running_loop() except RuntimeError: loop = None if loop and loop.is_running(): # Already in an async context — use nest_asyncio or a new thread import concurrent.futures with concurrent.futures.ThreadPoolExecutor() as pool: return pool.submit(asyncio.run, coro).result() return asyncio.run(coro) @pytest.fixture def mock_llm_gateway(): gateway = LLMGateway() mock_provider = AsyncMock() 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 evolution_store(): return InMemoryEvolutionStore() @pytest.fixture def app(mock_llm_gateway, evolution_store): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.evolution_store = evolution_store return app @pytest.fixture def client(app): return TestClient(app) class TestListEvolutionEvents: """GET /api/v1/evolution/events""" def test_returns_empty_list(self, client): response = client.get("/api/v1/evolution/events") assert response.status_code == 200 data = response.json() assert data["items"] == [] assert data["total"] == 0 def test_returns_events_after_record(self, client, evolution_store): from agentkit.core.protocol import EvolutionEvent event = EvolutionEvent( agent_name="test_agent", change_type="prompt", before={"old": "value"}, after={"new": "value"}, metrics={"quality_score": 0.9}, ) _run_async(evolution_store.record(event)) response = client.get("/api/v1/evolution/events") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["items"][0]["agent_name"] == "test_agent" assert data["items"][0]["change_type"] == "prompt" def test_filter_by_agent_name(self, client, evolution_store): from agentkit.core.protocol import EvolutionEvent event1 = EvolutionEvent( agent_name="agent_a", change_type="prompt", before={}, after={}, ) event2 = EvolutionEvent( agent_name="agent_b", change_type="strategy", before={}, after={}, ) _run_async(evolution_store.record(event1)) _run_async(evolution_store.record(event2)) response = client.get("/api/v1/evolution/events?agent_name=agent_a") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["items"][0]["agent_name"] == "agent_a" def test_filter_by_event_type(self, client, evolution_store): from agentkit.core.protocol import EvolutionEvent event1 = EvolutionEvent( agent_name="agent_a", change_type="prompt", before={}, after={}, ) event2 = EvolutionEvent( agent_name="agent_a", change_type="strategy", before={}, after={}, ) _run_async(evolution_store.record(event1)) _run_async(evolution_store.record(event2)) response = client.get("/api/v1/evolution/events?event_type=strategy") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["items"][0]["change_type"] == "strategy" def test_pagination(self, client, evolution_store): from agentkit.core.protocol import EvolutionEvent for i in range(5): event = EvolutionEvent( agent_name=f"agent_{i}", change_type="prompt", before={}, after={}, ) _run_async(evolution_store.record(event)) response = client.get("/api/v1/evolution/events?limit=2&offset=0") assert response.status_code == 200 data = response.json() assert len(data["items"]) == 2 assert data["total"] == 5 def test_returns_503_when_store_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.evolution_store = None client = TestClient(app) response = client.get("/api/v1/evolution/events") assert response.status_code == 503 class TestGetSkillVersions: """GET /api/v1/evolution/skills/{skill_name}/versions""" def test_returns_empty_versions(self, client): response = client.get("/api/v1/evolution/skills/unknown_skill/versions") assert response.status_code == 200 data = response.json() assert data["skill_name"] == "unknown_skill" assert data["versions"] == [] def test_returns_versions_after_record(self, client, evolution_store): _run_async( evolution_store.record_skill_version( skill_name="my_skill", version="1.0.0", content='{"prompt": "hello"}', ) ) _run_async( evolution_store.record_skill_version( skill_name="my_skill", version="2.0.0", content='{"prompt": "world"}', parent_version="1.0.0", ) ) response = client.get("/api/v1/evolution/skills/my_skill/versions") assert response.status_code == 200 data = response.json() assert data["skill_name"] == "my_skill" assert len(data["versions"]) == 2 # Most recent first assert data["versions"][0]["version"] == "2.0.0" assert data["versions"][0]["parent_version"] == "1.0.0" def test_returns_503_when_store_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.evolution_store = None client = TestClient(app) response = client.get("/api/v1/evolution/skills/test/versions") assert response.status_code == 503 class TestTriggerEvolution: """POST /api/v1/evolution/trigger""" def test_trigger_returns_404_for_unknown_agent(self, client): response = client.post( "/api/v1/evolution/trigger", json={"agent_name": "nonexistent"}, ) assert response.status_code == 404 def test_trigger_records_event(self, client, evolution_store): from agentkit.skills.base import Skill, SkillConfig # Register a skill and create an agent skill_config = SkillConfig( name="evo_skill", agent_type="evo_type", task_mode="llm_generate", prompt={"identity": "Evo Agent"}, ) skill = Skill(config=skill_config) client.app.state.skill_registry.register(skill) client.post("/api/v1/agents", json={"skill_name": "evo_skill"}) response = client.post( "/api/v1/evolution/trigger", json={"agent_name": "evo_skill", "skill_name": "evo_skill"}, ) assert response.status_code == 200 data = response.json() assert data["agent_name"] == "evo_skill" assert data["status"] == "triggered" assert "event_id" in data def test_returns_503_when_store_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.evolution_store = None client = TestClient(app) response = client.post( "/api/v1/evolution/trigger", json={"agent_name": "test"}, ) assert response.status_code == 503 class TestListABTests: """GET /api/v1/evolution/ab-tests""" def test_returns_empty_list(self, client): response = client.get("/api/v1/evolution/ab-tests") assert response.status_code == 200 data = response.json() assert data["items"] == [] assert data["total"] == 0 def test_returns_ab_test_results(self, client, evolution_store): _run_async( evolution_store.record_ab_test_result( test_id="test_1", variant="control", score=0.8, sample_count=10, ) ) _run_async( evolution_store.record_ab_test_result( test_id="test_1", variant="experiment", score=0.9, sample_count=10, ) ) response = client.get("/api/v1/evolution/ab-tests") assert response.status_code == 200 data = response.json() assert data["total"] == 2 def test_filter_by_status(self, client, evolution_store): _run_async( evolution_store.record_ab_test_result( test_id="test_1", variant="control", score=0.8, ) ) _run_async( evolution_store.record_ab_test_result( test_id="test_2", variant="experiment", score=0.9, ) ) response = client.get("/api/v1/evolution/ab-tests?status=control") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["items"][0]["variant"] == "control" def test_returns_503_when_store_not_configured(self, mock_llm_gateway): app = create_app( llm_gateway=mock_llm_gateway, skill_registry=SkillRegistry(), tool_registry=ToolRegistry(), ) app.state.evolution_store = None client = TestClient(app) response = client.get("/api/v1/evolution/ab-tests") assert response.status_code == 503