mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-26 01:01:40 +00:00
Fix variable name breakage (run_agent, hermes_constants, etc.) where import rewriter changed 'import X' to 'import hermes_agent.Y' but test code still referenced 'X' as a variable name. Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where single files became directories. Fix hardcoded file paths in tests pointing to old locations. Fix tool registry to discover tools in subpackage directories. Fix stale import in hermes_agent/tools/__init__.py. Part of #14182, #14183
193 lines
6.5 KiB
Python
193 lines
6.5 KiB
Python
import asyncio
|
|
import sys
|
|
import types
|
|
from types import SimpleNamespace
|
|
|
|
|
|
sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None))
|
|
sys.modules.setdefault("firecrawl", types.SimpleNamespace(Firecrawl=object))
|
|
sys.modules.setdefault("fal_client", types.SimpleNamespace())
|
|
|
|
import hermes_agent.cron.scheduler as cron_scheduler
|
|
import hermes_agent.gateway.run as gateway_run
|
|
from hermes_agent.agent import loop as run_agent
|
|
from hermes_agent.gateway.config import Platform
|
|
from hermes_agent.gateway.session import SessionSource
|
|
|
|
|
|
def _patch_agent_bootstrap(monkeypatch):
|
|
monkeypatch.setattr(
|
|
run_agent,
|
|
"get_tool_definitions",
|
|
lambda **kwargs: [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "terminal",
|
|
"description": "Run shell commands.",
|
|
"parameters": {"type": "object", "properties": {}},
|
|
},
|
|
}
|
|
],
|
|
)
|
|
monkeypatch.setattr(run_agent, "check_toolset_requirements", lambda: {})
|
|
|
|
|
|
def _codex_message_response(text: str):
|
|
return SimpleNamespace(
|
|
output=[
|
|
SimpleNamespace(
|
|
type="message",
|
|
content=[SimpleNamespace(type="output_text", text=text)],
|
|
)
|
|
],
|
|
usage=SimpleNamespace(input_tokens=5, output_tokens=3, total_tokens=8),
|
|
status="completed",
|
|
model="gpt-5-codex",
|
|
)
|
|
|
|
|
|
class _UnauthorizedError(RuntimeError):
|
|
def __init__(self):
|
|
super().__init__("Error code: 401 - unauthorized")
|
|
self.status_code = 401
|
|
|
|
|
|
class _FakeOpenAI:
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
def close(self):
|
|
return None
|
|
|
|
|
|
class _Codex401ThenSuccessAgent(run_agent.AIAgent):
|
|
refresh_attempts = 0
|
|
last_init = {}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("skip_context_files", True)
|
|
kwargs.setdefault("skip_memory", True)
|
|
kwargs.setdefault("max_iterations", 4)
|
|
type(self).last_init = dict(kwargs)
|
|
super().__init__(*args, **kwargs)
|
|
self._cleanup_task_resources = lambda task_id: None
|
|
self._persist_session = lambda messages, history=None: None
|
|
self._save_trajectory = lambda messages, user_message, completed: None
|
|
self._save_session_log = lambda messages: None
|
|
|
|
def _try_refresh_codex_client_credentials(self, *, force: bool = True) -> bool:
|
|
type(self).refresh_attempts += 1
|
|
return True
|
|
|
|
def run_conversation(self, user_message: str, conversation_history=None, task_id=None):
|
|
calls = {"api": 0}
|
|
|
|
def _fake_api_call(api_kwargs):
|
|
calls["api"] += 1
|
|
if calls["api"] == 1:
|
|
raise _UnauthorizedError()
|
|
return _codex_message_response("Recovered via refresh")
|
|
|
|
self._interruptible_api_call = _fake_api_call
|
|
return super().run_conversation(user_message, conversation_history=conversation_history, task_id=task_id)
|
|
|
|
|
|
def test_cron_run_job_codex_path_handles_internal_401_refresh(monkeypatch):
|
|
_patch_agent_bootstrap(monkeypatch)
|
|
monkeypatch.setattr(run_agent, "OpenAI", _FakeOpenAI)
|
|
monkeypatch.setattr(run_agent, "AIAgent", _Codex401ThenSuccessAgent)
|
|
monkeypatch.setattr(
|
|
"hermes_agent.cli.runtime_provider.resolve_runtime_provider",
|
|
lambda requested=None: {
|
|
"provider": "openai-codex",
|
|
"api_mode": "codex_responses",
|
|
"base_url": "https://chatgpt.com/backend-api/codex",
|
|
"api_key": "codex-token",
|
|
},
|
|
)
|
|
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
|
|
|
|
_Codex401ThenSuccessAgent.refresh_attempts = 0
|
|
_Codex401ThenSuccessAgent.last_init = {}
|
|
|
|
success, output, final_response, error = cron_scheduler.run_job(
|
|
{"id": "job-1", "name": "Codex Refresh Test", "prompt": "ping", "model": "gpt-5.3-codex"}
|
|
)
|
|
|
|
assert success is True
|
|
assert error is None
|
|
assert final_response == "Recovered via refresh"
|
|
assert "Recovered via refresh" in output
|
|
assert _Codex401ThenSuccessAgent.refresh_attempts == 1
|
|
assert _Codex401ThenSuccessAgent.last_init["provider"] == "openai-codex"
|
|
assert _Codex401ThenSuccessAgent.last_init["api_mode"] == "codex_responses"
|
|
|
|
|
|
def test_gateway_run_agent_codex_path_handles_internal_401_refresh(monkeypatch):
|
|
_patch_agent_bootstrap(monkeypatch)
|
|
monkeypatch.setattr(run_agent, "OpenAI", _FakeOpenAI)
|
|
monkeypatch.setattr(run_agent, "AIAgent", _Codex401ThenSuccessAgent)
|
|
monkeypatch.setattr(
|
|
gateway_run,
|
|
"_resolve_runtime_agent_kwargs",
|
|
lambda: {
|
|
"provider": "openai-codex",
|
|
"api_mode": "codex_responses",
|
|
"base_url": "https://chatgpt.com/backend-api/codex",
|
|
"api_key": "codex-token",
|
|
},
|
|
)
|
|
monkeypatch.setenv("HERMES_TOOL_PROGRESS", "false")
|
|
monkeypatch.setenv("HERMES_MODEL", "gpt-5.3-codex")
|
|
|
|
_Codex401ThenSuccessAgent.refresh_attempts = 0
|
|
_Codex401ThenSuccessAgent.last_init = {}
|
|
|
|
runner = gateway_run.GatewayRunner.__new__(gateway_run.GatewayRunner)
|
|
runner.adapters = {}
|
|
runner._ephemeral_system_prompt = ""
|
|
runner._prefill_messages = []
|
|
runner._reasoning_config = None
|
|
runner._provider_routing = {}
|
|
runner._fallback_model = None
|
|
runner._running_agents = {}
|
|
from unittest.mock import MagicMock, AsyncMock
|
|
runner.hooks = MagicMock()
|
|
runner.hooks.emit = AsyncMock()
|
|
runner.hooks.loaded_hooks = []
|
|
runner._session_db = None
|
|
# Ensure model resolution returns the codex model even if xdist
|
|
# leaked env vars cleared HERMES_MODEL.
|
|
monkeypatch.setattr(
|
|
gateway_run.GatewayRunner,
|
|
"_resolve_turn_agent_config",
|
|
lambda self, msg, model, runtime: {
|
|
"model": model or "gpt-5.3-codex",
|
|
"runtime": runtime,
|
|
},
|
|
)
|
|
|
|
source = SessionSource(
|
|
platform=Platform.LOCAL,
|
|
chat_id="cli",
|
|
chat_name="CLI",
|
|
chat_type="dm",
|
|
user_id="user-1",
|
|
)
|
|
|
|
result = asyncio.run(
|
|
runner._run_agent(
|
|
message="ping",
|
|
context_prompt="",
|
|
history=[],
|
|
source=source,
|
|
session_id="session-1",
|
|
session_key="agent:main:local:dm",
|
|
)
|
|
)
|
|
|
|
assert result["final_response"] == "Recovered via refresh"
|
|
assert _Codex401ThenSuccessAgent.refresh_attempts == 1
|
|
assert _Codex401ThenSuccessAgent.last_init["provider"] == "openai-codex"
|
|
assert _Codex401ThenSuccessAgent.last_init["api_mode"] == "codex_responses"
|