hermes-agent/tests/gateway/test_shutdown_watchdog.py

69 lines
2.0 KiB
Python

"""Shutdown watchdog + loop heartbeat coverage for #66892.
The drain path is asyncio-based; a frozen loop makes every asyncio timeout
structurally unable to fire. These tests pin the out-of-loop backstop
(thread watchdog) and the loop-liveness heartbeat file contract.
"""
from __future__ import annotations
import asyncio
import json
import threading
import time
from unittest.mock import patch
import pytest
from gateway.shutdown_watchdog import (
DEFAULT_SHUTDOWN_WATCHDOG_GRACE_S,
arm_shutdown_watchdog,
get_loop_heartbeat_path,
get_shutdown_watchdog_dump_path,
loop_heartbeat_forever,
resolve_shutdown_watchdog_delay,
write_loop_heartbeat,
)
def test_resolve_shutdown_watchdog_delay_adds_grace():
assert resolve_shutdown_watchdog_delay(180) == 180 + DEFAULT_SHUTDOWN_WATCHDOG_GRACE_S
assert resolve_shutdown_watchdog_delay(0) == DEFAULT_SHUTDOWN_WATCHDOG_GRACE_S
assert resolve_shutdown_watchdog_delay("bad") == DEFAULT_SHUTDOWN_WATCHDOG_GRACE_S
assert resolve_shutdown_watchdog_delay(10, grace_s=5) == 15.0
def test_arm_shutdown_watchdog_fires_with_dump_and_exit(tmp_path):
done = threading.Event()
fired = threading.Event()
dump = tmp_path / "logs" / "watchdog.log"
snapshot_calls = []
exit_codes = []
def snapshot():
snapshot_calls.append(1)
return {"active_agents": 1, "draining": True}
def fake_exit(code):
exit_codes.append(code)
fired.set()
with patch("gateway.shutdown_watchdog.os._exit", side_effect=fake_exit):
arm_shutdown_watchdog(
0.15,
done_event=done,
snapshot_fn=snapshot,
dump_path=dump,
exit_code=9,
)
assert fired.wait(timeout=5.0), "watchdog did not fire"
assert exit_codes == [9]
assert snapshot_calls == [1]
assert dump.is_file()
text = dump.read_text(encoding="utf-8")
assert "shutdown_watchdog_fired" in text
assert "faulthandler dump" in text
assert get_shutdown_watchdog_dump_path(tmp_path).name == "gateway-shutdown-watchdog.log"