personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_convey_callosum_sse.py
8.5 kB 260 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for Convey Callosum SSE route and bridge fan-out.""" 5 6from __future__ import annotations 7 8import json 9import time 10from collections.abc import Callable, Iterator 11 12import pytest 13 14import solstone.convey.bridge as convey_bridge 15from solstone.think.spl.health import LINK_HEALTH_EVENT 16 17 18@pytest.fixture(autouse=True) 19def clear_sse_subscribers() -> Iterator[None]: 20 with convey_bridge._SSE_LOCK: 21 convey_bridge._SSE_SUBSCRIBERS_BY_KEY.clear() 22 convey_bridge._SSE_LAST_CHAT_REQUEST_AT_BY_KEY.clear() 23 convey_bridge._STATE_CACHE["link_health"] = None 24 yield 25 with convey_bridge._SSE_LOCK: 26 convey_bridge._SSE_SUBSCRIBERS_BY_KEY.clear() 27 convey_bridge._SSE_LAST_CHAT_REQUEST_AT_BY_KEY.clear() 28 convey_bridge._STATE_CACHE["link_health"] = None 29 30 31def _next_chunk(response) -> str: 32 chunk = next(iter(response.response)) 33 if isinstance(chunk, bytes): 34 return chunk.decode("utf-8") 35 return str(chunk) 36 37 38def _parse_sse_data(chunk: str) -> dict: 39 for line in chunk.splitlines(): 40 if line.startswith("data: "): 41 return json.loads(line[len("data: ") :]) 42 raise AssertionError(f"No data line found in chunk: {chunk!r}") 43 44 45def _next_data(response) -> dict: 46 for chunk in response.response: 47 text = chunk.decode("utf-8") if isinstance(chunk, bytes) else str(chunk) 48 if "data: " in text: 49 return _parse_sse_data(text) 50 raise AssertionError("SSE stream ended before a data frame was received") 51 52 53def _wait_until(predicate: Callable[[], bool], timeout: float = 1.0) -> None: 54 deadline = time.monotonic() + timeout 55 while time.monotonic() < deadline: 56 if predicate(): 57 return 58 time.sleep(0.01) 59 raise AssertionError("timed out waiting for condition") 60 61 62def test_callosum_sse_success_headers(convey_env): 63 env = convey_env() 64 65 resp = env.client.get("/sse/events", buffered=False) 66 try: 67 assert resp.status_code == 200 68 assert resp.content_type.startswith("text/event-stream") 69 assert resp.headers["Cache-Control"] == "no-cache" 70 assert resp.headers["X-Accel-Buffering"] == "no" 71 finally: 72 resp.close() 73 74 75def test_callosum_sse_setup_complete_serves_with_proxy_header(convey_env): 76 env = convey_env() 77 78 resp = env.client.get( 79 "/sse/events", 80 headers={"X-Forwarded-For": "1.2.3.4"}, 81 buffered=False, 82 ) 83 try: 84 assert resp.status_code == 200 85 assert resp.content_type.startswith("text/event-stream") 86 finally: 87 resp.close() 88 89 90def test_callosum_sse_round_trip_payload(convey_env): 91 env = convey_env() 92 resp = env.client.get("/sse/events", buffered=False) 93 try: 94 assert resp.status_code == 200 95 assert _next_chunk(resp) == ": heartbeat\n\n" 96 assert convey_bridge.subscription_count("convey-ui") == 1 97 message = {"tract": "test", "event": "ping", "ts": 0, "extra": "value"} 98 convey_bridge._broadcast_callosum_event(message) 99 100 parsed = _next_data(resp) 101 assert parsed == message 102 finally: 103 resp.close() 104 105 106def _link_health_message( 107 *, 108 generation: int, 109 state: str = "connected", 110 ts: int = 1_700_000_000_000, 111) -> dict: 112 return { 113 "tract": "link", 114 "event": LINK_HEALTH_EVENT, 115 "state": state, 116 "listen_generation": generation, 117 "last_successful_relay_tunnel_at": 1_700_000_000_100, 118 "last_relay_tunnel_error": None, 119 "last_relay_tunnel_error_at": None, 120 "relay_tunnel_error_status": None, 121 "ts": ts, 122 } 123 124 125def test_bridge_caches_structured_link_health() -> None: 126 convey_bridge._broadcast_callosum_event(_link_health_message(generation=7)) 127 128 assert convey_bridge.get_cached_state()["link_health"] == { 129 "state": "connected", 130 "listen_generation": 7, 131 "last_successful_relay_tunnel_at": 1_700_000_000_100, 132 "last_relay_tunnel_error": None, 133 "last_relay_tunnel_error_at": None, 134 "relay_tunnel_error_status": None, 135 "ts": 1_700_000_000_000, 136 } 137 138 139def test_bridge_ignores_coarse_link_events_for_health_cache() -> None: 140 for event in ( 141 "connecting", 142 "connected", 143 "disconnect", 144 "tunnel_pair", 145 "tunnel_close", 146 ): 147 convey_bridge._broadcast_callosum_event({"tract": "link", "event": event}) 148 assert convey_bridge.get_cached_state()["link_health"] is None 149 150 151def test_bridge_drops_older_link_health_generation() -> None: 152 convey_bridge._broadcast_callosum_event( 153 _link_health_message(generation=3, state="connected") 154 ) 155 convey_bridge._broadcast_callosum_event( 156 _link_health_message(generation=2, state="reconnecting") 157 ) 158 159 health = convey_bridge.get_cached_state()["link_health"] 160 assert health["listen_generation"] == 3 161 assert health["state"] == "connected" 162 163 164def test_bridge_overwrites_equal_or_newer_link_health_generation() -> None: 165 convey_bridge._broadcast_callosum_event( 166 _link_health_message(generation=3, state="connected") 167 ) 168 convey_bridge._broadcast_callosum_event( 169 _link_health_message(generation=3, state="connecting") 170 ) 171 assert convey_bridge.get_cached_state()["link_health"]["state"] == "connecting" 172 173 convey_bridge._broadcast_callosum_event( 174 _link_health_message(generation=4, state="reconnecting") 175 ) 176 health = convey_bridge.get_cached_state()["link_health"] 177 assert health["listen_generation"] == 4 178 assert health["state"] == "reconnecting" 179 180 181def test_callosum_sse_multi_client_fanout(convey_env): 182 env = convey_env() 183 first_client = env.app.test_client() 184 second_client = env.app.test_client() 185 first = first_client.get("/sse/events", buffered=False) 186 second = second_client.get("/sse/events", buffered=False) 187 try: 188 assert _next_chunk(first) == ": heartbeat\n\n" 189 assert _next_chunk(second) == ": heartbeat\n\n" 190 assert convey_bridge.subscription_count("convey-ui") == 2 191 192 message = {"tract": "test", "event": "fanout", "ts": 1} 193 convey_bridge._broadcast_callosum_event(message) 194 195 assert _next_data(first) == message 196 assert _next_data(second) == message 197 finally: 198 second.close() 199 first.close() 200 201 202# test_slow_sse_subscriber_is_dropped_without_blocking_healthy_subscriber was 203# dropped here 2026-07-24: duplicated verbatim by 204# solstone/apps/observer/tests/test_callosum_sse.py, which CI already collects, 205# and it carried this module's only wall-clock assertion (elapsed < 0.5). 206 207 208def test_callosum_sse_dropped_subscriber_breaks_stream(convey_env): 209 env = convey_env() 210 resp = env.client.get("/sse/events", buffered=False) 211 extra = None 212 try: 213 assert _next_chunk(resp) == ": heartbeat\n\n" 214 extra = convey_bridge.register_sse_subscriber("convey-ui") 215 with convey_bridge._SSE_LOCK: 216 subscribers = set(convey_bridge._SSE_SUBSCRIBERS_BY_KEY["convey-ui"]) 217 route_handles = [handle for handle in subscribers if handle is not extra] 218 assert len(route_handles) == 1 219 route_handle = route_handles[0] 220 221 for i in range(convey_bridge._SSE_QUEUE_MAXSIZE + 1): 222 convey_bridge._broadcast_callosum_event( 223 {"tract": "test", "event": "overflow", "ts": i} 224 ) 225 226 assert route_handle.dropped.is_set() 227 assert route_handle not in convey_bridge._SSE_SUBSCRIBERS_BY_KEY.get( 228 "convey-ui", set() 229 ) 230 with pytest.raises(StopIteration): 231 next(iter(resp.response)) 232 finally: 233 if extra is not None: 234 convey_bridge.unregister_sse_subscriber(extra) 235 resp.close() 236 237 238def test_callosum_sse_clean_disconnect_unregisters_subscriber(convey_env): 239 env = convey_env() 240 resp = env.client.get("/sse/events", buffered=False) 241 242 assert _next_chunk(resp) == ": heartbeat\n\n" 243 assert convey_bridge.subscription_count("convey-ui") == 1 244 245 resp.close() 246 247 _wait_until(lambda: convey_bridge.subscription_count("convey-ui") == 0) 248 249 250def test_callosum_sse_heartbeat(convey_env, monkeypatch): 251 env = convey_env() 252 monkeypatch.setattr(convey_bridge, "_SSE_HEARTBEAT_SECONDS", 0.01) 253 254 resp = env.client.get("/sse/events", buffered=False) 255 try: 256 assert resp.status_code == 200 257 assert _next_chunk(resp) == ": heartbeat\n\n" 258 assert _next_chunk(resp) == ": heartbeat\n\n" 259 finally: 260 resp.close()