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:
committed by
GitHub
parent
e7beaaf184
commit
f73364b1c4
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user