109 lines
4.0 KiB
Python
109 lines
4.0 KiB
Python
"""Unit tests for PostgresModelUsageStore, using a mocked psycopg2 pool.
|
|||
|
|
|
||
|
|
Mirrors the mocking pattern in backend/tests/perception/test_postgres_event_store.py
|
||
|
|
— no real database is needed.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sys
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
from unittest.mock import MagicMock, patch
|
||
|
|
|
||
|
|
# Patch psycopg2 before importing the module under test.
|
||
|
|
mock_psycopg2 = MagicMock()
|
||
|
|
mock_psycopg2.extras = MagicMock()
|
||
|
|
sys.modules.setdefault("psycopg2", mock_psycopg2)
|
||
|
|
sys.modules.setdefault("psycopg2.extras", mock_psycopg2.extras)
|
||
|
|
sys.modules.setdefault("psycopg2.pool", MagicMock())
|
||
|
|
|
||
|
|
from app.shared.model_usage_tracker import ModelUsageEntry
|
||
|
|
|
||
|
|
|
||
|
|
def _cursor_returning(rows):
|
||
|
|
"""Build a MagicMock standing in for a psycopg2 cursor context manager."""
|
||
|
|
cursor = MagicMock()
|
||
|
|
cursor.__enter__ = lambda s: s
|
||
|
|
cursor.__exit__ = MagicMock(return_value=False)
|
||
|
|
cursor.fetchall.return_value = rows
|
||
|
|
return cursor
|
||
|
|
|
||
|
|
|
||
|
|
@patch("app.infrastructure.storage.postgres_model_usage_store.PostgresModelUsageStore._ensure_schema")
|
||
|
|
@patch("app.infrastructure.storage.postgres_model_usage_store.ThreadedConnectionPool")
|
||
|
|
def test_load_all_returns_entries_keyed_by_provider_model(mock_pool_class, mock_ensure):
|
||
|
|
"""load_all() must turn each row into a ModelUsageEntry keyed by 'provider:model'."""
|
||
|
|
row = {
|
||
|
|
"provider": "deepseek",
|
||
|
|
"model": "deepseek-v4-flash",
|
||
|
|
"total_tokens": 100,
|
||
|
|
"prompt_tokens": 60,
|
||
|
|
"completion_tokens": 40,
|
||
|
|
"call_count_ok": 5,
|
||
|
|
"call_count_error": 1,
|
||
|
|
"last_called_at": datetime(2026, 7, 23, tzinfo=timezone.utc),
|
||
|
|
"last_latency_ms": 250,
|
||
|
|
"last_error": None,
|
||
|
|
}
|
||
|
|
mock_pool = MagicMock()
|
||
|
|
mock_pool_class.return_value = mock_pool
|
||
|
|
conn = MagicMock()
|
||
|
|
conn.__enter__ = lambda s: s
|
||
|
|
conn.__exit__ = MagicMock(return_value=False)
|
||
|
|
conn.cursor.return_value = _cursor_returning([row])
|
||
|
|
mock_pool.getconn.return_value = conn
|
||
|
|
|
||
|
|
from app.infrastructure.storage.postgres_model_usage_store import PostgresModelUsageStore
|
||
|
|
store = PostgresModelUsageStore()
|
||
|
|
entries = store.load_all()
|
||
|
|
|
||
|
|
assert "deepseek:deepseek-v4-flash" in entries
|
||
|
|
entry = entries["deepseek:deepseek-v4-flash"]
|
||
|
|
assert isinstance(entry, ModelUsageEntry)
|
||
|
|
assert entry.total_tokens == 100
|
||
|
|
assert entry.call_count_error == 1
|
||
|
|
|
||
|
|
|
||
|
|
@patch("app.infrastructure.storage.postgres_model_usage_store.PostgresModelUsageStore._ensure_schema")
|
||
|
|
@patch("app.infrastructure.storage.postgres_model_usage_store.ThreadedConnectionPool")
|
||
|
|
def test_flush_upserts_every_entry(mock_pool_class, mock_ensure):
|
||
|
|
"""flush() must execute one UPSERT per tracked entry and commit once."""
|
||
|
|
mock_pool = MagicMock()
|
||
|
|
mock_pool_class.return_value = mock_pool
|
||
|
|
conn = MagicMock()
|
||
|
|
conn.__enter__ = lambda s: s
|
||
|
|
conn.__exit__ = MagicMock(return_value=False)
|
||
|
|
cursor = MagicMock()
|
||
|
|
cursor.__enter__ = lambda s: s
|
||
|
|
cursor.__exit__ = MagicMock(return_value=False)
|
||
|
|
conn.cursor.return_value = cursor
|
||
|
|
mock_pool.getconn.return_value = conn
|
||
|
|
|
||
|
|
from app.infrastructure.storage.postgres_model_usage_store import PostgresModelUsageStore
|
||
|
|
store = PostgresModelUsageStore()
|
||
|
|
entries = {
|
||
|
|
"deepseek:deepseek-v4-flash": ModelUsageEntry(
|
||
|
|
provider="deepseek", model="deepseek-v4-flash", total_tokens=100, call_count_ok=5,
|
||
|
|
),
|
||
|
|
}
|
||
|
|
|
||
|
|
store.flush(entries)
|
||
|
|
|
||
|
|
assert cursor.execute.call_count == 1
|
||
|
|
conn.commit.assert_called_once()
|
||
|
|
|
||
|
|
|
||
|
|
@patch("app.infrastructure.storage.postgres_model_usage_store.PostgresModelUsageStore._ensure_schema")
|
||
|
|
@patch("app.infrastructure.storage.postgres_model_usage_store.ThreadedConnectionPool")
|
||
|
|
def test_flush_with_no_entries_does_not_touch_the_database(mock_pool_class, mock_ensure):
|
||
|
|
"""flush({}) must be a no-op — no point opening a connection for nothing."""
|
||
|
|
mock_pool = MagicMock()
|
||
|
|
mock_pool_class.return_value = mock_pool
|
||
|
|
|
||
|
|
from app.infrastructure.storage.postgres_model_usage_store import PostgresModelUsageStore
|
||
|
|
store = PostgresModelUsageStore()
|
||
|
|
|
||
|
|
store.flush({})
|
||
|
|
|
||
|
|
mock_pool.getconn.assert_not_called()
|