fix(dashboard): OAuth start routes resolve pollers late so test mocks intercept the spawned thread
The oauth router imported _nous_poller/_minimax_poller/_xai_device_poller
from web_server_oauth at module level, so tests patching the owning module
("hermes_cli.web_server_oauth._minimax_poller") patched a binding the
router never read. The REAL poller then ran on the leaked daemon thread,
called the live MiniMax token endpoint from CI, and the in-flight
getaddrinfo segfaulted the interpreter during a later test's fixture setup
(CI run 34323790818, tests/hermes_cli/test_web_oauth_dispatch.py flake).
Route the three pollers through the existing late() seam (web_deps), the
same mechanism every other monkeypatch-sensitive symbol in this router
already uses, so the patch wins at thread-spawn time. Regression test
proves the mock intercepts and the real poller body never runs; it fails
on the old module-level import (sabotage-verified).
This commit is contained in:
@@ -18,7 +18,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from hermes_cli.web_deps import LateState, late
|
||||
from hermes_cli.web_server_oauth import (
|
||||
_external_process_cli_command, _minimax_poller, _nous_plain_poller, _nous_promotion_poller, _oauth_profile_name, _oauth_sessions, _oauth_sessions_lock, _truncate_token, _xai_device_poller,
|
||||
_external_process_cli_command, _oauth_profile_name, _oauth_sessions, _oauth_sessions_lock, _truncate_token,
|
||||
)
|
||||
from hermes_cli.web_models import OAuthSubmitBody
|
||||
from hermes_cli.web_routers._common import scoped_to_thread
|
||||
@@ -31,6 +31,14 @@ _profile_scope = late("_profile_scope", "hermes_cli.web_server_profiles")
|
||||
_require_token = late("_require_token")
|
||||
_resolve_profile_dir = late("_resolve_profile_dir", "hermes_cli.web_server_profiles")
|
||||
_OAUTH_PROVIDER_CATALOG = LateState("_OAUTH_PROVIDER_CATALOG", "hermes_cli.web_server_oauth")
|
||||
# Pollers are late-bound: they run on a background thread AFTER the route returns, so a
|
||||
# test's monkeypatch on web_server_oauth must win at spawn time, not router-import time.
|
||||
# A direct import here made those mocks no-ops — the real poller then hit the network
|
||||
# from the leaked thread and segfaulted a later test's collection (CI flake, 2026-09-09).
|
||||
_nous_plain_poller = late("_nous_plain_poller", "hermes_cli.web_server_oauth")
|
||||
_nous_promotion_poller = late("_nous_promotion_poller", "hermes_cli.web_server_oauth")
|
||||
_minimax_poller = late("_minimax_poller", "hermes_cli.web_server_oauth")
|
||||
_xai_device_poller = late("_xai_device_poller", "hermes_cli.web_server_oauth")
|
||||
|
||||
_CODEX_ISSUER = "https://auth.openai.com"
|
||||
_JSON_HEADERS = {"Content-Type": "application/json"}
|
||||
|
||||
@@ -114,6 +114,56 @@ def test_minimax_login_does_not_launch_anthropic_flow():
|
||||
|
||||
|
||||
|
||||
def test_minimax_start_route_honors_poller_mock_on_owning_module(tmp_path, monkeypatch):
|
||||
"""A monkeypatch on ``web_server_oauth._minimax_poller`` must intercept the
|
||||
background thread the /start route spawns.
|
||||
|
||||
Regression: the router imported the poller functions at module level, so a
|
||||
test's patch on the owning module was a no-op — the REAL poller ran on the
|
||||
leaked daemon thread, hit the live MiniMax endpoint from CI, and its
|
||||
getaddrinfo call segfaulted a later test's collection (CI flake, run
|
||||
34323790818). The router must resolve pollers late, at spawn time.
|
||||
"""
|
||||
import threading
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
fake_user_code_resp = {
|
||||
"user_code": "ABCD-1234",
|
||||
"verification_uri": "https://api.minimax.io/oauth/verify",
|
||||
"expired_in": 600,
|
||||
"interval": 2000,
|
||||
"state": "stub-state",
|
||||
}
|
||||
mock_ran = threading.Event()
|
||||
real_network_hit = threading.Event()
|
||||
|
||||
def fake_poller(session_id):
|
||||
mock_ran.set()
|
||||
|
||||
def fail_poll_token(**kwargs):
|
||||
real_network_hit.set()
|
||||
raise AssertionError("real _minimax_poller body must not run under the mock")
|
||||
|
||||
with patch(
|
||||
"hermes_cli.auth._minimax_request_user_code",
|
||||
return_value=fake_user_code_resp,
|
||||
), patch(
|
||||
"hermes_cli.auth._minimax_pkce_pair",
|
||||
return_value=("verifier-stub", "challenge-stub", "stub-state"),
|
||||
), patch(
|
||||
"hermes_cli.auth._minimax_poll_token",
|
||||
fail_poll_token,
|
||||
), patch(
|
||||
"hermes_cli.web_server_oauth._minimax_poller",
|
||||
fake_poller,
|
||||
):
|
||||
resp = client.post("/api/providers/oauth/minimax-oauth/start", headers=HEADERS)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert mock_ran.wait(timeout=5), "patched poller never ran — router bypassed the seam"
|
||||
assert not real_network_hit.is_set()
|
||||
_web_server_oauth._oauth_sessions.pop(resp.json()["session_id"], None)
|
||||
|
||||
|
||||
def test_oauth_provider_status_uses_profile_query(tmp_path, monkeypatch):
|
||||
from hermes_cli import web_server as ws
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
Reference in New Issue
Block a user