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