113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""Unit tests: settings defaults & env overrides."""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
from pydantic_settings import SettingsError
|
||
|
||
from app.config import Settings
|
||
|
||
|
||
def _settings(**kwargs: Any) -> Settings:
|
||
"""Build Settings without reading a .env file (deterministic tests)."""
|
||
kwargs.setdefault("_env_file", None)
|
||
return Settings(**kwargs) # pyright: ignore[reportCallIssue] (kwarg exists at runtime)
|
||
|
||
|
||
def test_defaults_match_locked_decisions(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
# The test process sets BOR_RELEVANCE_THRESHOLD=0.30 for the mock-
|
||
# calibrated in-process suites (see tests/conftest.py) — the *default*
|
||
# under test is the production one.
|
||
monkeypatch.delenv("BOR_RELEVANCE_THRESHOLD", raising=False)
|
||
s = _settings()
|
||
assert s.llm_chat_model == "turbo"
|
||
assert s.llm_embed_model == "embed"
|
||
assert s.embedding_dim == 768
|
||
assert s.llm_base_url.endswith("/v1")
|
||
# A8 (revised): the honesty gate input is the best cosine, default 0.62.
|
||
assert s.relevance_threshold == 0.62
|
||
# A7 (revised): hybrid retrieval — cosine top-N ∪ FTS top-N, RRF-fused.
|
||
assert s.hybrid_vector_candidates >= 1
|
||
assert s.hybrid_lexical_candidates >= 1
|
||
assert s.rrf_k >= 1
|
||
assert s.top_n_docs >= 1
|
||
# Owner instruction 2026-08-22: answers may run up to 32 768 tokens.
|
||
assert s.max_output_tokens == 32_768
|
||
assert len(s.suggestions) >= 3
|
||
# A9 (revised): the import scope covers the seven A9 formats.
|
||
assert s.import_extension_set == {
|
||
".md", ".markdown", ".txt", ".yaml", ".yml", ".json", ".py"
|
||
}
|
||
|
||
|
||
def test_env_override(monkeypatch) -> None:
|
||
monkeypatch.setenv("BOR_RELEVANCE_THRESHOLD", "0.42")
|
||
monkeypatch.setenv("BOR_LLM_CHAT_MODEL", "juggernaut")
|
||
s = _settings()
|
||
assert s.relevance_threshold == 0.42
|
||
assert s.llm_chat_model == "juggernaut"
|
||
|
||
|
||
def test_max_output_tokens_env_override(monkeypatch) -> None:
|
||
monkeypatch.setenv("BOR_MAX_OUTPUT_TOKENS", "1234")
|
||
s = _settings()
|
||
assert s.max_output_tokens == 1234
|
||
|
||
|
||
def test_import_extensions_env_override_is_a_csv_list(monkeypatch) -> None:
|
||
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,yml")
|
||
s = _settings()
|
||
assert s.import_extension_set == {".md", ".yml"}
|
||
|
||
|
||
def test_import_extensions_rejects_unknown_format(monkeypatch) -> None:
|
||
"""A typo in the CSV fails at startup (loudly), not by silently
|
||
walking zero files."""
|
||
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,docx")
|
||
with pytest.raises(ValidationError, match="docx"):
|
||
_settings()
|
||
|
||
|
||
def test_import_extensions_rejects_empty(monkeypatch) -> None:
|
||
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", " ")
|
||
with pytest.raises(ValidationError):
|
||
_settings()
|
||
|
||
|
||
def test_suggestions_default_is_three_plus_real_questions() -> None:
|
||
s = _settings()
|
||
assert len(s.suggestions) >= 3
|
||
assert all(isinstance(q, str) and q.strip() for q in s.suggestions)
|
||
# Distinct chips only — duplicates in the onboarding row are noise.
|
||
assert len({q.strip().lower() for q in s.suggestions}) == len(s.suggestions)
|
||
|
||
|
||
def test_suggestions_env_override_is_json_list(monkeypatch) -> None:
|
||
override = [
|
||
"How do I back up with Borg?",
|
||
"How is my K3S cluster set up?",
|
||
"How do I deploy a service?",
|
||
]
|
||
monkeypatch.setenv("BOR_SUGGESTIONS", json.dumps(override))
|
||
s = _settings()
|
||
assert s.suggestions == override
|
||
|
||
|
||
def test_suggestions_malformed_json_fails_loudly(monkeypatch) -> None:
|
||
monkeypatch.setenv("BOR_SUGGESTIONS", "[not valid json")
|
||
with pytest.raises(SettingsError):
|
||
_settings()
|
||
|
||
|
||
def test_effective_api_key_fallback(monkeypatch) -> None:
|
||
monkeypatch.delenv("AIPI_KEY", raising=False)
|
||
s = _settings()
|
||
assert s.effective_api_key == "not-needed"
|
||
|
||
monkeypatch.setenv("AIPI_KEY", "sk-from-env")
|
||
s2 = _settings()
|
||
assert s2.effective_api_key == "sk-from-env"
|