"""Tests for src/timmy/backends.py — backend helpers and classes.""" from unittest.mock import MagicMock, patch # ── is_apple_silicon ────────────────────────────────────────────────────────── def test_is_apple_silicon_true_on_arm_darwin(): with ( patch("timmy.backends.platform.system", return_value="Darwin"), patch("timmy.backends.platform.machine", return_value="arm64"), ): from timmy.backends import is_apple_silicon assert is_apple_silicon() is True def test_is_apple_silicon_false_on_linux(): with ( patch("timmy.backends.platform.system", return_value="Linux"), patch("timmy.backends.platform.machine", return_value="x86_64"), ): from timmy.backends import is_apple_silicon assert is_apple_silicon() is False def test_is_apple_silicon_false_on_intel_mac(): with ( patch("timmy.backends.platform.system", return_value="Darwin"), patch("timmy.backends.platform.machine", return_value="x86_64"), ): from timmy.backends import is_apple_silicon assert is_apple_silicon() is False # ── ClaudeBackend ───────────────────────────────────────────────────────── def test_claude_available_false_when_no_key(): """claude_available() returns False when ANTHROPIC_API_KEY is empty.""" with patch("config.settings") as mock_settings: mock_settings.anthropic_api_key = "" from timmy.backends import claude_available assert claude_available() is False def test_claude_available_true_when_key_set(): """claude_available() returns True when ANTHROPIC_API_KEY is set.""" with patch("config.settings") as mock_settings: mock_settings.anthropic_api_key = "sk-ant-test-key" from timmy.backends import claude_available assert claude_available() is True def test_claude_backend_init_with_explicit_params(): """ClaudeBackend can be created with explicit api_key and model.""" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") assert backend._api_key == "sk-ant-test" assert "haiku" in backend._model def test_claude_backend_init_resolves_short_names(): """ClaudeBackend resolves short model names to full IDs.""" from timmy.backends import CLAUDE_MODELS, ClaudeBackend backend = ClaudeBackend(api_key="sk-test", model="sonnet") assert backend._model == CLAUDE_MODELS["sonnet"] def test_claude_backend_init_passes_through_full_model_id(): """ClaudeBackend passes through full model IDs unchanged.""" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-test", model="claude-haiku-4-5-20251001") assert backend._model == "claude-haiku-4-5-20251001" def test_claude_backend_run_no_key_returns_error(): """run() gracefully returns error message when no API key.""" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="", model="haiku") result = backend.run("hello") assert "not configured" in result.content def test_claude_backend_run_success(): """run() returns content from the Anthropic API on success.""" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") mock_content = MagicMock() mock_content.text = "Sir, affirmative. I am Timmy." mock_response = MagicMock() mock_response.content = [mock_content] mock_client = MagicMock() mock_client.messages.create.return_value = mock_response with patch.object(backend, "_get_client", return_value=mock_client): result = backend.run("Who are you?") assert "Timmy" in result.content assert len(backend._history) == 2 # user + assistant def test_claude_backend_run_handles_api_error(): """run() returns a graceful error when the API raises.""" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") mock_client = MagicMock() mock_client.messages.create.side_effect = ConnectionError("network down") with patch.object(backend, "_get_client", return_value=mock_client): result = backend.run("hello") assert "unavailable" in result.content def test_claude_backend_history_rolling_window(): """History should be capped at 20 entries (10 exchanges).""" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") mock_content = MagicMock() mock_content.text = "OK." mock_response = MagicMock() mock_response.content = [mock_content] mock_client = MagicMock() mock_client.messages.create.return_value = mock_response with patch.object(backend, "_get_client", return_value=mock_client): for i in range(15): backend.run(f"message {i}") assert len(backend._history) <= 20 # ── ClaudeBackend prompt formatting ───────────────────────────────────────── def test_claude_prompt_contains_formatted_model_name(): """Claude system prompt should have actual model name, not literal {model_name}.""" with patch("config.settings") as mock_settings: mock_settings.ollama_model = "llama3.2:3b" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") # Mock the client to capture the system parameter mock_client = MagicMock() mock_content = MagicMock() mock_content.text = "test response" mock_response = MagicMock() mock_response.content = [mock_content] mock_client.messages.create.return_value = mock_response with patch.object(backend, "_get_client", return_value=mock_client): backend.run("test message") # Get the system parameter from the create call call_kwargs = mock_client.messages.create.call_args[1] system_prompt = call_kwargs.get("system", "") # Should contain the actual model name, not the placeholder assert "{model_name}" not in system_prompt assert "llama3.2:3b" in system_prompt def test_claude_prompt_gets_full_tier(): """Claude should get FULL tier prompt (tools_enabled=True).""" with patch("config.settings") as mock_settings: mock_settings.ollama_model = "test-model" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") mock_client = MagicMock() mock_content = MagicMock() mock_content.text = "test response" mock_response = MagicMock() mock_response.content = [mock_content] mock_client.messages.create.return_value = mock_response with patch.object(backend, "_get_client", return_value=mock_client): backend.run("test message") call_kwargs = mock_client.messages.create.call_args[1] system_prompt = call_kwargs.get("system", "") # FULL tier should have TOOL USAGE section assert "TOOL USAGE" in system_prompt # FULL tier should have the full voice and brevity section assert "VOICE AND BREVITY" in system_prompt def test_claude_prompt_contains_session_id(): """Claude prompt should have session_id formatted, not placeholder.""" with patch("config.settings") as mock_settings: mock_settings.ollama_model = "test-model" from timmy.backends import ClaudeBackend backend = ClaudeBackend(api_key="sk-ant-test", model="haiku") mock_client = MagicMock() mock_content = MagicMock() mock_content.text = "test response" mock_response = MagicMock() mock_response.content = [mock_content] mock_client.messages.create.return_value = mock_response with patch.object(backend, "_get_client", return_value=mock_client): backend.run("test message") call_kwargs = mock_client.messages.create.call_args[1] system_prompt = call_kwargs.get("system", "") # Should contain the session_id, not the placeholder assert '{session_id}"' not in system_prompt assert 'session "claude"' in system_prompt