mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
refactor(core): lift list + saved verb bodies across both kinds (Stage 2 verb lift)
New ``make_list_handler(cfg)`` and ``make_saved_handler(cfg)``
factories in ``turnstone/core/session_routes.py`` replace four
pre-lift bodies (interactive ``list_workstreams`` +
``list_saved_workstreams``; coord ``coordinator_list`` +
``coordinator_saved``). Same factory + capability-flag pattern as
the merged cancel / open / events / create lifts.
Four new ``SessionEndpointConfig`` fields:
- ``list_resolve_titles: ListResolveTitles | None`` — bulk lookup
``(ws_ids) -> {ws_id: title-or-None}``. Interactive wires
``get_workstream_display_names`` (new bulk helper added on the
storage layer + memory.py); the lifted body resolves every active
row in ONE ``SELECT ... WHERE ws_id IN (...)`` instead of the
pre-lift N+1 (one SELECT per row).
- ``list_kind: WorkstreamKind | None`` — explicit kind classifier
for the saved-list storage filter. Replaces the initial draft's
``audit_action_prefix == "coordinator"`` string compare which
would have silently leaked INTERACTIVE rows for any future kind
whose audit prefix didn't match. Required when a kind mounts
list/saved; misconfig surfaces as a 500 with a clear log line.
- ``saved_state_filter: str | None`` — coord wires ``"closed"``;
interactive wires ``None``.
- ``saved_loaded_lookup: SavedLoadedLookup | None`` — coord-only
defence-in-depth filter that excludes ws_ids in the warm pool.
Behaviour changes (all observable in CHANGELOG):
- **Active-list row shape converges on always-include** ``{ws_id,
name, state, kind, parent_ws_id, user_id}``. Interactive renames
``id`` → ``ws_id``; both kinds populate every field (coord adds
kind + parent_ws_id; interactive adds user_id).
- **Top-level response key converges on ``"workstreams"``** on
both endpoints. Coord ``coordinators`` key removed — coord is a
1.5.0aN-only surface (never shipped stable) so the convergence
has no compat shim; SDK / frontend consumers swap once.
- **Storage + manager-lock work moved off the event loop on
interactive**. ``list_workstreams_with_history`` runs through
``asyncio.to_thread`` on both kinds (matches coord's pre-existing
perf-2 pattern from the saved-coordinators review); ``mgr.list_all``
+ per-row work also offloaded.
- **N+1 storage round-trips on /v1/api/workstreams eliminated**.
Pre-lift interactive resolved the alias for every active row in a
separate SELECT (up to 50 round-trips per dashboard refresh on a
saturated node). Lifted body issues one bulk SELECT.
Pydantic schemas: ``WorkstreamInfo.id`` renamed → ``ws_id``,
``WorkstreamInfo.user_id`` field added. ``CoordinatorInfo`` and
``CoordinatorListResponse`` removed (folded into the unified
``WorkstreamInfo`` / ``ListWorkstreamsResponse``). OpenAPI spec
snapshots regenerated. TS SDK types updated (``WorkstreamInfo``
interface gains ws_id + the always-include fields); TS test
mock + assertion updated to match.
``GET /v1/api/dashboard`` is intentionally NOT in this PR's scope
and still returns rows keyed on ``id``. Tracked as a separate
cleanup PR (tombstone-note added at the dashboard handler).
/review pipeline run; the four Major findings + one Minor + six
nits all addressed in the same commit:
- M1: TS SDK ``WorkstreamInfo`` interface stale (id: string) →
renamed + fields added.
- M2: TS SDK test masked the type-mismatch with stale mock → updated.
- M3: N+1 alias resolution on active list → bulk
``get_workstream_display_names`` helper + ``list_resolve_titles``
bulk cfg hook.
- M4: Missing interactive parity regression test for unified row
shape → mirror of coord's added in test_server_authz.py.
- Mi1: ``audit_action_prefix`` string-compare deriving kind →
explicit ``cfg.list_kind: WorkstreamKind`` field.
- Six nits: redundant inner asyncio import, forward-ref quotes on
Awaitable, duplicated frontend comments, dashboard ``id`` field
has no tombstone-note, empty-coord_mgr short-circuit on
``saved_loaded_lookup``.
4512 tests passing; ruff + mypy clean.
This commit is contained in:
committed by
Patrick Buckley
parent
c77b237033
commit
edf52016ac
@@ -37,14 +37,13 @@ from turnstone.console.server import (
|
||||
_coord_create_build_kwargs,
|
||||
_coord_create_post_install,
|
||||
_coord_create_validate_request,
|
||||
_coord_saved_loaded_lookup,
|
||||
_require_admin_coordinator,
|
||||
_require_coord_mgr,
|
||||
cluster_ws_detail,
|
||||
coordinator_children,
|
||||
coordinator_detail,
|
||||
coordinator_history,
|
||||
coordinator_list,
|
||||
coordinator_saved,
|
||||
coordinator_tasks,
|
||||
)
|
||||
from turnstone.core.attachments import (
|
||||
@@ -65,10 +64,13 @@ from turnstone.core.session_routes import (
|
||||
make_cancel_handler,
|
||||
make_close_handler,
|
||||
make_create_handler,
|
||||
make_list_handler,
|
||||
make_open_handler,
|
||||
make_saved_handler,
|
||||
make_send_handler,
|
||||
)
|
||||
from turnstone.core.storage._sqlite import SQLiteBackend
|
||||
from turnstone.core.workstream import WorkstreamKind
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
@@ -115,6 +117,10 @@ _coord_endpoint_config = SessionEndpointConfig(
|
||||
create_validate_request=_coord_create_validate_request,
|
||||
create_build_kwargs=_coord_create_build_kwargs,
|
||||
create_post_install=_coord_create_post_install,
|
||||
list_resolve_titles=None,
|
||||
list_kind=WorkstreamKind.COORDINATOR,
|
||||
saved_state_filter="closed",
|
||||
saved_loaded_lookup=_coord_saved_loaded_lookup,
|
||||
)
|
||||
|
||||
|
||||
@@ -142,12 +148,16 @@ def _make_client(
|
||||
coord_create_handler,
|
||||
methods=["POST"],
|
||||
),
|
||||
Route("/v1/api/workstreams", coordinator_list, methods=["GET"]),
|
||||
Route(
|
||||
"/v1/api/workstreams",
|
||||
make_list_handler(_coord_endpoint_config),
|
||||
methods=["GET"],
|
||||
),
|
||||
# Literal path before the /{ws_id} routes below so Starlette
|
||||
# matches "saved" as the literal, not as a ws_id.
|
||||
Route(
|
||||
"/v1/api/workstreams/saved",
|
||||
coordinator_saved,
|
||||
make_saved_handler(_coord_endpoint_config),
|
||||
methods=["GET"],
|
||||
),
|
||||
Route(
|
||||
@@ -338,6 +348,42 @@ def test_unresolvable_alias_returns_503(storage):
|
||||
_COORD_HEADERS = {"X-Test-User": "user-1", "X-Test-Perms": "admin.coordinator"}
|
||||
|
||||
|
||||
def test_active_list_row_shape_includes_unified_fields(storage):
|
||||
"""Stage 2 list-verb-lift parity regression — coord active-list row
|
||||
carries the always-include fields (ws_id, name, state, kind,
|
||||
parent_ws_id, user_id) that the lifted ``make_list_handler``
|
||||
produces on every kind. SDK consumers don't have to branch on
|
||||
kind to read any of these. Pre-lift coord returned a smaller
|
||||
row ({ws_id, name, state, user_id}) under a different top-level
|
||||
key (``coordinators`` vs ``workstreams``)."""
|
||||
mgr = _build_mgr(storage)
|
||||
mgr.create(user_id="u1", name="lifted-coord")
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
resp = client.get("/v1/api/workstreams", headers=_COORD_HEADERS)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
# Top-level key converged on `workstreams`.
|
||||
assert "workstreams" in body
|
||||
assert "coordinators" not in body
|
||||
rows = body["workstreams"]
|
||||
assert len(rows) == 1
|
||||
row = rows[0]
|
||||
# Always-include row shape — coord populates kind=COORDINATOR
|
||||
# and parent_ws_id=None (coordinators have no parent today).
|
||||
assert set(row.keys()) == {
|
||||
"ws_id",
|
||||
"name",
|
||||
"state",
|
||||
"kind",
|
||||
"parent_ws_id",
|
||||
"user_id",
|
||||
}
|
||||
assert row["name"] == "lifted-coord"
|
||||
assert row["kind"] == "coordinator"
|
||||
assert row["parent_ws_id"] is None
|
||||
assert row["user_id"] == "u1"
|
||||
|
||||
|
||||
def test_create_returns_ws_id_and_records_audit(storage):
|
||||
mgr = _build_mgr(storage)
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
@@ -561,7 +607,7 @@ def test_list_returns_cluster_wide(storage):
|
||||
resp = client.get("/v1/api/workstreams", headers=_COORD_HEADERS)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
names = {c["name"] for c in body["coordinators"]}
|
||||
names = {c["name"] for c in body["workstreams"]}
|
||||
assert names == {"mine", "theirs"}
|
||||
|
||||
|
||||
@@ -616,7 +662,7 @@ def test_saved_returns_cluster_wide(saved_storage):
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
resp = client.get("/v1/api/workstreams/saved", headers=_COORD_HEADERS)
|
||||
assert resp.status_code == 200
|
||||
assert {c["ws_id"] for c in resp.json()["coordinators"]} == {a, b}
|
||||
assert {c["ws_id"] for c in resp.json()["workstreams"]} == {a, b}
|
||||
|
||||
|
||||
def test_saved_excludes_currently_loaded(saved_storage):
|
||||
@@ -638,7 +684,7 @@ def test_saved_excludes_currently_loaded(saved_storage):
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
resp = client.get("/v1/api/workstreams/saved", headers=_COORD_HEADERS)
|
||||
assert resp.status_code == 200
|
||||
saved_ids = {c["ws_id"] for c in resp.json()["coordinators"]}
|
||||
saved_ids = {c["ws_id"] for c in resp.json()["workstreams"]}
|
||||
assert closed_id in saved_ids
|
||||
assert loaded_ws.id not in saved_ids
|
||||
|
||||
@@ -665,7 +711,7 @@ def test_saved_excludes_active_state_rows(saved_storage):
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
resp = client.get("/v1/api/workstreams/saved", headers=_COORD_HEADERS)
|
||||
assert resp.status_code == 200
|
||||
saved_ids = {c["ws_id"] for c in resp.json()["coordinators"]}
|
||||
saved_ids = {c["ws_id"] for c in resp.json()["workstreams"]}
|
||||
assert saved_ids == {closed_id}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user