From 35e2fdc832a49b100f96e087fb40883c355cf54c Mon Sep 17 00:00:00 2001 From: ghzhost Date: Sun, 23 Aug 2026 14:17:26 +0000 Subject: [PATCH] fix(diagnostics): re-raise cleanup IO errors on directory removal instead of swallowing as retained remove_cleanup previously caught a bare except OSError on rmdir(), which caused any directory deletion failure (permissions, hardware IO error, etc.) to be silently reclassified as an intentional retained entry. Narrow the catch to errno.ENOTEMPTY and errno.EEXIST, and raise ContextSafeError('cleanup_io_error', ...) for all other OSErrors, matching the file unlink branch immediately below. Fixes ChelseaKR/contextsafe#36. --- src/contextsafe/diagnostics.py | 11 ++++++++--- tests/test_diagnostics.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/contextsafe/diagnostics.py b/src/contextsafe/diagnostics.py index dd36cc8..8096e79 100644 --- a/src/contextsafe/diagnostics.py +++ b/src/contextsafe/diagnostics.py @@ -34,6 +34,7 @@ from __future__ import annotations +import errno import os import platform import sys @@ -231,9 +232,13 @@ def remove_cleanup(plan: CleanupPlan) -> tuple[int, int]: # deleting the thing we just declined to delete. try: target.rmdir() - except OSError: - retained += 1 - continue + except OSError as exc: + if exc.errno in (errno.ENOTEMPTY, errno.EEXIST): + retained += 1 + continue + raise ContextSafeError( + "cleanup_io_error", "$", "a workspace entry could not be removed" + ) from exc else: try: target.unlink() diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 864ce30..8dada83 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -16,6 +16,7 @@ from __future__ import annotations +import errno import json import os import unicodedata @@ -549,6 +550,21 @@ def refuse(self: Path, *args: Any, **kwargs: Any) -> None: assert excinfo.value.code == "cleanup_io_error" +def test_removal_reports_a_directory_it_cannot_delete_for_non_empty_error( + monkeypatch: pytest.MonkeyPatch, populated_workspace: Path +) -> None: + """A directory rmdir failure from permission/IO error raises cleanup_io_error.""" + + def refuse(self: Path, *args: Any, **kwargs: Any) -> None: + raise PermissionError(errno.EACCES, "Permission denied") + + plan = enumerate_cleanup(populated_workspace) + monkeypatch.setattr(Path, "rmdir", refuse) + with pytest.raises(ContextSafeError) as excinfo: + remove_cleanup(plan) + assert excinfo.value.code == "cleanup_io_error" + + def test_an_unreadable_log_is_reported_not_overwritten( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: