"""Server Routes 单元测试 - 使用 FastAPI TestClient""" import pytest from unittest.mock import AsyncMock, MagicMock, patch from fastapi.testclient import TestClient from agentkit.core.agent_pool import AgentPool from agentkit.core.config_driven import AgentConfig from agentkit.core.protocol import AgentStatus from agentkit.llm.gateway import LLMGateway from agentkit.llm.protocol import LLMResponse, TokenUsage from agentkit.skills.base import Skill, SkillConfig 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() # Register a mock provider so gateway.chat() works mock_provider = AsyncMock() mock_provider.chat.return_value = LLMResponse( content='{"result": "mocked output"}', model="test-model", usage=TokenUsage(prompt_tokens=10, completion_tokens=20), ) gateway.register_provider("test", mock_provider) return gateway @pytest.fixture def skill_registry(): return SkillRegistry() @pytest.fixture def tool_registry(): return ToolRegistry() @pytest.fixture def app(mock_llm_gateway, skill_registry, tool_registry): return create_app( llm_gateway=mock_llm_gateway, skill_registry=skill_registry, tool_registry=tool_registry, ) @pytest.fixture def client(app): return TestClient(app) class TestHealthRoute: """GET /api/v1/health""" def test_health_returns_ok(self, client): response = client.get("/api/v1/health") assert response.status_code == 200 data = response.json() assert data["status"] in ("ok", "healthy", "degraded") assert data["version"] == "2.0.0" assert "checks" in data class TestAgentRoutes: """Agent CRUD 路由测试""" def test_create_agent_201(self, client): response = client.post( "/api/v1/agents", json={ "config": { "name": "test_agent", "agent_type": "test_type", "task_mode": "llm_generate", "prompt": {"identity": "Test", "instructions": "Do test"}, } }, ) assert response.status_code == 201 data = response.json() assert data["name"] == "test_agent" assert data["agent_type"] == "test_type" def test_create_agent_from_skill_201(self, client, skill_registry): skill_config = SkillConfig( name="my_skill", agent_type="skill_type", task_mode="llm_generate", prompt={"identity": "Skill Agent"}, intent={"keywords": ["skill"], "description": "A skill"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) response = client.post( "/api/v1/agents", json={"skill_name": "my_skill"}, ) assert response.status_code == 201 data = response.json() assert data["name"] == "my_skill" def test_list_agents_empty(self, client): response = client.get("/api/v1/agents") assert response.status_code == 200 assert response.json() == [] def test_list_agents_after_create(self, client): client.post( "/api/v1/agents", json={ "config": { "name": "agent1", "agent_type": "type1", "task_mode": "llm_generate", "prompt": {"identity": "Agent 1"}, } }, ) response = client.get("/api/v1/agents") assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["name"] == "agent1" def test_get_agent_detail(self, client): client.post( "/api/v1/agents", json={ "config": { "name": "detail_agent", "agent_type": "detail_type", "task_mode": "llm_generate", "prompt": {"identity": "Detail Agent"}, } }, ) response = client.get("/api/v1/agents/detail_agent") assert response.status_code == 200 data = response.json() assert data["name"] == "detail_agent" assert data["agent_type"] == "detail_type" def test_get_agent_not_found_404(self, client): response = client.get("/api/v1/agents/nonexistent") assert response.status_code == 404 def test_delete_agent_204(self, client): client.post( "/api/v1/agents", json={ "config": { "name": "to_delete", "agent_type": "del_type", "task_mode": "llm_generate", "prompt": {"identity": "Delete me"}, } }, ) response = client.delete("/api/v1/agents/to_delete") assert response.status_code == 204 # Verify agent is gone response = client.get("/api/v1/agents/to_delete") assert response.status_code == 404 class TestTaskRoutes: """Task 提交路由测试""" def test_submit_task_with_skill_name(self, client, skill_registry): # Register a skill first skill_config = SkillConfig( name="task_skill", agent_type="task_type", task_mode="llm_generate", prompt={"identity": "Task Skill", "instructions": "Handle tasks"}, intent={"keywords": ["task"], "description": "Task skill"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) response = client.post( "/api/v1/tasks", json={ "input_data": {"query": "test query"}, "skill_name": "task_skill", }, ) assert response.status_code == 200 data = response.json() assert "skill_name" in data or "data" in data or "output" in data def test_submit_task_with_agent_name(self, client): # Create an agent first client.post( "/api/v1/agents", json={ "config": { "name": "task_agent", "agent_type": "task_type", "task_mode": "llm_generate", "prompt": {"identity": "Task Agent"}, } }, ) response = client.post( "/api/v1/tasks", json={ "input_data": {"query": "test query"}, "agent_name": "task_agent", }, ) assert response.status_code == 200 def test_submit_task_no_skill_no_agent_error(self, client): response = client.post( "/api/v1/tasks", json={ "input_data": {"query": "test query"}, }, ) # Should return 400 or 422 since no skill or agent specified and no skills registered assert response.status_code in (400, 422) def test_get_task_status_placeholder(self, client): response = client.get("/api/v1/tasks/some-task-id") # Placeholder implementation assert response.status_code in (200, 404) class TestSkillRoutes: """Skill 注册路由测试""" def test_register_skill_201(self, client): response = client.post( "/api/v1/skills", json={ "config": { "name": "new_skill", "agent_type": "skill_type", "task_mode": "llm_generate", "prompt": {"identity": "New Skill"}, "intent": {"keywords": ["new"], "description": "A new skill"}, } }, ) assert response.status_code == 201 data = response.json() assert data["name"] == "new_skill" def test_list_skills_empty(self, client): response = client.get("/api/v1/skills") assert response.status_code == 200 assert response.json() == [] def test_list_skills_after_register(self, client): client.post( "/api/v1/skills", json={ "config": { "name": "listed_skill", "agent_type": "skill_type", "task_mode": "llm_generate", "prompt": {"identity": "Listed Skill"}, "intent": {"keywords": ["listed"], "description": "A listed skill"}, } }, ) response = client.get("/api/v1/skills") assert response.status_code == 200 data = response.json() assert len(data) >= 1 names = [s["name"] for s in data] assert "listed_skill" in names class TestLLMRoute: """LLM Usage 路由测试""" def test_get_usage(self, client): response = client.get("/api/v1/llm/usage") assert response.status_code == 200 data = response.json() assert "total_tokens" in data or "total_cost" in data def test_get_usage_with_agent_name(self, client): response = client.get("/api/v1/llm/usage?agent_name=test_agent") assert response.status_code == 200 class TestSSEStreamUsesAgentConfig: """U8: SSE stream uses agent's configuration (max_steps, model, tools, system_prompt)""" def test_stream_uses_agent_model(self, client, skill_registry): """Stream endpoint should use the agent's configured model, not hardcoded default""" skill_config = SkillConfig( name="stream_skill", agent_type="stream_type", task_mode="llm_generate", prompt={"identity": "Stream Agent", "instructions": "Handle streams"}, intent={"keywords": ["stream"], "description": "Stream skill"}, llm={"model": "gpt-4-turbo"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) # Create agent so it's in the pool client.post("/api/v1/agents", json={"skill_name": "stream_skill"}) # Verify the agent's get_model() returns the configured model pool = client.app.state.agent_pool agent = pool.get_agent("stream_skill") assert agent is not None assert agent.get_model() == "gpt-4-turbo" def test_stream_uses_agent_max_steps(self, client, skill_registry): """Stream endpoint should use agent's max_steps, not default 10""" skill_config = SkillConfig( name="maxsteps_skill", agent_type="maxsteps_type", task_mode="llm_generate", prompt={"identity": "MaxSteps Agent"}, intent={"keywords": ["maxsteps"], "description": "MaxSteps skill"}, max_steps=3, ) skill = Skill(config=skill_config) skill_registry.register(skill) client.post("/api/v1/agents", json={"skill_name": "maxsteps_skill"}) pool = client.app.state.agent_pool agent = pool.get_agent("maxsteps_skill") assert agent is not None react_config = agent.get_react_config() assert react_config["max_steps"] == 3 def test_stream_uses_agent_tools(self, client, skill_registry): """Stream endpoint should use agent.get_tools(), not private _tool_registry""" skill_config = SkillConfig( name="tools_skill", agent_type="tools_type", task_mode="llm_generate", prompt={"identity": "Tools Agent"}, intent={"keywords": ["tools"], "description": "Tools skill"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) client.post("/api/v1/agents", json={"skill_name": "tools_skill"}) pool = client.app.state.agent_pool agent = pool.get_agent("tools_skill") assert agent is not None # get_tools() should return a list (may be empty) tools = agent.get_tools() assert isinstance(tools, list) def test_stream_uses_agent_system_prompt(self, client, skill_registry): """Stream endpoint should use agent.get_system_prompt(), not private _system_prompt""" skill_config = SkillConfig( name="prompt_skill", agent_type="prompt_type", task_mode="llm_generate", prompt={"identity": "Prompt Agent", "instructions": "Do stuff"}, intent={"keywords": ["prompt"], "description": "Prompt skill"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) client.post("/api/v1/agents", json={"skill_name": "prompt_skill"}) pool = client.app.state.agent_pool agent = pool.get_agent("prompt_skill") assert agent is not None prompt = agent.get_system_prompt() assert prompt is not None assert "Prompt Agent" in prompt class TestSSEStreamFallback: """U8: SSE stream fallback when provider fails during streaming""" def test_stream_fallback_no_chunks_sent(self, client, skill_registry, mock_llm_gateway): """When provider fails before any chunks, fallback model is attempted""" from agentkit.core.exceptions import LLMProviderError skill_config = SkillConfig( name="fallback_skill", agent_type="fallback_type", task_mode="llm_generate", prompt={"identity": "Fallback Agent"}, intent={"keywords": ["fallback"], "description": "Fallback skill"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) client.post("/api/v1/agents", json={"skill_name": "fallback_skill"}) pool = client.app.state.agent_pool agent = pool.get_agent("fallback_skill") assert agent is not None # Verify the gateway has _get_fallback_model method assert hasattr(mock_llm_gateway, "_get_fallback_model") def test_stream_error_event_on_mid_stream_failure(self, client, skill_registry): """When provider fails mid-stream, an error event is yielded""" skill_config = SkillConfig( name="midskill", agent_type="mid_type", task_mode="llm_generate", prompt={"identity": "Mid Agent"}, intent={"keywords": ["mid"], "description": "Mid skill"}, ) skill = Skill(config=skill_config) skill_registry.register(skill) client.post("/api/v1/agents", json={"skill_name": "midskill"}) pool = client.app.state.agent_pool agent = pool.get_agent("midskill") assert agent is not None