Files
hermes-webui/tests/test_issue4812_session_sse_stream.py
T
2477d992f4 release: lock-disciplined STREAMS reads (#7339) (#7488)
* fix(streaming): lock-disciplined STREAMS reads via peek_stream()

Writers pop STREAMS under STREAMS_LOCK, but four hot reads used bare
STREAMS.get(): the streaming worker start, the gateway run start, and the two
SSE attach paths in routes.py. Each is individually saved today by GIL-atomic
dict.get plus a None-guard, but the check-then-act windows (get → register,
get → subscribe) race teardown pops, and the bare pattern invites unprotected
copies. peek_stream() centralizes the locked read; all four sites migrate with
no behavioral change.

* test: dual-patch config.STREAMS alongside routes.STREAMS in SSE seam tests

peek_stream() reads the canonical api.config.STREAMS registry; tests that
inject a fake registry by patching only the routes-level re-export left the
helper reading the real (empty) registry and hung the SSE loop to the
pytest-timeout ceiling. Mirror the test_issue6623 pattern: patch both
bindings.

* docs(changelog): stamp #7339 lock-disciplined STREAMS reads

---------

Co-authored-by: Sebastian B <sb@wmc.sh>
Co-authored-by: n <a@n>
2026-09-09 20:22:36 +00:00

905 lines
33 KiB
Python

from __future__ import annotations
import io
import queue
from types import SimpleNamespace
from urllib.parse import urlparse
from api.run_journal import append_run_event, read_session_run_events
class _FakeHandler:
def __init__(self):
self.status = None
self.headers = {}
self.wfile = io.BytesIO()
self.command = "GET"
self.path = "/"
self.client_address = ("127.0.0.1", 12345)
def send_response(self, status):
self.status = status
def send_header(self, key, value):
self.headers[key] = value
def end_headers(self):
pass
def _capture(monkeypatch):
cap = {}
def _j(_h, obj, *_, **kwargs):
cap["ok"] = obj
cap["status"] = kwargs.get("status", 200)
return True
def _bad(_h, msg, code=400):
cap["bad"] = (msg, code)
return True
monkeypatch.setattr("api.routes.j", _j)
monkeypatch.setattr("api.routes.bad", _bad)
return cap
def _stop_after_first_heartbeat(monkeypatch):
calls = {"count": 0}
def _sleep(_seconds):
calls["count"] += 1
raise BrokenPipeError("stop after the first heartbeat")
monkeypatch.setattr("api.routes.time.sleep", _sleep)
return calls
def test_session_route_and_global_route_stay_separate(monkeypatch):
import api.routes as routes
calls = {"global": 0, "session": 0}
monkeypatch.setattr(routes, "_handle_extension_sidecar_proxy", lambda *_args, **_kwargs: False)
monkeypatch.setattr(
routes,
"_handle_session_events_stream",
lambda *_args, **_kwargs: calls.__setitem__("global", calls["global"] + 1) or True,
)
monkeypatch.setattr(
routes,
"_handle_session_sse_stream_for_session",
lambda *_args, **_kwargs: calls.__setitem__("session", calls["session"] + 1) or True,
)
assert routes._session_events_path_session_id("/api/sessions/events") is None
assert routes._session_events_path_session_id("/api/sessions/session_1/events") == "session_1"
assert routes._session_events_path_session_id("/api/sessions/session_1/events/extra") is None
handler = _FakeHandler()
routes.handle_get(handler, urlparse("/api/sessions/events"))
routes.handle_get(handler, urlparse("/api/sessions/session_1/events"))
assert calls == {"global": 1, "session": 1}
def test_session_journal_replays_later_runs_after_cursor(tmp_path):
append_run_event(
"session_1",
"run_a",
"token",
{"text": "a1"},
session_dir=tmp_path,
seq=1,
created_at=100.0,
)
append_run_event(
"session_1",
"run_a",
"token",
{"text": "a2"},
session_dir=tmp_path,
seq=2,
created_at=101.0,
)
append_run_event(
"session_1",
"run_b",
"token",
{"text": "b1"},
session_dir=tmp_path,
seq=1,
created_at=200.0,
)
append_run_event(
"session_1",
"run_b",
"done",
{"session": {"session_id": "session_1"}},
session_dir=tmp_path,
seq=2,
created_at=201.0,
)
append_run_event(
"session_2",
"run_other",
"token",
{"text": "other"},
session_dir=tmp_path,
seq=1,
created_at=150.0,
)
replay = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path)
assert replay["status"] == "ok"
assert [event["event_id"] for event in replay["events"]] == ["run_a:2", "run_b:1", "run_b:2"]
def test_session_journal_distinguishes_missing_and_foreign_cursor(tmp_path):
append_run_event(
"session_1",
"run_a",
"token",
{"text": "a1"},
session_dir=tmp_path,
seq=1,
created_at=100.0,
)
append_run_event(
"session_2",
"run_b",
"token",
{"text": "b1"},
session_dir=tmp_path,
seq=1,
created_at=200.0,
)
missing = read_session_run_events("session_1", after_event_id="run_missing:1", session_dir=tmp_path)
foreign = read_session_run_events("session_1", after_event_id="run_b:1", session_dir=tmp_path)
assert missing["status"] == "cursor_run_missing"
assert foreign["status"] == "cursor_session_mismatch"
def test_session_journal_rejects_hostile_cursor_without_replay(tmp_path, monkeypatch):
append_run_event(
"session_1",
"run_a",
"token",
{"text": "a1"},
session_dir=tmp_path,
seq=1,
created_at=100.0,
)
monkeypatch.setattr(
"api.run_journal.find_run_summary",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("invalid cursor should not scan summaries")),
)
malformed = read_session_run_events("session_1", after_event_id="bad/run:1", session_dir=tmp_path)
negative = read_session_run_events("session_1", after_event_id="run_a:-5", session_dir=tmp_path)
zero = read_session_run_events("session_1", after_event_id="run_a:0", session_dir=tmp_path)
assert malformed["status"] == "cursor_invalid"
assert malformed["events"] == []
assert negative["status"] == "cursor_invalid"
assert negative["events"] == []
assert zero["status"] == "cursor_invalid"
assert zero["events"] == []
def test_session_journal_blank_summary_session_stays_missing(tmp_path, monkeypatch):
monkeypatch.setattr("api.run_journal.find_run_summary", lambda *_args, **_kwargs: {"session_id": ""})
replay = read_session_run_events("session_1", after_event_id="run_missing:1", session_dir=tmp_path)
assert replay["status"] == "cursor_run_missing"
def test_session_journal_rejects_bounds_and_invalid_contiguous_rows(tmp_path):
append_run_event("session_1", "run_a", "token", {"text": "one"}, session_dir=tmp_path, seq=1)
append_run_event("session_1", "run_a", "token", {"text": "two"}, session_dir=tmp_path, seq=2)
rows_limited = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path, max_rows=1)
bytes_limited = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path, max_bytes=1)
assert rows_limited["status"] != "ok" and rows_limited["events"] == []
assert bytes_limited["status"] != "ok" and bytes_limited["events"] == []
path = tmp_path / "_run_journal" / "session_1" / "run_a.jsonl"
path.write_text(path.read_text(encoding="utf-8").replace('"event_id":"run_a:2"', '"event_id":"wrong:2"'), encoding="utf-8")
invalid = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path)
assert invalid["status"] == "replay_noncontiguous"
assert invalid["events"] == []
def test_session_journal_rejects_oversized_line_before_decode(tmp_path, monkeypatch):
path = tmp_path / "_run_journal" / "session_1" / "run_a.jsonl"
path.parent.mkdir(parents=True, exist_ok=True)
payload = '{"event_id":"run_a:1","seq":1,"run_id":"run_a","session_id":"session_1","event":"done","type":"done","payload":{"text":"' + ("x" * 5000) + '"}}\n'
path.write_bytes(payload.encode("utf-8"))
monkeypatch.setattr("api.run_journal.json.loads", lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("oversized line should not decode")))
limited = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path, max_bytes=1024)
assert limited["status"] == "replay_limit_bytes"
assert limited["events"] == []
def test_session_journal_accepts_line_at_cap_and_rejects_next_byte(tmp_path):
path = tmp_path / "_run_journal" / "session_1" / "run_a.jsonl"
path.parent.mkdir(parents=True, exist_ok=True)
payload = '{"event_id":"run_a:1","seq":1,"run_id":"run_a","session_id":"session_1","event":"done","type":"done","payload":{"text":"ok"}}\n'
path.write_bytes(payload.encode("utf-8"))
cap = len(payload.encode("utf-8"))
at_cap = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path, max_bytes=cap)
over = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path, max_bytes=cap - 1)
assert at_cap["status"] == "ok"
assert at_cap["events"] == []
assert over["status"] == "replay_limit_bytes"
assert over["events"] == []
def test_session_journal_requires_exact_cursor_and_contiguous_suffix(tmp_path):
append_run_event("session_1", "run_a", "token", {}, session_dir=tmp_path, seq=1)
exact_missing = read_session_run_events("session_1", after_event_id="run_a:2", session_dir=tmp_path)
append_run_event("session_1", "run_a", "token", {}, session_dir=tmp_path, seq=3)
gap = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path)
assert exact_missing["status"] != "ok"
assert exact_missing["events"] == []
assert gap["events"] == []
def test_session_journal_accepts_final_cursor_and_replays_later_runs(tmp_path):
append_run_event("session_1", "run_a", "token", {}, session_dir=tmp_path, seq=1, created_at=100.0)
append_run_event("session_1", "run_b", "token", {}, session_dir=tmp_path, seq=1, created_at=200.0)
replay = read_session_run_events("session_1", after_event_id="run_a:1", session_dir=tmp_path)
assert replay["status"] == "ok"
assert [event["event_id"] for event in replay["events"]] == ["run_b:1"]
def test_session_route_prefers_last_event_id_then_query_fallback(monkeypatch):
import api.routes as routes
captured = []
stop = _stop_after_first_heartbeat(monkeypatch)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda _session_id, *, after_event_id=None, session_dir=None: captured.append(after_event_id) or {"status": "ok", "events": []},
)
handler = _FakeHandler()
handler.headers["Last-Event-ID"] = "run_a:2"
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_b:1"),
"session_1",
)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_b:1"),
"session_1",
)
assert stop["count"] >= 2
assert captured == ["run_a:2", "run_b:1"]
def test_session_route_arms_deadline_immediately_after_headers(monkeypatch):
import api.routes as routes
calls = []
stop = _stop_after_first_heartbeat(monkeypatch)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(routes, "get_session", lambda sid, metadata_only=False: SimpleNamespace(session_id=sid, compact=lambda **_kwargs: {}))
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: None)
monkeypatch.setattr(routes, "end_sse_headers", lambda _handler: calls.append("headers"))
monkeypatch.setattr(routes, "_sse_set_write_deadline", lambda _handler: calls.append("deadline"))
routes._handle_session_sse_stream_for_session(_FakeHandler(), urlparse("/api/sessions/session_1/events"), "session_1")
assert calls[:2] == ["headers", "deadline"]
assert stop["count"] == 1
def test_session_route_emits_snapshot_without_id_for_missing_cursor_and_keepalive(monkeypatch):
import api.routes as routes
stop = _stop_after_first_heartbeat(monkeypatch)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: {"status": "cursor_run_missing", "events": []},
)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_missing:1"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
assert "event: session_snapshot\n" in body
assert ": keepalive\n\n" in body
assert "id: " not in body
assert stop["count"] == 1
def test_session_route_resyncs_when_run_completes_inside_idle_wait(monkeypatch):
"""Idle no-cursor subscriber: a run that starts+finishes inside one keepalive
tick advances the journal with no live stream to attach. The handler must emit
a session_snapshot re-sync so the client catches that run instead of silently
missing it until manual refresh (Fable gate finding on #5677)."""
import api.routes as routes
stop = _stop_after_first_heartbeat(monkeypatch)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_a, **_k: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
# No live stream ever attaches (the run completed before we could subscribe).
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_a, **_k: None)
# No cursor → no replay; the missing-cursor path returns ok/empty.
monkeypatch.setattr(routes, "read_session_run_events", lambda *_a, **_k: {"status": "ok", "events": []})
# Fingerprint advances between the pre-loop baseline and the in-loop check,
# simulating a run completing during the wait.
fps = iter([(0, 0.0, 0), (1, 123.0, 42)])
monkeypatch.setattr(routes, "session_journal_fingerprint", lambda *_a, **_k: next(fps, (1, 123.0, 42)))
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
assert "event: session_snapshot\n" in body, "idle journal advance must emit a snapshot re-sync"
assert ": keepalive\n\n" in body
assert stop["count"] == 1
def test_session_route_baselines_journal_before_first_attach(monkeypatch):
"""TOCTOU regression: the idle-wait fingerprint baseline must be captured BEFORE
the first live-attach lookup. If it were captured afterward, a run completing
during that first attach would fold into the baseline and be silently missed.
Assert the call ORDER: session_journal_fingerprint fires before the attach loop's
active-stream lookup, and a subsequent journal advance still emits a snapshot."""
import api.routes as routes
stop = _stop_after_first_heartbeat(monkeypatch)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_a, **_k: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
order = []
fp_calls = {"n": 0}
def _active(*_a, **_k):
order.append("attach")
return None
def _fp(*_a, **_k):
fp_calls["n"] += 1
order.append("fp")
# First fp call = pre-attach baseline (old); any later call = advanced.
return (0, 0.0, 0) if fp_calls["n"] == 1 else (1, 123.0, 42)
monkeypatch.setattr(routes, "_active_run_stream_for_session", _active)
monkeypatch.setattr(routes, "read_session_run_events", lambda *_a, **_k: {"status": "ok", "events": []})
monkeypatch.setattr(routes, "session_journal_fingerprint", _fp)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
# The baseline fp must precede the FIRST in-loop attach that follows it. Concretely:
# the first fp call happens before the attach-lookup that enters the idle wait.
assert "fp" in order, "the idle path must baseline the journal fingerprint"
first_fp = order.index("fp")
# There must be an attach recorded AFTER the baseline (the loop attach) and the
# subsequent advance must surface as a snapshot re-sync.
assert "attach" in order[first_fp:], "an attach must follow the baseline in the idle loop"
assert body.count("event: session_snapshot\n") == 1, "a journal advance after baseline must emit one re-sync"
assert stop["count"] == 1
def test_session_route_no_resync_when_idle_journal_unchanged(monkeypatch):
"""The idle-wait re-sync must fire ONLY on a genuine journal advance — a quiet
idle connection (fingerprint unchanged) must NOT spam snapshots every tick."""
import api.routes as routes
stop = _stop_after_first_heartbeat(monkeypatch)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_a, **_k: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_a, **_k: None)
monkeypatch.setattr(routes, "read_session_run_events", lambda *_a, **_k: {"status": "ok", "events": []})
# Fingerprint never changes → no re-sync.
monkeypatch.setattr(routes, "session_journal_fingerprint", lambda *_a, **_k: (2, 99.0, 77))
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
assert "event: session_snapshot\n" not in body, "quiet idle wait must not emit a snapshot"
assert ": keepalive\n\n" in body
assert stop["count"] == 1
def test_session_route_blocks_hidden_sessions_before_replay_or_live_attach(monkeypatch):
import api.routes as routes
cap = _capture(monkeypatch)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(session_id=sid, profile="other"),
)
monkeypatch.setattr(routes, "_get_active_profile_name", lambda: "default")
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("replay should not run")),
)
monkeypatch.setattr(
routes,
"_active_run_stream_for_session",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("live attach should not run")),
)
handler = _FakeHandler()
routes.handle_get(handler, urlparse("/api/sessions/hidden_session/events"))
assert cap["bad"] == ("Session not found", 404)
def test_session_route_live_delivery_skips_replayed_active_run_items(monkeypatch):
import api.routes as routes
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
self.q.put_nowait(("token", {"text": "live replayed"}, "run_active:1"))
self.q.put_nowait(("stream_end", {"status": "done"}, "run_active:2"))
self.unsubscribed = False
def subscribe_with_snapshot(self):
return self.q, {"last_event_id": "run_active:1", "offline_buffered_events": 1}
def unsubscribe(self, q):
self.unsubscribed = q is self.q
stream = _FakeStream()
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: "run_active")
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: {
"status": "ok",
"events": [
{
"run_id": "run_prev",
"seq": 1,
"event": "token",
"payload": {"text": "replayed"},
"event_id": "run_prev:1",
},
{
"run_id": "run_active",
"seq": 1,
"event": "token",
"payload": {"text": "replayed current"},
"event_id": "run_active:1",
},
],
},
)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_prev:0"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
assert "id: run_prev:1\n" in body
assert body.count("id: run_active:1\n") == 1
assert "id: run_active:2\n" in body
assert "event: stream_end\n" in body
assert stream.unsubscribed is True
def test_session_route_breaks_on_terminal_even_when_reconciliation_replayed_it(monkeypatch):
"""CORE regression (#5677 gate r3): if reconciliation replays the active run's
TERMINAL event at the subscribe cutoff, the live queue's copy is deduped — but
the loop must STILL break on it and unsubscribe. Otherwise the handler stays
blocked on the dead run's queue and misses subsequent session runs."""
import api.routes as routes
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
# The live queue re-delivers the terminal event that reconciliation
# already replayed at/below the cutoff (same event_id) — it must be
# deduped for output yet still end the loop.
self.q.put_nowait(("stream_end", {"status": "done"}, "run_active:1"))
self.unsubscribed = False
def subscribe_with_snapshot(self):
return self.q, {"last_event_id": "run_active:1", "offline_buffered_events": 0}
def unsubscribe(self, q):
self.unsubscribed = q is self.q
stream = _FakeStream()
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_a, **_k: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_a, **_k: "run_active")
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
# Reconciliation replays the active run's terminal event (run_active:1) — same id
# as the queued live copy, at the cutoff.
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_a, **_k: {
"status": "ok",
"events": [
{"run_id": "run_active", "seq": 1, "event": "stream_end", "payload": {"status": "done"}, "event_id": "run_active:1"},
],
},
)
handler = _FakeHandler()
# If the fix regresses, the loop never breaks and blocks on subscriber.get();
# the _stop_after_first_heartbeat safety turns an unexpected wait into a failure.
_stop_after_first_heartbeat(monkeypatch)
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_active:0"),
"session_1",
)
# The handler must have exited cleanly and unsubscribed — not hung on the queue.
assert stream.unsubscribed is True, "handler must unsubscribe after a terminal, even when deduped"
def test_session_route_unsubscribes_when_replay_disconnects(monkeypatch):
import api.routes as routes
class _DisconnectingWfile:
def write(self, _data):
raise BrokenPipeError("client closed")
def flush(self):
pass
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
self.unsubscribed = False
def subscribe_with_snapshot(self):
return self.q, {"last_event_id": "run_active:1"}
def unsubscribe(self, q):
self.unsubscribed = q is self.q
stream = _FakeStream()
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: "run_active")
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: {
"status": "ok",
"events": [
{
"run_id": "run_prev",
"seq": 1,
"event": "token",
"payload": {"text": "replayed"},
"event_id": "run_prev:1",
},
],
},
)
handler = _FakeHandler()
handler.wfile = _DisconnectingWfile()
assert routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_prev:0"),
"session_1",
)
assert stream.unsubscribed is True
def test_session_route_live_delivery_without_cursor_keeps_buffered_active_items(monkeypatch):
import api.routes as routes
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
self.q.put_nowait(("token", {"text": "buffered"}, "run_active:1"))
self.q.put_nowait(("stream_end", {"status": "done"}, "run_active:2"))
def subscribe_with_snapshot(self):
return self.q, {"last_event_id": "run_active:1", "offline_buffered_events": 1}
stream = _FakeStream()
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: SimpleNamespace(
session_id=sid,
compact=lambda **_kwargs: {"session_id": sid, "title": "Session"},
),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: "run_active")
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("replay should not run without a cursor")),
)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
assert "id: run_active:1\n" in body
assert "id: run_active:2\n" in body
assert "event: stream_end\n" in body
def test_session_route_reconciles_late_attach_cutoff_once(monkeypatch):
import api.routes as routes
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
self.q.put_nowait(("token", {"text": "six"}, "run_active:6"))
self.q.put_nowait(("stream_end", {"status": "done"}, "run_active:7"))
self.unsubscribed = 0
def subscribe_with_snapshot(self):
return self.q, {"last_event_id": "run_active:6"}
def unsubscribe(self, q):
self.unsubscribed += q is self.q
stream = _FakeStream()
available = {"value": False}
reads = []
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(routes, "get_session", lambda sid, metadata_only=False: SimpleNamespace(session_id=sid, compact=lambda **_kwargs: {}))
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: "run_active" if available["value"] else None)
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: reads.append(1) or {"status": "ok", "events": [] if len(reads) == 1 else [{"event": "token", "payload": {"text": "six"}, "event_id": "run_active:6"}]},
)
monkeypatch.setattr(routes.time, "sleep", lambda _seconds: available.__setitem__("value", True))
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(handler, urlparse("/api/sessions/session_1/events?after_event_id=run_active:5"), "session_1")
body = handler.wfile.getvalue().decode("utf-8")
assert body.count("id: run_active:6\n") == 1
assert "id: run_active:7\n" in body
assert stream.unsubscribed == 1
def test_session_route_emits_snapshot_when_reconciliation_fails(monkeypatch):
import api.routes as routes
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
self.q.put_nowait(("token", {"text": "buffered"}, "run_active:1"))
self.q.put_nowait(("stream_end", {"status": "done"}, "run_active:2"))
def subscribe_with_snapshot(self):
return self.q, {"last_event_id": "run_active:1"}
stream = _FakeStream()
reads = []
sessions = iter(
[
SimpleNamespace(session_id="session_1", compact=lambda **_kwargs: {"session_id": "session_1", "title": "stale"}),
SimpleNamespace(session_id="session_1", compact=lambda **_kwargs: {"session_id": "session_1", "title": "fresh"}),
]
)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
routes,
"get_session",
lambda sid, metadata_only=False: next(sessions),
)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: "run_active")
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
monkeypatch.setattr(
routes,
"read_session_run_events",
lambda *_args, **_kwargs: reads.append(1) or (
{"status": "ok", "events": [{"event": "token", "payload": {"text": "replayed"}, "event_id": "run_prev:1"}]}
if len(reads) == 1
else {"status": "replay_noncontiguous", "events": []}
),
)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(
handler,
urlparse("/api/sessions/session_1/events?after_event_id=run_prev:0"),
"session_1",
)
body = handler.wfile.getvalue().decode("utf-8")
assert "event: session_snapshot\n" in body
assert '"title": "fresh"' in body
assert '"title": "stale"' not in body
assert "id: run_prev:1\n" not in body
assert body.count("id: run_active:1\n") == 0
assert "id: run_active:2\n" in body
def test_session_route_bounds_sent_event_id_deduplication(monkeypatch):
import api.routes as routes
class _FakeStream:
def __init__(self):
self.q = queue.Queue()
for seq in (1, 2, 3, 1):
self.q.put_nowait(("token", {"seq": seq}, f"run_active:{seq}"))
self.q.put_nowait(("stream_end", {}, "run_active:4"))
def subscribe_with_snapshot(self):
return self.q, {}
stream = _FakeStream()
monkeypatch.setattr(routes, "_SESSION_SSE_SENT_EVENT_ID_LIMIT", 2)
monkeypatch.setattr(routes, "_session_id_visible_to_request_profile", lambda *_args, **_kwargs: True)
monkeypatch.setattr(routes, "get_session", lambda sid, metadata_only=False: SimpleNamespace(session_id=sid, compact=lambda **_kwargs: {}))
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *_args, **_kwargs: "run_active")
monkeypatch.setattr(routes, "STREAMS", {"run_active": stream})
import api.config as config
monkeypatch.setattr(
config,
"STREAMS",
{"run_active": stream},
)
handler = _FakeHandler()
routes._handle_session_sse_stream_for_session(handler, urlparse("/api/sessions/session_1/events"), "session_1")
body = handler.wfile.getvalue().decode("utf-8")
assert body.count("id: run_active:1\n") == 2
assert "id: run_active:4\n" in body