diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 93c4684fc20..ba51fdbd70a 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -3391,7 +3391,7 @@ async def _broadcast_event(channel: str, payload: str) -> None: except Exception: # Subscriber went away mid-send; the /api/events finally clause # will remove it from the registry on its next iteration. - pass + _log.warning("broadcast send failed for subscriber on %s", channel, exc_info=True) def _channel_or_close_code(ws: WebSocket) -> Optional[str]: diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index f5c06205621..d3143a4092a 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -2325,7 +2325,34 @@ class TestPtyWebSocket: with self.client.websocket_connect(pub_path) as pub: pub.send_text('{"type":"tool.start","payload":{"tool_id":"t1"}}') - received = sub.receive_text() + # Yield control so the server-side broadcast handler can + # process the frame. TestClient runs the ASGI app in a + # background thread; a small sleep gives that thread time + # to call _broadcast_event before we start blocking on + # receive_text(). Without this, under heavy CI load the + # receive can race the broadcast and hang until + # pytest-timeout kills us. + import queue, threading + recv_q: queue.Queue = queue.Queue() + + def _recv(): + try: + recv_q.put(sub.receive_text()) + except Exception as exc: + recv_q.put(exc) + + t = threading.Thread(target=_recv, daemon=True) + t.start() + try: + received = recv_q.get(timeout=10.0) + except queue.Empty: + raise AssertionError( + "broadcast not received within 10s — server likely " + "dropped the frame silently (see _broadcast_event " + "except Exception: pass)" + ) + if isinstance(received, Exception): + raise received assert "tool.start" in received assert '"tool_id":"t1"' in received diff --git a/tests/tools/test_browser_secret_exfil.py b/tests/tools/test_browser_secret_exfil.py index 893fb11fe74..82fa7e490e1 100644 --- a/tests/tools/test_browser_secret_exfil.py +++ b/tests/tools/test_browser_secret_exfil.py @@ -31,7 +31,13 @@ class TestBrowserSecretExfil: def test_allows_normal_url(self): """Normal URLs pass the secret check (may fail for other reasons).""" from tools.browser_tool import browser_navigate - result = browser_navigate("https://github.com/NousResearch/hermes-agent") + # Patch the actual browser command — we only care that the secret + # check doesn't block a clean URL, not that Chrome starts in CI. + mock_result = {"success": True, "data": {"title": "ok", "url": "https://github.com/NousResearch/hermes-agent"}} + with patch("tools.browser_tool._run_browser_command", return_value=mock_result), \ + patch("tools.browser_tool._get_session_info", return_value={"_first_nav": False}), \ + patch("tools.browser_tool._is_local_backend", return_value=True): + result = browser_navigate("https://github.com/NousResearch/hermes-agent") parsed = json.loads(result) # Should NOT be blocked by secret detection assert "API key or token" not in parsed.get("error", "")