The attempt-local first-progress deadline added `progress_timeout` to `_NonStreamWatchdogs`, but the SimpleNamespace stubs in test_nonstream_wait_notice.py and test_wait_notice_cadence.py were not updated. `_emit_wait_notice` reads `wd.progress_timeout` inside its blanket `except Exception`, so the AttributeError silently produced no notice and no liveness touch: 11 tests red. The new "Codex stream opened" debug line also called `time.time()` eagerly, consuming a tick of the 3-tick stub in test_codex_first_event_timing.py and shifting the first-event stamp. The first-event log a few lines below already carries the attempt's timeline anchor, so the open marker logs without an epoch.
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
"""Wait status describes request-local silence, never active generation."""
|
|
|
|
from types import SimpleNamespace
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from agent import chat_completion_helpers as h
|
|
from agent.chat_completion_nonstream import _NonStreamRequest
|
|
from agent.chat_completion_wait_notice import WaitNoticeState
|
|
|
|
|
|
def _request():
|
|
request = _NonStreamRequest.__new__(_NonStreamRequest)
|
|
notices, touches = [], []
|
|
request.agent = SimpleNamespace(
|
|
_emit_wait_notice=notices.append,
|
|
_touch_activity=touches.append,
|
|
_interrupt_requested=False,
|
|
_codex_stream_last_event_ts=9999.0, # Another request cannot hide this one's stall.
|
|
)
|
|
request.api_kwargs = {"model": "test-model"}
|
|
request.call_start = 1000.0
|
|
request.wd = SimpleNamespace(
|
|
codex=True, stale_timeout=600.0, ttfb_enabled=True, ttfb_timeout=120.0,
|
|
idle_enabled=True, idle_timeout=180.0, idle_requires_progress=False, progress_timeout=0.0,
|
|
)
|
|
request.codex_watchdog_state = SimpleNamespace(
|
|
lock=threading.Lock(), last_event_ts=None, last_progress_ts=None,
|
|
retry_started_ts=None,
|
|
)
|
|
request.wait_notice_started_ts = None
|
|
request.wait_notice = WaitNoticeState()
|
|
request.result = {"error": None, "response": None}
|
|
return request, notices, touches
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event,progress,retry,expected",
|
|
[
|
|
(59.0, 59.0, None, None), # Reasoning/text/tool arguments still arriving.
|
|
(59.0, None, None, None), # Lifecycle traffic is not transport silence either.
|
|
(0.0, 0.0, None, "provider stream active; 60s without stream events"),
|
|
(0.0, None, None, "provider stream active; 60s without stream events"),
|
|
(1.0, None, None, None), # 59 seconds of silence is still quiet.
|
|
(None, None, None, "60s waiting for the first provider event"),
|
|
(10.0, 10.0, 59.0, None), # Internal reconnect gets a fresh first-event wait.
|
|
(0.0, 0.0, 0.0, "60s waiting for the first provider event after reconnect"),
|
|
(0.0, 0.0, 1.0, None),
|
|
],
|
|
)
|
|
def test_wait_notice_tracks_current_attempt_silence(event, progress, retry, expected):
|
|
request, notices, touches = _request()
|
|
state = request.codex_watchdog_state
|
|
state.last_event_ts = None if event is None else request.call_start + event
|
|
state.last_progress_ts = None if progress is None else request.call_start + progress
|
|
state.retry_started_ts = None if retry is None else request.call_start + retry
|
|
request._emit_wait_notice(30.0)
|
|
request._emit_wait_notice(59.0)
|
|
assert notices == []
|
|
assert touches
|
|
if event is None and retry is None:
|
|
assert "receiving" not in touches[-1]
|
|
request._emit_wait_notice(60.0)
|
|
if expected is None:
|
|
assert notices == []
|
|
assert touches, "Quiet heartbeat must survive suppressing a visible warning"
|
|
else:
|
|
assert len(notices) == 1
|
|
assert expected in notices[0]
|
|
assert "auto-reconnect:" in notices[0]
|
|
|
|
|
|
def test_resumed_events_clear_only_this_requests_wait_notice(monkeypatch):
|
|
request, notices, _ = _request()
|
|
sentinel = object()
|
|
ticks = [0]
|
|
|
|
class Worker:
|
|
def __init__(self, **kwargs):
|
|
pass
|
|
|
|
def start(self):
|
|
pass
|
|
|
|
def is_alive(self):
|
|
return ticks[0] < 204
|
|
|
|
def join(self, timeout):
|
|
ticks[0] += 1
|
|
if ticks[0] >= 201:
|
|
request.codex_watchdog_state.last_event_ts = 1000.0 + ticks[0] * 0.3
|
|
if ticks[0] == 204:
|
|
request.result["response"] = sentinel
|
|
|
|
monkeypatch.setattr(h.threading, "Thread", Worker)
|
|
monkeypatch.setattr(h.time, "time", lambda: 1000.0 + ticks[0] * 0.3)
|
|
assert request.run() is sentinel
|
|
assert len(notices) == 2
|
|
assert "waiting for the first provider event" in notices[0]
|
|
# Nonempty thinking.delta payloads enter TUI reasoning history.
|
|
assert notices[1] == ""
|