From ae4a84e552bf0ff22baf0991bc67be4891958629 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Sun, 26 Apr 2026 20:17:52 -0700 Subject: [PATCH] refactor(routes): wire DELETE on path-keyed workstreams/{ws_id}/send MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-flight for the legacy URL adapter removal: the path-keyed `/v1/api/workstreams/{ws_id}/send` route only mounted POST today; the dequeue handler was reachable only via the legacy `DELETE /v1/api/send` body-keyed URL through `_make_method_dispatch`. Add a new `dequeue: Handler | None = None` slot on `SharedSessionVerbHandlers` next to `send`, mounted as a second `Route` on the same path with `methods=["DELETE"]` (two distinct Routes rather than collapsing methods on one Route — different handler callables, and collapsing would force the same method-dispatch wrapper this cleanup is tearing out). Wire `dequeue=dequeue_handler` in `turnstone/server.py`'s `SharedSessionVerbHandlers(...)` call so DELETE on the path-keyed shape works in the same merge as the legacy mount removal. Adds a regression-locking test covering both the POST+DELETE and the dequeue-alone cases. --- tests/test_session_routes.py | 28 ++++++++++++++++++++++++++++ turnstone/core/session_routes.py | 3 +++ turnstone/server.py | 1 + 3 files changed, 32 insertions(+) diff --git a/tests/test_session_routes.py b/tests/test_session_routes.py index a42518de..c4e896fe 100644 --- a/tests/test_session_routes.py +++ b/tests/test_session_routes.py @@ -119,6 +119,34 @@ def test_attachment_routes_mount_when_quartet_provided() -> None: ) in paths +def test_send_mounts_post_and_delete_when_dequeue_provided() -> None: + """``handlers.send`` mounts POST {prefix}/{ws_id}/send and + ``handlers.dequeue`` mounts DELETE on the same path. The two + routes register as separate ``Route`` entries with disjoint + method sets — Starlette dispatches by (path, method).""" + routes: list[Any] = [] + register_session_routes( + routes, + prefix="/api/workstreams", + handlers=SharedSessionVerbHandlers(send=_stub, dequeue=_stub), + ) + paths = {(p, m) for p, m in _route_paths(routes)} + assert ("/api/workstreams/{ws_id}/send", frozenset({"POST"})) in paths + assert ("/api/workstreams/{ws_id}/send", frozenset({"DELETE"})) in paths + + # ``dequeue`` is independent of ``send`` — providing it alone + # mounts only the DELETE half (no POST regression). + routes_dequeue_only: list[Any] = [] + register_session_routes( + routes_dequeue_only, + prefix="/api/workstreams", + handlers=SharedSessionVerbHandlers(dequeue=_stub), + ) + paths_dequeue_only = {(p, m) for p, m in _route_paths(routes_dequeue_only)} + assert ("/api/workstreams/{ws_id}/send", frozenset({"DELETE"})) in paths_dequeue_only + assert ("/api/workstreams/{ws_id}/send", frozenset({"POST"})) not in paths_dequeue_only + + def test_close_legacy_mounts_when_handler_provided() -> None: """The legacy body-keyed close (``POST {prefix}/close``) mounts when ``handlers.close_legacy`` is non-``None`` — there is no diff --git a/turnstone/core/session_routes.py b/turnstone/core/session_routes.py index 25380b34..123c3881 100644 --- a/turnstone/core/session_routes.py +++ b/turnstone/core/session_routes.py @@ -482,6 +482,7 @@ class SharedSessionVerbHandlers: # Per-``{ws_id}`` interaction (coord shape today; interactive # adopts these in Priority 1's worker dispatch unification) send: Handler | None = None # POST {prefix}/{ws_id}/send + dequeue: Handler | None = None # DELETE {prefix}/{ws_id}/send approve: Handler | None = None # POST {prefix}/{ws_id}/approve plan: Handler | None = None # POST {prefix}/{ws_id}/plan cancel: Handler | None = None # POST {prefix}/{ws_id}/cancel @@ -566,6 +567,8 @@ def register_session_routes( routes.append(Route(f"{p}/{{ws_id}}/title", handlers.set_title, methods=["POST"])) if handlers.send is not None: routes.append(Route(f"{p}/{{ws_id}}/send", handlers.send, methods=["POST"])) + if handlers.dequeue is not None: + routes.append(Route(f"{p}/{{ws_id}}/send", handlers.dequeue, methods=["DELETE"])) if handlers.approve is not None: routes.append(Route(f"{p}/{{ws_id}}/approve", handlers.approve, methods=["POST"])) if handlers.plan is not None: diff --git a/turnstone/server.py b/turnstone/server.py index 920075c2..f844daee 100644 --- a/turnstone/server.py +++ b/turnstone/server.py @@ -3556,6 +3556,7 @@ def create_app( refresh_title=refresh_workstream_title, set_title=set_workstream_title, send=send_handler, # lifted: shared body (P1.5) + dequeue=dequeue_handler, # lifted (P1.5) — DELETE /send approve=approve_handler, # lifted: shared body cancel=cancel_handler, # lifted: shared body events=events_handler, # lifted: shared body