Files
hermes-webui/tests/_wakeup_helpers.py
nesquena-hermes 26e133e3e8
Some checks failed
Release & Docker / release (push) Has been cancelled
[HELD — independent review pending] Release v0.51.340 — bg_task agent wakeup (trio #2968+#2971+#2979) (#3867)
* stage bg_task trio combined (#2979 superset) on master for deep review

* fix(bg_task): unsubscribe SessionChannel on header-write failure (Codex deep-review catch) + regression test

* test: realign on-subscribe-recovery anchor to subscribe_to_session_channel after leak fix

* CHANGELOG: bg_task trio as v0.51.340 LD (HELD pending independent review)

* bg_task trio: apply 3 independent-review (greptile) fixes

1. start_session_turn now threads the session PROFILE model defaults
   (_read_profile_model_config) into the wakeup model-resolve, so a brand-new
   session with an empty model falls back to the profile default not global
   DEFAULT_MODEL. Updated the white-box spy test signature accordingly.
2. /api/session/stream omits the Connection header (HTTP/1.1 keep-alive
   default) to match the #3103 long-lived-SSE pattern.
3. Reaper now prunes _LAST_EMIT_TS for collected sessions so the coalesce
   timestamp map can't grow one permanent entry per session forever.

nesquena APPROVED the PR; these are the 3 non-blocking greptile suggestions.

* test: realign _start_session_turn adapter stub lambda to new profile-defaults signature
2026-06-08 22:36:18 -07:00

97 lines
3.8 KiB
Python

"""Shared test helpers for the server-side wakeup test suites.
Consolidates the ``_install_fake_start_session_turn`` / ``_wait_for_wakeup``
pair that ``test_session_channel_option_x.py`` and ``test_wakeup_defer_race.py``
both need so the two suites can't drift. Per Copilot review on PR #2971
(r3305700944).
"""
from __future__ import annotations
import queue
import sys
import threading
import types
class FakeProcessRegistry:
"""Minimal stand-in for ``tools.process_registry.process_registry``.
Consolidated from the verbatim ``_FakeProcessRegistry`` copies that lived
in ``test_bg_task_complete_wakeup.py``, ``test_bg_task_complete_throttle.py``
and ``test_bg_task_complete_ab_coexistence.py`` (Greptile review on PR #2979).
Keeping one definition prevents the three suites from drifting to subtly
different stub shapes.
"""
def __init__(self):
self._lock = threading.Lock()
self._completion_consumed: set[str] = set()
self.completion_queue: queue.Queue = queue.Queue()
self._procs: dict[str, types.SimpleNamespace] = {}
def register(self, process_id: str, session_key: str) -> None:
self._procs[process_id] = types.SimpleNamespace(session_key=session_key)
def get(self, process_id: str):
return self._procs.get(process_id)
def is_completion_consumed(self, process_id: str) -> bool:
with self._lock:
return process_id in self._completion_consumed
def install_fake_registry(monkeypatch, fake) -> None:
"""Inject ``fake`` under ``tools.process_registry`` for the test.
IMPORTANT (rebase isolation): uses ONLY ``monkeypatch.setitem`` so both
``sys.modules`` entries are restored to their real/absent state on
teardown. A prior implementation used ``sys.modules.setdefault("tools", …)``
which is an UNTRACKED mutation — when the real ``tools`` package was not yet
imported it permanently leaked a non-package fake ``tools`` into
``sys.modules``, breaking any later test doing
``from tools.process_registry import …``. Both setitem calls below are
monkeypatch-tracked: on teardown each key is restored to its prior value,
or deleted if it was absent — no leak.
"""
mod = types.ModuleType("tools.process_registry")
mod.process_registry = fake # type: ignore[attr-defined]
tools_mod = types.ModuleType("tools")
tools_mod.process_registry = mod # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "tools", tools_mod)
monkeypatch.setitem(sys.modules, "tools.process_registry", mod)
def install_fake_start_session_turn(monkeypatch, *, status: int = 200):
"""Patch ``api.routes.start_session_turn`` to record calls instead of
running a real agent turn.
The drain helper does ``from api.routes import start_session_turn``
inside a daemon thread, so patching the attribute on the ``api.routes``
module is what the thread resolves at call time.
Returns a ``holder`` dict with ``calls`` (list of recorded call kwargs)
and ``event`` (a ``threading.Event`` set on first call) — pair it with
``wait_for_wakeup`` below.
"""
import api.routes as _routes
holder = {"calls": [], "event": threading.Event()}
def _fake(session_id, message, *, source="process_wakeup"):
holder["calls"].append(
{"session_id": session_id, "message": message, "source": source}
)
holder["event"].set()
return {"stream_id": "fake-stream", "session_id": session_id, "_status": status}
monkeypatch.setattr(_routes, "start_session_turn", _fake, raising=True)
return holder
def wait_for_wakeup(holder, timeout: float = 3.0) -> bool:
"""Block until the server-side wakeup runner thread recorded a call.
Returns True if the holder's event fired within ``timeout`` seconds.
"""
return holder["event"].wait(timeout=timeout)