fischer-agentkit/tests/e2e/test_basic_api.py

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)