"""Unit and integration tests for Model Usage & Performance Analytics (#651).""" from __future__ import annotations import os import tempfile import unittest from starlette.testclient import TestClient import control_plane_db from webui.analytics_loader import ( ANALYTICS_SCHEMA_VERSION, compute_percentile, load_analytics, record_usage, ) from webui.app import create_app from webui import console_redaction class AnalyticsLoaderTest(unittest.TestCase): def setUp(self) -> None: self.temp_dir = tempfile.TemporaryDirectory() self.db_path = os.path.join(self.temp_dir.name, "test_control_plane.sqlite3") os.environ["GITEA_CONTROL_PLANE_DB"] = self.db_path self.db = control_plane_db.ControlPlaneDB(db_path=self.db_path) def tearDown(self) -> None: self.temp_dir.cleanup() def test_compute_percentile(self) -> None: self.assertIsNone(compute_percentile([], 50.0)) self.assertEqual(compute_percentile([100], 50.0), 100.0) # 2 elements: [100, 200] self.assertEqual(compute_percentile([100, 200], 50.0), 150.0) # 100 elements: 1..100 vals = list(range(1, 101)) self.assertEqual(compute_percentile(vals, 50.0), 50.5) self.assertAlmostEqual(compute_percentile(vals, 90.0), 90.1) def test_record_and_aggregate_usage(self) -> None: # Record event 1 (complete data) u1 = record_usage( db_path=self.db_path, remote="dadeschools", org="Scaled-Tech-Consulting", repo="Gitea-Tools", role="author", model="gemini-3.6-flash", issue_number=651, stage="implementation", input_tokens=1000, output_tokens=500, estimated_cost_usd=0.0015, latency_ms=200, duration_ms=3000, metadata={"secret_key": "secret123", "note": "token=secret123"}, ) self.assertGreater(u1, 0) # Record event 2 (missing tokens and cost -> unknown) u2 = record_usage( db_path=self.db_path, remote="dadeschools", org="Scaled-Tech-Consulting", repo="Gitea-Tools", role="reviewer", model="claude-3-5-sonnet", pr_number=846, stage="review", latency_ms=500, duration_ms=6000, ) self.assertGreater(u2, u1) snapshot = load_analytics( db_path=self.db_path, remote="dadeschools", org="Scaled-Tech-Consulting", repo="Gitea-Tools", ) self.assertTrue(snapshot.ok) self.assertEqual(snapshot.schema_version, ANALYTICS_SCHEMA_VERSION) self.assertEqual(snapshot.total_events, 2) # Verify overall summary summary = snapshot.overall_summary self.assertEqual(summary.total_events, 2) self.assertEqual(summary.events_with_tokens, 1) self.assertEqual(summary.total_tokens, 1500) self.assertEqual(summary.events_with_cost, 1) self.assertEqual(summary.estimated_cost_usd, 0.0015) self.assertEqual(summary.events_with_latency, 2) self.assertEqual(summary.latency_p50_ms, 350.0) # Verify missing data handling (AC 3: not zero-fabricated) reviewer_model = snapshot.by_model.get("claude-3-5-sonnet") self.assertIsNotNone(reviewer_model) self.assertEqual(reviewer_model.total_events, 1) self.assertEqual(reviewer_model.events_with_tokens, 0) self.assertIsNone(reviewer_model.total_tokens) self.assertEqual(reviewer_model.display_tokens, "Unknown") self.assertEqual(reviewer_model.events_with_cost, 0) self.assertIsNone(reviewer_model.estimated_cost_usd) self.assertEqual(reviewer_model.display_cost, "Unknown") # Verify redaction (AC 4) e1 = [e for e in snapshot.events if e.usage_id == u1][0] self.assertIsNotNone(e1.metadata) self.assertNotIn("secret123", e1.metadata) self.assertIn("[REDACTED]", e1.metadata) def test_missing_db_fail_soft(self) -> None: invalid_path = "/nonexistent_path_dir/db.sqlite3" snapshot = load_analytics(db_path=invalid_path) self.assertFalse(snapshot.ok) self.assertIn("control_plane_db_unavailable", snapshot.reason) self.assertEqual(snapshot.overall_summary.display_tokens, "Unknown") class AnalyticsWebUITest(unittest.TestCase): def setUp(self) -> None: self.temp_dir = tempfile.TemporaryDirectory() self.db_path = os.path.join(self.temp_dir.name, "test_webui.sqlite3") os.environ["GITEA_CONTROL_PLANE_DB"] = self.db_path self.app = create_app() self.client = TestClient(self.app) record_usage( db_path=self.db_path, remote="dadeschools", org="Scaled-Tech-Consulting", repo="Gitea-Tools", role="author", model="gemini-3.6-flash", issue_number=651, stage="implementation", input_tokens=2000, output_tokens=1000, estimated_cost_usd=0.003, latency_ms=150, duration_ms=2500, ) def tearDown(self) -> None: self.temp_dir.cleanup() def test_analytics_html_route(self) -> None: response = self.client.get("/analytics") self.assertEqual(response.status_code, 200) self.assertIn("Model Usage & Performance Analytics", response.text) self.assertIn("gemini-3.6-flash", response.text) self.assertIn("3,000", response.text) def test_analytics_api_route(self) -> None: response = self.client.get("/api/v1/analytics") self.assertEqual(response.status_code, 200) data = response.json() self.assertTrue(data["ok"]) self.assertEqual(data["total_events"], 1) self.assertIn("gemini-3.6-flash", data["by_model"]) def test_analytics_ingest_unauthorized_denied(self) -> None: """F2: unauthenticated POST must not write the control-plane DB.""" payload = { "remote": "dadeschools", "org": "Scaled-Tech-Consulting", "repo": "Gitea-Tools", "role": "reviewer", "model": "claude-3-5-sonnet", "pr_number": 846, "stage": "review", "input_tokens": 500, "output_tokens": 100, "latency_ms": 400, "metadata": "Review note token=secret456", } response = self.client.post("/api/v1/analytics/usage", json=payload) self.assertEqual(response.status_code, 403) res_json = response.json() self.assertFalse(res_json.get("ok", True)) self.assertEqual(res_json.get("error"), "unauthorized") authorization = res_json.get("authorization") or {} self.assertFalse(authorization.get("allowed")) self.assertFalse(authorization.get("execution_enabled")) # No new row written res2 = self.client.get("/api/v1/analytics") self.assertEqual(res2.status_code, 200) self.assertEqual(res2.json()["total_events"], 1) def test_html_escapes_script_bearing_model_role_stage(self) -> None: """F1: stored XSS — dynamic model/role/stage must render escaped.""" xss = '' record_usage( db_path=self.db_path, remote="dadeschools", org="Scaled-Tech-Consulting", repo="Gitea-Tools", role=xss, model=xss, stage=xss, issue_number=999, status="success", ) response = self.client.get("/analytics") self.assertEqual(response.status_code, 200) # Raw tag must not appear; escaped form must. self.assertNotIn("", response.text) self.assertIn("<script>alert(1)</script>", response.text) def test_load_analytics_coerces_none_scope(self) -> None: """F4: None remote/org/repo become empty strings, never None.""" snapshot = load_analytics(db_path=self.db_path, remote=None, org=None, repo=None) self.assertIsInstance(snapshot.remote, str) self.assertIsInstance(snapshot.org, str) self.assertIsInstance(snapshot.repo, str) self.assertEqual(snapshot.remote, "") self.assertEqual(snapshot.org, "") self.assertEqual(snapshot.repo, "") def test_usage_events_retention_max_rows(self) -> None: """F3: record_usage_event enforces USAGE_EVENTS_MAX_ROWS.""" db = control_plane_db.ControlPlaneDB(db_path=self.db_path) original_max = db.USAGE_EVENTS_MAX_ROWS try: db.USAGE_EVENTS_MAX_ROWS = 3 for i in range(5): db.record_usage_event( remote="dadeschools", org="org", repo="repo", role="author", model=f"model-{i}", stage="test", ) rows = db.query_usage_events(limit=100) self.assertLessEqual(len(rows), 3) # Newest three retained models = {r["model"] for r in rows} self.assertEqual(models, {"model-2", "model-3", "model-4"}) finally: db.USAGE_EVENTS_MAX_ROWS = original_max if __name__ == "__main__": unittest.main()