158 lines
6.1 KiB
Python
158 lines
6.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""test_retention_archive_jobs.py — Unit tests for Retention Piece 2: Job Archival & Pruning."""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
sys.path.insert(0, str(REPO_ROOT / "bin"))
|
||
|
|
|
||
|
|
import importlib.util
|
||
|
|
spec = importlib.util.spec_from_file_location("retention_archive_jobs", str(REPO_ROOT / "bin" / "retention-archive-jobs.py"))
|
||
|
|
raj = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(raj)
|
||
|
|
|
||
|
|
|
||
|
|
class TestRetentionArchiveJobs(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.test_dir = tempfile.mkdtemp()
|
||
|
|
self.root = Path(self.test_dir)
|
||
|
|
self.jobs_dir = self.root / "jobs"
|
||
|
|
self.archive_dir = self.jobs_dir / "archive"
|
||
|
|
self.jobs_dir.mkdir(parents=True)
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||
|
|
|
||
|
|
def _create_job(self, name, schedule="manual", extra=None):
|
||
|
|
data = {
|
||
|
|
"name": name,
|
||
|
|
"description": f"Test job {name}",
|
||
|
|
"agent": "pip",
|
||
|
|
"schedule": schedule,
|
||
|
|
"timeout": 300,
|
||
|
|
}
|
||
|
|
if extra:
|
||
|
|
data.update(extra)
|
||
|
|
p = self.jobs_dir / f"{name}.json"
|
||
|
|
p.write_text(json.dumps(data, indent=2))
|
||
|
|
return p
|
||
|
|
|
||
|
|
def test_core_protected_jobs(self):
|
||
|
|
protected = raj.get_protected_jobs(self.root)
|
||
|
|
self.assertIn("heartbeat", protected)
|
||
|
|
self.assertIn("refine-system", protected)
|
||
|
|
self.assertIn("canary-test", protected)
|
||
|
|
|
||
|
|
def test_systemd_unit_protection(self):
|
||
|
|
sdir = self.root / "systemd"
|
||
|
|
sdir.mkdir()
|
||
|
|
svc = sdir / "job-custom-worker.service"
|
||
|
|
svc.write_text("[Service]\nExecStart=/home/super/Projects/NetVM/bin/job-dispatch.py custom-worker\n")
|
||
|
|
|
||
|
|
protected = raj.get_protected_jobs(self.root)
|
||
|
|
self.assertIn("custom-worker", protected)
|
||
|
|
self.assertIn("systemd unit", protected["custom-worker"])
|
||
|
|
|
||
|
|
def test_active_pipeline_protection(self):
|
||
|
|
pipe_file = self.root / "pipelines.json"
|
||
|
|
pipe_data = {
|
||
|
|
"run-001": {
|
||
|
|
"status": "running",
|
||
|
|
"pipeline_name": "live-pipe-root",
|
||
|
|
"steps": [{"job_name": "live-pipe-step2"}]
|
||
|
|
},
|
||
|
|
"run-002": {
|
||
|
|
"status": "completed",
|
||
|
|
"pipeline_name": "dead-pipe-root",
|
||
|
|
"steps": [{"job_name": "dead-pipe-step"}]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
pipe_file.write_text(json.dumps(pipe_data))
|
||
|
|
|
||
|
|
protected = raj.get_protected_jobs(self.root)
|
||
|
|
self.assertIn("live-pipe-root", protected)
|
||
|
|
self.assertIn("live-pipe-step2", protected)
|
||
|
|
self.assertNotIn("dead-pipe-root", protected)
|
||
|
|
|
||
|
|
def test_chain_target_protection(self):
|
||
|
|
self._create_job("canary-test", schedule="manual", extra={"on_success": "canary-followup"})
|
||
|
|
self._create_job("canary-followup", schedule="manual")
|
||
|
|
|
||
|
|
protected = raj.get_protected_jobs(self.root)
|
||
|
|
self.assertIn("canary-followup", protected)
|
||
|
|
self.assertIn("linked by canary-test", protected["canary-followup"])
|
||
|
|
|
||
|
|
def test_eligibility_criteria(self):
|
||
|
|
manual_job = self._create_job("retired-task", schedule="manual")
|
||
|
|
cron_job = self._create_job("hourly-task", schedule="*/5 * * * *")
|
||
|
|
protected_job = self._create_job("heartbeat", schedule="manual")
|
||
|
|
|
||
|
|
prot = {"heartbeat": "core baseline"}
|
||
|
|
|
||
|
|
el1, _ = raj.is_eligible(manual_job, prot)
|
||
|
|
self.assertTrue(el1)
|
||
|
|
|
||
|
|
el2, _ = raj.is_eligible(cron_job, prot)
|
||
|
|
self.assertFalse(el2)
|
||
|
|
|
||
|
|
el3, _ = raj.is_eligible(protected_job, prot)
|
||
|
|
self.assertFalse(el3)
|
||
|
|
|
||
|
|
def test_archive_and_unarchive_roundtrip(self):
|
||
|
|
self._create_job("auto-task-x1", schedule="manual")
|
||
|
|
|
||
|
|
with patch.object(raj, "is_git_tracked", return_value=False):
|
||
|
|
# Archive
|
||
|
|
res_arch = raj.archive_job("auto-task-x1", dry_run=False, root_dir=self.root)
|
||
|
|
self.assertEqual(res_arch["status"], "archived")
|
||
|
|
self.assertFalse((self.jobs_dir / "auto-task-x1.json").exists())
|
||
|
|
self.assertTrue((self.archive_dir / "auto-task-x1.json").exists())
|
||
|
|
|
||
|
|
# Unarchive
|
||
|
|
res_unarch = raj.unarchive_job("auto-task-x1", dry_run=False, root_dir=self.root)
|
||
|
|
self.assertEqual(res_unarch["status"], "unarchived")
|
||
|
|
self.assertTrue((self.jobs_dir / "auto-task-x1.json").exists())
|
||
|
|
self.assertFalse((self.archive_dir / "auto-task-x1.json").exists())
|
||
|
|
|
||
|
|
def test_refuse_archive_protected_job(self):
|
||
|
|
self._create_job("heartbeat", schedule="manual")
|
||
|
|
res = raj.archive_job("heartbeat", dry_run=False, root_dir=self.root)
|
||
|
|
self.assertEqual(res["status"], "rejected")
|
||
|
|
self.assertTrue((self.jobs_dir / "heartbeat.json").exists())
|
||
|
|
|
||
|
|
def test_force_archive_protected_job(self):
|
||
|
|
self._create_job("heartbeat", schedule="manual")
|
||
|
|
with patch.object(raj, "is_git_tracked", return_value=False):
|
||
|
|
res = raj.archive_job("heartbeat", dry_run=False, force=True, root_dir=self.root)
|
||
|
|
self.assertEqual(res["status"], "archived")
|
||
|
|
self.assertTrue((self.archive_dir / "heartbeat.json").exists())
|
||
|
|
|
||
|
|
def test_scan_and_archive_dry_run(self):
|
||
|
|
self._create_job("task-1", schedule="manual")
|
||
|
|
self._create_job("task-2", schedule="*/10 * * * *")
|
||
|
|
|
||
|
|
res = raj.scan_and_archive(dry_run=True, root_dir=self.root)
|
||
|
|
self.assertEqual(res["eligible"], 1)
|
||
|
|
self.assertEqual(res["archived"], 1)
|
||
|
|
self.assertTrue((self.jobs_dir / "task-1.json").exists())
|
||
|
|
self.assertFalse((self.archive_dir / "task-1.json").exists())
|
||
|
|
|
||
|
|
def test_list_archived(self):
|
||
|
|
self._create_job("archived-task", schedule="manual")
|
||
|
|
with patch.object(raj, "is_git_tracked", return_value=False):
|
||
|
|
raj.archive_job("archived-task", dry_run=False, root_dir=self.root)
|
||
|
|
archived = raj.list_archived(root_dir=self.root)
|
||
|
|
self.assertEqual(len(archived), 1)
|
||
|
|
self.assertEqual(archived[0]["name"], "archived-task")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|