hermes-agent/tests/tools/test_browser_supervisor_healthcheck.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

133 lines
4.3 KiB
Python

"""Unit tests for _SupervisorRegistry cache-hit healthcheck.
Verifies that get_or_start() does NOT return a cached supervisor whose
thread has exited or whose event loop has stopped. Avoids a real Chrome —
the only thing under test is the registry's cache decision.
"""
from __future__ import annotations
import threading
from types import SimpleNamespace
import pytest
from tools import browser_supervisor as bs
class _FakeLoop:
def __init__(self, running: bool) -> None:
self._running = running
def is_running(self) -> bool:
return self._running
def _make_fake_supervisor(cdp_url: str, *, thread_alive: bool, loop_running: bool):
"""Build a minimal stand-in for a CDPSupervisor entry in the registry.
Only the attributes touched by the healthcheck (_thread, _loop, cdp_url)
and by the teardown path (stop()) need to exist.
"""
if thread_alive:
# A thread that is actually running — parks on an Event we never set.
hold = threading.Event()
t = threading.Thread(target=hold.wait, daemon=True)
t.start()
# Attach the release hook so the test can let the thread exit.
setattr(t, "_release", hold.set)
else:
# An un-started thread — is_alive() returns False.
t = threading.Thread(target=lambda: None)
stop_calls: list[bool] = []
fake = SimpleNamespace(
cdp_url=cdp_url,
_thread=t,
_loop=_FakeLoop(loop_running),
stop=lambda: stop_calls.append(True),
)
fake._stop_calls = stop_calls # type: ignore[attr-defined]
return fake
@pytest.fixture
def isolated_registry():
"""A fresh registry instance, independent of the global SUPERVISOR_REGISTRY."""
return bs._SupervisorRegistry()
@pytest.fixture
def stub_cdp_supervisor(monkeypatch):
"""Replace CDPSupervisor in the module so recreate paths don't touch Chrome.
Returns a callable that reads the last-constructed fake out.
"""
created: list[SimpleNamespace] = []
class _StubSupervisor:
def __init__(self, *, task_id, cdp_url, dialog_policy, dialog_timeout_s):
self.task_id = task_id
self.cdp_url = cdp_url
self.dialog_policy = dialog_policy
self.dialog_timeout_s = dialog_timeout_s
# Healthy by default — real thread, running "loop".
hold = threading.Event()
self._thread = threading.Thread(target=hold.wait, daemon=True)
self._thread.start()
self._thread_release = hold.set # type: ignore[attr-defined]
self._loop = _FakeLoop(True)
self.start_called = False
self.stop_called = False
created.append(self)
def start(self, timeout: float = 15.0) -> None:
self.start_called = True
def stop(self) -> None:
self.stop_called = True
# Release the parked thread so the process exits cleanly.
release = getattr(self, "_thread_release", None)
if release is not None:
release()
monkeypatch.setattr(bs, "CDPSupervisor", _StubSupervisor)
yield created
# Teardown: release any parked threads in stubs the test left behind.
for s in created:
release = getattr(s, "_thread_release", None)
if release is not None:
release()
def test_cache_hit_returns_same_instance_when_healthy(
isolated_registry, stub_cdp_supervisor
):
"""Sanity: healthy cached supervisor is returned without recreate."""
first = isolated_registry.get_or_start(task_id="t1", cdp_url="http://h/1")
second = isolated_registry.get_or_start(task_id="t1", cdp_url="http://h/1")
assert first is second
# Only one CDPSupervisor was ever constructed.
assert len(stub_cdp_supervisor) == 1
first.stop()
def test_missing_thread_and_loop_attrs_trigger_recreate(
isolated_registry, stub_cdp_supervisor
):
"""Defensive: None _thread or None _loop counts as unhealthy."""
cdp_url = "http://h/4"
broken = SimpleNamespace(
cdp_url=cdp_url,
_thread=None,
_loop=None,
stop=lambda: None,
)
isolated_registry._by_task["t4"] = broken
fresh = isolated_registry.get_or_start(task_id="t4", cdp_url=cdp_url)
assert fresh is not broken
assert isolated_registry._by_task["t4"] is fresh
fresh.stop()