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