278 lines
13 KiB
Python
278 lines
13 KiB
Python
"""E2E Basic Function Tests — REST API endpoints.
|
|
|
|
Verifies all API routes work correctly with proper request/response handling.
|
|
|
|
Test categories:
|
|
1. Health & metrics
|
|
2. Agent CRUD lifecycle
|
|
3. Skill registration & listing
|
|
4. Task submission (sync/async/SSE)
|
|
5. Chat session lifecycle
|
|
6. LLM usage tracking
|
|
7. Error handling & edge cases
|
|
"""
|
|
|
|
import pytest
|
|
import httpx
|
|
|
|
from tests.e2e.conftest import register_skill_via_api, create_session_via_api
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 1. Health & Metrics
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestHealthAPI:
|
|
def test_health_returns_ok(self, api_client: httpx.Client):
|
|
resp = api_client.get("/api/v1/health")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data.get("status") in ("ok", "healthy")
|
|
|
|
def test_metrics_endpoint(self, api_client: httpx.Client):
|
|
resp = api_client.get("/api/v1/metrics")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 2. Agent CRUD Lifecycle
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestAgentCRUD:
|
|
"""Full Agent CRUD lifecycle: create → list → get → delete."""
|
|
|
|
def test_create_agent_from_skill(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "crud_skill", keywords=["crud"])
|
|
resp = api_client.post("/api/v1/agents", json={"skill_name": "crud_skill"})
|
|
assert resp.status_code == 201
|
|
data = resp.json()
|
|
assert data["name"] == "crud_skill"
|
|
|
|
def test_list_agents(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "list_skill", keywords=["list_agent"])
|
|
api_client.post("/api/v1/agents", json={"skill_name": "list_skill"})
|
|
resp = api_client.get("/api/v1/agents")
|
|
assert resp.status_code == 200
|
|
agents = resp.json()
|
|
assert isinstance(agents, list)
|
|
assert any(a["name"] == "list_skill" for a in agents)
|
|
|
|
def test_get_agent_detail(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "detail_skill", keywords=["detail"])
|
|
api_client.post("/api/v1/agents", json={"skill_name": "detail_skill"})
|
|
resp = api_client.get("/api/v1/agents/detail_skill")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["name"] == "detail_skill"
|
|
|
|
def test_delete_agent(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "delete_skill", keywords=["delete_agent"])
|
|
api_client.post("/api/v1/agents", json={"skill_name": "delete_skill"})
|
|
resp = api_client.delete("/api/v1/agents/delete_skill")
|
|
assert resp.status_code == 204
|
|
# Verify deleted
|
|
resp = api_client.get("/api/v1/agents/delete_skill")
|
|
assert resp.status_code == 404
|
|
|
|
def test_create_agent_nonexistent_skill(self, api_client: httpx.Client):
|
|
resp = api_client.post("/api/v1/agents", json={"skill_name": "nonexistent_skill_xyz"})
|
|
assert resp.status_code in (400, 404)
|
|
|
|
def test_get_nonexistent_agent(self, api_client: httpx.Client):
|
|
resp = api_client.get("/api/v1/agents/does_not_exist")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 3. Skill Registration & Listing
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestSkillAPI:
|
|
def test_register_skill(self, api_client: httpx.Client):
|
|
resp = register_skill_via_api(api_client, "reg_skill", keywords=["reg"])
|
|
assert resp.status_code == 201
|
|
|
|
def test_list_skills(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "list_test_skill", keywords=["list_test"])
|
|
resp = api_client.get("/api/v1/skills")
|
|
assert resp.status_code == 200
|
|
skills = resp.json()
|
|
assert isinstance(skills, list)
|
|
assert len(skills) >= 1
|
|
|
|
def test_register_duplicate_skill(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "dup_skill", keywords=["dup"])
|
|
resp = register_skill_via_api(api_client, "dup_skill", keywords=["dup"])
|
|
# Should either overwrite or return conflict
|
|
assert resp.status_code in (200, 201, 409)
|
|
|
|
def test_skill_with_execution_mode(self, api_client: httpx.Client):
|
|
resp = register_skill_via_api(
|
|
api_client, "react_skill", keywords=["react_test"], execution_mode="react"
|
|
)
|
|
assert resp.status_code == 201
|
|
|
|
def test_skill_mention_suggest(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "mention_skill", keywords=["mention_test"])
|
|
resp = api_client.get("/api/v1/skills/mention-suggest", params={"q": "mention"})
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 4. Task Submission
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestTaskAPI:
|
|
def test_submit_task_sync(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "sync_task_skill", keywords=["sync_task"])
|
|
resp = api_client.post(
|
|
"/api/v1/tasks",
|
|
json={
|
|
"input_data": {"query": "test sync task"},
|
|
"skill_name": "sync_task_skill",
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "output" in data or "data" in data or "skill_name" in data
|
|
|
|
def test_submit_task_with_agent_name(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "agent_task_skill", keywords=["agent_task"])
|
|
api_client.post("/api/v1/agents", json={"skill_name": "agent_task_skill"})
|
|
resp = api_client.post(
|
|
"/api/v1/tasks",
|
|
json={
|
|
"input_data": {"query": "test agent task"},
|
|
"agent_name": "agent_task_skill",
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
def test_submit_task_auto_route(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "auto_route_skill", keywords=["auto_route"])
|
|
resp = api_client.post(
|
|
"/api/v1/tasks",
|
|
json={"input_data": {"query": "Please auto_route this for me"}},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
def test_list_tasks(self, api_client: httpx.Client):
|
|
resp = api_client.get("/api/v1/tasks")
|
|
assert resp.status_code == 200
|
|
|
|
def test_submit_task_missing_data(self, api_client: httpx.Client):
|
|
resp = api_client.post("/api/v1/tasks", json={})
|
|
# Should return 400 or 422
|
|
assert resp.status_code in (400, 422)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 5. Chat Session Lifecycle
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestChatSessionAPI:
|
|
def test_create_session(self, api_client: httpx.Client):
|
|
session_id = create_session_via_api(api_client)
|
|
assert session_id is not None
|
|
assert len(session_id) > 0
|
|
|
|
def test_list_sessions(self, api_client: httpx.Client):
|
|
create_session_via_api(api_client)
|
|
resp = api_client.get("/api/v1/chat/sessions")
|
|
assert resp.status_code == 200
|
|
sessions = resp.json()
|
|
assert isinstance(sessions, list)
|
|
assert len(sessions) >= 1
|
|
|
|
def test_get_session(self, api_client: httpx.Client):
|
|
session_id = create_session_via_api(api_client)
|
|
resp = api_client.get(f"/api/v1/chat/sessions/{session_id}")
|
|
assert resp.status_code == 200
|
|
|
|
def test_session_messages(self, api_client: httpx.Client):
|
|
session_id = create_session_via_api(api_client)
|
|
# Send a message
|
|
resp = api_client.post(
|
|
f"/api/v1/chat/sessions/{session_id}/messages",
|
|
json={"content": "Hello from e2e test"},
|
|
)
|
|
assert resp.status_code == 200
|
|
# Get messages
|
|
resp = api_client.get(f"/api/v1/chat/sessions/{session_id}/messages")
|
|
assert resp.status_code == 200
|
|
|
|
def test_close_session(self, api_client: httpx.Client):
|
|
session_id = create_session_via_api(api_client)
|
|
resp = api_client.delete(f"/api/v1/chat/sessions/{session_id}")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 6. LLM Usage Tracking
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestLLMUsageAPI:
|
|
def test_llm_usage_endpoint(self, api_client: httpx.Client):
|
|
resp = api_client.get("/api/v1/llm/usage")
|
|
assert resp.status_code == 200
|
|
|
|
def test_llm_usage_after_task(self, api_client: httpx.Client):
|
|
register_skill_via_api(api_client, "usage_track_skill", keywords=["usage_track"])
|
|
api_client.post(
|
|
"/api/v1/tasks",
|
|
json={
|
|
"input_data": {"query": "test usage tracking"},
|
|
"skill_name": "usage_track_skill",
|
|
},
|
|
)
|
|
resp = api_client.get("/api/v1/llm/usage")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# 7. Error Handling & Edge Cases
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.e2e_basic
|
|
class TestAPIErrorHandling:
|
|
def test_404_for_unknown_route(self, api_client: httpx.Client):
|
|
resp = api_client.get("/api/v1/nonexistent_route")
|
|
assert resp.status_code == 404
|
|
|
|
def test_invalid_json_body(self, api_client: httpx.Client):
|
|
resp = api_client.post(
|
|
"/api/v1/tasks",
|
|
content=b"not json",
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
assert resp.status_code in (400, 422)
|
|
|
|
def test_missing_api_key(self, e2e_server: str):
|
|
"""Requests without API key should be rejected (if auth enabled)."""
|
|
client = httpx.Client(base_url=e2e_server, timeout=10)
|
|
resp = client.get("/api/v1/agents")
|
|
# Should be 401/403 or still 200 if auth is not enforced on this endpoint
|
|
assert resp.status_code in (200, 401, 403)
|
|
|
|
def test_invalid_api_key(self, e2e_server: str):
|
|
client = httpx.Client(
|
|
base_url=e2e_server,
|
|
headers={"X-API-Key": "invalid_key"},
|
|
timeout=10,
|
|
)
|
|
resp = client.get("/api/v1/agents")
|
|
assert resp.status_code in (200, 401, 403)
|