"""Unit tests for AirLLM backend graceful degradation. Verifies that setting TIMMY_MODEL_BACKEND=airllm on non-Apple-Silicon hardware (Intel Mac, Linux, Windows) or when the airllm package is not installed falls back to the Ollama backend without crashing. Refs #1284 """ from unittest.mock import MagicMock, patch import pytest pytestmark = pytest.mark.unit class TestIsAppleSilicon: """is_apple_silicon() correctly identifies the host platform.""" def test_returns_true_on_arm64_darwin(self): from timmy.backends import is_apple_silicon with patch("platform.system", return_value="Darwin"), patch( "platform.machine", return_value="arm64" ): assert is_apple_silicon() is True def test_returns_false_on_intel_mac(self): from timmy.backends import is_apple_silicon with patch("platform.system", return_value="Darwin"), patch( "platform.machine", return_value="x86_64" ): assert is_apple_silicon() is False def test_returns_false_on_linux(self): from timmy.backends import is_apple_silicon with patch("platform.system", return_value="Linux"), patch( "platform.machine", return_value="x86_64" ): assert is_apple_silicon() is False def test_returns_false_on_windows(self): from timmy.backends import is_apple_silicon with patch("platform.system", return_value="Windows"), patch( "platform.machine", return_value="AMD64" ): assert is_apple_silicon() is False class TestAirLLMGracefulDegradation: """create_timmy(backend='airllm') falls back to Ollama on unsupported platforms.""" def _make_fake_ollama_agent(self): """Return a lightweight stub that satisfies the Agno Agent interface.""" agent = MagicMock() agent.run = MagicMock(return_value=MagicMock(content="ok")) return agent def test_falls_back_to_ollama_on_non_apple_silicon(self, caplog): """On Intel/Linux, airllm backend logs a warning and creates an Ollama agent.""" import logging from timmy.agent import create_timmy fake_agent = self._make_fake_ollama_agent() with ( patch("timmy.backends.is_apple_silicon", return_value=False), patch("timmy.agent._create_ollama_agent", return_value=fake_agent) as mock_create, patch("timmy.agent._resolve_model_with_fallback", return_value=("qwen3:8b", False)), patch("timmy.agent._check_model_available", return_value=True), patch("timmy.agent._build_tools_list", return_value=[]), patch("timmy.agent._build_prompt", return_value="test prompt"), caplog.at_level(logging.WARNING, logger="timmy.agent"), ): result = create_timmy(backend="airllm") assert result is fake_agent mock_create.assert_called_once() assert "Apple Silicon" in caplog.text def test_falls_back_to_ollama_when_airllm_not_installed(self, caplog): """When the airllm package is missing, log a warning and use Ollama.""" import logging from timmy.agent import create_timmy fake_agent = self._make_fake_ollama_agent() # Simulate Apple Silicon + missing airllm package def _import_side_effect(name, *args, **kwargs): if name == "airllm": raise ImportError("No module named 'airllm'") return original_import(name, *args, **kwargs) original_import = __builtins__["__import__"] if isinstance(__builtins__, dict) else __import__ with ( patch("timmy.backends.is_apple_silicon", return_value=True), patch("builtins.__import__", side_effect=_import_side_effect), patch("timmy.agent._create_ollama_agent", return_value=fake_agent) as mock_create, patch("timmy.agent._resolve_model_with_fallback", return_value=("qwen3:8b", False)), patch("timmy.agent._check_model_available", return_value=True), patch("timmy.agent._build_tools_list", return_value=[]), patch("timmy.agent._build_prompt", return_value="test prompt"), caplog.at_level(logging.WARNING, logger="timmy.agent"), ): result = create_timmy(backend="airllm") assert result is fake_agent mock_create.assert_called_once() assert "airllm" in caplog.text.lower() or "AirLLM" in caplog.text def test_airllm_backend_does_not_raise(self): """create_timmy(backend='airllm') never raises — it degrades gracefully.""" from timmy.agent import create_timmy fake_agent = self._make_fake_ollama_agent() with ( patch("timmy.backends.is_apple_silicon", return_value=False), patch("timmy.agent._create_ollama_agent", return_value=fake_agent), patch("timmy.agent._resolve_model_with_fallback", return_value=("qwen3:8b", False)), patch("timmy.agent._check_model_available", return_value=True), patch("timmy.agent._build_tools_list", return_value=[]), patch("timmy.agent._build_prompt", return_value="test prompt"), ): # Should not raise under any circumstances result = create_timmy(backend="airllm") assert result is not None