Files
hermes-agent/tests/hermes_cli/test_cron_dashboard_off_loop.py
kshitij afb46fdab4 refactor(cron): polish registration partial-failure surfaces
Follow-up to the salvaged registration contract:
- share one _raise_if_cron_registration_error() helper for the two
  byte-identical dashboard 424 except-blocks (web_server + cron router,
  via the existing late() seam)
- add endpoint-level 424 coverage for /api/cron/blueprints/instantiate
  (previously only the sync worker was tested)
- give chat/CLI surfaces a human-facing user_message() (job name, no
  exception class name) and add a recovery hint (pause/resume or update
  re-registers via provider reconcile) to the model/REST message
- consolidate five inline provider test doubles into one ABC-subclassing
  make_cron_provider conftest factory; the web_server test double now
  subclasses CronScheduler so an ABC rename fails loudly
- narrow the wrapper facade to keyword-only (**kwargs) and route the
  tool's partial-failure return through tool_error()
2026-08-07 17:45:06 +05:30

130 lines
4.3 KiB
Python

"""Regression tests: cron dashboard handlers must not run profile I/O on the event loop.
Guards the residual sites missed by the 49fa04a23/346e5673d threadpool
migration: POST /api/cron/fire (_find_cron_job_profile) and
POST /api/cron/blueprints/instantiate (_call_cron_for_profile create_job).
Each stub asserts it is running OFF the event loop thread by checking that
no running asyncio loop is present in its thread.
"""
import asyncio
import pytest
from starlette.testclient import TestClient
from hermes_cli import web_server
@pytest.fixture()
def loop_probe():
"""Collect (tag, on_loop) proof from stubbed profile-I/O helpers."""
seen = []
def probe(tag):
try:
asyncio.get_running_loop()
seen.append((tag, True))
except RuntimeError:
seen.append((tag, False))
return seen, probe
def test_cron_fire_profile_lookup_off_loop(monkeypatch, loop_probe):
seen, probe = loop_probe
def fake_find(job_id):
probe("find")
return None
monkeypatch.setattr(web_server, "_find_cron_job_profile", fake_find)
import plugins.cron_providers.chronos.verify as chv
monkeypatch.setattr(chv, "get_fire_verifier", lambda: (lambda **kw: {"sub": "t"}))
client = TestClient(web_server.app)
resp = client.post(
"/api/cron/fire",
json={"job_id": "missing-job"},
headers={"Authorization": "Bearer x"},
)
assert resp.status_code == 200
assert resp.json()["status"] == "gone"
assert ("find", False) in seen, (
f"_find_cron_job_profile must run off the event loop; proof: {seen}"
)
def test_blueprint_instantiate_create_job_off_loop(monkeypatch, loop_probe):
seen, probe = loop_probe
def fake_call(profile, fn, *args, **kwargs):
probe("call")
return {"id": "bp-job-1", "kwargs_seen": sorted(kwargs.keys())}
monkeypatch.setattr(web_server, "_call_cron_for_profile", fake_call)
monkeypatch.setattr(web_server, "_has_valid_session_token", lambda req: True)
import cron.blueprint_catalog as bc
monkeypatch.setattr(bc, "get_blueprint", lambda key: object())
monkeypatch.setattr(
bc,
"fill_blueprint",
lambda bp, vals: {"name": "t", "schedule": "0 9 * * *", "prompt": "hi"},
)
client = TestClient(web_server.app)
resp = client.post(
"/api/cron/blueprints/instantiate",
json={"blueprint": "morning-brief", "values": {}},
)
assert resp.status_code == 200
body = resp.json()
# **spec kwargs must arrive at create_job intact through the partial.
assert body["kwargs_seen"] == ["name", "prompt", "schedule"]
assert ("call", False) in seen, (
f"_call_cron_for_profile must run off the event loop; proof: {seen}"
)
def test_blueprint_instantiate_reports_saved_but_unregistered(monkeypatch):
"""The instantiate endpoint maps a registration partial-failure to 424.
Endpoint-level guard for the shared ``_raise_if_cron_registration_error``
seam — the unit tests cover ``_create_cron_job_sync``; this proves the
blueprint route surfaces the same structured envelope through FastAPI.
"""
from cron.scheduler import CronSchedulerRegistrationError
failure = CronSchedulerRegistrationError(
{"id": "bp-saved-job", "name": "bp job"},
RuntimeError("private callback URL and token"),
)
def fail_call(profile, fn, *args, **kwargs):
raise failure
monkeypatch.setattr(web_server, "_call_cron_for_profile", fail_call)
monkeypatch.setattr(web_server, "_has_valid_session_token", lambda req: True)
import cron.blueprint_catalog as bc
monkeypatch.setattr(bc, "get_blueprint", lambda key: object())
monkeypatch.setattr(
bc,
"fill_blueprint",
lambda bp, vals: {"name": "t", "schedule": "0 9 * * *", "prompt": "hi"},
)
client = TestClient(web_server.app)
resp = client.post(
"/api/cron/blueprints/instantiate",
json={"blueprint": "morning-brief", "values": {}},
)
assert resp.status_code == 424
detail = resp.json()["detail"]
assert detail["job_id"] == "bp-saved-job"
assert detail["job_saved"] is True
assert detail["scheduler_registered"] is False
assert detail["retry_create"] is False
assert "private callback URL and token" not in detail["error"]