diff --git a/codex-lens/src/codexlens/config.py b/codex-lens/src/codexlens/config.py index e49bff08..7e49b70d 100644 --- a/codex-lens/src/codexlens/config.py +++ b/codex-lens/src/codexlens/config.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import logging import os from dataclasses import dataclass, field from functools import cached_property @@ -18,6 +19,8 @@ WORKSPACE_DIR_NAME = ".codexlens" # Settings file name SETTINGS_FILE_NAME = "settings.json" +log = logging.getLogger(__name__) + def _default_global_dir() -> Path: """Get global CodexLens data directory.""" @@ -200,7 +203,15 @@ class Config: # Load embedding settings embedding = settings.get("embedding", {}) if "backend" in embedding: - self.embedding_backend = embedding["backend"] + backend = embedding["backend"] + if backend in {"fastembed", "litellm"}: + self.embedding_backend = backend + else: + log.warning( + "Invalid embedding backend in %s: %r (expected 'fastembed' or 'litellm')", + self.settings_path, + backend, + ) if "model" in embedding: self.embedding_model = embedding["model"] if "use_gpu" in embedding: @@ -224,8 +235,13 @@ class Config: self.llm_timeout_ms = llm["timeout_ms"] if "batch_size" in llm: self.llm_batch_size = llm["batch_size"] - except Exception: - pass # Silently ignore errors + except Exception as exc: + log.warning( + "Failed to load settings from %s (%s): %s", + self.settings_path, + type(exc).__name__, + exc, + ) @classmethod def load(cls) -> "Config": diff --git a/codex-lens/tests/test_config.py b/codex-lens/tests/test_config.py index 1562ac28..75e42b28 100644 --- a/codex-lens/tests/test_config.py +++ b/codex-lens/tests/test_config.py @@ -1,5 +1,8 @@ """Tests for CodexLens configuration system.""" +import builtins +import json +import logging import os import tempfile from pathlib import Path @@ -224,6 +227,99 @@ class TestConfig: del os.environ["CODEXLENS_DATA_DIR"] +class TestConfigLoadSettings: + """Tests for Config.load_settings behavior and logging.""" + + def test_load_settings_logs_warning_on_malformed_json(self, caplog): + """Malformed JSON in settings file should trigger warning log.""" + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + config.settings_path.write_text("{", encoding="utf-8") + + with caplog.at_level(logging.WARNING): + config.load_settings() + + records = [r for r in caplog.records if r.name == "codexlens.config"] + assert any("Failed to load settings from" in r.message for r in records) + assert any("JSONDecodeError" in r.message for r in records) + assert any(str(config.settings_path) in r.message for r in records) + + def test_load_settings_logs_warning_on_permission_error(self, monkeypatch, caplog): + """Permission errors opening settings file should trigger warning log.""" + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + config.settings_path.write_text("{}", encoding="utf-8") + + real_open = builtins.open + + def guarded_open(path, mode="r", *args, **kwargs): + if Path(path) == config.settings_path and "r" in mode: + raise PermissionError("Permission denied") + return real_open(path, mode, *args, **kwargs) + + monkeypatch.setattr(builtins, "open", guarded_open) + + with caplog.at_level(logging.WARNING): + config.load_settings() + + records = [r for r in caplog.records if r.name == "codexlens.config"] + assert any("Failed to load settings from" in r.message for r in records) + assert any("PermissionError" in r.message for r in records) + + def test_load_settings_loads_valid_settings_without_warning(self, caplog): + """Valid settings should load without warning logs.""" + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + config.settings_path.write_text( + json.dumps( + { + "embedding": { + "backend": "fastembed", + "model": "multilingual", + "use_gpu": False, + }, + "llm": { + "enabled": True, + "tool": "gemini", + "timeout_ms": 1234, + "batch_size": 7, + }, + } + ), + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + config.load_settings() + + records = [r for r in caplog.records if r.name == "codexlens.config"] + assert not records + assert config.embedding_backend == "fastembed" + assert config.embedding_model == "multilingual" + assert config.embedding_use_gpu is False + assert config.llm_enabled is True + assert config.llm_tool == "gemini" + assert config.llm_timeout_ms == 1234 + assert config.llm_batch_size == 7 + + def test_load_settings_logs_warning_on_invalid_embedding_backend(self, caplog): + """Invalid embedding backend should trigger warning log and keep default.""" + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + default_backend = config.embedding_backend + config.settings_path.write_text( + json.dumps({"embedding": {"backend": "invalid-backend"}}), + encoding="utf-8", + ) + + with caplog.at_level(logging.WARNING): + config.load_settings() + + records = [r for r in caplog.records if r.name == "codexlens.config"] + assert any("Invalid embedding backend in" in r.message for r in records) + assert config.embedding_backend == default_backend + + class TestWorkspaceConfig: """Tests for WorkspaceConfig class."""