feat(system): add S73 test coverage files for 12 branches (#184)
* feat(system): feat(system): FPLAN-0164 dispatch safety net + morning briefing timeout fix Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): fix(drone+backup): resolver BranchNotFoundError handling + backup snapshot quick-check Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): fix(ai_mail): test fix for JSONL-based startup detection in dispatch_monitor Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): feat(daemon): expand test coverage from 12% — 74 new tests for scheduler_cron and actions Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): S73 night shift: test coverage push to 100% + seedgo test depth fixes Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): README overhaul + HERALD update + S73 test files Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): S73 night shift: 51 new test files across 12 branches Co-Authored-By: @devpulse <devpulse@aipass> --------- Co-authored-by: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_caller.py
|
||||
# Description: Tests for OpenRouter caller detection handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-03
|
||||
# Modified: 2026-04-03
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Tests for openrouter.caller — caller detection handler.
|
||||
|
||||
Tests:
|
||||
- detect_caller_category for flow paths
|
||||
- detect_caller_category for prax paths
|
||||
- detect_caller_category for skills paths
|
||||
- detect_caller_category for unknown paths
|
||||
- detect_caller_category with 'skills' as substring in a part
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from aipass.api.apps.handlers.openrouter.caller import detect_caller_category
|
||||
|
||||
|
||||
# =============================================
|
||||
# detect_caller_category tests
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestDetectCallerCategory:
|
||||
"""Tests for caller.detect_caller_category()."""
|
||||
|
||||
def test_flow_path_returns_flow(self):
|
||||
"""Path containing 'flow' part should return 'flow'."""
|
||||
path = Path("/home/user/projects/aipass/src/aipass/flow/engine.py")
|
||||
assert detect_caller_category(path) == "flow"
|
||||
|
||||
def test_prax_path_returns_prax(self):
|
||||
"""Path containing 'prax' part should return 'prax'."""
|
||||
path = Path("/home/user/projects/aipass/src/aipass/prax/monitor.py")
|
||||
assert detect_caller_category(path) == "prax"
|
||||
|
||||
def test_skills_exact_part_returns_skills(self):
|
||||
"""Path with exact 'skills' directory should return 'skills'."""
|
||||
path = Path("/home/user/projects/aipass/src/aipass/skills/skills_api/tool.py")
|
||||
assert detect_caller_category(path) == "skills"
|
||||
|
||||
def test_skills_substring_returns_skills(self):
|
||||
"""Path with 'skills' as substring in a part (e.g., 'skills_api') should return 'skills'."""
|
||||
path = Path("/home/user/projects/aipass/src/aipass/modules/skills_custom/handler.py")
|
||||
assert detect_caller_category(path) == "skills"
|
||||
|
||||
def test_unknown_path_returns_unknown(self):
|
||||
"""Path without flow, prax, or skills should return 'unknown'."""
|
||||
path = Path("/home/user/projects/aipass/src/aipass/api/apps/handler.py")
|
||||
assert detect_caller_category(path) == "unknown"
|
||||
|
||||
def test_flow_takes_priority_over_later_prax(self):
|
||||
"""If 'flow' appears before 'prax' in path, should return 'flow'."""
|
||||
path = Path("/home/user/flow/prax/script.py")
|
||||
assert detect_caller_category(path) == "flow"
|
||||
|
||||
def test_prax_takes_priority_over_skills(self):
|
||||
"""If 'prax' appears before a skills part, should return 'prax'."""
|
||||
path = Path("/home/user/prax/skills_module/script.py")
|
||||
assert detect_caller_category(path) == "prax"
|
||||
|
||||
def test_root_path_returns_unknown(self):
|
||||
"""Root-level path should return 'unknown'."""
|
||||
path = Path("/somefile.py")
|
||||
assert detect_caller_category(path) == "unknown"
|
||||
|
||||
def test_deeply_nested_flow_path(self):
|
||||
"""Deeply nested path with 'flow' should still return 'flow'."""
|
||||
path = Path("/a/b/c/d/flow/e/f/g/handler.py")
|
||||
assert detect_caller_category(path) == "flow"
|
||||
|
||||
@patch("aipass.api.apps.handlers.openrouter.caller.logger")
|
||||
def test_exception_returns_unknown(self, mock_logger):
|
||||
"""If an exception occurs, should return 'unknown' and log error."""
|
||||
bad_path = MagicMock(spec=Path)
|
||||
bad_path.parts = property(lambda self: (_ for _ in ()).throw(RuntimeError("boom")))
|
||||
type(bad_path).parts = property(lambda self: (_ for _ in ()).throw(RuntimeError("boom")))
|
||||
|
||||
result = detect_caller_category(bad_path)
|
||||
|
||||
assert result == "unknown"
|
||||
mock_logger.error.assert_called_once()
|
||||
@@ -0,0 +1,199 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_config_provider.py
|
||||
# Description: Tests for provider configuration handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-03
|
||||
# Modified: 2026-04-03
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Tests for config.provider — provider configuration handler.
|
||||
|
||||
Tests:
|
||||
- merge_configs deep merge behavior
|
||||
- merge_configs in-place mutation and return value
|
||||
- merge_configs nested dict recursion
|
||||
- merge_configs non-dict overwrite
|
||||
- get_validation_rules known providers
|
||||
- get_validation_rules unknown provider returns None
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from aipass.api.apps.handlers.config import provider as config_provider
|
||||
|
||||
|
||||
# =============================================
|
||||
# merge_configs tests
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestMergeConfigs:
|
||||
"""Tests for config.provider.merge_configs()."""
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_simple_merge_adds_new_key(self, mock_jh):
|
||||
"""New key in updates should appear in base."""
|
||||
base = {"a": 1}
|
||||
updates = {"b": 2}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result["a"] == 1
|
||||
assert result["b"] == 2
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_simple_merge_overwrites_existing_key(self, mock_jh):
|
||||
"""Existing key should be overwritten by updates."""
|
||||
base = {"a": 1}
|
||||
updates = {"a": 99}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result["a"] == 99
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_nested_dict_merges_recursively(self, mock_jh):
|
||||
"""Nested dicts should merge recursively, preserving untouched keys."""
|
||||
base = {"a": 1, "b": {"c": 2, "d": 3}}
|
||||
updates = {"b": {"c": 99}, "e": 4}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result["a"] == 1
|
||||
assert result["b"]["c"] == 99
|
||||
assert result["b"]["d"] == 3
|
||||
assert result["e"] == 4
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_modifies_base_in_place(self, mock_jh):
|
||||
"""merge_configs should modify base dict in-place."""
|
||||
base = {"a": 1}
|
||||
updates = {"b": 2}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result is base
|
||||
assert base["b"] == 2
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_returns_same_object_as_base(self, mock_jh):
|
||||
"""Return value should be the same object as the input base."""
|
||||
base = {"x": "original"}
|
||||
updates = {"y": "added"}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result is base
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_non_dict_value_overwrites_dict(self, mock_jh):
|
||||
"""Non-dict update value should overwrite existing dict value."""
|
||||
base = {"a": {"nested": True}}
|
||||
updates = {"a": "flat_string"}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result["a"] == "flat_string"
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_dict_value_overwrites_non_dict(self, mock_jh):
|
||||
"""Dict update value should overwrite existing non-dict value."""
|
||||
base = {"a": "flat_string"}
|
||||
updates = {"a": {"nested": True}}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result["a"] == {"nested": True}
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_empty_updates_leaves_base_unchanged(self, mock_jh):
|
||||
"""Empty updates dict should not change base."""
|
||||
base = {"a": 1, "b": 2}
|
||||
original = base.copy()
|
||||
updates = {}
|
||||
|
||||
config_provider.merge_configs(base, updates)
|
||||
|
||||
assert base == original
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_deeply_nested_merge(self, mock_jh):
|
||||
"""Three levels of nesting should all merge correctly."""
|
||||
base = {"level1": {"level2": {"level3": "old", "keep": "yes"}}}
|
||||
updates = {"level1": {"level2": {"level3": "new"}}}
|
||||
|
||||
result = config_provider.merge_configs(base, updates)
|
||||
|
||||
assert result["level1"]["level2"]["level3"] == "new"
|
||||
assert result["level1"]["level2"]["keep"] == "yes"
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.json_handler")
|
||||
def test_logs_operation_on_merge(self, mock_jh):
|
||||
"""merge_configs should call json_handler.log_operation."""
|
||||
base = {"a": 1}
|
||||
updates = {"b": 2, "c": 3}
|
||||
|
||||
config_provider.merge_configs(base, updates)
|
||||
|
||||
mock_jh.log_operation.assert_called_once_with(
|
||||
"config_merged", {"keys_updated": 2}
|
||||
)
|
||||
|
||||
|
||||
# =============================================
|
||||
# get_validation_rules tests (config.provider)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestGetValidationRulesConfigProvider:
|
||||
"""Tests for config.provider.get_validation_rules()."""
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.logger")
|
||||
def test_openrouter_rules(self, mock_logger):
|
||||
"""openrouter should have prefix 'sk-or-v1-' and min_length 40."""
|
||||
rules = config_provider.get_validation_rules("openrouter")
|
||||
|
||||
assert rules is not None
|
||||
assert rules["prefix"] == "sk-or-v1-"
|
||||
assert rules["min_length"] == 40
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.logger")
|
||||
def test_openai_rules(self, mock_logger):
|
||||
"""openai should have prefix 'sk-' and min_length 40."""
|
||||
rules = config_provider.get_validation_rules("openai")
|
||||
|
||||
assert rules is not None
|
||||
assert rules["prefix"] == "sk-"
|
||||
assert rules["min_length"] == 40
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.logger")
|
||||
def test_unknown_provider_returns_none(self, mock_logger):
|
||||
"""Unknown provider should return None (no generic fallback)."""
|
||||
rules = config_provider.get_validation_rules("unknown_provider")
|
||||
|
||||
assert rules is None
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.logger")
|
||||
def test_unknown_provider_logs_info(self, mock_logger):
|
||||
"""Unknown provider should log an info message."""
|
||||
config_provider.get_validation_rules("nonexistent")
|
||||
|
||||
mock_logger.info.assert_called_once()
|
||||
assert "nonexistent" in mock_logger.info.call_args[0][0]
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.logger")
|
||||
def test_known_provider_does_not_log(self, mock_logger):
|
||||
"""Known provider should not trigger the info log."""
|
||||
config_provider.get_validation_rules("openrouter")
|
||||
|
||||
mock_logger.info.assert_not_called()
|
||||
|
||||
@patch("aipass.api.apps.handlers.config.provider.logger")
|
||||
def test_return_type_is_dict_for_known(self, mock_logger):
|
||||
"""Known providers should return a dict."""
|
||||
for name in ["openrouter", "openai"]:
|
||||
rules = config_provider.get_validation_rules(name)
|
||||
assert isinstance(rules, dict), f"Expected dict for {name}"
|
||||
@@ -0,0 +1,354 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_tracking.py
|
||||
# Description: Tests for usage tracking handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-03
|
||||
# Modified: 2026-04-03
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Tests for tracking.py -- usage tracking handler.
|
||||
|
||||
Tests:
|
||||
- get_generation_metrics() HTTP success, non-200, invalid structure, exception
|
||||
- store_usage_data() new file creation, existing file update, per-caller stats,
|
||||
daily totals, newest-first ordering, exception handling
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from aipass.api.apps.handlers.usage.tracking import (
|
||||
get_generation_metrics,
|
||||
store_usage_data,
|
||||
)
|
||||
|
||||
_TRACKING_MOD = "aipass.api.apps.handlers.usage.tracking"
|
||||
|
||||
|
||||
# =============================================
|
||||
# get_generation_metrics tests
|
||||
# =============================================
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_success(mock_requests: MagicMock):
|
||||
"""Returns metrics dict when API returns 200 with valid structure."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": {
|
||||
"total_cost": 0.0042,
|
||||
"tokens_prompt": 150,
|
||||
"tokens_completion": 80,
|
||||
"generation_time": 1200,
|
||||
"latency": 1500,
|
||||
"provider_name": "anthropic",
|
||||
}
|
||||
}
|
||||
mock_requests.get.return_value = mock_response
|
||||
|
||||
result = get_generation_metrics("gen-abc-123", "sk-or-test-key")
|
||||
|
||||
assert result is not None
|
||||
assert result["total_cost"] == 0.0042
|
||||
assert result["tokens_prompt"] == 150
|
||||
assert result["tokens_completion"] == 80
|
||||
assert result["generation_time"] == 1200
|
||||
assert result["latency"] == 1500
|
||||
assert result["provider_name"] == "anthropic"
|
||||
|
||||
# Verify request was made with correct params
|
||||
mock_requests.get.assert_called_once()
|
||||
call_kwargs = mock_requests.get.call_args
|
||||
assert call_kwargs[1]["params"] == {"id": "gen-abc-123"}
|
||||
assert "Bearer sk-or-test-key" in call_kwargs[1]["headers"]["Authorization"]
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_non_200(mock_requests: MagicMock):
|
||||
"""Returns None when API returns non-200 status."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_requests.get.return_value = mock_response
|
||||
|
||||
result = get_generation_metrics("gen-missing", "sk-or-key")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_invalid_structure_no_data_key(mock_requests: MagicMock):
|
||||
"""Returns None when response JSON lacks 'data' key."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"error": "not found"}
|
||||
mock_requests.get.return_value = mock_response
|
||||
|
||||
result = get_generation_metrics("gen-bad", "sk-or-key")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_invalid_structure_empty_response(mock_requests: MagicMock):
|
||||
"""Returns None when response JSON is empty/None."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = None
|
||||
mock_requests.get.return_value = mock_response
|
||||
|
||||
result = get_generation_metrics("gen-empty", "sk-or-key")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_request_exception(mock_requests: MagicMock):
|
||||
"""Returns None when requests raises an exception."""
|
||||
import requests as real_requests
|
||||
|
||||
mock_requests.get.side_effect = real_requests.exceptions.ConnectionError("refused")
|
||||
mock_requests.exceptions = real_requests.exceptions
|
||||
|
||||
result = get_generation_metrics("gen-fail", "sk-or-key")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_timeout(mock_requests: MagicMock):
|
||||
"""Returns None on request timeout."""
|
||||
import requests as real_requests
|
||||
|
||||
mock_requests.get.side_effect = real_requests.exceptions.Timeout("timed out")
|
||||
mock_requests.exceptions = real_requests.exceptions
|
||||
|
||||
result = get_generation_metrics("gen-timeout", "sk-or-key")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.requests")
|
||||
def test_get_generation_metrics_defaults_missing_fields(mock_requests: MagicMock):
|
||||
"""Missing fields in metrics default to 0 / 'unknown'."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": {}}
|
||||
mock_requests.get.return_value = mock_response
|
||||
|
||||
result = get_generation_metrics("gen-sparse", "sk-or-key")
|
||||
|
||||
assert result is not None
|
||||
assert result["total_cost"] == 0.0
|
||||
assert result["tokens_prompt"] == 0
|
||||
assert result["tokens_completion"] == 0
|
||||
assert result["generation_time"] == 0
|
||||
assert result["latency"] == 0
|
||||
assert result["provider_name"] == "unknown"
|
||||
|
||||
|
||||
# =============================================
|
||||
# store_usage_data tests
|
||||
# =============================================
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.API_JSON_DIR")
|
||||
def test_store_usage_data_creates_new_file(mock_dir: MagicMock, tmp_path: Path):
|
||||
"""store_usage_data creates initial structure when file doesn't exist."""
|
||||
mock_dir.__truediv__ = lambda self, other: tmp_path / other
|
||||
mock_dir.mkdir = MagicMock()
|
||||
|
||||
metrics = {
|
||||
"total_cost": 0.005,
|
||||
"tokens_prompt": 100,
|
||||
"tokens_completion": 50,
|
||||
"generation_time": 800,
|
||||
"latency": 1000,
|
||||
"provider_name": "anthropic",
|
||||
}
|
||||
|
||||
result = store_usage_data("test_caller", "anthropic/claude-3.5-sonnet", "gen-001", metrics)
|
||||
|
||||
assert result is True
|
||||
|
||||
data_path = tmp_path / "usage_tracker_data.json"
|
||||
assert data_path.exists()
|
||||
|
||||
with open(data_path, "r", encoding="utf-8") as f:
|
||||
wrapper = json.load(f)
|
||||
|
||||
assert wrapper["module_name"] == "api_usage"
|
||||
data = wrapper["data"]
|
||||
|
||||
# Session totals
|
||||
assert data["current_session"]["total_requests"] == 1
|
||||
assert data["current_session"]["total_cost"] == 0.005
|
||||
assert data["current_session"]["total_tokens"] == 150
|
||||
|
||||
# Per-caller stats
|
||||
assert "test_caller" in data["usage_by_caller"]
|
||||
caller_data = data["usage_by_caller"]["test_caller"]
|
||||
assert caller_data["requests"] == 1
|
||||
assert caller_data["total_cost"] == 0.005
|
||||
assert caller_data["total_tokens"] == 150
|
||||
assert caller_data["models_used"]["anthropic/claude-3.5-sonnet"] == 1
|
||||
|
||||
# Generation tracking
|
||||
assert "gen-001" in data["generation_tracking"]
|
||||
entry = data["generation_tracking"]["gen-001"]
|
||||
assert entry["caller"] == "test_caller"
|
||||
assert entry["model"] == "anthropic/claude-3.5-sonnet"
|
||||
assert entry["usage_data"] == metrics
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.API_JSON_DIR")
|
||||
def test_store_usage_data_updates_existing(mock_dir: MagicMock, tmp_path: Path):
|
||||
"""store_usage_data increments counters in existing file."""
|
||||
mock_dir.__truediv__ = lambda self, other: tmp_path / other
|
||||
mock_dir.mkdir = MagicMock()
|
||||
|
||||
metrics = {
|
||||
"total_cost": 0.01,
|
||||
"tokens_prompt": 200,
|
||||
"tokens_completion": 100,
|
||||
"generation_time": 500,
|
||||
"latency": 700,
|
||||
"provider_name": "openai",
|
||||
}
|
||||
|
||||
# First call creates the file
|
||||
store_usage_data("caller_a", "openai/gpt-4", "gen-100", metrics)
|
||||
|
||||
# Second call updates
|
||||
result = store_usage_data("caller_a", "openai/gpt-4", "gen-101", metrics)
|
||||
|
||||
assert result is True
|
||||
|
||||
data_path = tmp_path / "usage_tracker_data.json"
|
||||
with open(data_path, "r", encoding="utf-8") as f:
|
||||
wrapper = json.load(f)
|
||||
|
||||
data = wrapper["data"]
|
||||
assert data["current_session"]["total_requests"] == 2
|
||||
assert data["current_session"]["total_cost"] == pytest.approx(0.02)
|
||||
assert data["current_session"]["total_tokens"] == 600
|
||||
|
||||
caller_data = data["usage_by_caller"]["caller_a"]
|
||||
assert caller_data["requests"] == 2
|
||||
assert caller_data["models_used"]["openai/gpt-4"] == 2
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.API_JSON_DIR")
|
||||
def test_store_usage_data_newest_first_ordering(mock_dir: MagicMock, tmp_path: Path):
|
||||
"""Generation tracking stores newest entry first."""
|
||||
mock_dir.__truediv__ = lambda self, other: tmp_path / other
|
||||
mock_dir.mkdir = MagicMock()
|
||||
|
||||
metrics = {
|
||||
"total_cost": 0.001,
|
||||
"tokens_prompt": 10,
|
||||
"tokens_completion": 5,
|
||||
"generation_time": 100,
|
||||
"latency": 200,
|
||||
"provider_name": "test",
|
||||
}
|
||||
|
||||
store_usage_data("caller", "model/a", "gen-first", metrics)
|
||||
store_usage_data("caller", "model/a", "gen-second", metrics)
|
||||
|
||||
data_path = tmp_path / "usage_tracker_data.json"
|
||||
with open(data_path, "r", encoding="utf-8") as f:
|
||||
wrapper = json.load(f)
|
||||
|
||||
tracking_keys = list(wrapper["data"]["generation_tracking"].keys())
|
||||
assert tracking_keys[0] == "gen-second"
|
||||
assert tracking_keys[1] == "gen-first"
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.API_JSON_DIR")
|
||||
def test_store_usage_data_multiple_callers(mock_dir: MagicMock, tmp_path: Path):
|
||||
"""store_usage_data tracks multiple callers independently."""
|
||||
mock_dir.__truediv__ = lambda self, other: tmp_path / other
|
||||
mock_dir.mkdir = MagicMock()
|
||||
|
||||
metrics = {
|
||||
"total_cost": 0.003,
|
||||
"tokens_prompt": 50,
|
||||
"tokens_completion": 25,
|
||||
"generation_time": 300,
|
||||
"latency": 400,
|
||||
"provider_name": "test",
|
||||
}
|
||||
|
||||
store_usage_data("caller_x", "model/x", "gen-x1", metrics)
|
||||
store_usage_data("caller_y", "model/y", "gen-y1", metrics)
|
||||
|
||||
data_path = tmp_path / "usage_tracker_data.json"
|
||||
with open(data_path, "r", encoding="utf-8") as f:
|
||||
wrapper = json.load(f)
|
||||
|
||||
by_caller = wrapper["data"]["usage_by_caller"]
|
||||
assert "caller_x" in by_caller
|
||||
assert "caller_y" in by_caller
|
||||
assert by_caller["caller_x"]["requests"] == 1
|
||||
assert by_caller["caller_y"]["requests"] == 1
|
||||
assert by_caller["caller_x"]["models_used"]["model/x"] == 1
|
||||
assert by_caller["caller_y"]["models_used"]["model/y"] == 1
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.API_JSON_DIR")
|
||||
def test_store_usage_data_daily_totals(mock_dir: MagicMock, tmp_path: Path):
|
||||
"""store_usage_data updates daily totals for today's date."""
|
||||
from datetime import datetime
|
||||
|
||||
mock_dir.__truediv__ = lambda self, other: tmp_path / other
|
||||
mock_dir.mkdir = MagicMock()
|
||||
|
||||
metrics = {
|
||||
"total_cost": 0.002,
|
||||
"tokens_prompt": 40,
|
||||
"tokens_completion": 20,
|
||||
"generation_time": 200,
|
||||
"latency": 300,
|
||||
"provider_name": "test",
|
||||
}
|
||||
|
||||
store_usage_data("caller", "model/a", "gen-daily", metrics)
|
||||
|
||||
data_path = tmp_path / "usage_tracker_data.json"
|
||||
with open(data_path, "r", encoding="utf-8") as f:
|
||||
wrapper = json.load(f)
|
||||
|
||||
today = datetime.now().date().isoformat()
|
||||
daily = wrapper["data"]["daily_totals"]
|
||||
assert today in daily
|
||||
assert daily[today]["requests"] == 1
|
||||
assert daily[today]["cost"] == 0.002
|
||||
assert daily[today]["tokens"] == 60
|
||||
|
||||
|
||||
@patch(f"{_TRACKING_MOD}.API_JSON_DIR")
|
||||
def test_store_usage_data_returns_false_on_exception(mock_dir: MagicMock, tmp_path: Path):
|
||||
"""store_usage_data returns False when an exception occurs."""
|
||||
# Point to a path that will fail (parent is a file, not a dir)
|
||||
blocker = tmp_path / "blocker_file"
|
||||
blocker.write_text("not a dir", encoding="utf-8")
|
||||
mock_dir.__truediv__ = lambda self, other: blocker / other
|
||||
mock_dir.mkdir = MagicMock(side_effect=OSError("cannot create"))
|
||||
|
||||
metrics = {
|
||||
"total_cost": 0.0,
|
||||
"tokens_prompt": 0,
|
||||
"tokens_completion": 0,
|
||||
"generation_time": 0,
|
||||
"latency": 0,
|
||||
"provider_name": "test",
|
||||
}
|
||||
|
||||
result = store_usage_data("caller", "model", "gen-err", metrics)
|
||||
|
||||
assert result is False
|
||||
Reference in New Issue
Block a user