7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""High-value tests for metrics/pricing module.
|
|
|
|
Covers gaps in PricingCache and CostCalculator not addressed by existing tests:
|
|
- Cache overwrite behavior, TTL-based expiration
|
|
- CostCalculator.calculate_cost_sync exact arithmetic and edge cases
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.metrics.pricing.pricing_cache import PricingCache
|
|
from local_deep_research.metrics.pricing.cost_calculator import CostCalculator
|
|
|
|
|
|
class TestPricingCacheOverwrite:
|
|
"""Test cache key overwrite behavior."""
|
|
|
|
def test_set_overwrites_existing_key_data(self):
|
|
"""Overwriting a key replaces the data."""
|
|
cache = PricingCache(cache_ttl=3600)
|
|
cache.set("k", {"old": True})
|
|
cache.set("k", {"new": True})
|
|
assert cache.get("k") == {"new": True}
|
|
|
|
def test_get_with_primitive_value_round_trips(self):
|
|
"""Non-dict values (strings) are preserved through set/get."""
|
|
cache = PricingCache(cache_ttl=3600)
|
|
cache.set("k", "hello")
|
|
assert cache.get("k") == "hello"
|
|
|
|
|
|
class TestPricingCacheTTLExpiration:
|
|
"""Test TTL-based expiration via cachetools.TTLCache."""
|
|
|
|
def test_zero_ttl_makes_every_entry_expired(self):
|
|
"""With cache_ttl=0, entries are immediately expired."""
|
|
cache = PricingCache(cache_ttl=0)
|
|
cache.set("k", "data")
|
|
# Give a tiny amount of time for clock to advance
|
|
assert cache.get("k") is None
|
|
|
|
def test_clear_then_stats_all_zeros(self):
|
|
"""After clear(), stats show zero entries."""
|
|
cache = PricingCache(cache_ttl=3600)
|
|
cache.set("a", 1)
|
|
cache.set("b", 2)
|
|
cache.clear()
|
|
stats = cache.get_cache_stats()
|
|
assert stats["total_entries"] == 0
|
|
|
|
|
|
class TestCostCalculatorSync:
|
|
"""Test CostCalculator.calculate_cost_sync."""
|
|
|
|
def test_unknown_model_returns_zero_cost(self):
|
|
"""Unknown model returns zero cost with error message."""
|
|
calc = CostCalculator()
|
|
result = calc.calculate_cost_sync("totally-unknown-model-xyz", 500, 500)
|
|
assert result["prompt_cost"] == 0.0
|
|
assert result["completion_cost"] == 0.0
|
|
assert result["total_cost"] == 0.0
|
|
assert result["pricing_used"] is None
|
|
assert "error" in result
|
|
|
|
def test_prompt_plus_completion_equals_total(self):
|
|
"""prompt_cost + completion_cost == total_cost for known models."""
|
|
calc = CostCalculator()
|
|
result = calc.calculate_cost_sync("gpt-4", 3000, 1500)
|
|
if result["pricing_used"] is not None:
|
|
assert (
|
|
abs(
|
|
(result["prompt_cost"] + result["completion_cost"])
|
|
- result["total_cost"]
|
|
)
|
|
< 1e-9
|
|
)
|
|
|
|
def test_zero_completion_tokens(self):
|
|
"""Zero completion tokens yields completion_cost == 0."""
|
|
calc = CostCalculator()
|
|
result = calc.calculate_cost_sync("gpt-4", 1000, 0)
|
|
if result["pricing_used"] is not None:
|
|
assert result["completion_cost"] == 0.0
|
|
assert result["total_cost"] == result["prompt_cost"]
|
|
|
|
def test_provider_prefixed_model_resolved(self):
|
|
"""Model with provider prefix (e.g. 'openai/gpt-4') is resolved."""
|
|
calc = CostCalculator()
|
|
result = calc.calculate_cost_sync("openai/gpt-4", 1000, 1000)
|
|
# Should find gpt-4 via the split("/")[-1] fallback
|
|
if result["pricing_used"] is not None:
|
|
assert result["total_cost"] > 0.0
|
|
|
|
def test_provider_prefix_unresolvable_returns_zero(self):
|
|
"""Unresolvable provider-prefixed model returns zero cost."""
|
|
calc = CostCalculator()
|
|
result = calc.calculate_cost_sync(
|
|
"fakeprovider/no-such-model", 500, 500
|
|
)
|
|
assert result["total_cost"] == 0.0
|
|
assert result["pricing_used"] is None
|
|
|
|
def test_cache_used_over_static_pricing(self):
|
|
"""Pre-seeded cache values are used instead of static pricing."""
|
|
calc = CostCalculator()
|
|
custom_pricing = {"prompt": 0.1, "completion": 0.2}
|
|
calc.cache.set_model_pricing("my-custom-model", custom_pricing)
|
|
result = calc.calculate_cost_sync("my-custom-model", 1000, 1000)
|
|
assert result["pricing_used"] == custom_pricing
|
|
assert result["prompt_cost"] == pytest.approx(0.1)
|
|
assert result["completion_cost"] == pytest.approx(0.2)
|
|
|
|
def test_success_result_has_expected_keys(self):
|
|
"""Successful result has exactly 4 keys (no error key)."""
|
|
calc = CostCalculator()
|
|
calc.cache.set_model_pricing(
|
|
"test-model", {"prompt": 0.01, "completion": 0.02}
|
|
)
|
|
result = calc.calculate_cost_sync("test-model", 100, 100)
|
|
assert set(result.keys()) == {
|
|
"prompt_cost",
|
|
"completion_cost",
|
|
"total_cost",
|
|
"pricing_used",
|
|
}
|