"""Tests for the ``ollama_num_predict`` chat-output cap. The cap bounds worst-case reply latency by limiting how many tokens the chat model may generate per turn. Spoken (TTS) answers are 1-2 sentences, so the default headroom never clips a normal answer and stays above tool-call JSON. These tests verify behaviour: - the config default is present, - the value is threaded into the Ollama request as the ``num_predict`` option, - the reply loop forwards it to the chat call (and disables it at 0). """ from unittest.mock import Mock, patch import pytest from src.jarvis.config import get_default_config from src.jarvis.memory.conversation import DialogueMemory from src.jarvis.reply.engine import run_reply_engine # --------------------------------------------------------------------------- # Config default # --------------------------------------------------------------------------- def test_default_config_has_num_predict_cap(): config = get_default_config() assert config["ollama_num_predict"] == 512 # --------------------------------------------------------------------------- # Transport: extra_options.num_predict reaches the Ollama payload options # --------------------------------------------------------------------------- @patch("jarvis.llm.requests.post") def test_chat_with_messages_forwards_num_predict(mock_post): from jarvis.llm import chat_with_messages mock_resp = Mock() mock_resp.status_code = 200 mock_resp.json.return_value = {"message": {"content": "ok"}} mock_resp.raise_for_status = Mock() mock_post.return_value = mock_resp chat_with_messages( "http://localhost:11434", "test-large", [{"role": "user", "content": "hi"}], extra_options={"num_predict": 512}, ) _, kwargs = mock_post.call_args options = (kwargs.get("json") or {}).get("options") or {} assert options.get("num_predict") == 512 # --------------------------------------------------------------------------- # Reply loop wiring # --------------------------------------------------------------------------- def _mock_cfg(num_predict): cfg = Mock() cfg.ollama_base_url = "http://localhost:11434" cfg.ollama_chat_model = "test-large" # avoid SMALL-model text-tool path cfg.ollama_num_predict = num_predict cfg.voice_debug = False cfg.llm_tools_timeout_sec = 8.0 cfg.llm_embed_timeout_sec = 10.0 cfg.llm_chat_timeout_sec = 45.0 cfg.llm_digest_timeout_sec = 8.0 cfg.memory_enrichment_max_results = 5 cfg.memory_enrichment_source = "diary" cfg.memory_digest_enabled = False cfg.tool_result_digest_enabled = False cfg.location_ip_address = None cfg.location_auto_detect = False cfg.location_enabled = False cfg.agentic_max_turns = 8 cfg.tool_search_max_calls = 3 cfg.tool_selection_strategy = "all" cfg.tool_carryover_max_turns = 2 cfg.tool_carryover_per_entry_chars = 1200 cfg.mcps = {} cfg.llm_thinking_enabled = False cfg.tts_engine = "none" cfg.ollama_embed_model = "test-embed" return cfg def _run_single_turn(cfg): """Drive one reply turn that answers in plain text and capture the chat call's extra_options.""" with patch("src.jarvis.reply.engine.plan_query", return_value=[]), \ patch("src.jarvis.reply.engine.extract_search_params_for_memory", return_value={}), \ patch("src.jarvis.reply.engine.extract_text_from_response", return_value="Hello."), \ patch("src.jarvis.reply.engine.chat_with_messages") as mock_chat: mock_chat.return_value = {"message": {"content": "Hello."}} run_reply_engine(db=Mock(), cfg=cfg, tts=None, text="hi", dialogue_memory=DialogueMemory()) assert mock_chat.called return mock_chat.call_args.kwargs.get("extra_options") @pytest.mark.unit def test_reply_loop_caps_output_when_enabled(): extra = _run_single_turn(_mock_cfg(512)) assert extra == {"num_predict": 512} @pytest.mark.unit def test_reply_loop_no_cap_when_zero(): extra = _run_single_turn(_mock_cfg(0)) assert extra is None