"""Unit tests for hindsight_litellm configuration and defaults.""" import os from dataclasses import fields from unittest.mock import MagicMock, patch from hindsight_litellm import configure, wrap_openai, wrap_anthropic from hindsight_litellm.config import ( DEFAULT_HINDSIGHT_API_URL, DEFAULT_BANK_ID, HINDSIGHT_API_KEY_ENV, reset_config, is_configured, ) from hindsight_litellm.wrappers import ( HindsightOpenAI, HindsightAnthropic, HindsightCallSettings, _merge_settings, ) class TestDefaults: """Test default configuration values.""" def test_default_api_url(self): """Test default API URL is production.""" assert DEFAULT_HINDSIGHT_API_URL == "https://api.hindsight.vectorize.io" def test_default_bank_id(self): """Test default bank ID is 'default'.""" assert DEFAULT_BANK_ID == "default" def test_env_var_name(self): """Test environment variable name for API key.""" assert HINDSIGHT_API_KEY_ENV == "HINDSIGHT_API_KEY" class TestConfigure: """Test configure() function.""" def setup_method(self): """Reset config before each test.""" reset_config() def teardown_method(self): """Reset config after each test.""" reset_config() def test_configure_with_no_arguments(self): """Test configure() with no arguments uses defaults.""" config = configure() assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL assert config.bank_id == DEFAULT_BANK_ID def test_configure_reads_api_key_from_env(self): """Test configure() reads API key from environment variable.""" with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "test-api-key-123"}): config = configure() assert config.api_key == "test-api-key-123" def test_configure_explicit_api_key_overrides_env(self): """Test explicit api_key parameter overrides environment variable.""" with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "env-key"}): config = configure(api_key="explicit-key") assert config.api_key == "explicit-key" def test_configure_explicit_values_override_defaults(self): """Test explicit values override defaults.""" config = configure( hindsight_api_url="http://custom-url:8888", bank_id="custom-bank", api_key="custom-key", ) assert config.hindsight_api_url == "http://custom-url:8888" assert config.bank_id == "custom-bank" assert config.api_key == "custom-key" def test_is_configured_true_with_defaults(self): """Test is_configured() returns True with default config.""" configure() assert is_configured() is True def test_is_configured_false_when_not_configured(self): """Test is_configured() returns False when not configured.""" reset_config() assert is_configured() is False class TestWrapOpenAI: """Test wrap_openai() function.""" def setup_method(self): """Reset config before each test.""" reset_config() def teardown_method(self): """Reset config after each test.""" reset_config() def test_wrap_openai_with_only_client(self): """Test wrap_openai() works with only the client argument.""" mock_client = MagicMock() with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "test-key"}): wrapped = wrap_openai(mock_client) assert isinstance(wrapped, HindsightOpenAI) assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL assert wrapped._api_key == "test-key" def test_wrap_openai_uses_defaults(self): """Test wrap_openai() uses default values.""" mock_client = MagicMock() wrapped = wrap_openai(mock_client) assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL def test_wrap_openai_reads_api_key_from_env(self): """Test wrap_openai() reads API key from environment.""" mock_client = MagicMock() with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "env-api-key"}): wrapped = wrap_openai(mock_client) assert wrapped._api_key == "env-api-key" def test_wrap_openai_explicit_overrides_defaults(self): """Test wrap_openai() explicit values override defaults.""" mock_client = MagicMock() wrapped = wrap_openai( mock_client, bank_id="my-bank", hindsight_api_url="http://localhost:9999", api_key="my-key", ) assert wrapped._default_settings.bank_id == "my-bank" assert wrapped._api_url == "http://localhost:9999" assert wrapped._api_key == "my-key" def test_wrap_openai_all_settings_kwargs(self): """Test wrap_openai() accepts all HindsightCallSettings fields as kwargs.""" mock_client = MagicMock() # Create kwargs with all settings settings_kwargs = { "bank_id": "test-bank", "document_id": "test-doc", "session_id": "test-session", "store_conversations": False, "inject_memories": False, "budget": "high", "fact_types": ["world", "experience"], "max_memories": 10, "max_memory_tokens": 2048, "include_entities": False, "trace": True, "tags": ["user:alice", "session:123"], "recall_tags": ["user:alice"], "recall_tags_match": "all", "use_reflect": True, "reflect_context": "test context", "reflect_response_schema": {"type": "object"}, "reflect_include_facts": True, "query": "test query", "verbose": True, } wrapped = wrap_openai(mock_client, **settings_kwargs) # Verify all settings were applied for field_name, expected_value in settings_kwargs.items(): actual_value = getattr(wrapped._default_settings, field_name) assert actual_value == expected_value, ( f"Field {field_name}: expected {expected_value}, got {actual_value}" ) class TestWrapAnthropic: """Test wrap_anthropic() function.""" def setup_method(self): """Reset config before each test.""" reset_config() def teardown_method(self): """Reset config after each test.""" reset_config() def test_wrap_anthropic_with_only_client(self): """Test wrap_anthropic() works with only the client argument.""" mock_client = MagicMock() with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "test-key"}): wrapped = wrap_anthropic(mock_client) assert isinstance(wrapped, HindsightAnthropic) assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL assert wrapped._api_key == "test-key" def test_wrap_anthropic_uses_defaults(self): """Test wrap_anthropic() uses default values.""" mock_client = MagicMock() wrapped = wrap_anthropic(mock_client) assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL def test_wrap_anthropic_reads_api_key_from_env(self): """Test wrap_anthropic() reads API key from environment.""" mock_client = MagicMock() with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "env-api-key"}): wrapped = wrap_anthropic(mock_client) assert wrapped._api_key == "env-api-key" def test_wrap_anthropic_explicit_overrides_defaults(self): """Test wrap_anthropic() explicit values override defaults.""" mock_client = MagicMock() wrapped = wrap_anthropic( mock_client, bank_id="my-bank", hindsight_api_url="http://localhost:9999", api_key="my-key", ) assert wrapped._default_settings.bank_id == "my-bank" assert wrapped._api_url == "http://localhost:9999" assert wrapped._api_key == "my-key" def test_wrap_anthropic_all_settings_kwargs(self): """Test wrap_anthropic() accepts all HindsightCallSettings fields as kwargs.""" mock_client = MagicMock() # Create kwargs with all settings settings_kwargs = { "bank_id": "test-bank", "document_id": "test-doc", "session_id": "test-session", "store_conversations": False, "inject_memories": False, "budget": "high", "fact_types": ["world", "experience"], "max_memories": 10, "max_memory_tokens": 2048, "include_entities": False, "trace": True, "tags": ["user:alice", "session:123"], "recall_tags": ["user:alice"], "recall_tags_match": "all", "use_reflect": True, "reflect_context": "test context", "reflect_response_schema": {"type": "object"}, "reflect_include_facts": True, "query": "test query", "verbose": True, } wrapped = wrap_anthropic(mock_client, **settings_kwargs) # Verify all settings were applied for field_name, expected_value in settings_kwargs.items(): actual_value = getattr(wrapped._default_settings, field_name) assert actual_value == expected_value, ( f"Field {field_name}: expected {expected_value}, got {actual_value}" ) class TestMergeSettings: """Test _merge_settings() function for per-call overrides.""" def test_merge_settings_no_overrides(self): """Test _merge_settings() returns defaults when no overrides provided.""" defaults = HindsightCallSettings(bank_id="default-bank", budget="mid") kwargs = {"model": "gpt-4", "messages": []} # No hindsight_* kwargs merged = _merge_settings(defaults, kwargs) assert merged.bank_id == "default-bank" assert merged.budget == "mid" def test_merge_settings_with_overrides(self): """Test _merge_settings() applies hindsight_* overrides.""" defaults = HindsightCallSettings(bank_id="default-bank", budget="mid") kwargs = { "model": "gpt-4", "hindsight_bank_id": "override-bank", "hindsight_budget": "high", } merged = _merge_settings(defaults, kwargs) assert merged.bank_id == "override-bank" assert merged.budget == "high" def test_merge_settings_partial_override(self): """Test _merge_settings() only overrides specified fields.""" defaults = HindsightCallSettings( bank_id="default-bank", budget="mid", verbose=True, max_memories=5, ) kwargs = { "hindsight_bank_id": "override-bank", # budget, verbose, max_memories not overridden } merged = _merge_settings(defaults, kwargs) assert merged.bank_id == "override-bank" # Overridden assert merged.budget == "mid" # Default preserved assert merged.verbose is True # Default preserved assert merged.max_memories == 5 # Default preserved def test_merge_settings_ignores_invalid_fields(self): """Test _merge_settings() ignores hindsight_* kwargs for non-existent fields.""" defaults = HindsightCallSettings(bank_id="default-bank") kwargs = { "hindsight_bank_id": "valid-override", "hindsight_nonexistent_field": "should-be-ignored", } merged = _merge_settings(defaults, kwargs) assert merged.bank_id == "valid-override" assert not hasattr(merged, "nonexistent_field") def test_merge_settings_all_fields(self): """Test _merge_settings() works with all HindsightCallSettings fields.""" # Create defaults with non-default values defaults = HindsightCallSettings( bank_id="default-bank", document_id="default-doc", session_id="default-session", store_conversations=True, inject_memories=True, budget="mid", fact_types=None, max_memories=None, max_memory_tokens=4096, include_entities=True, trace=False, tags=None, recall_tags=None, recall_tags_match="any", use_reflect=False, reflect_context=None, reflect_response_schema=None, reflect_include_facts=False, query=None, verbose=False, ) # Override every field override_values = { "hindsight_bank_id": "override-bank", "hindsight_document_id": "override-doc", "hindsight_session_id": "override-session", "hindsight_store_conversations": False, "hindsight_inject_memories": False, "hindsight_budget": "high", "hindsight_fact_types": ["world", "experience"], "hindsight_max_memories": 10, "hindsight_max_memory_tokens": 2048, "hindsight_include_entities": False, "hindsight_trace": True, "hindsight_tags": ["user:bob", "org:acme"], "hindsight_recall_tags": ["user:bob"], "hindsight_recall_tags_match": "all", "hindsight_use_reflect": True, "hindsight_reflect_context": "test context", "hindsight_reflect_response_schema": {"type": "object"}, "hindsight_reflect_include_facts": True, "hindsight_query": "test query", "hindsight_verbose": True, } merged = _merge_settings(defaults, override_values) # Verify all fields were overridden assert merged.bank_id == "override-bank" assert merged.document_id == "override-doc" assert merged.session_id == "override-session" assert merged.store_conversations is False assert merged.inject_memories is False assert merged.budget == "high" assert merged.fact_types == ["world", "experience"] assert merged.max_memories == 10 assert merged.max_memory_tokens == 2048 assert merged.include_entities is False assert merged.trace is True assert merged.tags == ["user:bob", "org:acme"] assert merged.recall_tags == ["user:bob"] assert merged.recall_tags_match == "all" assert merged.use_reflect is True assert merged.reflect_context == "test context" assert merged.reflect_response_schema == {"type": "object"} assert merged.reflect_include_facts is True assert merged.query == "test query" assert merged.verbose is True class TestHindsightCallSettingsConsistency: """Test that HindsightCallSettings works consistently across wrappers.""" def test_same_settings_for_openai_and_anthropic(self): """Test that OpenAI and Anthropic wrappers use the same HindsightCallSettings.""" mock_openai_client = MagicMock() mock_anthropic_client = MagicMock() settings_kwargs = { "bank_id": "shared-bank", "budget": "high", "use_reflect": True, "verbose": True, } openai_wrapped = wrap_openai(mock_openai_client, **settings_kwargs) anthropic_wrapped = wrap_anthropic(mock_anthropic_client, **settings_kwargs) # Both should have identical settings assert ( openai_wrapped._default_settings.bank_id == anthropic_wrapped._default_settings.bank_id ) assert ( openai_wrapped._default_settings.budget == anthropic_wrapped._default_settings.budget ) assert ( openai_wrapped._default_settings.use_reflect == anthropic_wrapped._default_settings.use_reflect ) assert ( openai_wrapped._default_settings.verbose == anthropic_wrapped._default_settings.verbose ) def test_new_field_works_for_both_wrappers(self): """Test that all HindsightCallSettings fields work for both wrappers.""" mock_openai_client = MagicMock() mock_anthropic_client = MagicMock() # Get all fields from the dataclass all_field_names = [f.name for f in fields(HindsightCallSettings)] # Verify both wrappers can access all fields openai_wrapped = wrap_openai(mock_openai_client) anthropic_wrapped = wrap_anthropic(mock_anthropic_client) for field_name in all_field_names: # Both should have the field accessible assert hasattr(openai_wrapped._default_settings, field_name), ( f"OpenAI wrapper missing field: {field_name}" ) assert hasattr(anthropic_wrapped._default_settings, field_name), ( f"Anthropic wrapper missing field: {field_name}" )