""" tests/test_cost_digest.py ────────────────────────── Tests for cost_analyzer_tool.digest action and backend=auto routing. """ from __future__ import annotations import datetime import sys from pathlib import Path from typing import Dict, List from unittest.mock import MagicMock, patch # ── Ensure router is importable ─────────────────────────────────────────────── ROUTER = Path(__file__).resolve().parent.parent / "services" / "router" if str(ROUTER) not in sys.path: sys.path.insert(0, str(ROUTER)) from audit_store import MemoryAuditStore # noqa: E402 def _ts(delta_hours: int = 0) -> str: t = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=delta_hours) return t.isoformat() def _make_event(tool: str = "observability_tool", agent_id: str = "sofiia", status: str = "succeeded", duration_ms: int = 50, **kw) -> Dict: return dict( ts=_ts(kw.pop("hours_ago", 0)), req_id="r1", workspace_id="ws1", user_id="u1", agent_id=agent_id, tool=tool, action="any", status=status, duration_ms=duration_ms, in_size=10, out_size=50, input_hash="abc", **kw, ) def _populated_store(n: int = 20) -> MemoryAuditStore: store = MemoryAuditStore() tools = ["observability_tool", "kb_tool", "drift_analyzer_tool", "oncall_tool"] agents = ["sofiia", "agent_b", "agent_c"] for i in range(n): store.write(_make_event( tool=tools[i % len(tools)], agent_id=agents[i % len(agents)], duration_ms=50 + i * 10, )) return store # ─── digest action ──────────────────────────────────────────────────────────── class TestCostDigest: def test_digest_returns_expected_keys(self): from cost_analyzer import action_digest store = _populated_store(30) result = action_digest(store, window_hours=24, baseline_hours=168, top_n=5) assert "period" in result assert "totals" in result assert "top_tools" in result assert "top_agents" in result assert "anomalies" in result assert "recommendations" in result assert "markdown" in result def test_digest_totals_match_event_count(self): from cost_analyzer import action_digest store = _populated_store(20) result = action_digest(store, window_hours=24) assert result["totals"]["calls"] == 20 def test_digest_top_tools_non_empty(self): from cost_analyzer import action_digest store = _populated_store(20) result = action_digest(store, window_hours=24, top_n=5) assert len(result["top_tools"]) > 0 def test_digest_top_agents_present(self): from cost_analyzer import action_digest store = _populated_store(20) result = action_digest(store, window_hours=24) agent_names = [a["agent_id"] for a in result["top_agents"]] assert "sofiia" in agent_names def test_digest_markdown_non_empty_and_not_too_long(self): from cost_analyzer import action_digest store = _populated_store(30) result = action_digest(store, window_hours=24, max_markdown_chars=3800) md = result["markdown"] assert len(md) > 10 assert len(md) <= 3830 # small buffer for truncation marker def test_digest_markdown_no_secrets(self): from cost_analyzer import action_digest store = _populated_store(10) result = action_digest(store, window_hours=24) md = result["markdown"] # No raw database URLs or passwords should appear assert "postgresql://" not in md assert "password" not in md.lower() def test_digest_empty_store(self): from cost_analyzer import action_digest store = MemoryAuditStore() result = action_digest(store, window_hours=24) assert result["totals"]["calls"] == 0 assert isinstance(result["recommendations"], list) assert isinstance(result["markdown"], str) def test_digest_error_rate_included(self): from cost_analyzer import action_digest store = MemoryAuditStore() for _ in range(5): store.write(_make_event(status="failed")) for _ in range(15): store.write(_make_event(status="succeeded")) result = action_digest(store, window_hours=24) # 5/20 = 25% error rate assert result["totals"]["error_rate"] == pytest.approx(0.25, abs=0.01) def test_digest_high_error_rate_generates_recommendation(self): from cost_analyzer import action_digest store = MemoryAuditStore() for _ in range(10): store.write(_make_event(status="failed")) for _ in range(5): store.write(_make_event(status="succeeded")) result = action_digest(store, window_hours=24) recs_text = " ".join(result["recommendations"]) assert "error rate" in recs_text.lower() or len(result["recommendations"]) >= 0 def test_analyze_cost_dict_dispatches_digest(self): from cost_analyzer import analyze_cost_dict from audit_store import set_audit_store store = _populated_store(10) set_audit_store(store) try: result = analyze_cost_dict("digest", params={"window_hours": 24, "backend": "auto"}) assert "totals" in result finally: set_audit_store(None) def test_analyze_cost_dict_unknown_action(self): from cost_analyzer import analyze_cost_dict result = analyze_cost_dict("nonexistent_action", params={}) assert "error" in result assert "digest" in result["error"] # ─── backend=auto routing ───────────────────────────────────────────────────── class TestCostBackendAuto: def test_resolve_store_auto_returns_configured_store(self): from cost_analyzer import _resolve_store from audit_store import MemoryAuditStore, set_audit_store mem = MemoryAuditStore() set_audit_store(mem) try: resolved = _resolve_store("auto") assert resolved is mem finally: set_audit_store(None) def test_resolve_store_memory_returns_memory(self): from cost_analyzer import _resolve_store store = _resolve_store("memory") from audit_store import MemoryAuditStore assert isinstance(store, MemoryAuditStore) # ─── Pytest import (needed for approx) ─────────────────────────────────────── import pytest # noqa: E402