Backend (4 files, 73 tests): - test_agent_superset_routes.py (27 tests, 35% -> 92%) - test_agent_lifecycle_routes.py (11 tests, 50% -> 100%) - test_agent_status_routes.py (6 tests, 57% -> 100%) - test_git_release_routes.py (31 tests, 30% -> 99%) Frontend (~15 files, ~120 tests): - cron.ts: 0% -> 100% - ReportsLogModel: 0% -> 99% - parseCot.ts: 10% -> 100% - sessionTimeout.ts: 64% -> 93% - MappingsModel: 65% -> 100% - TranslateHistoryModel: 65% -> 93% - Migration.ExecutorModel: 70% -> 100% - GitManagerModel: 78% -> 90% - TranslationJobModel: 77% -> 80% - ConfirmDialog: 59% -> 80% - api.ts: 78% -> 80% Coverage: frontend 0 violations, backend 7518 passed.
281 lines
10 KiB
Python
281 lines
10 KiB
Python
# #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
|