Files
omnigent-ai--omnigent/tests/test_native_forwarder_health.py
2026-07-13 13:12:00 +08:00

94 lines
3.4 KiB
Python

"""Tests for the process-local native-forwarder POST-failure record.
``omnigent/_native_forwarder_health.py`` is the small shared sink that lets a
native forwarder report a connectivity failure to the harness idle-turn
watchdog (issue #1119). These cover its record / recency / clear contract; the
writer (forwarder retry loops) and reader (watchdog) integrations are tested in
``tests/test_native_post_delivery.py``, ``tests/test_codex_native_forwarder.py``,
and ``tests/runtime/harnesses/test_scaffold.py``.
"""
from __future__ import annotations
import httpx
import pytest
from omnigent import _native_forwarder_health as health
class _FakeClock:
"""Controllable monotonic clock so recency is tested over real intervals."""
def __init__(self, start: float) -> None:
"""Start the clock at *start* seconds."""
self.now = start
def monotonic(self) -> float:
"""Return the current fake monotonic time."""
return self.now
def test_recent_post_failure_round_trips_within_window() -> None:
"""A recorded failure is returned, with its event type and error repr."""
health.clear()
try:
assert health.recent_post_failure(60.0) is None
health.record_post_failure(
"external_session_status", httpx.ConnectError("No route to host")
)
detail = health.recent_post_failure(60.0)
assert detail is not None
assert "external_session_status" in detail
assert "No route to host" in detail
finally:
health.clear()
def test_recent_post_failure_respects_recency_window(monkeypatch: pytest.MonkeyPatch) -> None:
"""A failure older than the window is suppressed; one inside it is returned.
Uses a controlled clock so the boundary is exercised over a realistic
interval (record at t=1000, read 100s later) rather than a degenerate
zero-length window — pinning the actual ``elapsed > within_s`` comparison.
"""
clock = _FakeClock(start=1000.0)
monkeypatch.setattr(health, "time", clock)
health.clear()
try:
health.record_post_failure(
"external_session_status", httpx.ConnectError("No route to host")
)
# 100s elapse before any read.
clock.now = 1100.0
# Window shorter than the elapsed gap → stale → suppressed.
assert health.recent_post_failure(60.0) is None
# Window longer than the gap → still surfaced, unchanged by the
# earlier stale read.
assert health.recent_post_failure(240.0) is not None
finally:
health.clear()
def test_clear_forgets_the_record() -> None:
"""``clear`` resets the slot so later reads see nothing."""
health.record_post_failure("external_session_status", httpx.ConnectError("boom"))
health.clear()
assert health.recent_post_failure(60.0) is None
def test_note_post_success_clears_a_prior_failure() -> None:
"""A successful POST clears the slot so a recovered connection isn't blamed.
Guards the misattribution fix: once connectivity returns and a POST gets a
response, the recorded failure must not survive to be attached to a later,
unrelated idle-watchdog stall.
"""
health.clear()
try:
health.record_post_failure("external_session_status", httpx.ConnectError("boom"))
assert health.recent_post_failure(60.0) is not None
health.note_post_success()
assert health.recent_post_failure(60.0) is None
finally:
health.clear()