94 lines
3.4 KiB
Python
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()
|