d73ff9b0fb
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix Lockfile Fix / auto-fix-main (push) Has been cancelled
Nix Lockfile Fix / fix (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
Tests / save-durations (push) Has been cancelled
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
"""Tests for tui_gateway.entry.wait_for_mcp_discovery (PR #35245).
|
|
|
|
MCP tool discovery runs in a background daemon thread so a slow/dead server
|
|
can't freeze ``gateway.ready``. The agent snapshots its tool list once at
|
|
build time and never re-reads it, so ``_make_agent`` briefly joins the
|
|
discovery thread before building — bounded, so a dead server can't re-introduce
|
|
the startup hang, and a no-op once discovery has finished.
|
|
"""
|
|
|
|
import threading
|
|
import time
|
|
|
|
import tui_gateway.entry as entry
|
|
|
|
|
|
def _restore_thread_slot(saved):
|
|
entry._mcp_discovery_thread = saved
|
|
|
|
|
|
def test_no_thread_is_noop():
|
|
"""When no discovery thread was started (the common no-MCP case), the
|
|
helper returns immediately and never blocks."""
|
|
saved = entry._mcp_discovery_thread
|
|
try:
|
|
entry._mcp_discovery_thread = None
|
|
start = time.monotonic()
|
|
entry.wait_for_mcp_discovery(timeout=5.0)
|
|
assert time.monotonic() - start < 0.1
|
|
finally:
|
|
_restore_thread_slot(saved)
|
|
|
|
|
|
def test_already_finished_thread_is_noop():
|
|
"""A thread that has already finished is not joined-on (dead thread)."""
|
|
saved = entry._mcp_discovery_thread
|
|
try:
|
|
t = threading.Thread(target=lambda: None, daemon=True)
|
|
t.start()
|
|
t.join() # ensure it's finished
|
|
entry._mcp_discovery_thread = t
|
|
start = time.monotonic()
|
|
entry.wait_for_mcp_discovery(timeout=5.0)
|
|
assert time.monotonic() - start < 0.1
|
|
finally:
|
|
_restore_thread_slot(saved)
|
|
|
|
|
|
def test_fast_thread_is_joined():
|
|
"""A reachable-but-still-connecting (fast) server lands before the agent
|
|
snapshots tools — the helper waits for it to finish."""
|
|
saved = entry._mcp_discovery_thread
|
|
try:
|
|
t = threading.Thread(target=lambda: time.sleep(0.05), daemon=True)
|
|
t.start()
|
|
entry._mcp_discovery_thread = t
|
|
entry.wait_for_mcp_discovery(timeout=1.0)
|
|
assert not t.is_alive() # joined to completion
|
|
finally:
|
|
_restore_thread_slot(saved)
|
|
|
|
|
|
def test_hung_thread_is_bounded_by_timeout():
|
|
"""A slow/dead server must NOT re-introduce the startup hang — the join is
|
|
bounded by the timeout and returns even though the thread is still alive."""
|
|
saved = entry._mcp_discovery_thread
|
|
stop = threading.Event()
|
|
try:
|
|
t = threading.Thread(target=stop.wait, daemon=True) # blocks until set
|
|
t.start()
|
|
entry._mcp_discovery_thread = t
|
|
start = time.monotonic()
|
|
entry.wait_for_mcp_discovery(timeout=0.3)
|
|
elapsed = time.monotonic() - start
|
|
assert 0.25 <= elapsed < 1.0 # bounded near the timeout, not forever
|
|
assert t.is_alive() # thread still running; we did not block on it
|
|
finally:
|
|
stop.set()
|
|
_restore_thread_slot(saved)
|