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
187 lines
6.9 KiB
Python
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
|