|
| 1 | +""" |
| 2 | +Unit tests for AIAgent class, focusing on chimera model functionality. |
| 3 | +
|
| 4 | +These tests verify that the chimera model is properly set up to use OpenRouter |
| 5 | +and expose the issue where "chimera" is passed directly as the model name |
| 6 | +instead of a valid OpenRouter model identifier. |
| 7 | +""" |
| 8 | +import pytest |
| 9 | +from unittest.mock import Mock, patch, MagicMock |
| 10 | +import os |
| 11 | +import sys |
| 12 | + |
| 13 | + |
| 14 | +# Mock streamlit before importing aiagent |
| 15 | +@pytest.fixture(autouse=True) |
| 16 | +def mock_streamlit(): |
| 17 | + """Mock streamlit to avoid import issues in tests""" |
| 18 | + mock_st = MagicMock() |
| 19 | + mock_st.secrets = {} |
| 20 | + with patch.dict('sys.modules', {'streamlit': mock_st}): |
| 21 | + yield mock_st |
| 22 | + |
| 23 | + |
| 24 | +@pytest.fixture |
| 25 | +def mock_env_vars(): |
| 26 | + """Set up mock environment variables for API keys""" |
| 27 | + env_vars = { |
| 28 | + "GOOGLE_API_KEY": "fake-google-key", |
| 29 | + "OPENAI_API_KEY": "fake-openai-key", |
| 30 | + "OPEN_ROUTER_API_KEY": "fake-openrouter-key", |
| 31 | + "ANTHROPIC_API_KEY": "fake-anthropic-key", |
| 32 | + "TOGETHER_API_KEY": "fake-together-key", |
| 33 | + } |
| 34 | + with patch.dict(os.environ, env_vars): |
| 35 | + yield env_vars |
| 36 | + |
| 37 | + |
| 38 | +@pytest.fixture |
| 39 | +def mock_genai(): |
| 40 | + """Mock Google generative AI""" |
| 41 | + with patch('aiagent.genai') as mock: |
| 42 | + mock.configure = Mock() |
| 43 | + mock.GenerativeModel = Mock() |
| 44 | + yield mock |
| 45 | + |
| 46 | + |
| 47 | +@pytest.fixture |
| 48 | +def mock_openai(): |
| 49 | + """Mock OpenAI client""" |
| 50 | + with patch('aiagent.openai') as mock: |
| 51 | + mock_client = Mock() |
| 52 | + mock.OpenAI = Mock(return_value=mock_client) |
| 53 | + yield mock |
| 54 | + |
| 55 | + |
| 56 | +class TestChimeraModelSetup: |
| 57 | + """Tests for chimera model setup and configuration""" |
| 58 | + |
| 59 | + def test_set_model_chimera_uses_openrouter_base_url(self, mock_env_vars, mock_genai, mock_openai): |
| 60 | + """Test that chimera model uses OpenRouter API endpoint""" |
| 61 | + from aiagent import AIAgent |
| 62 | + |
| 63 | + agent = AIAgent(model="chimera") |
| 64 | + |
| 65 | + # Verify OpenAI client was called with OpenRouter base URL |
| 66 | + mock_openai.OpenAI.assert_called() |
| 67 | + call_args = mock_openai.OpenAI.call_args |
| 68 | + assert call_args.kwargs.get('base_url') == "https://openrouter.ai/api/v1" |
| 69 | + assert call_args.kwargs.get('api_key') == "fake-openrouter-key" |
| 70 | + |
| 71 | + def test_set_model_chimera_stores_model_name(self, mock_env_vars, mock_genai, mock_openai): |
| 72 | + """Test that chimera model name is stored""" |
| 73 | + from aiagent import AIAgent |
| 74 | + |
| 75 | + agent = AIAgent(model="chimera") |
| 76 | + |
| 77 | + assert agent.model == "chimera" |
| 78 | + |
| 79 | + def test_set_summary_model_chimera_uses_openrouter(self, mock_env_vars, mock_genai, mock_openai): |
| 80 | + """Test that chimera summary model uses OpenRouter API""" |
| 81 | + from aiagent import AIAgent |
| 82 | + |
| 83 | + agent = AIAgent(model="gpt-4o-mini") |
| 84 | + agent.set_summary_model("chimera") |
| 85 | + |
| 86 | + # Verify OpenAI client was created with OpenRouter base URL |
| 87 | + calls = mock_openai.OpenAI.call_args_list |
| 88 | + openrouter_calls = [c for c in calls if c.kwargs.get('base_url') == "https://openrouter.ai/api/v1"] |
| 89 | + assert len(openrouter_calls) > 0 |
| 90 | + |
| 91 | + |
| 92 | +class TestChimeraModelQuery: |
| 93 | + """Tests for chimera model query functionality - exposes the silent failure bug""" |
| 94 | + |
| 95 | + def test_query_with_chimera_passes_model_name_to_api(self, mock_env_vars, mock_genai, mock_openai): |
| 96 | + """ |
| 97 | + Test that documents the bug: 'chimera' is passed directly to OpenRouter API. |
| 98 | + |
| 99 | + OpenRouter expects actual model identifiers (e.g., 'openai/gpt-4', |
| 100 | + 'anthropic/claude-3-haiku'), not 'chimera'. This causes silent failures. |
| 101 | + """ |
| 102 | + from aiagent import AIAgent |
| 103 | + |
| 104 | + # Setup mock response |
| 105 | + mock_completion = Mock() |
| 106 | + mock_completion.choices = [Mock()] |
| 107 | + mock_completion.choices[0].message.content = "Test response" |
| 108 | + mock_completion.usage = Mock() |
| 109 | + mock_completion.usage.prompt_tokens = 100 |
| 110 | + mock_completion.usage.completion_tokens = 50 |
| 111 | + |
| 112 | + mock_client = Mock() |
| 113 | + mock_client.chat.completions.create.return_value = mock_completion |
| 114 | + mock_openai.OpenAI.return_value = mock_client |
| 115 | + |
| 116 | + # Mock moderation API |
| 117 | + mock_moderation = Mock() |
| 118 | + mock_moderation.results = [Mock()] |
| 119 | + mock_moderation.results[0].flagged = False |
| 120 | + mock_client.moderations.create.return_value = mock_moderation |
| 121 | + |
| 122 | + agent = AIAgent(model="chimera") |
| 123 | + agent.query("Hello") |
| 124 | + |
| 125 | + # Get what model name was passed to the API |
| 126 | + create_call_kwargs = mock_client.chat.completions.create.call_args.kwargs |
| 127 | + model_passed = create_call_kwargs.get('model') |
| 128 | + |
| 129 | + # This documents the bug: "chimera" is passed as model name |
| 130 | + # OpenRouter expects real model identifiers like "openai/gpt-4" |
| 131 | + assert model_passed == "chimera", ( |
| 132 | + "Expected 'chimera' to be passed to API (documenting the bug). " |
| 133 | + "When this test fails with a different model name, the bug is fixed." |
| 134 | + ) |
| 135 | + |
| 136 | + def test_query_with_chimera_uses_chat_completions_api(self, mock_env_vars, mock_genai, mock_openai): |
| 137 | + """Test that chimera uses the OpenAI-compatible chat completions API""" |
| 138 | + from aiagent import AIAgent |
| 139 | + |
| 140 | + # Setup mock |
| 141 | + mock_completion = Mock() |
| 142 | + mock_completion.choices = [Mock()] |
| 143 | + mock_completion.choices[0].message.content = "Test response" |
| 144 | + mock_completion.usage = Mock() |
| 145 | + mock_completion.usage.prompt_tokens = 100 |
| 146 | + mock_completion.usage.completion_tokens = 50 |
| 147 | + |
| 148 | + mock_client = Mock() |
| 149 | + mock_client.chat.completions.create.return_value = mock_completion |
| 150 | + mock_openai.OpenAI.return_value = mock_client |
| 151 | + |
| 152 | + mock_moderation = Mock() |
| 153 | + mock_moderation.results = [Mock()] |
| 154 | + mock_moderation.results[0].flagged = False |
| 155 | + mock_client.moderations.create.return_value = mock_moderation |
| 156 | + |
| 157 | + agent = AIAgent(model="chimera") |
| 158 | + response = agent.query("Hello") |
| 159 | + |
| 160 | + # Verify chat.completions.create was called |
| 161 | + mock_client.chat.completions.create.assert_called_once() |
| 162 | + assert response == "Test response" |
| 163 | + |
| 164 | + def test_query_chimera_includes_required_parameters(self, mock_env_vars, mock_genai, mock_openai): |
| 165 | + """Test that chimera query includes all required parameters""" |
| 166 | + from aiagent import AIAgent |
| 167 | + |
| 168 | + mock_completion = Mock() |
| 169 | + mock_completion.choices = [Mock()] |
| 170 | + mock_completion.choices[0].message.content = "Test response" |
| 171 | + mock_completion.usage = Mock() |
| 172 | + mock_completion.usage.prompt_tokens = 100 |
| 173 | + mock_completion.usage.completion_tokens = 50 |
| 174 | + |
| 175 | + mock_client = Mock() |
| 176 | + mock_client.chat.completions.create.return_value = mock_completion |
| 177 | + mock_openai.OpenAI.return_value = mock_client |
| 178 | + |
| 179 | + mock_moderation = Mock() |
| 180 | + mock_moderation.results = [Mock()] |
| 181 | + mock_moderation.results[0].flagged = False |
| 182 | + mock_client.moderations.create.return_value = mock_moderation |
| 183 | + |
| 184 | + agent = AIAgent(model="chimera") |
| 185 | + agent.query("Hello", temperature=0.5, max_tokens=100) |
| 186 | + |
| 187 | + call_kwargs = mock_client.chat.completions.create.call_args.kwargs |
| 188 | + |
| 189 | + assert 'model' in call_kwargs |
| 190 | + assert 'messages' in call_kwargs |
| 191 | + assert call_kwargs['temperature'] == 0.5 |
| 192 | + assert call_kwargs['max_tokens'] == 100 |
| 193 | + |
| 194 | + |
| 195 | +class TestChimeraModelErrorHandling: |
| 196 | + """Tests for error handling when chimera model fails""" |
| 197 | + |
| 198 | + def test_chimera_invalid_model_error(self, mock_env_vars, mock_genai, mock_openai): |
| 199 | + """Test behavior when OpenRouter returns an invalid model error""" |
| 200 | + from aiagent import AIAgent |
| 201 | + |
| 202 | + # Simulate OpenRouter error for invalid model |
| 203 | + mock_client = Mock() |
| 204 | + mock_client.chat.completions.create.side_effect = Exception( |
| 205 | + "Error: The model `chimera` does not exist" |
| 206 | + ) |
| 207 | + mock_openai.OpenAI.return_value = mock_client |
| 208 | + |
| 209 | + agent = AIAgent(model="chimera") |
| 210 | + |
| 211 | + with pytest.raises(Exception) as exc_info: |
| 212 | + agent.query("Hello") |
| 213 | + |
| 214 | + assert "chimera" in str(exc_info.value) |
| 215 | + |
| 216 | + |
| 217 | +class TestModelSwitching: |
| 218 | + """Tests for switching between models including chimera""" |
| 219 | + |
| 220 | + def test_switch_to_chimera_updates_base_url(self, mock_env_vars, mock_genai, mock_openai): |
| 221 | + """Test that switching to chimera uses OpenRouter endpoint""" |
| 222 | + from aiagent import AIAgent |
| 223 | + |
| 224 | + agent = AIAgent(model="gpt-4o-mini") |
| 225 | + agent.set_model("chimera") |
| 226 | + |
| 227 | + # Find the call that sets up chimera |
| 228 | + calls = mock_openai.OpenAI.call_args_list |
| 229 | + last_call = calls[-1] |
| 230 | + assert last_call.kwargs.get('base_url') == "https://openrouter.ai/api/v1" |
| 231 | + |
| 232 | + def test_switch_from_chimera_to_gpt(self, mock_env_vars, mock_genai, mock_openai): |
| 233 | + """Test that switching from chimera to GPT works correctly""" |
| 234 | + from aiagent import AIAgent |
| 235 | + |
| 236 | + agent = AIAgent(model="chimera") |
| 237 | + agent.set_model("gpt-4o-mini") |
| 238 | + |
| 239 | + calls = mock_openai.OpenAI.call_args_list |
| 240 | + last_call = calls[-1] |
| 241 | + assert last_call.kwargs.get('base_url') == "https://api.openai.com/v1" |
| 242 | + |
| 243 | + |
| 244 | +class TestApiKeyHandling: |
| 245 | + """Tests for API key handling with chimera model""" |
| 246 | + |
| 247 | + def test_chimera_uses_open_router_api_key(self, mock_genai, mock_openai): |
| 248 | + """Test that chimera model uses OPEN_ROUTER_API_KEY""" |
| 249 | + with patch.dict(os.environ, { |
| 250 | + "GOOGLE_API_KEY": "fake-google-key", |
| 251 | + "OPENAI_API_KEY": "fake-openai-key", |
| 252 | + "OPEN_ROUTER_API_KEY": "test-openrouter-key-123", |
| 253 | + }): |
| 254 | + from aiagent import AIAgent |
| 255 | + |
| 256 | + agent = AIAgent(model="chimera") |
| 257 | + |
| 258 | + calls = mock_openai.OpenAI.call_args_list |
| 259 | + chimera_calls = [c for c in calls if c.kwargs.get('base_url') == "https://openrouter.ai/api/v1"] |
| 260 | + |
| 261 | + assert len(chimera_calls) > 0 |
| 262 | + assert chimera_calls[-1].kwargs.get('api_key') == "test-openrouter-key-123" |
| 263 | + |
| 264 | + |
| 265 | +if __name__ == "__main__": |
| 266 | + pytest.main([__file__, "-v"]) |
0 commit comments