diff --git a/navi/config.py b/navi/config.py index 3104ca2..98ae75a 100644 --- a/navi/config.py +++ b/navi/config.py @@ -5,7 +5,9 @@ class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") + model_config = SettingsConfigDict( + env_file=".env", env_file_encoding="utf-8", extra="ignore", frozen=True + ) ollama_host: str = "http://localhost:11434" ollama_api_key: str = "" @@ -118,14 +120,16 @@ navi_persona: str = "" navi_persona_file: str = "" - @model_validator(mode="after") - def _load_persona_from_file(self) -> "Settings": - if not self.navi_persona and self.navi_persona_file: + @model_validator(mode="before") + def _load_persona_from_file(cls, values: dict) -> dict: + if not values.get("navi_persona") and values.get("navi_persona_file"): try: - self.navi_persona = Path(self.navi_persona_file).read_text(encoding="utf-8").strip() + values["navi_persona"] = Path(values["navi_persona_file"]).read_text( + encoding="utf-8" + ).strip() except Exception: pass - return self + return values @property def fs_allowed_paths_list(self) -> list[str]: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 82f0a7f..7a5f08e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -38,7 +38,9 @@ import navi.config as _config # Ensure database_url is set so _make_memory_store doesn't raise - _config.settings.database_url = "postgresql://fake" + monkeypatch.setattr( + _config, "settings", _config.Settings(database_url="postgresql://fake") + ) store = InMemorySessionStore() profiles = make_profile_registry() diff --git a/tests/unit/api/test_session_files.py b/tests/unit/api/test_session_files.py index 698beae..eb83297 100644 --- a/tests/unit/api/test_session_files.py +++ b/tests/unit/api/test_session_files.py @@ -18,13 +18,18 @@ @pytest.fixture(autouse=True) def _session_files_dir(monkeypatch, tmp_path): + from navi.config import Settings + async def _to_thread(func, *args, **kwargs): return func(*args, **kwargs) monkeypatch.setattr(sessions_mod.asyncio, "to_thread", _to_thread) - monkeypatch.setattr(sessions_mod.settings, "session_files_dir", str(tmp_path / "sessions")) - monkeypatch.setattr(session_files_mod.settings, "session_files_dir", str(tmp_path / "sessions")) - monkeypatch.setattr(sessions_mod.settings, "session_files_max_size_mb", 1) + _test_settings = Settings( + session_files_dir=str(tmp_path / "sessions"), + session_files_max_size_mb=1, + ) + monkeypatch.setattr(sessions_mod, "settings", _test_settings) + monkeypatch.setattr(session_files_mod, "settings", _test_settings) async def _session_id(store: InMemorySessionStore) -> str: diff --git a/tests/unit/core/test_context_builder.py b/tests/unit/core/test_context_builder.py index f07f80a..800491d 100644 --- a/tests/unit/core/test_context_builder.py +++ b/tests/unit/core/test_context_builder.py @@ -19,11 +19,13 @@ class TestBuildSystemPrompt: - def test_includes_persona(self): + def test_includes_persona(self, monkeypatch): import navi.config as _config + from navi.config import Settings - _config.settings.navi_persona = "You are Navi." - _config.settings.navi_persona_file = "" + monkeypatch.setattr( + _config, "settings", Settings(navi_persona="You are Navi.", navi_persona_file="") + ) builder = ContextBuilder(profile_registry=make_profile_registry()) profile = make_profile("test") prompt = builder.build_system_prompt(profile) diff --git a/tests/unit/core/test_registry.py b/tests/unit/core/test_registry.py index 908e07b..bf92eec 100644 --- a/tests/unit/core/test_registry.py +++ b/tests/unit/core/test_registry.py @@ -84,6 +84,7 @@ def test_primary_ollama_uses_expanded_timeout(self, monkeypatch): import navi.core.registry as registry_mod import navi.llm.fallback as fallback_mod + from navi.config import Settings captured = {} @@ -91,11 +92,14 @@ def __init__(self, **kwargs): captured.update(kwargs) - monkeypatch.setattr(registry_mod.settings, "ollama_backends_file", "") - monkeypatch.setattr(registry_mod.settings, "openai_api_key", "") - monkeypatch.setattr(registry_mod.settings, "ollama_request_timeout", 30) - monkeypatch.setattr(registry_mod.settings, "llm_complete_timeout", 120) - monkeypatch.setattr(registry_mod.settings, "llm_stream_first_chunk_timeout", 180) + test_settings = Settings( + ollama_backends_file="", + openai_api_key="", + ollama_request_timeout=30, + llm_complete_timeout=120, + llm_stream_first_chunk_timeout=180, + ) + monkeypatch.setattr(registry_mod, "settings", test_settings) monkeypatch.setattr(fallback_mod, "OllamaBackend", FakeOllamaBackend) discovered = registry_mod._discover_backends() diff --git a/tests/unit/llm/test_ollama.py b/tests/unit/llm/test_ollama.py index 3906f2a..a8d36e9 100644 --- a/tests/unit/llm/test_ollama.py +++ b/tests/unit/llm/test_ollama.py @@ -23,6 +23,7 @@ def test_fallback_client_uses_expanded_timeout(monkeypatch): import navi.llm.fallback as fallback_mod import navi.config as config_mod + from navi.config import Settings captured = {} @@ -31,9 +32,14 @@ captured.update(kwargs) monkeypatch.setattr(fallback_mod, "OllamaBackend", FakeOllamaBackend) - monkeypatch.setattr(config_mod.settings, "ollama_request_timeout", 30) - monkeypatch.setattr(config_mod.settings, "llm_complete_timeout", 120) - monkeypatch.setattr(config_mod.settings, "llm_stream_first_chunk_timeout", 180) + monkeypatch.setattr( + config_mod, "settings", + Settings( + ollama_request_timeout=30, + llm_complete_timeout=120, + llm_stream_first_chunk_timeout=180, + ) + ) backend = FallbackOllamaBackend([ServerEntry(host="http://ollama.test")]) backend._get_client(ServerEntry(host="http://ollama.test", api_key="secret")) @@ -45,8 +51,9 @@ def test_think_resolves_from_global_setting(monkeypatch): import navi.config as config_mod + from navi.config import Settings - monkeypatch.setattr(config_mod.settings, "ollama_think", True) + monkeypatch.setattr(config_mod, "settings", Settings(ollama_think=True)) assert _resolve_think(None) is True assert _resolve_think(False) is False @@ -54,8 +61,9 @@ def test_base_options_do_not_include_think(monkeypatch): import navi.llm.ollama as ollama_mod + from navi.config import Settings - monkeypatch.setattr(ollama_mod.settings, "ollama_num_ctx", 1234) + monkeypatch.setattr(ollama_mod, "settings", Settings(ollama_num_ctx=1234)) opts = _base_options(0.2, top_k=20, top_p=0.8) diff --git a/tests/unit/test_content_store.py b/tests/unit/test_content_store.py index 6c3075d..8b235e2 100644 --- a/tests/unit/test_content_store.py +++ b/tests/unit/test_content_store.py @@ -6,6 +6,7 @@ import pytest import navi.content_store as content_store +import navi.session_files as session_files_mod from tests.conftest_factory import FakeConnection, FakePool @@ -49,8 +50,13 @@ class TestPublish: @pytest.fixture(autouse=True) def _session_dir(self, monkeypatch, tmp_path): - monkeypatch.setattr(content_store.settings, "session_files_dir", str(tmp_path / "sessions")) - monkeypatch.setattr(content_store.settings, "public_url", "http://localhost:8000") + from navi.config import Settings + + _test_settings = Settings( + session_files_dir=str(tmp_path / "sessions"), public_url="http://localhost:8000" + ) + monkeypatch.setattr(content_store, "settings", _test_settings) + monkeypatch.setattr(session_files_mod, "settings", _test_settings) async def test_upserts_by_session_and_filename(self, monkeypatch, tmp_path): conn = FakeConnection() @@ -156,8 +162,13 @@ class TestListForSession: @pytest.fixture(autouse=True) def _settings(self, monkeypatch, tmp_path): - monkeypatch.setattr(content_store.settings, "public_url", "http://localhost:8000") - monkeypatch.setattr(content_store.settings, "session_files_dir", str(tmp_path / "sessions")) + from navi.config import Settings + + _test_settings = Settings( + public_url="http://localhost:8000", session_files_dir=str(tmp_path / "sessions") + ) + monkeypatch.setattr(content_store, "settings", _test_settings) + monkeypatch.setattr(session_files_mod, "settings", _test_settings) async def test_returns_urls_for_published_content(self, monkeypatch, tmp_path): conn = FakeConnection() diff --git a/tests/unit/tools/test_content_publish.py b/tests/unit/tools/test_content_publish.py index be742ad..4f42557 100644 --- a/tests/unit/tools/test_content_publish.py +++ b/tests/unit/tools/test_content_publish.py @@ -2,6 +2,8 @@ import pytest +import navi.content_store as content_store_mod +import navi.session_files as session_files_mod import navi.tools.content_publish as content_publish_mod from navi.tools._internal.base import current_session_id from navi.tools.content_publish import ContentPublishTool @@ -10,7 +12,12 @@ class TestContentPublishTool: @pytest.fixture def tool(self, monkeypatch, tmp_path): - monkeypatch.setattr(content_publish_mod.settings, "session_files_dir", str(tmp_path / "sessions")) + from navi.config import Settings + + _test_settings = Settings(session_files_dir=str(tmp_path / "sessions")) + monkeypatch.setattr(content_publish_mod, "settings", _test_settings) + monkeypatch.setattr(session_files_mod, "settings", _test_settings) + monkeypatch.setattr(content_store_mod, "settings", _test_settings) token = current_session_id.set("sess-1") try: yield ContentPublishTool() diff --git a/tests/unit/tools/test_filesystem.py b/tests/unit/tools/test_filesystem.py index b8e8485..0d558a0 100644 --- a/tests/unit/tools/test_filesystem.py +++ b/tests/unit/tools/test_filesystem.py @@ -10,7 +10,9 @@ @pytest.fixture(autouse=True) def _allow_all(self, monkeypatch): import navi.tools.filesystem as _fs_mod - monkeypatch.setattr(_fs_mod.settings, "fs_allowed_paths", "*") + from navi.config import Settings + + monkeypatch.setattr(_fs_mod, "settings", Settings(fs_allowed_paths="*")) def test_resolves_relative(self): p = _check_path(".") @@ -26,11 +28,13 @@ def test_restricted_paths(self, monkeypatch, tmp_path): import navi.tools.filesystem as _fs_mod + from navi.config import Settings + allowed = tmp_path / "allowed" allowed.mkdir() blocked = tmp_path / "blocked" blocked.mkdir() - monkeypatch.setattr(_fs_mod.settings, "fs_allowed_paths", str(allowed)) + monkeypatch.setattr(_fs_mod, "settings", Settings(fs_allowed_paths=str(allowed))) assert _check_path(str(allowed / "file.txt")) is not None assert _check_path(str(blocked / "file.txt")) is None @@ -39,10 +43,12 @@ @pytest.fixture(autouse=True) def _allow_all(self, monkeypatch): import navi.tools.filesystem as _fs_mod + from navi.config import Settings + async def _to_thread(func, *args, **kwargs): return func(*args, **kwargs) - monkeypatch.setattr(_fs_mod.settings, "fs_allowed_paths", "*") + monkeypatch.setattr(_fs_mod, "settings", Settings(fs_allowed_paths="*")) monkeypatch.setattr(_fs_mod.asyncio, "to_thread", _to_thread) @pytest.fixture diff --git a/tests/unit/tools/test_share_file.py b/tests/unit/tools/test_share_file.py index 7d39c2e..bac9422 100644 --- a/tests/unit/tools/test_share_file.py +++ b/tests/unit/tools/test_share_file.py @@ -4,7 +4,9 @@ import pytest +import navi.session_files as session_files_mod import navi.tools.share_file as share_file_mod +from navi.config import Settings from navi.tools._internal.base import current_session_id from navi.tools.share_file import ShareFileTool @@ -16,9 +18,13 @@ return func(*args, **kwargs) monkeypatch.setattr(share_file_mod.asyncio, "to_thread", _to_thread) - monkeypatch.setattr(share_file_mod.settings, "session_files_dir", str(tmp_path / "sessions")) - monkeypatch.setattr(share_file_mod.settings, "share_file_max_size_mb", 1024) - monkeypatch.setattr(share_file_mod.settings, "public_url", "http://localhost:8000") + _test_settings = Settings( + session_files_dir=str(tmp_path / "sessions"), + share_file_max_size_mb=1024, + public_url="http://localhost:8000", + ) + monkeypatch.setattr(share_file_mod, "settings", _test_settings) + monkeypatch.setattr(session_files_mod, "settings", _test_settings) token = current_session_id.set("sess 1") try: yield ShareFileTool() @@ -43,7 +49,13 @@ assert result.metadata["filename"] == "clean report.txt" async def test_rejects_files_over_share_limit(self, tool, monkeypatch, tmp_path): - monkeypatch.setattr(share_file_mod.settings, "share_file_max_size_mb", 0) + _test_settings = Settings( + session_files_dir=str(tmp_path / "sessions"), + share_file_max_size_mb=0, + public_url="http://localhost:8000", + ) + monkeypatch.setattr(share_file_mod, "settings", _test_settings) + monkeypatch.setattr(session_files_mod, "settings", _test_settings) src = tmp_path / "too_large.bin" src.write_bytes(b"x")