fix(ci): stabilize main test suite regressions (#17660)

* fix: stabilize main test suite regressions

* test(agent): update MiniMax normalization expectation

* test: stabilize remaining CI assertions

* test: harden config helper monkeypatching

* test: harden CI-only assertions

* fix(agent): propagate fast streaming interrupts
This commit is contained in:
Stephen Schoettler
2026-04-29 23:18:55 -07:00
committed by GitHub
parent e7beaaf184
commit f73364b1c4
37 changed files with 450 additions and 127 deletions

View File

@@ -6,6 +6,7 @@ import shutil
import signal
import subprocess
import tempfile
import time
from tools.environments.base import BaseEnvironment, _pipe_stdin
@@ -369,6 +370,11 @@ class LocalEnvironment(BaseEnvironment):
preexec_fn=None if _IS_WINDOWS else os.setsid,
cwd=self.cwd,
)
if not _IS_WINDOWS:
try:
proc._hermes_pgid = os.getpgid(proc.pid)
except ProcessLookupError:
pass
if stdin_data is not None:
_pipe_stdin(proc, stdin_data)
@@ -381,12 +387,42 @@ class LocalEnvironment(BaseEnvironment):
if _IS_WINDOWS:
proc.terminate()
else:
pgid = os.getpgid(proc.pid)
try:
pgid = os.getpgid(proc.pid)
except ProcessLookupError:
pgid = getattr(proc, "_hermes_pgid", None)
if pgid is None:
raise
os.killpg(pgid, signal.SIGTERM)
deadline = time.monotonic() + 1.0
while time.monotonic() < deadline:
if proc.poll() is not None:
try:
os.killpg(pgid, 0)
except ProcessLookupError:
return
time.sleep(0.05)
# The shell can exit quickly while a child in the same process
# group is still shutting down. Escalate based on the process
# group, not just the shell wrapper, so interrupted commands do
# not leave orphaned grandchildren under load.
try:
# _IS_WINDOWS is guarded by the enclosing else branch.
os.killpg(pgid, signal.SIGKILL)
except ProcessLookupError:
return
try:
proc.wait(timeout=1.0)
except subprocess.TimeoutExpired:
os.killpg(pgid, signal.SIGKILL)
pass
deadline = time.monotonic() + 1.0
while time.monotonic() < deadline:
try:
os.killpg(pgid, 0)
except ProcessLookupError:
return
time.sleep(0.05)
except (ProcessLookupError, PermissionError):
try:
proc.kill()

View File

@@ -489,12 +489,15 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
task_data = _read_tracker.setdefault(task_id, {
"last_key": None, "consecutive": 0,
"read_history": set(), "dedup": {},
"dedup_hits": {},
"dedup_hits": {}, "read_timestamps": {},
})
# Backward-compat for pre-existing tracker entries that predate
# dedup_hits (long-lived task or crossed an upgrade boundary).
# dedup_hits/read_timestamps (long-lived task or crossed an
# upgrade boundary).
if "dedup_hits" not in task_data:
task_data["dedup_hits"] = {}
if "read_timestamps" not in task_data:
task_data["read_timestamps"] = {}
cached_mtime = task_data.get("dedup", {}).get(dedup_key)
if cached_mtime is not None:

View File

@@ -915,11 +915,12 @@ class MCPServerTask:
except Exception:
logger.exception("MCP server '%s': dynamic tool refresh failed", self.name)
def _schedule_tools_refresh(self) -> None:
def _schedule_tools_refresh(self) -> asyncio.Task:
"""Schedule a background tool refresh and keep it strongly referenced."""
task = asyncio.create_task(self._refresh_tools_task())
self._pending_refresh_tasks.add(task)
task.add_done_callback(self._pending_refresh_tasks.discard)
return task
def _make_message_handler(self):
"""Build a ``message_handler`` callback for ``ClientSession``.
@@ -950,6 +951,10 @@ class MCPServerTask:
# a separate task and let the handler return
# promptly.
self._schedule_tools_refresh()
# Yield one loop tick so tests and short-lived
# notification contexts can observe the scheduled
# refresh without awaiting the full server RPC.
await asyncio.sleep(0)
case PromptListChangedNotification():
logger.debug("MCP server '%s': prompts/list_changed (ignored)", self.name)
case ResourceListChangedNotification():
@@ -2005,8 +2010,12 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
}, ensure_ascii=False)
async def _call():
async with server._rpc_lock:
rpc_lock = getattr(server, "_rpc_lock", None)
if rpc_lock is None:
result = await server.session.call_tool(tool_name, arguments=args)
else:
async with rpc_lock:
result = await server.session.call_tool(tool_name, arguments=args)
# MCP CallToolResult has .content (list of content blocks) and .isError
if result.isError:
error_text = ""

View File

@@ -42,11 +42,19 @@ from tools.tool_backend_helpers import managed_nous_tools_enabled, resolve_opena
logger = logging.getLogger(__name__)
try:
from hermes_cli.config import get_env_value
except ImportError:
def get_env_value(name, default=None):
def get_env_value(name, default=None):
"""Read env values through the live config module.
Tests may monkeypatch and later restore ``hermes_cli.config.get_env_value``
before this module is imported. Resolve the helper at call time so STT does
not keep a stale imported function for the rest of the test process.
"""
try:
from hermes_cli.config import get_env_value as _get_env_value
except ImportError:
return os.getenv(name, default)
value = _get_env_value(name)
return default if value is None else value
# ---------------------------------------------------------------------------
# Optional imports — graceful degradation

View File

@@ -44,11 +44,19 @@ from urllib.parse import urljoin
from hermes_constants import display_hermes_home
logger = logging.getLogger(__name__)
try:
from hermes_cli.config import get_env_value
except ImportError:
def get_env_value(name, default=None):
def get_env_value(name, default=None):
"""Read env values through the live config module.
Tests may monkeypatch and later restore ``hermes_cli.config.get_env_value``
before this module is imported. Resolve the helper at call time so TTS does
not keep a stale imported function for the rest of the test process.
"""
try:
from hermes_cli.config import get_env_value as _get_env_value
except ImportError:
return os.getenv(name, default)
value = _get_env_value(name)
return default if value is None else value
from tools.managed_tool_gateway import resolve_managed_tool_gateway
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway, resolve_openai_audio_api_key
from tools.xai_http import hermes_xai_user_agent