Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""CLI rendering of missed-run dispatch visibility (#99879).
|
||||
|
||||
`hermes cron list` shows a Dispatch line (scheduled vs actual, lateness,
|
||||
disposition) and `hermes cron status` calls out jobs whose last dispatch
|
||||
was a late/missed-fire catch-up.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from cron.jobs import create_job, load_jobs, save_jobs
|
||||
from hermes_cli.cron import (
|
||||
_dispatch_display,
|
||||
_format_lateness,
|
||||
_print_active_jobs_summary,
|
||||
cron_list,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_cron_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("cron.jobs.CRON_DIR", tmp_path / "cron")
|
||||
monkeypatch.setattr("cron.jobs.JOBS_FILE", tmp_path / "cron" / "jobs.json")
|
||||
monkeypatch.setattr("cron.jobs.OUTPUT_DIR", tmp_path / "cron" / "output")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _stamp_last_dispatch(job_id, stamp):
|
||||
jobs = load_jobs()
|
||||
for job in jobs:
|
||||
if job["id"] == job_id:
|
||||
job["last_dispatch"] = stamp
|
||||
save_jobs(jobs)
|
||||
|
||||
|
||||
def _catch_up_stamp(late_seconds=1860.0, kind="catch_up"):
|
||||
scheduled = datetime(2026, 9, 1, 9, 0, tzinfo=timezone.utc)
|
||||
return {
|
||||
"scheduled_at": scheduled.isoformat(),
|
||||
"dispatched_at": (scheduled + timedelta(seconds=late_seconds)).isoformat(),
|
||||
"lateness_seconds": late_seconds,
|
||||
"kind": kind,
|
||||
}
|
||||
|
||||
|
||||
class TestCronListDispatchLine:
|
||||
def test_catch_up_dispatch_rendered(self, tmp_cron_dir, capsys, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.cron._warn_if_gateway_not_running", lambda: None
|
||||
)
|
||||
job = create_job(prompt="daily report", schedule="0 9 * * *")
|
||||
_stamp_last_dispatch(job["id"], _catch_up_stamp())
|
||||
|
||||
cron_list()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Dispatch:" in out
|
||||
assert "catch-up after missed fire" in out
|
||||
assert "2026-09-01T09:00:00+00:00" in out # scheduled instant
|
||||
assert "31m late" in out
|
||||
|
||||
def test_on_time_dispatch_rendered_quietly(self, tmp_cron_dir, capsys, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.cron._warn_if_gateway_not_running", lambda: None
|
||||
)
|
||||
job = create_job(prompt="daily report", schedule="0 9 * * *")
|
||||
_stamp_last_dispatch(job["id"], _catch_up_stamp(30.0, kind="on_time"))
|
||||
|
||||
cron_list()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Dispatch:" in out
|
||||
assert "on time" in out
|
||||
assert "catch-up" not in out
|
||||
|
||||
def test_no_stamp_no_dispatch_line(self, tmp_cron_dir, capsys, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.cron._warn_if_gateway_not_running", lambda: None
|
||||
)
|
||||
create_job(prompt="daily report", schedule="0 9 * * *")
|
||||
|
||||
cron_list()
|
||||
|
||||
assert "Dispatch:" not in capsys.readouterr().out
|
||||
|
||||
|
||||
class TestStatusLateJobsCallout:
|
||||
def test_late_jobs_called_out(self, capsys):
|
||||
jobs = [
|
||||
{
|
||||
"id": "abc123",
|
||||
"name": "daily 9am",
|
||||
"next_run_at": "2026-09-02T09:00:00+00:00",
|
||||
"last_dispatch": _catch_up_stamp(),
|
||||
},
|
||||
{
|
||||
"id": "def456",
|
||||
"name": "hourly ping",
|
||||
"next_run_at": "2026-09-01T10:00:00+00:00",
|
||||
"last_dispatch": _catch_up_stamp(10.0, kind="on_time"),
|
||||
},
|
||||
]
|
||||
|
||||
_print_active_jobs_summary(jobs)
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "1 job(s) last fired late (missed-fire catch-up)" in out
|
||||
assert "abc123" in out
|
||||
assert "31m late" in out
|
||||
# On-time job is not in the callout.
|
||||
assert "def456" not in out
|
||||
|
||||
def test_no_late_jobs_no_callout(self, capsys):
|
||||
jobs = [
|
||||
{
|
||||
"id": "def456",
|
||||
"name": "hourly ping",
|
||||
"next_run_at": "2026-09-01T10:00:00+00:00",
|
||||
"last_dispatch": _catch_up_stamp(10.0, kind="on_time"),
|
||||
}
|
||||
]
|
||||
|
||||
_print_active_jobs_summary(jobs)
|
||||
|
||||
assert "fired late" not in capsys.readouterr().out
|
||||
|
||||
|
||||
class TestDisplayHelpers:
|
||||
def test_format_lateness(self):
|
||||
assert _format_lateness(45) == "45s"
|
||||
assert _format_lateness(1860) == "31m"
|
||||
assert _format_lateness(9000) == "2h 30m"
|
||||
assert _format_lateness(90000) == "1d 1h"
|
||||
assert _format_lateness("bogus") == "?"
|
||||
|
||||
def test_dispatch_display_malformed_returns_none(self):
|
||||
assert _dispatch_display(None) is None
|
||||
assert _dispatch_display("late") is None
|
||||
assert _dispatch_display({}) is None
|
||||
assert _dispatch_display({"scheduled_at": "x"}) is None
|
||||
|
||||
def test_dispatch_display_late_kind(self):
|
||||
line = _dispatch_display(_catch_up_stamp(600.0, kind="late"))
|
||||
assert "late" in line
|
||||
assert "10m" in line
|
||||
Reference in New Issue
Block a user