fischer-agentkit/tests/unit/test_server_routes.py

428 lines
14 KiB
Python

"""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