refactor(server): lift close handler into shared session_routes body

Stage 2 Priority 0 Step 0.2 body-convergence — second verb.
``make_close_handler(audit_emit=..., supports_close_reason=...)``
factory in ``turnstone/core/session_routes.py`` produces the lifted
body; both interactive and coord pass their kind-specific audit
emitter at app construction.

The two body-keyed close URL aliases on the interactive side reach
the same lifted body:

- ``POST /v1/api/workstreams/{ws_id}/close`` (new, path-keyed)
  via ``register_session_routes(handlers.close=...)``.
- ``POST /v1/api/workstreams/close`` (legacy, body-keyed) via
  ``make_legacy_body_keyed_adapter(close_handler)``.

Coord exposes only the path-keyed shape.

Behavior gains:

- ``supports_close_reason=True`` (interactive only) keeps the 512-
  byte UTF-8 cap + credential redaction + ``workstream_config``
  persistence path. Coord stays at ``False``; if coord ever wants
  close-reason metadata, flipping the flag is a one-line change.
- ``audit_emit`` is per-kind so each owns its detail dict shape
  (``{kind, parent_ws_id, reason}`` vs ``{coord_ws_id, src}``) and
  audit action name (``workstream.closed`` vs ``coordinator.close``).
- Standardizes the close-failure status code to 404 across both
  kinds. The coord code previously returned 500 on a
  ``mgr.close()`` race-loss, which was overly pessimistic — the
  semantic is "the ws was popped between .get() and .close()", i.e.
  not-found.

Coord-side test fixtures (``test_coordinator_endpoints``,
``test_coordinator_end_to_end``) swap the imported
``coordinator_close`` for the lifted handler + a local audit_emit
adapter so the tests exercise the same code path the live console
does.

ruff + mypy + 4366 pytest pass. Live console smoke against
``POST /v1/api/workstreams/abc/close`` returns 503 (no coord_mgr
loaded in the smoke env) — proves the lifted handler is reachable
+ the manager_lookup callable fires correctly.

Two verbs converged so far (``approve`` + ``close``); the remaining
pairs (``send``, ``cancel``, ``open``, ``events``, ``create``,
``list``, ``saved``, ``history``, ``detail``) have substantive
behavior divergence that doesn't factor cleanly into the
SessionEndpointConfig + factory-handler pattern — see the
session_routes module docstring for the per-verb status.
This commit is contained in:
Patrick Buckley
2026-04-24 16:16:33 -07:00
committed by Patrick Buckley
parent 6415eeb91e
commit 06c91294a4
5 changed files with 250 additions and 131 deletions
+34 -3
View File
@@ -8,6 +8,7 @@ enforcement, and lazy rehydration on GET /{ws_id}.
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
import httpx
@@ -28,12 +29,12 @@ from tests._coord_test_helpers import (
)
from turnstone.console.coordinator_ui import ConsoleCoordinatorUI
from turnstone.console.server import (
_auth_user_id,
_require_admin_coordinator,
_require_coord_mgr,
cluster_ws_detail,
coordinator_cancel,
coordinator_children,
coordinator_close,
coordinator_create,
coordinator_detail,
coordinator_history,
@@ -43,10 +44,37 @@ from turnstone.console.server import (
coordinator_send,
coordinator_tasks,
)
from turnstone.core.audit import record_audit
from turnstone.core.auth import AuthResult
from turnstone.core.session_routes import SessionEndpointConfig, make_approve_handler
from turnstone.core.session_routes import (
SessionEndpointConfig,
make_approve_handler,
make_close_handler,
)
from turnstone.core.storage._sqlite import SQLiteBackend
if TYPE_CHECKING:
from turnstone.core.workstream import Workstream
def _audit_close_coordinator_for_test(
request,
ws_id: str,
ws_before: Workstream, # noqa: ARG001
reason: str, # noqa: ARG001
) -> None:
storage = request.app.state.auth_storage
record_audit(
storage,
_auth_user_id(request),
"coordinator.close",
"workstream",
ws_id,
{"coord_ws_id": ws_id, "src": "coordinator"},
request.client.host if request.client else "",
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@@ -97,7 +125,10 @@ def _make_client(
),
Route(
"/v1/api/workstreams/{ws_id}/close",
coordinator_close,
make_close_handler(
audit_emit=_audit_close_coordinator_for_test,
supports_close_reason=False,
),
methods=["POST"],
),
Route(