From 3070bc4eb54ccae0ecd5b3ab84cc6e925bdfd279 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Sat, 30 May 2026 04:08:57 -0700 Subject: [PATCH] fix(sse): coerce non-int ui._event_id to None when stamping saves When the active UI is a MagicMock test double, _ui_event_id() returned the auto-vivified _event_id mock (getattr finds it, so the None default never applies). That mock reached the conversations INSERT and failed to bind ("type 'MagicMock' is not supported"), so save_message raised, the row was dropped, and tests on the real-storage + mock-UI path broke (CI: test_session_attachments::test_db_row_stores_text_only). Coerce a non-int _event_id to None so mock UIs -- and counterless CLI/eval/placeholder UIs -- stamp NULL (the synthetic-snapshot floor), matching the documented contract. Production UIs always carry an int, so behaviour there is unchanged. Also drop two redundant local `import json` in the new /history integration tests; the module-level import already covers them. --- tests/test_workstream_endpoints.py | 4 ---- turnstone/core/session.py | 13 ++++++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/test_workstream_endpoints.py b/tests/test_workstream_endpoints.py index 1af21f90..ad5d78d9 100644 --- a/tests/test_workstream_endpoints.py +++ b/tests/test_workstream_endpoints.py @@ -1304,8 +1304,6 @@ class TestHistoryInteractive: ``cursor`` = the resolved boundary's event_id. The client opens its initial SSE with that cursor so the delta rebuilds the turn. """ - import json - ws_id = "ws-cursor" _inject_storage.register_workstream(ws_id, kind="interactive", user_id="test-user") _inject_storage.save_message(ws_id, "user", "kick off", event_id=10) @@ -1340,8 +1338,6 @@ class TestHistoryInteractive: rendered block) and returns ``cursor: null`` — the client connects fresh to the synthetic-snapshot floor, never leaving the turn unrenderable.""" - import json - ws_id = "ws-cursor-reload" _inject_storage.register_workstream(ws_id, kind="interactive", user_id="test-user") _inject_storage.save_message(ws_id, "user", "kick off", event_id=10) diff --git a/turnstone/core/session.py b/turnstone/core/session.py index b3205dc6..f099e7bd 100644 --- a/turnstone/core/session.py +++ b/turnstone/core/session.py @@ -2134,12 +2134,15 @@ class ChatSession: """Current per-ws SSE ring-buffer high-water mark for stamping saved messages with the ``Last-Event-ID`` resume cursor. - Returns ``getattr(self.ui, "_event_id", None)`` — ``None`` for - UIs without the counter (CLI / eval / placeholder), whose rows - then stay NULL and are treated by ``/history`` as "no - fast-forward cursor available" (the synthetic-snapshot floor). + ``None`` for UIs without an integer counter — CLI / eval / + placeholder UIs (no ``_event_id``), and test doubles whose + ``self.ui`` is a ``MagicMock`` (a non-int ``_event_id`` would + otherwise reach the INSERT and fail to bind). Those rows stay + NULL and are treated by ``/history`` as "no fast-forward cursor + available" (the synthetic-snapshot floor). """ - return getattr(self.ui, "_event_id", None) + eid = getattr(self.ui, "_event_id", None) + return eid if isinstance(eid, int) else None def _remaining_token_budget(self) -> int: """Estimate how many tokens are available for new content.