From 136837f654b58ff9444a7a90c8f384203aba3461 Mon Sep 17 00:00:00 2001 From: ErSpell <114104796+erSpell@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:00:58 -0400 Subject: [PATCH] fix: validate startup env choices --- tests/test_config.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ vaultrag/cli.py | 14 ++++++++----- vaultrag/config.py | 46 +++++++++++++++++++++++++++++++++-------- 3 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 tests/test_config.py diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..72d197b --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,49 @@ +from io import StringIO + +import pytest +from rich.console import Console + +from vaultrag import cli +from vaultrag.config import StartupConfigError, get_settings + + +def test_invalid_embedder_raises_structured_startup_error(monkeypatch): + monkeypatch.setenv("EMBEDDER", "typo") + + with pytest.raises(StartupConfigError) as excinfo: + get_settings() + + error = excinfo.value + assert error.variable == "EMBEDDER" + assert error.value == "typo" + assert error.valid_values == ("local", "fake") + assert "Invalid EMBEDDER='typo'" in str(error) + assert "valid values: local, fake" in str(error) + + +def test_invalid_llm_raises_structured_startup_error(monkeypatch): + monkeypatch.setenv("LLM", "typo") + + with pytest.raises(StartupConfigError) as excinfo: + get_settings() + + error = excinfo.value + assert error.variable == "LLM" + assert error.value == "typo" + assert error.valid_values == ("groq", "fake") + assert "Invalid LLM='typo'" in str(error) + assert "valid values: groq, fake" in str(error) + + +def test_cli_reports_invalid_embedder_without_traceback(monkeypatch): + output = StringIO() + monkeypatch.setenv("EMBEDDER", "typo") + monkeypatch.setattr(cli, "console", Console(file=output, width=120, color_system=None)) + + assert cli.main(["health"]) == 2 + + rendered = output.getvalue() + assert "configuration error" in rendered + assert "Invalid EMBEDDER='typo'" in rendered + assert "valid values: local, fake" in rendered + assert "Traceback" not in rendered diff --git a/vaultrag/cli.py b/vaultrag/cli.py index 6ccb1d0..90597db 100644 --- a/vaultrag/cli.py +++ b/vaultrag/cli.py @@ -19,7 +19,7 @@ from rich.markup import escape from rich.table import Table -from .config import get_settings +from .config import StartupConfigError, get_settings from .conflicts import corpus_freshness, detect_conflicts, detect_stale from .db import init_schema from .embeddings import get_embedder @@ -153,7 +153,7 @@ async def _health(args) -> int: def _diff(args) -> int: - from .evaluate import EvalReport, CaseResult + from .evaluate import CaseResult, EvalReport def load(p): d = json.loads(Path(p).read_text()) @@ -203,9 +203,13 @@ def main(argv: list[str] | None = None) -> int: d.set_defaults(func=_diff) args = p.parse_args(argv) - if hasattr(args, "afunc"): - return asyncio.run(args.afunc(args)) - return args.func(args) + try: + if hasattr(args, "afunc"): + return asyncio.run(args.afunc(args)) + return args.func(args) + except StartupConfigError as exc: + console.print(f"[red]configuration error[/] {escape(str(exc))}") + return 2 if __name__ == "__main__": diff --git a/vaultrag/config.py b/vaultrag/config.py index 0d70cd8..fcf5c6d 100644 --- a/vaultrag/config.py +++ b/vaultrag/config.py @@ -5,18 +5,46 @@ import os from dataclasses import dataclass +VALID_EMBEDDERS = ("local", "fake") +VALID_LLMS = ("groq", "fake") + + +class StartupConfigError(ValueError): + """Raised when startup configuration is invalid.""" + + def __init__(self, variable: str, value: str, valid_values: tuple[str, ...]) -> None: + self.variable = variable + self.value = value + self.valid_values = valid_values + super().__init__( + f"Invalid {variable}={value!r}; valid values: {', '.join(valid_values)}" + ) + @dataclass(frozen=True) class Settings: - database_url: str = os.getenv( - "DATABASE_URL", "postgresql://vaultrag:vaultrag@localhost:5433/vaultrag" - ) - embedder: str = os.getenv("EMBEDDER", "local") - embed_model: str = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2") - llm: str = os.getenv("LLM", "groq") - groq_api_key: str | None = os.getenv("GROQ_API_KEY") - groq_model: str = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile") + database_url: str + embedder: str + embed_model: str + llm: str + groq_api_key: str | None + groq_model: str + + +def _validated_choice(variable: str, value: str, valid_values: tuple[str, ...]) -> str: + if value in valid_values: + return value + raise StartupConfigError(variable, value, valid_values) def get_settings() -> Settings: - return Settings() + return Settings( + database_url=os.getenv( + "DATABASE_URL", "postgresql://vaultrag:***@localhost:5433/vaultrag" + ), + embedder=_validated_choice("EMBEDDER", os.getenv("EMBEDDER", "local"), VALID_EMBEDDERS), + embed_model=os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2"), + llm=_validated_choice("LLM", os.getenv("LLM", "groq"), VALID_LLMS), + groq_api_key=os.getenv("GROQ_API_KEY"), + groq_model=os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile"), + )