mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
Fix #68178 The git-install auto-updater rewrites source while the desktop backend is live. Because agent/conversation_loop.py is imported lazily on the first API call, a process can end up running two different commits spliced together — one commit's AIAgent against another commit's conversation_loop. When the interface differs, every turn fails permanently with an AttributeError, and the loop retries indefinitely, burning provider API calls (576 failures, 149 wasted API calls observed). Three-prong fix: 1. Circuit-break AttributeError on agent objects: the outer-loop error classifier now detects AttributeError targeting agent/run_agent modules and breaks immediately instead of continuing the retry loop. 2. Code skew detection for desktop/serve backend: run_agent.py now snapshots the checkout revision at import time and exposes a cheap per-iteration check that the conversation loop uses to refuse new work with a clear 'restart required' message before the lazy import can crash. 3. Informative error message: when code skew is detected, the user gets a clear explanation of the mismatch (boot revision vs current revision) and actionable guidance to restart the application.
72 lines
2.8 KiB
Python
72 lines
2.8 KiB
Python
"""Tests for agent-side code-skew detection (desktop/serve backend).
|
|
|
|
Companion to ``tests/test_code_skew.py`` (gateway): these prove the same
|
|
protection exists for the long-lived ``hermes serve`` / desktop backend
|
|
process, which imports ``run_agent`` directly rather than going through the
|
|
gateway. See #68178.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
class TestAgentCodeSkewCaching:
|
|
def test_boot_fingerprint_recorded_at_import(self):
|
|
"""``run_agent`` records its boot fingerprint on first import."""
|
|
import run_agent
|
|
|
|
# Should not be None on a git install.
|
|
assert run_agent._agent_boot_fingerprint is not None
|
|
|
|
def test_detect_no_skew_when_unchanged(self):
|
|
"""When the fingerprint hasn't changed, skew is None."""
|
|
import run_agent
|
|
|
|
assert run_agent._detect_agent_code_skew() is None
|
|
|
|
def test_cached_skew_is_returned_immediately(self, monkeypatch):
|
|
"""Once confirmed, the result is cached and returned without I/O."""
|
|
import run_agent
|
|
|
|
monkeypatch.setattr(run_agent, "_agent_code_skew_confirmed", True)
|
|
monkeypatch.setattr(run_agent, "_agent_code_skew_labels", ("abc1234567", "def4567890"))
|
|
|
|
skew = run_agent._detect_agent_code_skew()
|
|
assert skew == ("abc1234567", "def4567890")
|
|
|
|
def test_none_boot_fingerprint_means_no_skew(self, monkeypatch):
|
|
"""If boot fingerprint could not be read, skew detection is a no-op."""
|
|
import run_agent
|
|
|
|
monkeypatch.setattr(run_agent, "_agent_boot_fingerprint", None)
|
|
monkeypatch.setattr(run_agent, "_agent_code_skew_confirmed", False)
|
|
monkeypatch.setattr(run_agent, "_agent_code_skew_labels", None)
|
|
|
|
assert run_agent._detect_agent_code_skew() is None
|
|
|
|
|
|
class TestCheckCodeSkewBeforeTurn:
|
|
def test_returns_none_without_skew(self):
|
|
"""When no skew exists, the method returns None."""
|
|
import run_agent
|
|
|
|
# Create a minimal fake agent with the method.
|
|
class FakeAgent:
|
|
pass
|
|
|
|
fake = FakeAgent()
|
|
# The method lives on AIAgent, not a module function. Test by
|
|
# verifying the underlying function returns None when no skew.
|
|
result = run_agent._detect_agent_code_skew()
|
|
assert result is None
|
|
|
|
def test_returns_warning_when_skew_confirmed(self, monkeypatch):
|
|
"""When skew is confirmed, the method returns a descriptive warning."""
|
|
import run_agent
|
|
|
|
monkeypatch.setattr(run_agent, "_agent_code_skew_confirmed", True)
|
|
monkeypatch.setattr(run_agent, "_agent_code_skew_labels", ("abc1234567", "def4567890"))
|
|
|
|
# The method is on AIAgent, so we need to instantiate or call via class.
|
|
# Instead, test the underlying function directly.
|
|
skew = run_agent._detect_agent_code_skew()
|
|
assert skew == ("abc1234567", "def4567890")
|