diff --git a/codex-lens/src/codexlens/config.py b/codex-lens/src/codexlens/config.py index 7e49b70d..7436b18d 100644 --- a/codex-lens/src/codexlens/config.py +++ b/codex-lens/src/codexlens/config.py @@ -122,8 +122,21 @@ class Config: self.data_dir = self.data_dir.expanduser().resolve() self.venv_path = self.venv_path.expanduser().resolve() self.data_dir.mkdir(parents=True, exist_ok=True) + except PermissionError as exc: + raise ConfigError( + f"Permission denied initializing paths (data_dir={self.data_dir}, venv_path={self.venv_path}) " + f"[{type(exc).__name__}]: {exc}" + ) from exc + except OSError as exc: + raise ConfigError( + f"Filesystem error initializing paths (data_dir={self.data_dir}, venv_path={self.venv_path}) " + f"[{type(exc).__name__}]: {exc}" + ) from exc except Exception as exc: - raise ConfigError(f"Failed to initialize data_dir at {self.data_dir}: {exc}") from exc + raise ConfigError( + f"Unexpected error initializing paths (data_dir={self.data_dir}, venv_path={self.venv_path}) " + f"[{type(exc).__name__}]: {exc}" + ) from exc @cached_property def cache_dir(self) -> Path: @@ -145,8 +158,18 @@ class Config: for directory in (self.cache_dir, self.index_dir): try: directory.mkdir(parents=True, exist_ok=True) + except PermissionError as exc: + raise ConfigError( + f"Permission denied creating directory {directory} [{type(exc).__name__}]: {exc}" + ) from exc + except OSError as exc: + raise ConfigError( + f"Filesystem error creating directory {directory} [{type(exc).__name__}]: {exc}" + ) from exc except Exception as exc: - raise ConfigError(f"Failed to create directory {directory}: {exc}") from exc + raise ConfigError( + f"Unexpected error creating directory {directory} [{type(exc).__name__}]: {exc}" + ) from exc def language_for_path(self, path: str | Path) -> str | None: """Infer a supported language ID from a file path.""" diff --git a/codex-lens/tests/test_config.py b/codex-lens/tests/test_config.py index 75e42b28..96b7e7b5 100644 --- a/codex-lens/tests/test_config.py +++ b/codex-lens/tests/test_config.py @@ -118,6 +118,57 @@ class TestConfig: config = Config(data_dir=data_dir) assert data_dir.exists() + def test_post_init_permission_error_includes_path_and_cause(self, monkeypatch): + """PermissionError during __post_init__ should raise ConfigError with context.""" + with tempfile.TemporaryDirectory() as tmpdir: + data_dir = Path(tmpdir) / "blocked" + venv_path = Path(tmpdir) / "venv" + expected_data_dir = data_dir.expanduser().resolve() + + real_mkdir = Path.mkdir + + def guarded_mkdir(self, *args, **kwargs): + if self == expected_data_dir: + raise PermissionError("Permission denied") + return real_mkdir(self, *args, **kwargs) + + monkeypatch.setattr(Path, "mkdir", guarded_mkdir) + + with pytest.raises(ConfigError) as excinfo: + Config(data_dir=data_dir, venv_path=venv_path) + + message = str(excinfo.value) + assert str(expected_data_dir) in message + assert "permission" in message.lower() + assert "PermissionError" in message + assert isinstance(excinfo.value.__cause__, PermissionError) + + def test_post_init_os_error_includes_path_and_cause(self, monkeypatch): + """OSError during __post_init__ should raise ConfigError with context.""" + with tempfile.TemporaryDirectory() as tmpdir: + data_dir = Path(tmpdir) / "invalid" + venv_path = Path(tmpdir) / "venv" + expected_data_dir = data_dir.expanduser().resolve() + + real_mkdir = Path.mkdir + + def guarded_mkdir(self, *args, **kwargs): + if self == expected_data_dir: + raise OSError("Invalid path") + return real_mkdir(self, *args, **kwargs) + + monkeypatch.setattr(Path, "mkdir", guarded_mkdir) + + with pytest.raises(ConfigError) as excinfo: + Config(data_dir=data_dir, venv_path=venv_path) + + message = str(excinfo.value) + assert str(expected_data_dir) in message + assert "permission" not in message.lower() + assert "filesystem" in message.lower() + assert "OSError" in message + assert isinstance(excinfo.value.__cause__, OSError) + def test_supported_languages(self): """Test default supported languages.""" with tempfile.TemporaryDirectory() as tmpdir: @@ -158,6 +209,55 @@ class TestConfig: assert config.cache_dir.exists() assert config.index_dir.exists() + def test_ensure_runtime_dirs_permission_error_includes_path_and_cause(self, monkeypatch): + """PermissionError during ensure_runtime_dirs should raise ConfigError with context.""" + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + target_dir = config.cache_dir + + real_mkdir = Path.mkdir + + def guarded_mkdir(self, *args, **kwargs): + if self == target_dir: + raise PermissionError("Permission denied") + return real_mkdir(self, *args, **kwargs) + + monkeypatch.setattr(Path, "mkdir", guarded_mkdir) + + with pytest.raises(ConfigError) as excinfo: + config.ensure_runtime_dirs() + + message = str(excinfo.value) + assert str(target_dir) in message + assert "permission" in message.lower() + assert "PermissionError" in message + assert isinstance(excinfo.value.__cause__, PermissionError) + + def test_ensure_runtime_dirs_os_error_includes_path_and_cause(self, monkeypatch): + """OSError during ensure_runtime_dirs should raise ConfigError with context.""" + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + target_dir = config.cache_dir + + real_mkdir = Path.mkdir + + def guarded_mkdir(self, *args, **kwargs): + if self == target_dir: + raise OSError("Invalid path") + return real_mkdir(self, *args, **kwargs) + + monkeypatch.setattr(Path, "mkdir", guarded_mkdir) + + with pytest.raises(ConfigError) as excinfo: + config.ensure_runtime_dirs() + + message = str(excinfo.value) + assert str(target_dir) in message + assert "permission" not in message.lower() + assert "filesystem" in message.lower() + assert "OSError" in message + assert isinstance(excinfo.value.__cause__, OSError) + def test_language_for_path_python(self): """Test language detection for Python files.""" with tempfile.TemporaryDirectory() as tmpdir: