mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-19 15:18:03 +00:00
fix(cron): contain any per-job exception in the due scan; harden as a class
Structural completion of the malformed-job freeze fixes (#61382 id-less, #61525 non-dict schedule, #61581 bad next_run_at): wrap the per-job body of _get_due_jobs_locked in try/except so any FUTURE malformed-field variant degrades to skipping that one job for the tick instead of aborting the scan before save_jobs() and freezing the whole profile's scheduler. Also: restore test_repeated_concurrent_runs_accumulate_completed_count to TestMarkJobRunConcurrency (accidentally re-parented by the #61581 diff), add a containment regression test, and AUTHOR_MAP for hydracoco7. E2E: one jobs.json carrying all five malformed shapes (drifted job_id, missing id, null schedule, garbage next_run_at, non-string last_run_at) plus a healthy sibling — single tick contains all five, sibling fires, repairs persist, second tick stable. 670 cron tests green.
This commit is contained in:
parent
26f040ef20
commit
10c0d9b2a7
3 changed files with 229 additions and 161 deletions
|
|
@ -1503,6 +1503,60 @@ class TestBadNextRunAtRecovery:
|
|||
assert any(j["id"] == "good-sibling" for j in due2)
|
||||
|
||||
|
||||
class TestPerJobScanContainment:
|
||||
"""Structural guard: ANY per-job exception in the due scan must degrade to
|
||||
skipping that one job for the tick — never abort the scan and starve
|
||||
healthy siblings (the freeze class behind bad id / schedule / next_run_at).
|
||||
"""
|
||||
|
||||
def test_unforeseen_per_job_exception_does_not_starve_siblings(self, tmp_cron_dir):
|
||||
"""Simulate a FUTURE malformed-field variant none of the shape
|
||||
normalizers repair, by making grace computation raise for one job
|
||||
only. The per-job guard must skip it and still return the sibling."""
|
||||
from datetime import timezone, timedelta as td
|
||||
from unittest.mock import patch as mock_patch
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
past = (now - td(seconds=30)).isoformat()
|
||||
|
||||
poison = {
|
||||
"id": "poison",
|
||||
# minutes=7 tags this schedule so the patched helper can target it
|
||||
"schedule": {"kind": "interval", "minutes": 7},
|
||||
"next_run_at": past,
|
||||
"enabled": True,
|
||||
"created_at": past,
|
||||
}
|
||||
good = {
|
||||
"id": "good-sibling",
|
||||
"schedule": {"kind": "interval", "minutes": 5},
|
||||
"next_run_at": past,
|
||||
"enabled": True,
|
||||
"created_at": past,
|
||||
}
|
||||
save_jobs([poison, good])
|
||||
|
||||
import cron.jobs as jobs_mod
|
||||
real_grace = jobs_mod._compute_grace_seconds
|
||||
|
||||
def selective_grace(schedule):
|
||||
if schedule.get("minutes") == 7:
|
||||
raise RuntimeError("simulated unforeseen malformed field")
|
||||
return real_grace(schedule)
|
||||
|
||||
with mock_patch.object(jobs_mod, "_compute_grace_seconds", selective_grace):
|
||||
due = get_due_jobs() # must not raise
|
||||
|
||||
ids = [j["id"] for j in due]
|
||||
assert "good-sibling" in ids, f"healthy sibling starved: {ids}"
|
||||
assert "poison" not in ids
|
||||
|
||||
# Scheduler stays alive on subsequent ticks too.
|
||||
with mock_patch.object(jobs_mod, "_compute_grace_seconds", selective_grace):
|
||||
due2 = get_due_jobs()
|
||||
assert any(j["id"] == "good-sibling" for j in due2)
|
||||
|
||||
|
||||
class TestSaveJobOutput:
|
||||
def test_creates_output_file(self, tmp_cron_dir):
|
||||
output_file = save_job_output("test123", "# Results\nEverything ok.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue