test(analytics): add PSA metrics endpoint tests — funnel, time entries, daily trend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -454,3 +454,227 @@ class TestFlowQualityEndpoint:
|
|||||||
flows = response.json()["flows"]
|
flows = response.json()["flows"]
|
||||||
usage_counts = [f["usage_count"] for f in flows]
|
usage_counts = [f["usage_count"] for f in flows]
|
||||||
assert usage_counts == sorted(usage_counts, reverse=True)
|
assert usage_counts == sorted(usage_counts, reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── PSA metrics endpoint tests ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestPsaMetrics:
|
||||||
|
"""Tests for GET /api/v1/analytics/flowpilot/psa-metrics."""
|
||||||
|
|
||||||
|
async def test_requires_auth(self, client: AsyncClient, test_db: AsyncSession):
|
||||||
|
"""Unauthenticated requests are rejected."""
|
||||||
|
response = await client.get("/api/v1/analytics/flowpilot/psa-metrics")
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
async def test_empty_state(
|
||||||
|
self,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_db: AsyncSession,
|
||||||
|
team_admin_headers: dict,
|
||||||
|
):
|
||||||
|
"""When no PSA activity logs exist, returns zeros gracefully."""
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/analytics/flowpilot/psa-metrics",
|
||||||
|
headers=team_admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
assert data["total_time_entries"] == 0
|
||||||
|
assert data["total_hours_logged"] == 0.0
|
||||||
|
assert data["avg_hours_per_session"] == 0.0
|
||||||
|
assert data["daily_trend"] == []
|
||||||
|
|
||||||
|
funnel = data["push_funnel"]
|
||||||
|
assert funnel["total_sessions"] == 0
|
||||||
|
assert funnel["linked_to_ticket"] == 0
|
||||||
|
assert funnel["doc_pushed"] == 0
|
||||||
|
assert funnel["time_entry_logged"] == 0
|
||||||
|
|
||||||
|
async def test_returns_time_entry_metrics(
|
||||||
|
self,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_db: AsyncSession,
|
||||||
|
team_admin: dict,
|
||||||
|
team_admin_headers: dict,
|
||||||
|
):
|
||||||
|
"""Seeded time_entry_posted logs produce correct totals and averages."""
|
||||||
|
from app.models.psa_activity_log import PsaActivityLog
|
||||||
|
|
||||||
|
account_id = team_admin["user"].account_id
|
||||||
|
|
||||||
|
# 3 time entries: 1.5 + 2.0 + 0.5 = 4.0 hours total, avg = 4.0/3 ≈ 1.33
|
||||||
|
for hours in (1.5, 2.0, 0.5):
|
||||||
|
log = PsaActivityLog(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
account_id=account_id,
|
||||||
|
activity_type="time_entry_posted",
|
||||||
|
hours_logged=hours,
|
||||||
|
)
|
||||||
|
test_db.add(log)
|
||||||
|
await test_db.commit()
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/analytics/flowpilot/psa-metrics",
|
||||||
|
headers=team_admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
assert data["total_time_entries"] == 3
|
||||||
|
assert data["total_hours_logged"] == pytest.approx(4.0, abs=0.01)
|
||||||
|
assert data["avg_hours_per_session"] == pytest.approx(4.0 / 3, abs=0.01)
|
||||||
|
|
||||||
|
async def test_non_time_entry_logs_excluded(
|
||||||
|
self,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_db: AsyncSession,
|
||||||
|
team_admin: dict,
|
||||||
|
team_admin_headers: dict,
|
||||||
|
):
|
||||||
|
"""Activity logs with a different activity_type do not count as time entries."""
|
||||||
|
from app.models.psa_activity_log import PsaActivityLog
|
||||||
|
|
||||||
|
account_id = team_admin["user"].account_id
|
||||||
|
|
||||||
|
# One real time entry, one "note_posted" that should be ignored
|
||||||
|
test_db.add(PsaActivityLog(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
account_id=account_id,
|
||||||
|
activity_type="time_entry_posted",
|
||||||
|
hours_logged=1.0,
|
||||||
|
))
|
||||||
|
test_db.add(PsaActivityLog(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
account_id=account_id,
|
||||||
|
activity_type="note_posted",
|
||||||
|
hours_logged=5.0,
|
||||||
|
))
|
||||||
|
await test_db.commit()
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/analytics/flowpilot/psa-metrics",
|
||||||
|
headers=team_admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total_time_entries"] == 1
|
||||||
|
assert data["total_hours_logged"] == pytest.approx(1.0, abs=0.01)
|
||||||
|
|
||||||
|
async def test_funnel_counts(
|
||||||
|
self,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_db: AsyncSession,
|
||||||
|
team_admin: dict,
|
||||||
|
team_admin_headers: dict,
|
||||||
|
):
|
||||||
|
"""Funnel steps count the correct subset of sessions."""
|
||||||
|
from app.models.ai_session import AISession
|
||||||
|
from app.models.psa_activity_log import PsaActivityLog
|
||||||
|
from app.models.psa_post_log import PsaPostLog
|
||||||
|
|
||||||
|
account_id = team_admin["user"].account_id
|
||||||
|
user_id = team_admin["user"].id
|
||||||
|
|
||||||
|
# 4 total sessions — 2 linked to a ticket
|
||||||
|
session_ids = [uuid.uuid4() for _ in range(4)]
|
||||||
|
for i, sid in enumerate(session_ids):
|
||||||
|
s = AISession(
|
||||||
|
id=sid,
|
||||||
|
user_id=user_id,
|
||||||
|
account_id=account_id,
|
||||||
|
status="resolved",
|
||||||
|
psa_ticket_id="TICKET-123" if i < 2 else None,
|
||||||
|
)
|
||||||
|
test_db.add(s)
|
||||||
|
await test_db.commit()
|
||||||
|
|
||||||
|
# 1 session with a successful doc push
|
||||||
|
push_session_id = session_ids[0]
|
||||||
|
post_log = PsaPostLog(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
ai_session_id=push_session_id,
|
||||||
|
ticket_id="TICKET-123",
|
||||||
|
note_type="internal",
|
||||||
|
content_posted="Session summary",
|
||||||
|
status="success",
|
||||||
|
posted_by=user_id,
|
||||||
|
)
|
||||||
|
test_db.add(post_log)
|
||||||
|
|
||||||
|
# 1 session with a time entry logged (same session as push for realism)
|
||||||
|
activity_log = PsaActivityLog(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
account_id=account_id,
|
||||||
|
session_id=push_session_id,
|
||||||
|
activity_type="time_entry_posted",
|
||||||
|
hours_logged=1.0,
|
||||||
|
)
|
||||||
|
test_db.add(activity_log)
|
||||||
|
await test_db.commit()
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/analytics/flowpilot/psa-metrics",
|
||||||
|
headers=team_admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
funnel = response.json()["push_funnel"]
|
||||||
|
|
||||||
|
assert funnel["total_sessions"] == 4
|
||||||
|
assert funnel["linked_to_ticket"] == 2
|
||||||
|
assert funnel["doc_pushed"] == 1
|
||||||
|
assert funnel["time_entry_logged"] == 1
|
||||||
|
|
||||||
|
async def test_daily_trend(
|
||||||
|
self,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_db: AsyncSession,
|
||||||
|
team_admin: dict,
|
||||||
|
team_admin_headers: dict,
|
||||||
|
):
|
||||||
|
"""Time entries grouped by date produce the correct daily trend array."""
|
||||||
|
from app.models.psa_activity_log import PsaActivityLog
|
||||||
|
|
||||||
|
account_id = team_admin["user"].account_id
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# Day -2: 2 entries totalling 3.0 hours
|
||||||
|
# Day -1: 1 entry with 1.5 hours
|
||||||
|
entries = [
|
||||||
|
(now - timedelta(days=2), 1.5),
|
||||||
|
(now - timedelta(days=2), 1.5),
|
||||||
|
(now - timedelta(days=1), 1.5),
|
||||||
|
]
|
||||||
|
for created_at, hours in entries:
|
||||||
|
log = PsaActivityLog(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
account_id=account_id,
|
||||||
|
activity_type="time_entry_posted",
|
||||||
|
hours_logged=hours,
|
||||||
|
created_at=created_at,
|
||||||
|
)
|
||||||
|
test_db.add(log)
|
||||||
|
await test_db.commit()
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/analytics/flowpilot/psa-metrics?period=30d",
|
||||||
|
headers=team_admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
trend = response.json()["daily_trend"]
|
||||||
|
|
||||||
|
assert len(trend) == 2
|
||||||
|
|
||||||
|
# Trend is ordered by date ascending
|
||||||
|
dates = [t["date"] for t in trend]
|
||||||
|
assert dates == sorted(dates)
|
||||||
|
|
||||||
|
# Oldest day: 2 entries, 3.0 hours
|
||||||
|
oldest = trend[0]
|
||||||
|
assert oldest["entries"] == 2
|
||||||
|
assert oldest["hours"] == pytest.approx(3.0, abs=0.01)
|
||||||
|
|
||||||
|
# Most recent day: 1 entry, 1.5 hours
|
||||||
|
newest = trend[1]
|
||||||
|
assert newest["entries"] == 1
|
||||||
|
assert newest["hours"] == pytest.approx(1.5, abs=0.01)
|
||||||
|
|||||||
Reference in New Issue
Block a user