hermes-agent/tests/cron/test_scheduler_shutdown_guard.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

122 lines
4.7 KiB
Python

"""Regression coverage for #58720 / #55924 — cron scheduling races
interpreter finalization.
When the gateway tears down (SIGTERM from ``hermes update`` /
``hermes gateway stop`` / systemd restart, or an OOM-kill), a cron tick can
still fire. Once the Python interpreter is finalizing, ``concurrent.futures``
refuses new work with ``RuntimeError: cannot schedule new futures after
interpreter shutdown`` and asyncio's default executor is gone. The cron
delivery + dispatch paths used to hit that unguarded, crashing the tick and
spraying a traceback into ``errors.log`` on every restart-race.
The fix adds ``_interpreter_shutting_down()`` and guards the scheduling
sites so they skip gracefully with a warning instead of raising.
"""
from __future__ import annotations
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestInterpreterShuttingDownHelper:
def test_true_when_finalizing(self):
from cron.scheduler import _interpreter_shutting_down
with patch("sys.is_finalizing", return_value=True):
assert _interpreter_shutting_down() is True
def test_false_when_not_finalizing_and_no_exc(self):
from cron.scheduler import _interpreter_shutting_down
with patch("sys.is_finalizing", return_value=False):
assert _interpreter_shutting_down() is False
def test_matches_shutdown_error_text_as_fallback(self):
"""The concurrent.futures module-global flag can be set a hair before
``sys.is_finalizing()`` flips — matching the error text catches that
race so a shutdown RuntimeError isn't misread as a real failure."""
from cron.scheduler import _interpreter_shutting_down
exc = RuntimeError("cannot schedule new futures after interpreter shutdown")
with patch("sys.is_finalizing", return_value=False):
assert _interpreter_shutting_down(exc) is True
def test_unrelated_error_is_not_shutdown(self):
from cron.scheduler import _interpreter_shutting_down
exc = RuntimeError("some other problem")
with patch("sys.is_finalizing", return_value=False):
assert _interpreter_shutting_down(exc) is False
class TestStandaloneDeliverySkipsDuringShutdown:
def _telegram_cfg(self):
from gateway.config import Platform
pconfig = MagicMock()
pconfig.enabled = True
mock_cfg = MagicMock()
mock_cfg.platforms = {Platform.TELEGRAM: pconfig}
return mock_cfg
def test_standalone_path_skips_without_scheduling(self):
"""With the interpreter finalizing, the standalone delivery path must
skip BEFORE attempting to schedule the send — no ``_send_to_platform``
call, a graceful warning-level skip, and an error string returned
(not a raised exception)."""
from cron.scheduler import _deliver_result
job = {
"id": "gov-job",
"name": "model-governor",
"deliver": "origin",
"origin": {"platform": "telegram", "chat_id": "123"},
}
send_mock = AsyncMock(return_value={"success": True})
with patch("gateway.config.load_gateway_config", return_value=self._telegram_cfg()), \
patch("tools.send_message_tool._send_to_platform", new=send_mock), \
patch("sys.is_finalizing", return_value=True):
result = _deliver_result(job, "daily report body")
send_mock.assert_not_called()
assert result is not None
assert "shutting down" in result
def test_normal_delivery_still_works_when_not_finalizing(self):
"""Guard must not regress the happy path: a normal (non-finalizing)
run still delivers via the standalone send."""
from cron.scheduler import _deliver_result
job = {
"id": "gov-job",
"name": "model-governor",
"deliver": "origin",
"origin": {"platform": "telegram", "chat_id": "123"},
}
send_mock = AsyncMock(return_value={"success": True})
with patch("gateway.config.load_gateway_config", return_value=self._telegram_cfg()), \
patch("tools.send_message_tool._send_to_platform", new=send_mock), \
patch("sys.is_finalizing", return_value=False):
result = _deliver_result(job, "daily report body")
send_mock.assert_called_once()
assert result is None
class TestSourceGuardrail:
@pytest.fixture
def source(self) -> str:
from pathlib import Path
return (
Path(__file__).resolve().parents[2] / "cron" / "scheduler.py"
).read_text(encoding="utf-8")
def test_helper_defined(self, source):
assert "def _interpreter_shutting_down(" in source
assert "#58720" in source