""" Tests for the LLM thinking mode feature. Verifies that the ``llm_thinking_enabled`` config option correctly controls the ``think`` parameter sent to Ollama across all call sites. """ import json import threading from unittest.mock import patch, MagicMock import pytest from jarvis.config import get_default_config # --------------------------------------------------------------------------- # Config defaults # --------------------------------------------------------------------------- class TestThinkingConfig: """Config layer tests for thinking settings.""" def test_default_config_has_chat_thinking_disabled(self): """llm_thinking_enabled should default to False.""" config = get_default_config() assert "llm_thinking_enabled" in config assert config["llm_thinking_enabled"] is False def test_default_config_has_intent_judge_thinking_disabled(self): """intent_judge_thinking_enabled should default to False.""" config = get_default_config() assert "intent_judge_thinking_enabled" in config assert config["intent_judge_thinking_enabled"] is False def test_default_config_has_dictation_thinking_disabled(self): """dictation_thinking_enabled should default to False.""" config = get_default_config() assert "dictation_thinking_enabled" in config assert config["dictation_thinking_enabled"] is False # --------------------------------------------------------------------------- # llm.py — payload construction # --------------------------------------------------------------------------- class TestLlmThinkingPayload: """Verify the ``think`` key appears in Ollama request payloads.""" def _capture_payload(self, mock_post, *, expect_stream=False): """Extract the JSON payload from the first call to requests.post.""" assert mock_post.called, "requests.post was never called" _, kwargs = mock_post.call_args payload = kwargs.get("json") or {} return payload @patch("jarvis.llm.requests.post") def test_call_llm_direct_thinking_false(self, mock_post): from jarvis.llm import call_llm_direct mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"message": {"content": "ok"}} mock_post.return_value = mock_resp call_llm_direct("http://localhost:11434", "gemma4:e2b", "sys", "hi", thinking=False) payload = self._capture_payload(mock_post) assert payload["think"] is False @patch("jarvis.llm.requests.post") def test_call_llm_direct_thinking_true(self, mock_post): from jarvis.llm import call_llm_direct mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"message": {"content": "ok"}} mock_post.return_value = mock_resp call_llm_direct("http://localhost:11434", "gemma4:e2b", "sys", "hi", thinking=True) payload = self._capture_payload(mock_post) assert payload["think"] is True @patch("jarvis.llm.requests.post") def test_call_llm_direct_thinking_defaults_false(self, mock_post): from jarvis.llm import call_llm_direct mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"message": {"content": "ok"}} mock_post.return_value = mock_resp call_llm_direct("http://localhost:11434", "gemma4:e2b", "sys", "hi") payload = self._capture_payload(mock_post) assert payload["think"] is False @patch("jarvis.llm.requests.post") def test_call_llm_streaming_thinking(self, mock_post): from jarvis.llm import call_llm_streaming mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.iter_lines.return_value = [ json.dumps({"message": {"content": "hi"}}).encode() ] mock_resp.raise_for_status = MagicMock() mock_post.return_value = mock_resp call_llm_streaming("http://localhost:11434", "gemma4:e2b", "sys", "hi", thinking=True) payload = self._capture_payload(mock_post) assert payload["think"] is True @patch("jarvis.llm.requests.post") def test_chat_with_messages_thinking(self, mock_post): from jarvis.llm import chat_with_messages mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"message": {"content": "ok"}} mock_resp.raise_for_status = MagicMock() mock_post.return_value = mock_resp msgs = [{"role": "user", "content": "hi"}] chat_with_messages("http://localhost:11434", "gemma4:e2b", msgs, thinking=True) payload = self._capture_payload(mock_post) assert payload["think"] is True @patch("jarvis.llm.requests.post") def test_chat_with_messages_thinking_defaults_false(self, mock_post): from jarvis.llm import chat_with_messages mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"message": {"content": "ok"}} mock_resp.raise_for_status = MagicMock() mock_post.return_value = mock_resp msgs = [{"role": "user", "content": "hi"}] chat_with_messages("http://localhost:11434", "gemma4:e2b", msgs) payload = self._capture_payload(mock_post) assert payload["think"] is False # --------------------------------------------------------------------------- # Intent judge # --------------------------------------------------------------------------- class TestIntentJudgeThinking: """Intent judge respects the thinking config.""" def test_config_default_thinking_false(self): from jarvis.listening.intent_judge import IntentJudgeConfig config = IntentJudgeConfig() assert config.thinking is False def test_config_accepts_thinking_true(self): from jarvis.listening.intent_judge import IntentJudgeConfig config = IntentJudgeConfig(thinking=True) assert config.thinking is True def test_create_intent_judge_passes_thinking(self): """create_intent_judge should read intent_judge_thinking_enabled from cfg.""" from jarvis.listening.intent_judge import create_intent_judge cfg = MagicMock() cfg.wake_word = "jarvis" cfg.wake_aliases = [] cfg.intent_judge_model = "gemma4:e2b" cfg.ollama_base_url = "http://localhost:11434" cfg.intent_judge_timeout_sec = 10.0 cfg.intent_judge_thinking_enabled = True judge = create_intent_judge(cfg) assert judge is not None assert judge.config.thinking is True def test_create_intent_judge_independent_from_chat_thinking(self): """Intent judge thinking should be independent from chat thinking.""" from jarvis.listening.intent_judge import create_intent_judge cfg = MagicMock() cfg.wake_word = "jarvis" cfg.wake_aliases = [] cfg.intent_judge_model = "gemma4:e2b" cfg.ollama_base_url = "http://localhost:11434" cfg.intent_judge_timeout_sec = 10.0 cfg.llm_thinking_enabled = True cfg.intent_judge_thinking_enabled = False judge = create_intent_judge(cfg) assert judge.config.thinking is False # --------------------------------------------------------------------------- # Dictation engine # --------------------------------------------------------------------------- class TestDictationThinking: """Dictation engine respects the thinking config.""" def test_llm_clean_dictation_sends_think_false(self): from src.jarvis.dictation.dictation_engine import _llm_clean_dictation with patch("requests.post") as mock_post: mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"response": "cleaned"} mock_post.return_value = mock_resp _llm_clean_dictation("um hello", "http://localhost:11434", thinking=False) payload = mock_post.call_args[1].get("json") or mock_post.call_args[0][1] if len(mock_post.call_args[0]) > 1 else mock_post.call_args[1]["json"] assert payload["think"] is False def test_llm_clean_dictation_sends_think_true(self): from src.jarvis.dictation.dictation_engine import _llm_clean_dictation with patch("requests.post") as mock_post: mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"response": "cleaned"} mock_post.return_value = mock_resp _llm_clean_dictation("um hello", "http://localhost:11434", thinking=True) payload = mock_post.call_args[1].get("json") or mock_post.call_args[0][1] if len(mock_post.call_args[0]) > 1 else mock_post.call_args[1]["json"] assert payload["think"] is True def test_engine_stores_thinking(self): from src.jarvis.dictation.dictation_engine import DictationEngine engine = DictationEngine( whisper_model_ref=lambda: MagicMock(), whisper_backend_ref=lambda: "faster-whisper", mlx_repo_ref=lambda: None, hotkey="ctrl+shift+d", sample_rate=16000, transcribe_lock=threading.Lock(), thinking=True, ) assert engine._thinking is True # --------------------------------------------------------------------------- # Settings window metadata # --------------------------------------------------------------------------- class TestSettingsWindowThinking: """Settings window includes all three thinking fields.""" def test_field_metadata_includes_chat_thinking(self): from desktop_app.settings_window import FIELD_METADATA keys = [fm.key for fm in FIELD_METADATA] assert "llm_thinking_enabled" in keys def test_chat_thinking_field_is_bool_in_llm_category(self): from desktop_app.settings_window import FIELD_METADATA field = next(fm for fm in FIELD_METADATA if fm.key == "llm_thinking_enabled") assert field.field_type == "bool" assert field.category == "llm" def test_field_metadata_includes_intent_judge_thinking(self): from desktop_app.settings_window import FIELD_METADATA keys = [fm.key for fm in FIELD_METADATA] assert "intent_judge_thinking_enabled" in keys def test_intent_judge_thinking_field_is_bool_in_llm_category(self): from desktop_app.settings_window import FIELD_METADATA field = next(fm for fm in FIELD_METADATA if fm.key == "intent_judge_thinking_enabled") assert field.field_type == "bool" assert field.category == "llm" def test_field_metadata_includes_dictation_thinking(self): from desktop_app.settings_window import FIELD_METADATA keys = [fm.key for fm in FIELD_METADATA] assert "dictation_thinking_enabled" in keys def test_dictation_thinking_field_is_bool_in_features_category(self): from desktop_app.settings_window import FIELD_METADATA field = next(fm for fm in FIELD_METADATA if fm.key == "dictation_thinking_enabled") assert field.field_type == "bool" assert field.category == "features" # --------------------------------------------------------------------------- # Timeout / error paths — regression test for missing debug_log import # --------------------------------------------------------------------------- class TestCallLlmDirectFailurePaths: """Exercises the exception branches in call_llm_direct. These branches call debug_log; a missing import would surface here as a NameError instead of the intended graceful None return. """ def test_timeout_returns_none(self): import requests from jarvis.llm import call_llm_direct with patch('jarvis.llm.requests.post', side_effect=requests.exceptions.Timeout): result = call_llm_direct( base_url="http://localhost:99999", chat_model="test-model", system_prompt="sys", user_content="hi", timeout_sec=0.1, ) assert result is None def test_request_exception_returns_none(self): from jarvis.llm import call_llm_direct with patch('jarvis.llm.requests.post', side_effect=ConnectionError("boom")): result = call_llm_direct( base_url="http://localhost:99999", chat_model="test-model", system_prompt="sys", user_content="hi", timeout_sec=0.1, ) assert result is None