Files
hermes-webui/tests/test_bg_task_drain_loop_backoff.py
T
ai-ag2026andClaude Opus 4.8 141b49b74c test(bg): fix drain-loop backoff test import so it actually collects
The regression test used a bare `from _wakeup_helpers import install_fake_registry`,
which raises ModuleNotFoundError under the repo test runner (rootdir = repo root,
tests is a package). pytest treats that as a COLLECTION error for the file, not a
failure — so the whole suite stayed green while this file's tests silently never
ran, leaving the backoff fix with zero effective coverage.

Use the package-qualified `from tests._wakeup_helpers import install_fake_registry`,
matching the four sibling bg_task_complete suites. Confirmed the import now
resolves from the repo root (the bare form fails).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:18:21 +00:00

130 lines
4.4 KiB
Python

"""Regression test for the bg_task_complete drain-loop tight-spin (#2476, #4633).
``_drain_loop`` read ``process_registry.completion_queue`` directly and wrapped
it in a bare ``except Exception: continue``. A registry missing that attribute
raised ``AttributeError`` that was swallowed and retried with no backoff — a
100%-CPU tight loop with no log line. The loop now:
* reads the queue via ``getattr(process_registry, 'completion_queue', None)``
and backs off on the stop event when it is absent (mirrors streaming.py),
* catches ``queue.Empty`` explicitly (the normal idle path → plain continue),
* logs + backs off on any other queue error instead of tight-looping.
Uses the shared ``install_fake_registry`` stub helper (see ``_wakeup_helpers``).
"""
from __future__ import annotations
import queue
import threading
from tests._wakeup_helpers import install_fake_registry
class _CountingStop:
"""Stand-in for ``_DRAIN_STOP`` that records ``wait()`` and self-terminates.
In the OLD (buggy) code the missing-queue path hit ``continue`` and never
touched the stop event's ``wait`` — it just re-spun on ``is_set()``. So a
recorded ``wait(1.0)`` call is exactly the proof the backoff path was taken.
``wait`` sets the flag after ``exit_after`` calls so the loop exits promptly
without any real sleep, keeping the test deterministic and fast.
"""
def __init__(self, exit_after: int = 1):
self._flag = threading.Event()
self.wait_calls: list[float | None] = []
self._exit_after = exit_after
def is_set(self) -> bool:
return self._flag.is_set()
def set(self) -> None:
self._flag.set()
def clear(self) -> None:
self._flag.clear()
def wait(self, timeout=None):
self.wait_calls.append(timeout)
if len(self.wait_calls) >= self._exit_after:
self._flag.set()
return self._flag.is_set()
def _run_drain_with_stop(monkeypatch, fake_registry, stop) -> threading.Thread:
from api import background_process as bp
install_fake_registry(monkeypatch, fake_registry)
monkeypatch.setattr(bp, "_DRAIN_STOP", stop, raising=True)
t = threading.Thread(target=bp._drain_loop, name="test-drain", daemon=True)
t.start()
return t
def test_missing_completion_queue_backs_off_instead_of_spinning(monkeypatch):
"""A registry with no completion_queue must back off, not tight-loop."""
class _NoQueueRegistry:
pass # deliberately no completion_queue attribute
stop = _CountingStop(exit_after=1)
t = _run_drain_with_stop(monkeypatch, _NoQueueRegistry(), stop)
t.join(timeout=3.0)
assert not t.is_alive(), "drain loop did not terminate on stop"
# The backoff path ran: wait() was called with the 1.0s backoff timeout.
assert stop.wait_calls, "missing-queue path did not back off (tight loop)"
assert stop.wait_calls[0] == 1.0
def test_non_empty_queue_error_logs_and_backs_off(monkeypatch):
"""A non-Empty queue error is logged and backed off, not swallowed silently."""
class _BoomQueue:
def get(self, timeout=None):
raise RuntimeError("queue exploded")
class _BoomRegistry:
completion_queue = _BoomQueue()
stop = _CountingStop(exit_after=1)
t = _run_drain_with_stop(monkeypatch, _BoomRegistry(), stop)
t.join(timeout=3.0)
assert not t.is_alive()
assert stop.wait_calls and stop.wait_calls[0] == 1.0
def test_empty_queue_is_the_plain_continue_path(monkeypatch):
"""queue.Empty is the normal idle path: continue, no backoff wait()."""
class _EmptyQueue:
def __init__(self, stop):
self._stop = stop
def get(self, timeout=None):
# Stop the loop after being polled once so the test terminates,
# then raise Empty like a real idle queue.
self._stop.set()
raise queue.Empty
class _EmptyRegistry:
pass
stop = _CountingStop(exit_after=99) # effectively never self-terminates
reg = _EmptyRegistry()
reg.completion_queue = _EmptyQueue(stop)
t = _run_drain_with_stop(monkeypatch, reg, stop)
t.join(timeout=3.0)
assert not t.is_alive()
# Empty took the explicit `continue` path — no backoff wait() was recorded.
assert stop.wait_calls == []
if __name__ == "__main__": # pragma: no cover - manual invocation
import pytest
raise SystemExit(pytest.main([__file__, "-v"]))