fix(cleanup): invalidate sandbox_dirs_cache after purge (#7527) #11040

Merged
HAL9000 merged 4 commits from fix/7527-sandbox-cache-invalidation into master 2026-06-14 09:43:52 +00:00
2 changed files with 93 additions and 0 deletions
@@ -0,0 +1,17 @@
@unit @cleanup
Feature: Sandbox dirs cache invalidation after purge
This test verifies the fix for issue #7527: _purge_sandboxes() must
invalidate the sandbox_dirs_cache so subsequent scan() calls re-read
the filesystem. Without this fix, a stale cached list of directories
persisted even after those directories were deleted by shutil.rmtree().
Scenario: Cache is invalidated after purge completes removals
Outdated
Review

BLOCKING — Missing @tdd_issue_7527 regression tag

Per CONTRIBUTING.md, bug fix regression tests must carry a @tdd_issue_N tag. This marks the scenario as a permanent regression guard and ensures the CI test suite permanently tracks the fix.

The issue body itself states: "After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD with @tdd_expected_fail tags."

Fix: Add @tdd_issue_7527 tag to the primary regression scenario ("Cache is invalidated after purge completes removals").

Example:

@tdd_issue_7527
Scenario: Cache is invalidated after purge completes removals

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKING — Missing `@tdd_issue_7527` regression tag** Per CONTRIBUTING.md, bug fix regression tests must carry a `@tdd_issue_N` tag. This marks the scenario as a permanent regression guard and ensures the CI test suite permanently tracks the fix. The issue body itself states: *"After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD with @tdd_expected_fail tags."* **Fix:** Add `@tdd_issue_7527` tag to the primary regression scenario ("Cache is invalidated after purge completes removals"). Example: ```gherkin @tdd_issue_7527 Scenario: Cache is invalidated after purge completes removals ``` --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
Given cleanup coverage has a CleanupService with default settings
When cleanup coverage purges sandboxes and cache is cleared for tracking
Then cleanup coverage _sandbox_dirs_cache should be None after purge
Scenario: Purge with no stale dirs still invalidates cache
Given cleanup coverage has a CleanupService with default settings
When cleanup coverage purges sandboxes with an empty cache
Then cleanup coverage _sandbox_dirs_cache should be None after purge
2
@@ -425,3 +425,79 @@ def step_cleanup_cov_age_desc(context: Context, expected: str) -> None:
assert context.cleanup_age_desc == expected, (
f"Expected {expected!r}, got {context.cleanup_age_desc!r}"
)
# ── sandbox_cache_invalidation steps ────────────────────────────────
@when("cleanup coverage purges sandboxes and cache is cleared for tracking")
def step_cleanup_cov_purge_sandboxes_cache_tracking(context: Context) -> None:
"""Set up real sandbox dirs, preload cache, purge, then verify cache invalidation."""
svc = context.cleanup_service
report = CleanupReport(dry_run=False)
# Create temporary stub directories that look like sandbox dirs
tmp_dir = tempfile.mkdtemp()
sandbox_names = [f"ca-sandbox-plan{i}-stub{i}" for i in range(2)]
sandbox_paths = []
for name in sandbox_names:
p = Path(tmp_dir) / name
p.mkdir()
# Mark as stale (old mtime)
old_time = time.time() - 43200 * 180 # ~6 months ago
os.utime(p, (old_time, old_time))
sandbox_paths.append(p)
context.cleanup_stale_dir_paths = sandbox_paths
# Pre-populate cache with real Path objects
cached_dirs = [Path(tmp_dir) / name for name in sandbox_names]
svc._sandbox_dirs_cache = cached_dirs
# Temporarily replace gettempdir to point at tmp_dir so _purge_sandboxes
# will find our dirs
original_gettempdir = tempfile.gettempdir
try:
tempfile.gettempdir = lambda: tmp_dir
svc._purge_sandboxes(report)
finally:
tempfile.gettempdir = original_gettempdir
context.cleanup_report = report
@when("cleanup coverage purges sandboxes with an empty cache")
def step_cleanup_cov_purge_empty_cache(context: Context) -> None:
"""Purge when _get_sandbox_dirs returns nothing — cache still invalidated."""
svc = context.cleanup_service
report = CleanupReport(dry_run=False)
# Set cache to empty list to force fresh scan path
svc._sandbox_dirs_cache = []
def fake_gettempdir():
return "/nonexistent_tmp_xyz_empty"
original_gettempdir = tempfile.gettempdir
try:
tempfile.gettempdir = fake_gettempdir
# Patch Path.exists to return False so we hit the early-return path but
# still want to verify cache is set to None
with patch("cleveragents.application.services.cleanup_service.tempfile"):
# tempfile module was already imported in cleanup_service.py,
# but we patched gettempdir directly above
svc._purge_sandboxes(report)
finally:
tempfile.gettempdir = original_gettempdir
context.cleanup_report = report
@then("cleanup coverage _sandbox_dirs_cache should be None after purge")
def step_cleanup_cov_cache_is_none(context: Context) -> None:
"""Verify the cache was invalidated (set to None) by _purge_sandboxes."""
assert context.cleanup_service._sandbox_dirs_cache is None, (
"Expected _sandbox_dirs_cache to be None after purge, "
f"but got {context.cleanup_service._sandbox_dirs_cache!r}"
)