Files
EverOS/tests/unit/test_config/test_settings.py
tomtan 0910affc78
Some checks failed
CI / lint (push) Has been cancelled
CI / unit tests (push) Has been cancelled
CI / integration tests (push) Has been cancelled
CI / package build (push) Has been cancelled
Commit lint / pull request title (push) Has been cancelled
Commit lint / commit messages (push) Has been cancelled
Save local modifications for syncing
2026-06-10 10:05:52 +08:00

187 lines
6.9 KiB
Python

"""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
assert s.llm.timeout_seconds == 180.0
assert s.multimodal.timeout_seconds == 180.0
assert s.multimodal.resize_images_for_vlm is True
# 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_llm_timeout_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("EVEROS_LLM__TIMEOUT_SECONDS", "240")
monkeypatch.setenv("EVEROS_MULTIMODAL__TIMEOUT_SECONDS", "300")
monkeypatch.setenv("EVEROS_MULTIMODAL__RESIZE_IMAGES_FOR_VLM", "false")
s = Settings()
assert s.llm.timeout_seconds == 240.0
assert s.multimodal.timeout_seconds == 300.0
assert s.multimodal.resize_images_for_vlm is False
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