mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
refactor(server): mount coord-only verbs through register_coord_verbs
Stage 2 Priority 0 Step 0.3 — adds ``CoordVerbHandlers`` +
``register_coord_verbs`` to ``turnstone.core.session_routes`` and
wires the seven coord-only verbs (``children`` / ``tasks`` /
``metrics`` / ``trust`` / ``restrict`` / ``stop_cascade`` /
``close_all_children``) through it on the console.
These verbs are legitimately kind-specific — they read or mutate
state (children registry, parent quota, trust / restrict policy,
cascade controls) that doesn't exist on interactive workstreams —
so they live on a Protocol distinct from
``SessionRouteHandlers``. The unified URL prefix
``/api/workstreams/{ws_id}/`` is shared with the session verbs;
the separate registrar call keeps the kind separation explicit at
the wiring site.
Legacy ``/api/coordinator/{ws_id}/{verb}`` paths stay live during
the transition; both URL shapes resolve to the same handler
function. Step 0.4 deletes the legacy shape.
The route-table walk in ``test_session_routes`` now covers all
eighteen verb pairs (eleven session + seven coord-only) so a
future drift between legacy and unified handlers fails CI.
This commit is contained in:
committed by
Patrick Buckley
parent
e1ee84af42
commit
377bd58b67
@@ -22,8 +22,10 @@ from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
from turnstone.core.session_routes import (
|
||||
CoordVerbHandlers,
|
||||
SessionRouteConfig,
|
||||
SessionRouteHandlers,
|
||||
register_coord_verbs,
|
||||
register_session_routes,
|
||||
)
|
||||
|
||||
@@ -202,6 +204,35 @@ def test_specific_verbs_register_before_bare_detail() -> None:
|
||||
assert paths.index("/api/workstreams/{ws_id}/events") < detail_idx
|
||||
|
||||
|
||||
def test_register_coord_verbs_mounts_expected_paths() -> None:
|
||||
"""``register_coord_verbs`` mounts the seven coord-only verbs
|
||||
at the unified prefix."""
|
||||
routes: list[Any] = []
|
||||
register_coord_verbs(
|
||||
routes,
|
||||
prefix="/api/workstreams",
|
||||
handlers=CoordVerbHandlers(
|
||||
children=_stub,
|
||||
tasks=_stub,
|
||||
metrics=_stub,
|
||||
trust=_stub,
|
||||
restrict=_stub,
|
||||
stop_cascade=_stub,
|
||||
close_all_children=_stub,
|
||||
),
|
||||
)
|
||||
paths = {(p, m) for p, m in _route_paths(routes)}
|
||||
assert paths == {
|
||||
("/api/workstreams/{ws_id}/children", frozenset({"GET", "HEAD"})),
|
||||
("/api/workstreams/{ws_id}/tasks", frozenset({"GET", "HEAD"})),
|
||||
("/api/workstreams/{ws_id}/metrics", frozenset({"GET", "HEAD"})),
|
||||
("/api/workstreams/{ws_id}/trust", frozenset({"POST"})),
|
||||
("/api/workstreams/{ws_id}/restrict", frozenset({"POST"})),
|
||||
("/api/workstreams/{ws_id}/stop_cascade", frozenset({"POST"})),
|
||||
("/api/workstreams/{ws_id}/close_all_children", frozenset({"POST"})),
|
||||
}
|
||||
|
||||
|
||||
def test_console_create_app_exposes_unified_workstream_paths() -> None:
|
||||
"""The console's ``create_app`` mounts coord verbs at both the
|
||||
legacy ``/api/coordinator/`` shape and the unified
|
||||
@@ -274,6 +305,16 @@ def test_console_unified_paths_route_to_legacy_handlers() -> None:
|
||||
"/api/coordinator/{ws_id}/events": "/api/workstreams/{ws_id}/events",
|
||||
"/api/coordinator/{ws_id}/history": "/api/workstreams/{ws_id}/history",
|
||||
"/api/coordinator/{ws_id}": "/api/workstreams/{ws_id}",
|
||||
# Step 0.3: coord-only verbs.
|
||||
"/api/coordinator/{ws_id}/children": "/api/workstreams/{ws_id}/children",
|
||||
"/api/coordinator/{ws_id}/tasks": "/api/workstreams/{ws_id}/tasks",
|
||||
"/api/coordinator/{ws_id}/metrics": "/api/workstreams/{ws_id}/metrics",
|
||||
"/api/coordinator/{ws_id}/trust": "/api/workstreams/{ws_id}/trust",
|
||||
"/api/coordinator/{ws_id}/restrict": "/api/workstreams/{ws_id}/restrict",
|
||||
"/api/coordinator/{ws_id}/stop_cascade": "/api/workstreams/{ws_id}/stop_cascade",
|
||||
"/api/coordinator/{ws_id}/close_all_children": (
|
||||
"/api/workstreams/{ws_id}/close_all_children"
|
||||
),
|
||||
}
|
||||
# Walked Route paths are relative to their parent Mount; the
|
||||
# ``/v1`` prefix is applied at request time, not stored on the
|
||||
|
||||
@@ -55,8 +55,10 @@ from turnstone.core.auth import (
|
||||
)
|
||||
from turnstone.core.rendezvous import NoAvailableNodeError
|
||||
from turnstone.core.session_routes import (
|
||||
CoordVerbHandlers,
|
||||
SessionRouteConfig,
|
||||
SessionRouteHandlers,
|
||||
register_coord_verbs,
|
||||
register_session_routes,
|
||||
)
|
||||
from turnstone.core.skill_kind import SkillKind
|
||||
@@ -10171,6 +10173,22 @@ def create_app(
|
||||
history=coordinator_history,
|
||||
),
|
||||
)
|
||||
# Step 0.3: coord-only verbs (children registry, quotas, trust /
|
||||
# restrict policy, cascade controls) mount alongside under the
|
||||
# same unified prefix.
|
||||
register_coord_verbs(
|
||||
coord_workstream_routes,
|
||||
prefix="/api/workstreams",
|
||||
handlers=CoordVerbHandlers(
|
||||
children=coordinator_children,
|
||||
tasks=coordinator_tasks,
|
||||
metrics=coordinator_metrics,
|
||||
trust=coordinator_trust,
|
||||
restrict=coordinator_restrict,
|
||||
stop_cascade=coordinator_stop_cascade,
|
||||
close_all_children=coordinator_close_all_children,
|
||||
),
|
||||
)
|
||||
|
||||
app = Starlette(
|
||||
routes=[
|
||||
|
||||
@@ -255,3 +255,80 @@ def register_session_routes(
|
||||
# suffixed patterns above win for ``{ws_id}/...`` paths.
|
||||
if handlers.detail is not None:
|
||||
routes.append(Route(f"{p}/{{ws_id}}", handlers.detail, methods=["GET"]))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CoordVerbHandlers:
|
||||
"""Bundle of coord-only HTTP handler callables.
|
||||
|
||||
These verbs are legitimately kind-specific (they read or mutate
|
||||
coord-only state — children registry, parent quota, trust /
|
||||
restrict policy, cascade controls) so they live on a separate
|
||||
Protocol from :class:`SessionRouteHandlers`. Mounted alongside
|
||||
the shared session verbs at the same ``/api/workstreams/{ws_id}/``
|
||||
prefix so the URL surface stays unified, but registered through
|
||||
a distinct call so the kind separation is explicit at the wiring
|
||||
site.
|
||||
|
||||
Step 0.3 placeholder: handlers live in
|
||||
``turnstone/console/server.py`` and are passed in here; the
|
||||
body-convergence follow-on lifts them into a coord-specific
|
||||
module.
|
||||
"""
|
||||
|
||||
children: Handler # GET {prefix}/{ws_id}/children
|
||||
tasks: Handler # GET {prefix}/{ws_id}/tasks
|
||||
metrics: Handler # GET {prefix}/{ws_id}/metrics
|
||||
trust: Handler # POST {prefix}/{ws_id}/trust
|
||||
restrict: Handler # POST {prefix}/{ws_id}/restrict
|
||||
stop_cascade: Handler # POST {prefix}/{ws_id}/stop_cascade
|
||||
close_all_children: Handler # POST {prefix}/{ws_id}/close_all_children
|
||||
|
||||
|
||||
def register_coord_verbs(
|
||||
routes: list[BaseRoute],
|
||||
*,
|
||||
prefix: str,
|
||||
mgr: SessionManager | None = None,
|
||||
handlers: CoordVerbHandlers,
|
||||
) -> None:
|
||||
"""Mount coord-only verbs at the unified ``{prefix}/{ws_id}/...`` shape.
|
||||
|
||||
Call ordering vs :func:`register_session_routes` doesn't matter
|
||||
in practice — Starlette's default ``str`` path converter is
|
||||
single-segment, so ``{ws_id}/{verb}`` patterns can never collide
|
||||
with the bare ``{ws_id}`` detail GET registered by
|
||||
``register_session_routes``. The body-convergence follow-on will
|
||||
additionally have these handlers 404 on non-coord ws_ids via the
|
||||
manager's kind check; today the legacy ``admin.coordinator``
|
||||
permission gate and ``_require_coord_mgr`` 503 are the
|
||||
enforcement.
|
||||
|
||||
Args:
|
||||
routes: list to extend; typically the inner ``Mount`` route
|
||||
list a Starlette app uses.
|
||||
prefix: URL prefix relative to the mount, e.g.
|
||||
``"/api/workstreams"``.
|
||||
mgr: the coord session manager when available at
|
||||
app-construction time. Optional today because the console
|
||||
builds its coord manager in the lifespan; becomes required
|
||||
in the body-convergence follow-on.
|
||||
handlers: bundle of handler callables for the coord-only
|
||||
verbs.
|
||||
"""
|
||||
_ = mgr # forward-wired; see :func:`register_session_routes`
|
||||
|
||||
p = prefix.rstrip("/")
|
||||
routes.append(Route(f"{p}/{{ws_id}}/children", handlers.children, methods=["GET"]))
|
||||
routes.append(Route(f"{p}/{{ws_id}}/tasks", handlers.tasks, methods=["GET"]))
|
||||
routes.append(Route(f"{p}/{{ws_id}}/metrics", handlers.metrics, methods=["GET"]))
|
||||
routes.append(Route(f"{p}/{{ws_id}}/trust", handlers.trust, methods=["POST"]))
|
||||
routes.append(Route(f"{p}/{{ws_id}}/restrict", handlers.restrict, methods=["POST"]))
|
||||
routes.append(Route(f"{p}/{{ws_id}}/stop_cascade", handlers.stop_cascade, methods=["POST"]))
|
||||
routes.append(
|
||||
Route(
|
||||
f"{p}/{{ws_id}}/close_all_children",
|
||||
handlers.close_all_children,
|
||||
methods=["POST"],
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user