New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
182 lines
6.9 KiB
Python
182 lines
6.9 KiB
Python
"""
|
|
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
|