From f342808d2b4af551a17e47af744d2f19dee3c443 Mon Sep 17 00:00:00 2001 From: Rome Thorstenson <36779795+Rome-1@users.noreply.github.com> Date: Tue, 30 Jun 2026 04:31:29 -0700 Subject: [PATCH] test: add unit tests for config loader (strix/config/loader.py) (#596) Co-authored-by: Claude Opus 4.8 --- tests/test_config_loader.py | 178 ++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tests/test_config_loader.py diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py new file mode 100644 index 0000000..247c41f --- /dev/null +++ b/tests/test_config_loader.py @@ -0,0 +1,178 @@ +"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import pytest +from pydantic import AliasChoices, Field +from pydantic.fields import FieldInfo + +from strix.config import loader + + +if TYPE_CHECKING: + from pathlib import Path + + +_LLM_ENV_KEYS = [ + "STRIX_LLM", + "LLM_API_KEY", + "OPENAI_API_KEY", + "LLM_API_BASE", + "OPENAI_API_BASE", + "OPENAI_BASE_URL", + "LITELLM_BASE_URL", + "OLLAMA_API_BASE", + "STRIX_REASONING_EFFORT", + "LLM_TIMEOUT", + "PERPLEXITY_API_KEY", + # RuntimeSettings + "STRIX_IMAGE", + "STRIX_RUNTIME_BACKEND", + "STRIX_MAX_LOCAL_COPY_MB", + # TelemetrySettings + "STRIX_TELEMETRY", +] + + +@pytest.fixture(autouse=True) +def _reset_loader_state(monkeypatch: pytest.MonkeyPatch) -> None: + """Reset module globals and clear known env vars for deterministic runs.""" + for key in _LLM_ENV_KEYS: + monkeypatch.delenv(key, raising=False) + monkeypatch.setattr(loader, "_cached", None) + monkeypatch.setattr(loader, "_override", None) + + +# --------------------------------------------------------------------------- # +# _read_json_overrides +# --------------------------------------------------------------------------- # + + +def test_read_json_overrides_missing_file(tmp_path: Path) -> None: + assert loader._read_json_overrides(tmp_path / "nope.json") == {} + + +def test_read_json_overrides_corrupt_json(tmp_path: Path) -> None: + path = tmp_path / "cli-config.json" + path.write_text("{not valid json", encoding="utf-8") + assert loader._read_json_overrides(path) == {} + + +def test_read_json_overrides_non_dict_env(tmp_path: Path) -> None: + path = tmp_path / "cli-config.json" + path.write_text(json.dumps({"env": ["not", "a", "dict"]}), encoding="utf-8") + assert loader._read_json_overrides(path) == {} + + +def test_read_json_overrides_maps_to_nested_settings(tmp_path: Path) -> None: + path = tmp_path / "cli-config.json" + path.write_text( + json.dumps({"env": {"STRIX_LLM": "my-model", "PERPLEXITY_API_KEY": "pk"}}), + encoding="utf-8", + ) + assert loader._read_json_overrides(path) == { + "llm": {"model": "my-model"}, + "integrations": {"perplexity_api_key": "pk"}, + } + + +def test_read_json_overrides_skips_keys_already_in_environ( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("STRIX_LLM", "from-env") + path = tmp_path / "cli-config.json" + path.write_text(json.dumps({"env": {"STRIX_LLM": "from-file"}}), encoding="utf-8") + # env wins -> the JSON value is not surfaced as an init kwarg. + assert loader._read_json_overrides(path) == {} + + +# --------------------------------------------------------------------------- # +# _aliases_for +# --------------------------------------------------------------------------- # + + +def test_aliases_for_simple_alias() -> None: + finfo = FieldInfo(alias="SIMPLE_ALIAS") + assert loader._aliases_for(finfo) == ["SIMPLE_ALIAS"] + + +def test_aliases_for_alias_choices() -> None: + finfo: FieldInfo = Field( # type: ignore[assignment] + default=None, + validation_alias=AliasChoices("FIRST", "SECOND"), + ) + assert loader._aliases_for(finfo) == ["FIRST", "SECOND"] + + +def test_aliases_for_string_validation_alias() -> None: + finfo: FieldInfo = Field(default=None, validation_alias="STR_ALIAS") # type: ignore[assignment] + assert loader._aliases_for(finfo) == ["STR_ALIAS"] + + +def test_aliases_for_no_alias() -> None: + assert loader._aliases_for(FieldInfo()) == [] + + +# --------------------------------------------------------------------------- # +# apply_config_override + load_settings round-trip +# --------------------------------------------------------------------------- # + + +def test_apply_override_and_load_settings_round_trip(tmp_path: Path) -> None: + path = tmp_path / "cli-config.json" + path.write_text( + json.dumps({"env": {"STRIX_LLM": "round-trip-model", "PERPLEXITY_API_KEY": "pk"}}), + encoding="utf-8", + ) + + loader.apply_config_override(path) + settings = loader.load_settings() + + assert settings.llm.model == "round-trip-model" + assert settings.integrations.perplexity_api_key == "pk" + # Second call is memoized -> same object. + assert loader.load_settings() is settings + + +def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None: + first = tmp_path / "first.json" + first.write_text(json.dumps({"env": {"STRIX_LLM": "first-model"}}), encoding="utf-8") + second = tmp_path / "second.json" + second.write_text(json.dumps({"env": {"STRIX_LLM": "second-model"}}), encoding="utf-8") + + loader.apply_config_override(first) + assert loader.load_settings().llm.model == "first-model" + + loader.apply_config_override(second) + assert loader.load_settings().llm.model == "second-model" + + +# --------------------------------------------------------------------------- # +# persist_current +# --------------------------------------------------------------------------- # + + +def test_persist_current_writes_env_block(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("STRIX_LLM", "persisted-model") + target = tmp_path / "sub" / "cli-config.json" + loader.apply_config_override(target) + + loader.persist_current() + + assert target.exists() + assert json.loads(target.read_text(encoding="utf-8")) == { + "env": {"STRIX_LLM": "persisted-model"} + } + + +def test_persist_current_sets_0600_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("STRIX_LLM", "persisted-model") + target = tmp_path / "cli-config.json" + loader.apply_config_override(target) + + loader.persist_current() + + assert target.stat().st_mode & 0o777 == 0o600