diff --git a/src/specify_cli/bundler/services/installer.py b/src/specify_cli/bundler/services/installer.py index 58e220638d..78c1c90dd0 100644 --- a/src/specify_cli/bundler/services/installer.py +++ b/src/specify_cli/bundler/services/installer.py @@ -11,6 +11,7 @@ """ from __future__ import annotations +import logging from dataclasses import dataclass, field from pathlib import Path from typing import Protocol @@ -250,8 +251,10 @@ def _rollback( installer: PrimitiveInstaller, done: list[ComponentRef], ) -> None: + logger = logging.getLogger(__name__) for component in reversed(done): try: installer.remove(project_root, component) except Exception: # noqa: BLE001 - best-effort rollback + logger.debug("rollback failed for %s", component, exc_info=True) continue diff --git a/tests/unit/test_bundler_primitives.py b/tests/unit/test_bundler_primitives.py index dc39106b50..ea03bc3889 100644 --- a/tests/unit/test_bundler_primitives.py +++ b/tests/unit/test_bundler_primitives.py @@ -334,3 +334,24 @@ def _plan(manifest): effective_integration=None, components=components, ) + + +class TestRollbackLoggingRegression: + """Regression: _rollback() must log errors, not silently swallow them.""" + + def test_rollback_logs_exception_on_remove_failure(self, caplog, tmp_path): + """When installer.remove() fails, _rollback() must log the error.""" + import logging + from unittest.mock import MagicMock + from specify_cli.bundler.services.installer import _rollback + from specify_cli.bundler.models.manifest import ComponentRef + + installer = MagicMock() + installer.remove.side_effect = RuntimeError("simulated remove failure") + done = [ComponentRef(kind="extensions", id="test-ext")] + + with caplog.at_level(logging.DEBUG): + _rollback(tmp_path, installer, done) + + assert any("rollback failed" in record.message for record in caplog.records) + assert any("test-ext" in record.message for record in caplog.records)