Files
hermes-agent/tests/cron/test_cleanup_timeout.py
T
Teknium 28834a2098 test: raise tight wall-clock bounds that flaked on loaded CI runners
Seven test files asserted sub-2s wall-clock bounds (elapsed < 0.5/1.0s,
stop(timeout=1.0), event waits of 0.5-2s). Under CI load these fired on
healthy code: main run 33455779041 alone flaked 6 of them in one pass
(observed 1.01s vs 0.5, 1.20s vs 1.0, 3.61s vs 3.0, 1.55s vs 1.0,
stop(1.0) returning False, lease TTL 0.1s expiring before the authority
change was observed).

Per the AGENTS.md flake policy (waits >= 2s), bounds are raised to 5s+
while keeping their teeth: every hang path they guard blocks for 10s+
(release.wait holds), so the loosened bounds still distinguish bounded
from unbounded behavior. The authority-loss test gets a 30s lease TTL so
lease expiry can no longer preempt the authority-change assertion.
2026-09-01 10:52:42 -07:00

141 lines
4.8 KiB
Python

"""Regression tests for bounded cron post-run cleanup.
A cron worker must release its in-memory dispatch guard even when SQLite or an
agent resource finalizer stops returning after the model turn has ended.
"""
from __future__ import annotations
import threading
import time
from unittest.mock import MagicMock, patch
from cron.scheduler import run_job, _teardown_cron_agent
_RUNTIME = {
"api_key": "test-key",
"base_url": "https://example.invalid/v1",
"provider": "openrouter",
"api_mode": "chat_completions",
}
class HangingSessionDB:
def __init__(self, release: threading.Event):
self.release = release
self.entered = threading.Event()
def get_compression_tip(self, _session_id):
self.entered.set()
self.release.wait()
return None
def end_session(self, *_args, **_kwargs):
return None
def close(self):
return None
class HangingAgent:
def __init__(self, release: threading.Event):
self.release = release
self.entered = threading.Event()
def close(self):
self.entered.set()
self.release.wait()
def test_run_job_bounds_sessiondb_finalization(tmp_path):
release = threading.Event()
fake_db = HangingSessionDB(release)
job = {"id": "cleanup-sessiondb-hang", "name": "test", "prompt": "hello"}
try:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value=_RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls, \
patch("cron.scheduler._cron_cleanup_timeout_seconds", return_value=0.02):
mock_agent = MagicMock()
mock_agent.run_conversation.return_value = {"final_response": "ok"}
mock_agent_cls.return_value = mock_agent
started = time.monotonic()
success, _output, final_response, error = run_job(job)
elapsed = time.monotonic() - started
assert fake_db.entered.wait(timeout=2.0)
assert elapsed < 5.0
assert success is True
assert final_response == "ok"
assert error is None
finally:
release.set()
def test_agent_teardown_is_bounded():
release = threading.Event()
agent = HangingAgent(release)
try:
started = time.monotonic()
_teardown_cron_agent(agent, "cleanup-agent-hang", timeout_seconds=0.02)
elapsed = time.monotonic() - started
assert agent.entered.wait(timeout=2.0)
assert elapsed < 5.0
finally:
release.set()
def test_dispatch_guard_releases_after_sessiondb_finalization_hang(tmp_path):
"""A second scheduler tick can fire the same job after cleanup times out."""
import cron.scheduler as sched
release = threading.Event()
fake_db = HangingSessionDB(release)
job = {
"id": "cleanup-guard-hang",
"name": "cleanup-guard-hang",
"prompt": "hello",
"schedule": "every 5m",
"enabled": True,
"next_run_at": "2020-01-01T00:00:00",
"deliver": "local",
}
sched._parallel_pool = None
sched._parallel_pool_max_workers = None
sched._running_job_ids.clear()
try:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value=_RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls, \
patch("cron.scheduler._cron_cleanup_timeout_seconds", return_value=0.02), \
patch.object(sched, "get_due_jobs", return_value=[job]), \
patch.object(sched, "advance_next_runs"), \
patch.object(sched, "save_job_output", return_value="/tmp/out"), \
patch.object(sched, "mark_job_run"), \
patch.object(sched, "_deliver_result", return_value=None):
mock_agent = MagicMock()
mock_agent.run_conversation.return_value = {"final_response": "ok"}
mock_agent_cls.return_value = mock_agent
assert sched.tick(verbose=False) == 1
assert "cleanup-guard-hang" not in sched.get_running_job_ids()
assert sched.tick(verbose=False) == 1
finally:
release.set()
sched._running_job_ids.discard("cleanup-guard-hang")
sched._shutdown_parallel_pool()