import pytest def _make_config(): from pyra.config.schema import PyraConfig, ProviderConfig return PyraConfig(ai=ProviderConfig(provider_id="lmstudio", model="gemma")) @pytest.fixture def history(tmp_pyra_home, monkeypatch): monkeypatch.setattr("pyra.chat.history.load_context_for_session", lambda: "") from pyra.chat.history import ConversationHistory return ConversationHistory(_make_config()) def test_build_for_api_first_message_is_system(history): msgs = history.build_for_api() assert msgs[0]["role"] == "system" def test_build_for_api_system_contains_pyra(history): msgs = history.build_for_api() assert "Pyra" in msgs[0]["content"] def test_build_for_api_includes_memory_context(tmp_pyra_home, monkeypatch): monkeypatch.setattr( "pyra.chat.history.load_context_for_session", lambda: "## Long-term Memory\n\nSome remembered facts.", ) from pyra.chat.history import ConversationHistory h = ConversationHistory(_make_config()) msgs = h.build_for_api() assert "Long-term Memory" in msgs[0]["content"] def test_add_user_appears_in_api_payload(history): history.add_user("hello from user") msgs = history.build_for_api() user_msgs = [m for m in msgs if m["role"] == "user"] assert any(m["content"] == "hello from user" for m in user_msgs) def test_add_assistant_appears_in_api_payload(history): history.add_assistant("hello from assistant") msgs = history.build_for_api() asst_msgs = [m for m in msgs if m["role"] == "assistant"] assert any(m["content"] == "hello from assistant" for m in asst_msgs) def test_add_tool_result(history): history.add_tool_result("call_abc", "tool output") msgs = history.build_for_api() tool_msgs = [m for m in msgs if m.get("role") == "tool"] assert len(tool_msgs) == 1 assert tool_msgs[0]["tool_call_id"] == "call_abc" assert tool_msgs[0]["content"] == "tool output" def test_clear_removes_messages(history): history.add_user("hello") history.add_assistant("hi") history.clear() msgs = history.build_for_api() assert all(m["role"] == "system" for m in msgs) def test_trim_to_budget_drops_old_messages(): from pyra.chat.history import _trim_to_budget msgs = [ {"role": "user", "content": "a" * 4000}, {"role": "assistant", "content": "b" * 4000}, {"role": "user", "content": "new message"}, ] trimmed = _trim_to_budget(list(msgs), 100) assert len(trimmed) < 3 assert any(m["content"] == "new message" for m in trimmed) def test_trim_to_budget_no_trim_when_under_budget(): from pyra.chat.history import _trim_to_budget msgs = [{"role": "user", "content": "short"}] result = _trim_to_budget(list(msgs), 10000) assert len(result) == 1 assert result[0]["content"] == "short" def test_trim_to_budget_empty_list(): from pyra.chat.history import _trim_to_budget assert _trim_to_budget([], 1000) == [] # ── _build_system_base tests ─────────────────────────────────────────────────── def test_build_system_base_default_identity(): from pyra.chat.history import _build_system_base result = _build_system_base("User", "Pyra", "") assert "You are Pyra" in result assert "User" in result def test_build_system_base_custom_name_and_assistant(): from pyra.chat.history import _build_system_base result = _build_system_base("Alice", "Aria", "") assert "You are Aria" in result assert "Alice" in result def test_build_system_base_no_purpose_omits_focus_block(): from pyra.chat.history import _build_system_base result = _build_system_base("User", "Pyra", "") assert "primary purpose" not in result assert "not a general-purpose chatbot" not in result def test_build_system_base_with_purpose_includes_focus_block(): from pyra.chat.history import _build_system_base result = _build_system_base("User", "Pyra", "manage my Nextcloud server") assert "primary purpose" in result assert "manage my Nextcloud server" in result assert "not a general-purpose chatbot" in result def test_build_system_base_always_includes_security_constraints(): from pyra.chat.history import _build_system_base for purpose in ("", "manage servers"): result = _build_system_base("User", "Pyra", purpose) assert "vault" in result assert "shell commands" in result def test_build_for_api_uses_config_user_name(tmp_pyra_home, monkeypatch): monkeypatch.setattr("pyra.chat.history.load_context_for_session", lambda: "") from pyra.config.schema import PyraConfig, ProviderConfig, GeneralConfig from pyra.chat.history import ConversationHistory cfg = PyraConfig( ai=ProviderConfig(provider_id="lmstudio", model="gemma"), general=GeneralConfig(user_name="Alice", assistant_name="Aria", purpose=""), ) h = ConversationHistory(cfg) system = h.build_for_api()[0]["content"] assert "Alice" in system assert "Aria" in system def test_build_for_api_uses_purpose(tmp_pyra_home, monkeypatch): monkeypatch.setattr("pyra.chat.history.load_context_for_session", lambda: "") from pyra.config.schema import PyraConfig, ProviderConfig, GeneralConfig from pyra.chat.history import ConversationHistory cfg = PyraConfig( ai=ProviderConfig(provider_id="lmstudio", model="gemma"), general=GeneralConfig(purpose="manage my home server"), ) h = ConversationHistory(cfg) system = h.build_for_api()[0]["content"] assert "manage my home server" in system assert "primary purpose" in system