From bff74392002654b73652644b9cb185d17cc4ef27 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Wed, 20 May 2026 23:21:59 -0400 Subject: [PATCH 1/2] test(server): failing test for lazy mode state_change silent drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #793: ``_handle_buckaroo_state_change`` returns silently when ``session.mode != "buckaroo"``. Lazy-polars sessions (loaded via ``POST /load`` with ``mode=lazy``) therefore get their state_change messages dropped on the floor — no rebroadcast, no error frame, nothing. The WS client (AG-Grid in the real world, this test in CI) hangs waiting for a frame that never comes until its read times out. Test fires a state_change against a lazy session and asserts the server responds within 3 seconds with a structured error frame. Fails today (TimeoutError); will pass once the handler returns an explicit ``{"type": "error", "error_code": ...}`` message. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/unit/server/test_load_expr.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/unit/server/test_load_expr.py b/tests/unit/server/test_load_expr.py index e4d6cc9db..a53ede645 100644 --- a/tests/unit/server/test_load_expr.py +++ b/tests/unit/server/test_load_expr.py @@ -141,6 +141,48 @@ async def test_ws_search_pushdown(self): finally: shutil.rmtree(builds_root, ignore_errors=True) + @tornado.testing.gen_test + async def test_lazy_state_change_returns_explicit_error(self): + """Regression for #793: ``_handle_buckaroo_state_change`` + returned silently for ``mode != "buckaroo"`` — lazy-polars + sessions had their state_change messages dropped on the floor + with no response, hanging the client until its read timed out. + + Must return a structured error instead so callers can render + a sensible "not supported" message. + """ + import asyncio + csv_fd, csv_path = tempfile.mkstemp(suffix=".csv") + os.close(csv_fd) + try: + pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]}).to_csv(csv_path, index=False) + await _post(self.get_http_port(), "/load", + {"session": "lazy-sc", "path": csv_path, "mode": "lazy"}) + + ws = await tornado.websocket.websocket_connect( + f"ws://localhost:{self.get_http_port()}/ws/lazy-sc") + await ws.read_message() # discard initial_state + + ws.write_message(json.dumps({ + "type": "buckaroo_state_change", + "new_state": { + "post_processing": "", "cleaning_method": "", + "quick_command_args": {"search": ["x"]}, + "df_display": "main", "show_commands": False, + "sampled": False, "search_string": "x", + }})) + + # Today: nothing comes back; client times out. + # After fix: a structured error frame within a couple seconds. + frame = await asyncio.wait_for(ws.read_message(), timeout=3.0) + self.assertIsNotNone(frame) + d = json.loads(frame) + self.assertEqual(d.get("type"), "error") + self.assertIn("error_code", d) + ws.close() + finally: + os.unlink(csv_path) + @tornado.testing.gen_test async def test_session_reuse_xorq_then_pandas(self): """A client that POSTs /load_expr and then POSTs /load with the From 0ce51c66942931b433e6a9b35f4b6163647802be Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Wed, 20 May 2026 23:23:15 -0400 Subject: [PATCH 2/2] fix(server): explicit error for state_change on read-only modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #793. ``_handle_buckaroo_state_change`` returned silently when ``session.mode != "buckaroo"`` — lazy-polars sessions had their state_change messages silently dropped, leaving WS clients (AG-Grid, third-party callers, automated tests) waiting on a frame that never arrives until they time out. This patch replaces the silent ``return`` with a structured ``{"type": "error", "error_code": "state_change_unsupported_mode", "message": ...}`` frame. The client can now render "filtering not supported in lazy mode" instead of hanging. Two flavors were considered (per the issue): A. Wire state_change into lazy mode (apply filter via ldf.filter, re-broadcast initial_state). The product-right answer. B. Explicit error. This patch is B — stop-the-bleed. A is the natural follow-up if filter/cleaning support in lazy mode is on the roadmap; tracking the remaining work separately. Co-Authored-By: Claude Opus 4.7 (1M context) --- buckaroo/server/websocket_handler.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/buckaroo/server/websocket_handler.py b/buckaroo/server/websocket_handler.py index da5917932..0a00202eb 100644 --- a/buckaroo/server/websocket_handler.py +++ b/buckaroo/server/websocket_handler.py @@ -51,7 +51,21 @@ def on_message(self, message): def _handle_buckaroo_state_change(self, new_state): sessions = self.application.settings["sessions"] session = sessions.get(self.session_id) - if not session or session.mode != "buckaroo": + if not session: + return + if session.mode != "buckaroo": + # Lazy mode (and any future read-only mode) doesn't currently + # plumb state_change → filter into its dataflow. Return a + # structured error rather than the pre-#793 silent drop so the + # client can render "filtering not supported in this mode" + # instead of hanging on the WS read. + self.write_message(json.dumps({ + "type": "error", + "error_code": "state_change_unsupported_mode", + "message": ( + f"buckaroo_state_change is not supported in " + f"mode={session.mode!r}; this reader is read-only " + "(no filter / cleaning / post-processing).")})) return dataflow = session.xorq_dataflow if session.backend == "xorq" else session.dataflow if dataflow is None: