Hermes-agent
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Tests for the activity-heartbeat behavior of the blocking gateway approval wait.
|
||||
|
||||
Regression test for false gateway inactivity timeouts firing while the agent
|
||||
is legitimately blocked waiting for a user to respond to a dangerous-command
|
||||
approval prompt. Before the fix, ``entry.event.wait(timeout=...)`` blocked
|
||||
silently — no ``_touch_activity()`` calls — and the gateway's inactivity
|
||||
watchdog (``agent.gateway_timeout``, default 1800s) would kill the agent
|
||||
while the user was still choosing whether to approve.
|
||||
|
||||
The fix polls the event in short slices and fires ``touch_activity_if_due``
|
||||
between slices, mirroring ``_wait_for_process`` in ``tools/environments/base.py``.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def _clear_approval_state():
|
||||
"""Reset all module-level approval state between tests."""
|
||||
from tools import approval as mod
|
||||
mod._gateway_queues.clear()
|
||||
mod._gateway_notify_cbs.clear()
|
||||
mod._session_approved.clear()
|
||||
mod._permanent_approved.clear()
|
||||
mod._pending.clear()
|
||||
|
||||
|
||||
class TestApprovalHeartbeat:
|
||||
"""The blocking gateway approval wait must fire activity heartbeats.
|
||||
|
||||
Without heartbeats, the gateway's inactivity watchdog kills the agent
|
||||
thread while it's legitimately waiting for a slow user to respond to
|
||||
an approval prompt (observed in real user logs: MRB, April 2026).
|
||||
"""
|
||||
|
||||
SESSION_KEY = "heartbeat-test-session"
|
||||
|
||||
def setup_method(self):
|
||||
_clear_approval_state()
|
||||
self._saved_env = {
|
||||
k: os.environ.get(k)
|
||||
for k in ("HERMES_GATEWAY_SESSION", "HERMES_YOLO_MODE",
|
||||
"HERMES_SESSION_KEY")
|
||||
}
|
||||
os.environ.pop("HERMES_YOLO_MODE", None)
|
||||
os.environ["HERMES_GATEWAY_SESSION"] = "1"
|
||||
# The blocking wait path reads the session key via contextvar OR
|
||||
# os.environ fallback. Contextvars don't propagate across threads
|
||||
# by default, so env var is the portable way to drive this in tests.
|
||||
os.environ["HERMES_SESSION_KEY"] = self.SESSION_KEY
|
||||
|
||||
def teardown_method(self):
|
||||
for k, v in self._saved_env.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
_clear_approval_state()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user