""" Regression tests for #4633/#5788 — the SSE handler must ENFORCE run-journal coverage before draining a truncated offline tail. ``StreamChannel.subscribe_with_snapshot()`` reports ``offline_dropped_events`` when the capped offline buffer evicted frames during a disconnect gap. The retained tail is only safe to drain when the run journal provably backfills everything from the client's replay cursor through the snapshot cutoff — otherwise the browser would render later frames plus a normal ``stream_end`` with a silent transcript hole in the middle (the journal being missing, incomplete, or unreadable leaves the evicted frames unrecoverable). Enforcement contract pinned here: * dropped > 0 and the journal does NOT cover the gap → emit the established ``apperror`` + ``recovery_control: true`` event (same client contract as ``run_journal.stale_interrupted_event`` — the tab restores the transcript from persisted state) and do NOT stream the tail. * dropped > 0 and the journal DOES cover the gap → replay + tail as before. * a client cursor already at/past the snapshot cutoff has no gap to cover — no recovery even when the journal is missing. * the retained tail itself covers [first buffered frame → cutoff]: the journal only has to bridge (cursor → first buffered frame), a cursor already inside the retained range needs no journal at all, and the replay/dedup cutoff stops at the bridge so queued frames the journal never emitted are not skipped. """ import io import json import queue from urllib.parse import urlparse import api.routes as routes class _Handler: def __init__(self): self.wfile = io.BytesIO() def send_response(self, _code): pass def send_header(self, _name, _value): pass def end_headers(self): pass class _FakeStream: def __init__(self, snapshot, items): self.q = queue.Queue() for item in items: self.q.put_nowait(item) self._snapshot = dict(snapshot) self.unsubscribed = False def subscribe_with_snapshot(self): return self.q, dict(self._snapshot) def unsubscribe(self, q): self.unsubscribed = q is self.q def _run_handler(monkeypatch, stream, query): handler = _Handler() previous_streams = dict(routes.STREAMS) routes.STREAMS.clear() routes.STREAMS["run_1"] = stream try: routes._handle_sse_stream(handler, urlparse(f"/api/chat/stream?{query}")) finally: routes.STREAMS.clear() routes.STREAMS.update(previous_streams) return handler.wfile.getvalue().decode("utf-8") def _sse_payloads(body, event): """Extract parsed data payloads for all SSE frames of the given event.""" payloads = [] lines = body.splitlines() for i, line in enumerate(lines): if line == f"event: {event}" and i + 1 < len(lines): data = lines[i + 1] assert data.startswith("data: ") payloads.append(json.loads(data[len("data: "):])) return payloads def _journal_events(lo, hi, run_id="run_1"): return [ { "event": "token", "payload": {"text": f"journal-{seq}"}, "event_id": f"{run_id}:{seq}", "seq": seq, } for seq in range(lo, hi + 1) ] def test_dropped_frames_without_journal_emit_recovery_not_tail(monkeypatch): """Missing journal + evicted frames → recovery_control apperror, no tail.""" stream = _FakeStream( { "last_event_id": "run_1:9000", "offline_buffered_events": 2, "offline_dropped_events": 25, "offline_first_event_id": "run_1:8999", }, [ ("token", {"text": "tail-frame"}, "run_1:8999"), ("stream_end", {}, "run_1:9000"), ], ) monkeypatch.setattr(routes, "find_run_summary", lambda _sid: None) monkeypatch.setattr(routes, "stream_owner_session_id", lambda _sid: "session_1") body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=100") payloads = _sse_payloads(body, "apperror") assert len(payloads) == 1 assert payloads[0]["recovery_control"] is True assert payloads[0]["type"] == "interrupted" assert payloads[0]["session_id"] == "session_1" assert payloads[0]["stream_id"] == "run_1" assert payloads[0]["offline_dropped_events"] == 25 # The truncated tail must NOT be drained: no silent hole + stream_end. assert "tail-frame" not in body assert "event: stream_end" not in body # Every exit path returns the queue to the channel. assert stream.unsubscribed is True def test_dropped_frames_with_full_journal_coverage_stream_normally(monkeypatch): """Journal covers (cursor → cutoff] → replay + tail drain, no recovery.""" stream = _FakeStream( { "last_event_id": "run_1:105", "offline_buffered_events": 2, "offline_dropped_events": 25, "offline_first_event_id": "run_1:106", }, [ ("token", {"text": "tail-frame"}, "run_1:106"), ("stream_end", {}, "run_1:107"), ], ) monkeypatch.setattr( routes, "find_run_summary", lambda sid: {"session_id": "session_1", "run_id": sid, "terminal": False}, ) monkeypatch.setattr( routes, "read_run_events", lambda _session_id, run_id, after_seq=None, max_seq=None: { "events": _journal_events(101, 105, run_id=run_id) }, ) body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=100") assert _sse_payloads(body, "apperror") == [] assert "journal-101" in body and "journal-105" in body assert "tail-frame" in body assert "event: stream_end" in body assert stream.unsubscribed is True def test_incomplete_journal_coverage_emits_recovery(monkeypatch): """A journal that stops short of the snapshot cutoff is NOT coverage.""" stream = _FakeStream( { "last_event_id": "run_1:105", "offline_buffered_events": 2, "offline_dropped_events": 25, "offline_first_event_id": "run_1:106", }, [ ("token", {"text": "tail-frame"}, "run_1:106"), ("stream_end", {}, "run_1:107"), ], ) monkeypatch.setattr( routes, "find_run_summary", lambda sid: {"session_id": "session_1", "run_id": sid, "terminal": False}, ) monkeypatch.setattr( routes, "read_run_events", # Journal lost its newest entries: stops at 103 < cutoff 105. lambda _session_id, run_id, after_seq=None, max_seq=None: { "events": _journal_events(101, 103, run_id=run_id) }, ) monkeypatch.setattr(routes, "stream_owner_session_id", lambda _sid: "session_1") body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=100") payloads = _sse_payloads(body, "apperror") assert len(payloads) == 1 assert payloads[0]["recovery_control"] is True assert "tail-frame" not in body assert stream.unsubscribed is True def test_cursor_at_cutoff_has_no_gap_and_streams_tail(monkeypatch): """A client already at/past the snapshot cutoff missed nothing the buffer ever held — no recovery even when the journal is missing entirely.""" stream = _FakeStream( { "last_event_id": "run_1:105", "offline_buffered_events": 2, "offline_dropped_events": 25, "offline_first_event_id": "run_1:106", }, [ ("token", {"text": "tail-frame"}, "run_1:106"), ("stream_end", {}, "run_1:107"), ], ) monkeypatch.setattr(routes, "find_run_summary", lambda _sid: None) body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=105") assert _sse_payloads(body, "apperror") == [] assert "tail-frame" in body assert "event: stream_end" in body assert stream.unsubscribed is True def test_coverage_helper_rejects_mid_window_holes(monkeypatch): """Endpoint checks are not enough: a malformed/dropped line INSIDE the window (journal seqs are contiguous by construction) must fail coverage.""" monkeypatch.setattr( routes, "find_run_summary", lambda sid: {"session_id": "session_1", "run_id": sid, "terminal": False}, ) events = [e for e in _journal_events(101, 105) if e["seq"] != 103] monkeypatch.setattr( routes, "read_run_events", lambda _session_id, _run_id, after_seq=None, max_seq=None: {"events": events}, ) assert routes._run_journal_covers_offline_gap("run_1", 100, 105) is False def test_coverage_helper_requires_known_cutoff(): """Without a same-run journaled cutoff nothing can be proven → not covered.""" assert routes._run_journal_covers_offline_gap("run_1", 100, None) is False def test_cursor_inside_retained_tail_needs_no_journal(monkeypatch): """A reconnect whose cursor already sits inside the retained buffer range is contiguous from the queue alone — no journal required, no recovery, even though frames were evicted before the retained head (they are all ≤ the client's cursor).""" stream = _FakeStream( { "last_event_id": "run_1:9000", "offline_buffered_events": 2, "offline_dropped_events": 25, "offline_first_event_id": "run_1:8999", }, [ ("token", {"text": "tail-frame"}, "run_1:8999"), ("stream_end", {}, "run_1:9000"), ], ) monkeypatch.setattr(routes, "find_run_summary", lambda _sid: None) body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=8998") assert _sse_payloads(body, "apperror") == [] assert "tail-frame" in body assert "event: stream_end" in body assert stream.unsubscribed is True def test_replay_cutoff_stops_at_buffer_head_so_dedup_keeps_queued_frames(monkeypatch): """The journal only bridges (cursor → first buffered frame); the dedup cutoff must stop there too. With the cutoff at the snapshot's last_event_id the drain loop's `seq <= replay_cutoff_seq` filter would silently skip the queued frames the journal never emitted — a hole certified as covered.""" stream = _FakeStream( { "last_event_id": "run_1:105", "offline_buffered_events": 3, "offline_dropped_events": 25, "offline_first_event_id": "run_1:103", }, [ ("token", {"text": "buffered-103"}, "run_1:103"), ("token", {"text": "buffered-104"}, "run_1:104"), ("token", {"text": "buffered-105"}, "run_1:105"), ("stream_end", {}, "run_1:106"), ], ) monkeypatch.setattr( routes, "find_run_summary", lambda sid: {"session_id": "session_1", "run_id": sid, "terminal": False}, ) monkeypatch.setattr( routes, "read_run_events", # The journal only holds the bridge (100 → 102]; 103.. live in the buffer. lambda _session_id, run_id, after_seq=None, max_seq=None: { "events": _journal_events(101, 102, run_id=run_id) }, ) body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=100") assert _sse_payloads(body, "apperror") == [] assert "journal-101" in body and "journal-102" in body # The queued frames past the bridge must all survive the dedup filter. assert "buffered-103" in body assert "buffered-104" in body assert "buffered-105" in body assert "event: stream_end" in body assert stream.unsubscribed is True def test_cursor_past_buffer_head_does_not_double_render_queued_frames(monkeypatch): """A cursor AT/INSIDE the retained tail (after_seq >= first buffered frame) must not receive the queued copy of frames it already rendered. The journal-emission bound (buffer head) and the client-cursor bound are two DIFFERENT dedup cutoffs: collapsing them onto the buffer head would re-stream [head, cursor] from the queue — a visible double-render, since the drain loop's `seq <=` filter is the only dedup for replayed streams (the client does not de-duplicate token frames itself).""" stream = _FakeStream( { "last_event_id": "run_1:105", "offline_buffered_events": 3, "offline_dropped_events": 0, "offline_first_event_id": "run_1:103", }, [ ("token", {"text": "buffered-103"}, "run_1:103"), ("token", {"text": "buffered-104"}, "run_1:104"), ("token", {"text": "buffered-105"}, "run_1:105"), ("stream_end", {}, "run_1:106"), ], ) monkeypatch.setattr( routes, "find_run_summary", lambda sid: {"session_id": "session_1", "run_id": sid, "terminal": False}, ) all_events = _journal_events(101, 105) def _filtering_read(_session_id, _run_id, after_seq=None, max_seq=None): # Mirror the real read_run_events window filters. events = [ e for e in all_events if (after_seq is None or e["seq"] > after_seq) and (max_seq is None or e["seq"] <= max_seq) ] return {"events": events} monkeypatch.setattr(routes, "read_run_events", _filtering_read) # Client already rendered up to 104 (e.g. tab flap after a partial drain). body = _run_handler(monkeypatch, stream, "stream_id=run_1&replay=1&after_seq=104") assert _sse_payloads(body, "apperror") == [] assert "buffered-103" not in body assert "buffered-104" not in body assert "buffered-105" in body assert "event: stream_end" in body assert stream.unsubscribed is True def test_ahead_of_stream_cursor_does_not_filter_terminal_frame(monkeypatch): """An ahead-of-stream cursor (after_seq past the snapshot cutoff) is NOT clamped onto the snapshot's terminal fence. The old clamp (``min(cursor, snapshot_cutoff)``) set the dedup bound to the terminal frame's own seq, so the drain loop's ``seq <=`` filter — which runs BEFORE its terminal break — swallowed the queued terminal frame and pinned the loop on heartbeats until the write deadline (Codex CORE #2 terminal filter). Ahead-of-stream means the client already believes it holds everything the buffer has; nothing must be filtered and the terminal frame must survive.""" from urllib.parse import parse_qs monkeypatch.setattr(routes, "find_run_summary", lambda _sid: None) handler = _Handler() handled, cutoff = routes._sse_replay_run_journal_gap_checked( handler, parse_qs("stream_id=run_1&replay=1&after_seq=999"), "run_1", { "last_event_id": "run_1:105", "offline_buffered_events": 3, "offline_dropped_events": 0, "offline_first_event_id": "run_1:103", }, ) assert handled is False # Not clamped to 105: the cutoff stays None (journal replay found nothing, # and an ahead-of-stream cursor contributes no dedup bound), so the queued # terminal frame (seq 106) passes the drain loop's filter and terminates # the connection instead of being filtered onto the fence. assert cutoff is None