# #region Test.Api.AgentLifecycleRoutes [C:3] [TYPE Module] [SEMANTICS test,agent,lifecycle,events,api] # @BRIEF Unit tests for Agent Lifecycle event API — write and list endpoints. # @RELATION BINDS_TO -> [Api.AgentLifecycle] # @TEST_EDGE: non_admin_cannot_filter_by_user -> 403 import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") os.environ.setdefault("DEV_MODE", "true") import sys from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) def _make_mock_user(is_admin: bool = True, user_id: str = "user-1") -> MagicMock: """Build a mock user for auth bypass.""" from src.schemas.auth import RoleSchema, User as UserSchema roles = [] if is_admin: roles.append( RoleSchema(id="r1", name="Admin", description="", is_admin=True, permissions=[]) ) return UserSchema( id=user_id, username="admin" if is_admin else "user", email="admin@x.com" if is_admin else "user@x.com", auth_source="LOCAL", created_at=datetime.now(), roles=roles, ) def _make_client(user_mock=None, db_mock=None, overrides=None) -> TestClient: """Build a TestClient with the agent lifecycle router.""" from src.api.routes.agent_lifecycle import router from src.core.database import get_db from src.dependencies import get_current_user app = FastAPI() app.include_router(router) if user_mock is None: user_mock = _make_mock_user(is_admin=True) if db_mock is None: db_mock = MagicMock() app.dependency_overrides[get_current_user] = lambda: user_mock app.dependency_overrides[get_db] = lambda: db_mock if overrides: for dep, fn in overrides.items(): app.dependency_overrides[dep] = fn return TestClient(app, raise_server_exceptions=False) # ── create_event (POST) ── class TestCreateEvent: """POST /api/agent/events""" EVENT_PAYLOAD = { "trace_id": "trace-1", "conversation_id": "conv-1", "event_type": "tool_call", "tool_name": "superset_query", "status": "success", "elapsed_ms": 1500, "payload": {"action": "query", "attempt": 1}, } def test_success(self): """Creates a lifecycle event successfully (admin user).""" from src.schemas.agent_lifecycle import EventWriteResponse mock_write = MagicMock(return_value=EventWriteResponse(id="evt-1")) with patch("src.api.routes.agent_lifecycle.write_event", mock_write): client = _make_client() resp = client.post("/api/agent/events", json=self.EVENT_PAYLOAD) assert resp.status_code == 201 data = resp.json() assert data["id"] == "evt-1" assert data["written"] is True mock_write.assert_called_once() def test_success_regular_user(self): """Non-admin user can also create events.""" from src.schemas.agent_lifecycle import EventWriteResponse mock_write = MagicMock(return_value=EventWriteResponse(id="evt-2")) user_mock = _make_mock_user(is_admin=False) with patch("src.api.routes.agent_lifecycle.write_event", mock_write): client = _make_client(user_mock=user_mock) resp = client.post("/api/agent/events", json=self.EVENT_PAYLOAD) assert resp.status_code == 201 def test_payload_reduced(self): """Payload is reduced to safe keys via schema.""" from src.schemas.agent_lifecycle import EventWriteResponse mock_write = MagicMock(return_value=EventWriteResponse(id="evt-3")) payload_with_sensitive = { **self.EVENT_PAYLOAD, "payload": {"action": "query", "password": "secret", "token": "abc"}, } with patch("src.api.routes.agent_lifecycle.write_event", mock_write): client = _make_client() resp = client.post("/api/agent/events", json=payload_with_sensitive) assert resp.status_code == 201 # The write_event should receive payload with sensitive keys reduced call_kwargs = mock_write.call_args if call_kwargs: written_body = call_kwargs[0][1] # body arg if written_body.payload: assert "password" not in written_body.payload assert "action" in written_body.payload def test_db_rollback_on_error(self): """Rolls back DB transaction when write_event raises.""" mock_write = MagicMock(side_effect=ValueError("DB error")) db_mock = MagicMock() with patch("src.api.routes.agent_lifecycle.write_event", mock_write): client = _make_client(db_mock=db_mock) resp = client.post("/api/agent/events", json=self.EVENT_PAYLOAD) assert resp.status_code == 500 db_mock.rollback.assert_called_once() def test_missing_required_fields(self): """Returns 422 when required fields are missing.""" client = _make_client() resp = client.post("/api/agent/events", json={"event_type": "test"}) assert resp.status_code == 422 # ── read_events (GET) ── class TestReadEvents: """GET /api/agent/events""" def _make_event_item(self, **overrides): from src.schemas.agent_lifecycle import EventItem return EventItem( id=overrides.get("id", "evt-1"), trace_id=overrides.get("trace_id", "trace-1"), conversation_id=overrides.get("conversation_id", "conv-1"), user_id=overrides.get("user_id", "user-1"), event_type=overrides.get("event_type", "tool_call"), tool_name=overrides.get("tool_name", "superset_query"), status=overrides.get("status", "success"), created_at=datetime.now(), ) def test_success_admin(self): """Admin can list all events.""" from src.schemas.agent_lifecycle import EventListResponse items = [self._make_event_item()] mock_list = MagicMock( return_value=EventListResponse(items=items, total=1, page=1, page_size=50, has_next=False) ) with patch("src.api.routes.agent_lifecycle.list_events", mock_list): client = _make_client() resp = client.get("/api/agent/events") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert len(data["items"]) == 1 assert data["items"][0]["id"] == "evt-1" def test_success_regular_user(self): """Non-admin sees only own events (no user_id filter).""" from src.schemas.agent_lifecycle import EventListResponse items = [self._make_event_item(id="evt-2")] mock_list = MagicMock( return_value=EventListResponse(items=items, total=1, page=1, page_size=50, has_next=False) ) user_mock = _make_mock_user(is_admin=False) with patch("src.api.routes.agent_lifecycle.list_events", mock_list): client = _make_client(user_mock=user_mock) resp = client.get("/api/agent/events") assert resp.status_code == 200 assert resp.json()["total"] == 1 def test_non_admin_cannot_filter_by_user(self): """Non-admin gets 403 when trying to filter by user_id.""" user_mock = _make_mock_user(is_admin=False) client = _make_client(user_mock=user_mock) resp = client.get("/api/agent/events?user_id=other-user") assert resp.status_code == 403 assert "Only admin users can query events by user_id" in resp.json()["detail"] def test_admin_can_filter_by_user(self): """Admin can filter events by user_id.""" from src.schemas.agent_lifecycle import EventListResponse items = [self._make_event_item(user_id="other-user")] mock_list = MagicMock( return_value=EventListResponse(items=items, total=1, page=1, page_size=50, has_next=False) ) with patch("src.api.routes.agent_lifecycle.list_events", mock_list): client = _make_client() resp = client.get("/api/agent/events?user_id=other-user") assert resp.status_code == 200 assert resp.json()["total"] == 1 # Verify list_events received the correct args call_kwargs = mock_list.call_args[1] assert call_kwargs["user_id"] == "other-user" def test_with_filters(self): """Filters are passed through to list_events.""" from src.schemas.agent_lifecycle import EventListResponse mock_list = MagicMock( return_value=EventListResponse(items=[], total=0, page=1, page_size=50, has_next=False) ) with patch("src.api.routes.agent_lifecycle.list_events", mock_list): client = _make_client() resp = client.get( "/api/agent/events?event_type=tool_call&conversation_id=conv-1&status=success&tool_name=superset_query" ) assert resp.status_code == 200 call_kwargs = mock_list.call_args[1] assert call_kwargs["event_type"] == "tool_call" assert call_kwargs["conversation_id"] == "conv-1" def test_pagination(self): """Page and page_size are passed through.""" from src.schemas.agent_lifecycle import EventListResponse mock_list = MagicMock( return_value=EventListResponse(items=[], total=0, page=2, page_size=25, has_next=False) ) with patch("src.api.routes.agent_lifecycle.list_events", mock_list): client = _make_client() resp = client.get("/api/agent/events?page=2&page_size=25") assert resp.status_code == 200 assert resp.json()["page"] == 2 assert resp.json()["page_size"] == 25 def test_invalid_page_size(self): """Page_size outside 1-200 returns 422.""" client = _make_client() resp = client.get("/api/agent/events?page_size=500") assert resp.status_code == 422 # #endregion Test.Api.AgentLifecycleRoutes