"""Unit tests for Settings loading.""" from __future__ import annotations from pathlib import Path import pytest from everos.config import Settings, load_settings @pytest.fixture(autouse=True) def _isolate_env(monkeypatch: pytest.MonkeyPatch) -> None: """Strip any EVEROS_* env vars from the host so tests are deterministic.""" for key in list(__import__("os").environ): if key.startswith("EVEROS_"): monkeypatch.delenv(key, raising=False) load_settings.cache_clear() def test_load_settings_defaults_from_toml() -> None: s = load_settings() # Values straight out of config/default.toml assert s.memory.root == Path("~/.everos") assert s.memory.timezone == "UTC" assert s.sqlite.journal_mode == "WAL" assert s.sqlite.synchronous == "NORMAL" assert s.sqlite.foreign_keys is True assert s.sqlite.temp_store == "MEMORY" assert s.sqlite.busy_timeout_ms == 5000 assert s.sqlite.journal_size_limit_bytes == 64 * 1024 * 1024 assert s.sqlite.cache_size_kb == 2048 assert s.lancedb.read_consistency_seconds is None def test_env_overrides_toml(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EVEROS_SQLITE__BUSY_TIMEOUT_MS", "10000") monkeypatch.setenv("EVEROS_SQLITE__JOURNAL_MODE", "DELETE") s = Settings() assert s.sqlite.busy_timeout_ms == 10000 assert s.sqlite.journal_mode == "DELETE" # Untouched values stay at TOML defaults. assert s.sqlite.synchronous == "NORMAL" def test_init_args_override_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EVEROS_SQLITE__BUSY_TIMEOUT_MS", "10000") from everos.config.settings import SqliteSettings s = Settings(sqlite=SqliteSettings(busy_timeout_ms=99999)) assert s.sqlite.busy_timeout_ms == 99999 # init beats env def test_invalid_journal_mode_rejected() -> None: from pydantic import ValidationError with pytest.raises(ValidationError): Settings.model_validate({"sqlite": {"journal_mode": "BOGUS"}}) def test_negative_busy_timeout_rejected() -> None: from pydantic import ValidationError with pytest.raises(ValidationError): Settings.model_validate({"sqlite": {"busy_timeout_ms": -1}}) def test_lancedb_read_consistency_optional_float() -> None: s = Settings.model_validate({"lancedb": {"read_consistency_seconds": 5.0}}) assert s.lancedb.read_consistency_seconds == 5.0 s2 = Settings.model_validate({"lancedb": {"read_consistency_seconds": None}}) assert s2.lancedb.read_consistency_seconds is None def test_memory_timezone_overridable_via_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EVEROS_MEMORY__TIMEZONE", "Asia/Shanghai") s = Settings() assert s.memory.timezone == "Asia/Shanghai" def test_memory_timezone_invalid_rejected() -> None: from pydantic import ValidationError with pytest.raises(ValidationError, match="invalid timezone"): Settings.model_validate({"memory": {"timezone": "Not/A/Real_Zone"}}) def test_load_settings_is_cached() -> None: """Repeated calls return the same Settings object until cache_clear.""" a = load_settings() b = load_settings() assert a is b load_settings.cache_clear() c = load_settings() assert c is not a def test_embedding_rerank_defaults() -> None: """Embedding / rerank ship with runtime knobs but no model credentials.""" # ``_isolate_env`` already strips shell env; ``_env_file=None`` further # prevents a developer's ``.env`` (which typically sets MODEL / API_KEY / # BASE_URL for live runs) from leaking into this default-state check. s = Settings(_env_file=None) # type: ignore[call-arg] # Credentials must be set explicitly (no default). assert s.embedding.model is None assert s.embedding.api_key is None assert s.embedding.base_url is None # Runtime knobs come from default.toml. assert s.embedding.timeout_seconds == 30.0 assert s.embedding.max_retries == 3 assert s.embedding.batch_size == 10 assert s.embedding.max_concurrent == 5 # Rerank mirrors the shape. assert s.rerank.model is None assert s.rerank.timeout_seconds == 30.0 assert s.rerank.batch_size == 10 def test_embedding_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EVEROS_EMBEDDING__MODEL", "intfloat/e5-large-v2") monkeypatch.setenv("EVEROS_EMBEDDING__BASE_URL", "http://localhost:8000/v1") monkeypatch.setenv("EVEROS_EMBEDDING__BATCH_SIZE", "32") s = Settings() assert s.embedding.model == "intfloat/e5-large-v2" assert s.embedding.base_url == "http://localhost:8000/v1" assert s.embedding.batch_size == 32 def test_rerank_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EVEROS_RERANK__MODEL", "BAAI/bge-reranker-v2-m3") monkeypatch.setenv("EVEROS_RERANK__MAX_CONCURRENT", "8") s = Settings() assert s.rerank.model == "BAAI/bge-reranker-v2-m3" assert s.rerank.max_concurrent == 8 def test_user_toml_override_via_env_path( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """``EVEROS_CONFIG_FILE`` points pydantic-settings at a user toml.""" user_toml = tmp_path / "config.toml" user_toml.write_text( '[sqlite]\nbusy_timeout_ms = 7777\n[memory]\ntimezone = "Asia/Tokyo"\n', encoding="utf-8", ) monkeypatch.setenv("EVEROS_CONFIG_FILE", str(user_toml)) s = Settings() assert s.sqlite.busy_timeout_ms == 7777 assert s.memory.timezone == "Asia/Tokyo" # Values not touched by the user toml still come from the shipped default. assert s.sqlite.journal_mode == "WAL" def test_user_toml_loses_to_env( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """env vars beat the user-level toml.""" user_toml = tmp_path / "config.toml" user_toml.write_text("[sqlite]\nbusy_timeout_ms = 7777\n", encoding="utf-8") monkeypatch.setenv("EVEROS_CONFIG_FILE", str(user_toml)) monkeypatch.setenv("EVEROS_SQLITE__BUSY_TIMEOUT_MS", "9999") s = Settings() assert s.sqlite.busy_timeout_ms == 9999 def test_user_toml_missing_file_is_skipped( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """A non-existent user toml path is silently skipped, not an error.""" monkeypatch.setenv("EVEROS_CONFIG_FILE", str(tmp_path / "nope.toml")) s = Settings() # Falls back to shipped defaults. assert s.sqlite.busy_timeout_ms == 5000