Files
hermes-agent/tests/tools/test_startup_latency_regressions.py
T
Teknium 55f9e472a0 perf(cli): sub-400ms warm startup — probe-mode check_fns, lazy MCP SDK, banner snapshot, parallel worktree add
Cold CLI time-to-banner was ~1.8s (hermes) / ~2.8s (hermes -w). The banner
path was paying for work the session doesn't need before first input:

- aux availability probes built REAL OpenAI/httpx clients (openai import
  ~0.3s + SSL context) just to answer check_fns. New aux_probe_mode()
  returns a cache-excluded stub; resolution policy unchanged.
- tools/mcp_tool imported the mcp SDK (~260ms, mcp.types pydantic model
  construction) at module import even with zero MCP servers configured.
  SDK import is now lazy behind _ensure_mcp_sdk(); _MCP_AVAILABLE is a
  find_spec probe so every existing gate/test keeps its semantics.
- banner blocked 500ms on the update-check prefetch; now waits 50ms and
  defers the warning line to a daemon thread (prints above the prompt).
- banner recomputed get_tool_definitions + skills scan + git state every
  launch; now snapshotted to ~/.hermes/cache/banner_snapshot.json keyed on
  (config.yaml, .env, checkout rev, toolsets) and replayed on warm launches
  with a background refresh. Agent tool list is still computed fresh.
- _resolve_active_context_length probed the Nous portal /models (~200ms
  network) per launch; the tool-search gate now prefers the on-disk
  context cache when present.
- schema reconciliation re-executed SCHEMA_SQL in a scratch SQLite DB
  (~85ms) per SessionDB(); the reference parse is now disk-memoized by
  DDL hash (live-DB diffing still runs every startup).
- bundled-skills sync (~120-170ms rglob/hash) moved off the startup path
  to a daemon thread; plugin discovery starts in the background and every
  synchronous consumer joins via discover_plugins().
- hermes_cli.auth imported httpx eagerly (~30ms); now a lazy proxy that
  test monkeypatching still reaches (setattr forwards to the real module).
- fast chat launch: unambiguous 'hermes'/'hermes chat' invocations skip
  building all ~40 subcommand parsers (bails to full dispatch on anything
  else, incl. container mode).
- -w path: git worktree add runs with checkout.workers=8 (0.6s→0.2s) and
  overlaps HermesCLI construction; --skills preload runs in the background
  and is folded in at agent init (finalize_preloaded_skills, same
  fail-loud contract for fully-unknown skill lists); stale-worktree prune
  moved off the banner path.

Warm results (PTY time-to-banner, 5-run): hermes 1.80s → 0.38-0.40s;
hermes -w -s hermes-agent-dev --yolo 2.82s → 0.57-0.69s.
2026-08-10 10:40:19 -07:00

212 lines
7.4 KiB
Python

"""Startup-latency regressions: probe-mode aux clients, lazy MCP SDK,
non-blocking banner update check.
These pin the CLI cold-start contract established in the sub-400ms pass:
- check_fn availability probes must not import the OpenAI SDK or build
real HTTP clients (aux_probe_mode).
- tools/mcp_tool must not import the `mcp` SDK at module import time.
- build_welcome_banner must not block on the update-check prefetch.
"""
import sys
import threading
import time
from unittest.mock import patch
import pytest
class TestAuxProbeMode:
def test_probe_mode_returns_stub_without_openai_import(self):
import agent.auxiliary_client as aux
with aux.aux_probe_mode():
client = aux._create_openai_client(api_key="k", base_url="https://x.invalid/v1")
assert isinstance(client, aux._AuxProbeClientStub)
assert client.api_key == "k"
def test_probe_stub_never_cached(self):
import agent.auxiliary_client as aux
stub = aux._AuxProbeClientStub()
key = ("probe-test", False, "", "", "", (), False, "", None, "m")
aux._store_cached_client(key, stub, "m")
with aux._client_cache_lock:
assert key not in aux._client_cache
def test_probe_stub_raises_on_runtime_use(self):
import agent.auxiliary_client as aux
stub = aux._AuxProbeClientStub()
with pytest.raises(RuntimeError, match="availability checks only"):
_ = stub.chat
def test_probe_mode_is_scoped_and_reentrant(self):
import agent.auxiliary_client as aux
assert not aux._aux_probe_active()
with aux.aux_probe_mode():
assert aux._aux_probe_active()
with aux.aux_probe_mode():
assert aux._aux_probe_active()
# inner exit must not clear the outer scope
assert aux._aux_probe_active()
assert not aux._aux_probe_active()
def test_probe_mode_is_thread_local(self):
import agent.auxiliary_client as aux
seen = {}
def other_thread():
seen["active"] = aux._aux_probe_active()
with aux.aux_probe_mode():
t = threading.Thread(target=other_thread)
t.start()
t.join()
assert seen["active"] is False
def test_maybe_wrap_anthropic_passes_stub_through(self):
import agent.auxiliary_client as aux
stub = aux._AuxProbeClientStub(base_url="https://api.anthropic.com")
out = aux._maybe_wrap_anthropic(stub, "m", "key", "https://api.anthropic.com")
assert out is stub
def test_to_async_client_passes_stub_through(self):
import agent.auxiliary_client as aux
stub = aux._AuxProbeClientStub()
client, model = aux._to_async_client(stub, "m")
assert client is stub
assert model == "m"
class TestVisionCheckUsesProbeMode:
def test_check_vision_requirements_enters_probe_mode(self):
from tools import vision_tools
import agent.auxiliary_client as aux
states = []
def fake_resolver(*a, **k):
states.append(aux._aux_probe_active())
return ("nous", aux._AuxProbeClientStub(), "m")
with patch.object(aux, "resolve_vision_provider_client", fake_resolver):
assert vision_tools.check_vision_requirements() is True
assert states and all(states)
class TestLazyMcpSdk:
def test_module_import_does_not_import_mcp_sdk(self):
"""Importing tools.mcp_tool must not pull in the `mcp` package."""
import subprocess
code = (
"import sys; sys.modules.pop('mcp', None); "
"import tools.mcp_tool; "
"assert 'mcp' not in sys.modules, 'mcp imported eagerly'; "
"print('ok')"
)
proc = subprocess.run(
[sys.executable, "-c", code],
capture_output=True, text=True, timeout=120,
)
assert proc.returncode == 0, proc.stderr
assert "ok" in proc.stdout
def test_availability_flag_reflects_find_spec(self):
import importlib.util
from tools import mcp_tool
expected = importlib.util.find_spec("mcp") is not None
assert mcp_tool._MCP_AVAILABLE is expected
def test_ensure_mcp_sdk_binds_symbols(self):
import importlib.util
from tools import mcp_tool
if importlib.util.find_spec("mcp") is None:
pytest.skip("mcp SDK not installed")
assert mcp_tool._ensure_mcp_sdk() is True
assert mcp_tool.ClientSession is not None
assert mcp_tool.stdio_client is not None
def test_ensure_respects_patched_unavailable(self):
from tools import mcp_tool
with patch.object(mcp_tool, "_MCP_AVAILABLE", False):
assert mcp_tool._ensure_mcp_sdk() is False
def test_lazy_symbol_getattr_resolves_via_ensure(self):
import importlib.util
from tools import mcp_tool
if importlib.util.find_spec("mcp") is None:
pytest.skip("mcp SDK not installed")
# getattr through the module (what mock.patch does when saving the
# original) must materialize the symbol instead of AttributeError.
assert getattr(mcp_tool, "StdioServerParameters") is not None
class TestBannerUpdateCheckNonBlocking:
def test_banner_does_not_block_on_pending_update_check(self):
"""When the prefetch hasn't finished, the banner path must return in
well under the old 500ms blocking wait."""
import hermes_cli.banner as banner
class _NullConsole:
def print(self, *a, **k):
pass
with patch.object(banner, "_update_check_done", threading.Event()), \
patch.object(banner, "_deferred_update_notice_started", False):
start = time.perf_counter()
behind = banner.get_update_result(timeout=0.05)
if behind is None and not banner._update_check_done.is_set():
banner._defer_update_notice(_NullConsole())
elapsed = time.perf_counter() - start
assert elapsed < 0.3, f"banner update check blocked {elapsed:.3f}s"
def test_deferred_notice_prints_when_result_lands(self):
import hermes_cli.banner as banner
printed = []
class _Console:
def print(self, msg, *a, **k):
printed.append(msg)
done = threading.Event()
with patch.object(banner, "_update_check_done", done), \
patch.object(banner, "_update_result", None), \
patch.object(banner, "_deferred_update_notice_started", False):
banner._defer_update_notice(_Console(), max_wait=5.0)
banner._update_result = 3
done.set()
deadline = time.time() + 5
while not printed and time.time() < deadline:
time.sleep(0.02)
assert printed, "deferred update notice never printed"
assert "3 commits behind" in printed[0]
def test_deferred_notice_silent_when_up_to_date(self):
import hermes_cli.banner as banner
printed = []
class _Console:
def print(self, msg, *a, **k):
printed.append(msg)
done = threading.Event()
with patch.object(banner, "_update_check_done", done), \
patch.object(banner, "_update_result", 0), \
patch.object(banner, "_deferred_update_notice_started", False):
banner._defer_update_notice(_Console(), max_wait=2.0)
done.set()
time.sleep(0.3)
assert not printed