Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions plugins/disk-cleanup/disk_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,13 @@ def _is_protected_cron_path(p: Path) -> bool:
"""Return True if *p* is a cron control-plane file/directory that must
never be deleted.

This only matches the directory itself and known control-plane files
(``jobs.json``, ``.tick.lock``) — it does NOT blanket-protect
everything under ``cron/`` because ``cron/output/`` is disposable.
This matches, by EXACT path only, the ``cron/`` directory itself, known
control-plane files (``jobs.json``, ``.tick.lock``), and the ``output/``
root directory. It does NOT (and must not be "simplified" to) blanket-match
everything under ``cron/output/`` — those run artifacts are disposable and
are cleaned by retention policy; only the ``output/`` root itself is
protected, because deleting it wholesale erases every job's retained run
history at once.
"""
# Lazily build the set once per process so HERMES_HOME is resolved
# exactly once.
Expand All @@ -177,6 +181,7 @@ def _is_protected_cron_path(p: Path) -> bool:
for parent in ("cron", "cronjobs"):
base = hermes_home / parent
_PROTECTED_CRON_PATHS.add(str(base))
_PROTECTED_CRON_PATHS.add(str(base / "output"))
_PROTECTED_CRON_PATHS.add(str(base / "jobs.json"))
_PROTECTED_CRON_PATHS.add(str(base / ".tick.lock"))
resolved = str(p.resolve())
Expand Down Expand Up @@ -566,7 +571,7 @@ def guess_category(path: Path) -> Optional[str]:
# (e.g. ``jobs.json``, ``.tick.lock``) must never be
# auto-tracked — deleting it wipes the live scheduler
# registry. See issue #32164.
if len(rel.parts) >= 2 and rel.parts[1] == "output":
if len(rel.parts) >= 3 and rel.parts[1] == "output":
return "cron-output"
return None
if top == "cache":
Expand Down
30 changes: 30 additions & 0 deletions tests/plugins/test_disk_cleanup_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ def test_cron_subtree_categorised(self, _isolate_env):
p.write_text("x")
assert dg.guess_category(p) == "cron-output"

def test_cron_output_root_not_tracked(self, _isolate_env):
"""The cron/output root is durable container state, not an artifact."""
dg = _load_lib()
output_root = _isolate_env / "cron" / "output"
output_root.mkdir(parents=True)
assert dg.guess_category(output_root) is None

def test_cron_jobs_json_not_tracked(self, _isolate_env):
"""Regression for #32164: the cron registry must never be tracked."""
dg = _load_lib()
Expand Down Expand Up @@ -228,6 +235,29 @@ def test_quick_skips_stale_cron_output_for_cron_dir(self, _isolate_env):
assert summary["deleted"] == 0, "cron/ dir must not be deleted"
assert cron_dir.exists()

def test_quick_skips_stale_cron_output_for_output_root(self, _isolate_env):
"""Stale entry for cron/output itself must not delete all job output."""
dg = _load_lib()
output_root = _isolate_env / "cron" / "output"
job_dir = output_root / "job_1"
job_dir.mkdir(parents=True)
run_md = job_dir / "run.md"
run_md.write_text("x")

tracked_file = _isolate_env / "disk-cleanup" / "tracked.json"
tracked_file.parent.mkdir(parents=True, exist_ok=True)
tracked_file.write_text(json.dumps([{
"path": str(output_root),
"category": "cron-output",
"timestamp": "2025-01-01T00:00:00+00:00",
"size": 0,
}]))

summary = dg.quick()
assert summary["deleted"] == 0, "cron/output root must not be deleted"
assert output_root.exists()
assert run_md.exists()

def test_quick_skips_protected_cron_paths_defense_in_depth(self, _isolate_env):
"""Defense-in-depth: even if guess_category returned cron-output
(hypothetically), protected cron paths are never deleted."""
Expand Down
Loading