chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
This commit is contained in:
173
tests/unit/test_config/test_settings.py
Normal file
173
tests/unit/test_config/test_settings.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user