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:
Teknium
2026-09-09 02:46:34 -07:00
parent b35c0284a1
commit 0f3199bd65
2 changed files with 59 additions and 1 deletions

View File

@@ -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"}

View File

@@ -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