forked from cardosofelipe/fast-next-template
- Added timeout enforcement for token counting, scoring, and compression with detailed error handling. - Introduced tenant isolation in context caching using project and agent identifiers. - Enhanced budget management with stricter checks for critical context overspending and buffer limitations. - Optimized per-context locking with cleanup to prevent memory leaks in concurrent environments. - Updated default assembly timeout settings for improved performance and reliability. - Improved XML escaping in Claude adapter for safety against injection attacks. - Standardized token estimation using model-specific ratios.
244 lines
8.2 KiB
Python
244 lines
8.2 KiB
Python
"""Tests for context management configuration."""
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from app.services.context.config import (
|
|
ContextSettings,
|
|
get_context_settings,
|
|
get_default_settings,
|
|
reset_context_settings,
|
|
)
|
|
|
|
|
|
class TestContextSettings:
|
|
"""Tests for ContextSettings."""
|
|
|
|
def test_default_values(self) -> None:
|
|
"""Test default settings values."""
|
|
settings = ContextSettings()
|
|
|
|
# Budget defaults should sum to 1.0
|
|
total = (
|
|
settings.budget_system
|
|
+ settings.budget_task
|
|
+ settings.budget_knowledge
|
|
+ settings.budget_conversation
|
|
+ settings.budget_tools
|
|
+ settings.budget_response
|
|
+ settings.budget_buffer
|
|
)
|
|
assert abs(total - 1.0) < 0.001
|
|
|
|
# Scoring weights should sum to 1.0
|
|
weights_total = (
|
|
settings.scoring_relevance_weight
|
|
+ settings.scoring_recency_weight
|
|
+ settings.scoring_priority_weight
|
|
)
|
|
assert abs(weights_total - 1.0) < 0.001
|
|
|
|
def test_budget_allocation_values(self) -> None:
|
|
"""Test specific budget allocation values."""
|
|
settings = ContextSettings()
|
|
|
|
assert settings.budget_system == 0.05
|
|
assert settings.budget_task == 0.10
|
|
assert settings.budget_knowledge == 0.40
|
|
assert settings.budget_conversation == 0.20
|
|
assert settings.budget_tools == 0.05
|
|
assert settings.budget_response == 0.15
|
|
assert settings.budget_buffer == 0.05
|
|
|
|
def test_scoring_weights(self) -> None:
|
|
"""Test scoring weights."""
|
|
settings = ContextSettings()
|
|
|
|
assert settings.scoring_relevance_weight == 0.5
|
|
assert settings.scoring_recency_weight == 0.3
|
|
assert settings.scoring_priority_weight == 0.2
|
|
|
|
def test_cache_settings(self) -> None:
|
|
"""Test cache settings."""
|
|
settings = ContextSettings()
|
|
|
|
assert settings.cache_enabled is True
|
|
assert settings.cache_ttl_seconds == 3600
|
|
assert settings.cache_prefix == "ctx"
|
|
|
|
def test_performance_settings(self) -> None:
|
|
"""Test performance settings."""
|
|
settings = ContextSettings()
|
|
|
|
assert settings.max_assembly_time_ms == 2000
|
|
assert settings.parallel_scoring is True
|
|
assert settings.max_parallel_scores == 10
|
|
|
|
def test_get_budget_allocation(self) -> None:
|
|
"""Test get_budget_allocation method."""
|
|
settings = ContextSettings()
|
|
allocation = settings.get_budget_allocation()
|
|
|
|
assert isinstance(allocation, dict)
|
|
assert "system" in allocation
|
|
assert "knowledge" in allocation
|
|
assert allocation["system"] == 0.05
|
|
assert allocation["knowledge"] == 0.40
|
|
|
|
def test_get_scoring_weights(self) -> None:
|
|
"""Test get_scoring_weights method."""
|
|
settings = ContextSettings()
|
|
weights = settings.get_scoring_weights()
|
|
|
|
assert isinstance(weights, dict)
|
|
assert "relevance" in weights
|
|
assert "recency" in weights
|
|
assert "priority" in weights
|
|
assert weights["relevance"] == 0.5
|
|
|
|
def test_to_dict(self) -> None:
|
|
"""Test to_dict method."""
|
|
settings = ContextSettings()
|
|
result = settings.to_dict()
|
|
|
|
assert "budget" in result
|
|
assert "scoring" in result
|
|
assert "compression" in result
|
|
assert "cache" in result
|
|
assert "performance" in result
|
|
assert "knowledge" in result
|
|
assert "conversation" in result
|
|
|
|
def test_budget_validation_fails_on_wrong_sum(self) -> None:
|
|
"""Test that budget validation fails when sum != 1.0."""
|
|
with pytest.raises(ValueError) as exc_info:
|
|
ContextSettings(
|
|
budget_system=0.5,
|
|
budget_task=0.5,
|
|
# Other budgets default to non-zero, so total > 1.0
|
|
)
|
|
|
|
assert "sum to 1.0" in str(exc_info.value)
|
|
|
|
def test_scoring_validation_fails_on_wrong_sum(self) -> None:
|
|
"""Test that scoring validation fails when sum != 1.0."""
|
|
with pytest.raises(ValueError) as exc_info:
|
|
ContextSettings(
|
|
scoring_relevance_weight=0.8,
|
|
scoring_recency_weight=0.8,
|
|
scoring_priority_weight=0.8,
|
|
)
|
|
|
|
assert "sum to 1.0" in str(exc_info.value)
|
|
|
|
def test_search_type_validation(self) -> None:
|
|
"""Test search type validation."""
|
|
# Valid types should work
|
|
ContextSettings(knowledge_search_type="semantic")
|
|
ContextSettings(knowledge_search_type="keyword")
|
|
ContextSettings(knowledge_search_type="hybrid")
|
|
|
|
# Invalid type should fail
|
|
with pytest.raises(ValueError):
|
|
ContextSettings(knowledge_search_type="invalid")
|
|
|
|
def test_custom_budget_allocation(self) -> None:
|
|
"""Test custom budget allocation that sums to 1.0."""
|
|
settings = ContextSettings(
|
|
budget_system=0.10,
|
|
budget_task=0.10,
|
|
budget_knowledge=0.30,
|
|
budget_conversation=0.25,
|
|
budget_tools=0.05,
|
|
budget_response=0.15,
|
|
budget_buffer=0.05,
|
|
)
|
|
|
|
total = (
|
|
settings.budget_system
|
|
+ settings.budget_task
|
|
+ settings.budget_knowledge
|
|
+ settings.budget_conversation
|
|
+ settings.budget_tools
|
|
+ settings.budget_response
|
|
+ settings.budget_buffer
|
|
)
|
|
assert abs(total - 1.0) < 0.001
|
|
|
|
|
|
class TestSettingsSingleton:
|
|
"""Tests for settings singleton pattern."""
|
|
|
|
def setup_method(self) -> None:
|
|
"""Reset settings before each test."""
|
|
reset_context_settings()
|
|
|
|
def teardown_method(self) -> None:
|
|
"""Clean up after each test."""
|
|
reset_context_settings()
|
|
|
|
def test_get_context_settings_returns_instance(self) -> None:
|
|
"""Test that get_context_settings returns a settings instance."""
|
|
settings = get_context_settings()
|
|
assert isinstance(settings, ContextSettings)
|
|
|
|
def test_get_context_settings_returns_same_instance(self) -> None:
|
|
"""Test that get_context_settings returns the same instance."""
|
|
settings1 = get_context_settings()
|
|
settings2 = get_context_settings()
|
|
assert settings1 is settings2
|
|
|
|
def test_reset_creates_new_instance(self) -> None:
|
|
"""Test that reset creates a new instance."""
|
|
settings1 = get_context_settings()
|
|
reset_context_settings()
|
|
settings2 = get_context_settings()
|
|
|
|
# Should be different instances
|
|
assert settings1 is not settings2
|
|
|
|
def test_get_default_settings_cached(self) -> None:
|
|
"""Test that get_default_settings is cached."""
|
|
settings1 = get_default_settings()
|
|
settings2 = get_default_settings()
|
|
assert settings1 is settings2
|
|
|
|
|
|
class TestEnvironmentOverrides:
|
|
"""Tests for environment variable overrides."""
|
|
|
|
def setup_method(self) -> None:
|
|
"""Reset settings before each test."""
|
|
reset_context_settings()
|
|
|
|
def teardown_method(self) -> None:
|
|
"""Clean up after each test."""
|
|
reset_context_settings()
|
|
# Clean up any env vars we set
|
|
for key in list(os.environ.keys()):
|
|
if key.startswith("CTX_"):
|
|
del os.environ[key]
|
|
|
|
def test_env_override_cache_enabled(self) -> None:
|
|
"""Test that CTX_CACHE_ENABLED env var works."""
|
|
with patch.dict(os.environ, {"CTX_CACHE_ENABLED": "false"}):
|
|
reset_context_settings()
|
|
settings = ContextSettings()
|
|
assert settings.cache_enabled is False
|
|
|
|
def test_env_override_cache_ttl(self) -> None:
|
|
"""Test that CTX_CACHE_TTL_SECONDS env var works."""
|
|
with patch.dict(os.environ, {"CTX_CACHE_TTL_SECONDS": "7200"}):
|
|
reset_context_settings()
|
|
settings = ContextSettings()
|
|
assert settings.cache_ttl_seconds == 7200
|
|
|
|
def test_env_override_max_assembly_time(self) -> None:
|
|
"""Test that CTX_MAX_ASSEMBLY_TIME_MS env var works."""
|
|
with patch.dict(os.environ, {"CTX_MAX_ASSEMBLY_TIME_MS": "200"}):
|
|
reset_context_settings()
|
|
settings = ContextSettings()
|
|
assert settings.max_assembly_time_ms == 200
|