Files
turnstone/tests/test_phase6_endpoints.py
T
Patrick Buckley 7d2d7db9d2 feat(approve): pass pending_approval_detail through cluster live-bulk
Threads the field added by Chunk 1 through the console's live-bulk
endpoint so coord tree UI can read it without a separate per-child
fetch. Three touchpoints:

- _CLUSTER_WS_LIVE_KEYS gains the new key so _fetch_live_block's
  projection forwards it from the upstream /dashboard response on
  node-backed child rows.
- _coordinator_live_snapshot synthesizes the same shape from
  ConsoleCoordinatorUI._pending_approval for in-process coord
  rows (no upstream /dashboard exists on the console pseudo-node).
- One source of truth: SessionUIBase.serialize_pending_approval_detail.

Both branches now emit the same 12-key live block; coord judge isn't
wired today so coord-self judge_verdict is always None — flagged in
the plan as a stretch follow-up.

Plan: docs/design/inline-child-approvals.md (chunk 2 of 4).
2026-04-27 11:41:14 -07:00

503 lines
17 KiB
Python

"""Tests for the phase-6 polish endpoints (#q-1).
Covers:
- GET /v1/api/cluster/ws/live — bulk live-block fetch (admin.cluster.inspect).
- GET /v1/api/workstreams/{ws_id}/metrics — per-coordinator health snapshot.
Both endpoints ride on the same test harness as
``test_coordinator_endpoints.py`` — a minimal Starlette app with an
auth-injecting middleware, TestClient + MockTransport for the
upstream node fetches.
"""
from __future__ import annotations
import pytest
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Route
from starlette.testclient import TestClient
from tests._coord_test_helpers import (
_AuthMiddleware,
_build_mgr,
_fake_registry,
_FakeConfigStore,
)
from turnstone.console.server import (
cluster_ws_live_bulk,
coordinator_metrics,
)
from turnstone.core.storage._sqlite import SQLiteBackend
@pytest.fixture
def storage(tmp_path):
return SQLiteBackend(str(tmp_path / "phase6.db"))
def _make_client(storage, *, coord_mgr=None) -> TestClient:
app = Starlette(
routes=[
Route("/v1/api/cluster/ws/live", cluster_ws_live_bulk, methods=["GET"]),
Route(
"/v1/api/workstreams/{ws_id}/metrics",
coordinator_metrics,
methods=["GET"],
),
],
middleware=[Middleware(_AuthMiddleware)],
)
app.state.coord_mgr = coord_mgr
app.state.coord_adapter = coord_mgr._adapter if coord_mgr is not None else None
app.state.config_store = _FakeConfigStore({"coordinator.model_alias": "gpt-4"})
app.state.coord_registry = _fake_registry() if coord_mgr is not None else None
app.state.coord_registry_error = "" if coord_mgr else "registry missing"
app.state.auth_storage = storage
app.state.jwt_secret = "x" * 64
return TestClient(app)
def _seed_workstream(
storage: SQLiteBackend,
*,
ws_id: str,
node_id: str,
user_id: str = "user-1",
kind: str = "interactive",
state: str = "idle",
parent_ws_id: str | None = None,
created: str | None = None,
) -> None:
storage.register_workstream(
ws_id,
node_id=node_id,
user_id=user_id,
name=f"ws-{ws_id[:4]}",
state=state,
kind=kind,
parent_ws_id=parent_ws_id,
)
if created is not None:
# Override the created timestamp directly — register_workstream
# stamps "now", so we need a second write to test the
# spawns_last_hour boundary.
import sqlalchemy as sa
from turnstone.core.storage._sqlite import workstreams
with storage._conn() as conn:
conn.execute(
sa.update(workstreams).where(workstreams.c.ws_id == ws_id).values(created=created)
)
conn.commit()
# ---------------------------------------------------------------------------
# GET /v1/api/cluster/ws/live — bulk live-block fetch
# ---------------------------------------------------------------------------
_ADMIN_HEADERS = {"X-Test-User": "user-1", "X-Test-Perms": "admin.cluster.inspect"}
_OWNER_HEADERS = _ADMIN_HEADERS # same caller; permission grants inspect
def test_bulk_live_requires_permission(storage):
client = _make_client(storage, coord_mgr=_build_mgr(storage))
resp = client.get(
"/v1/api/cluster/ws/live?ids=" + "a" * 32,
headers={"X-Test-User": "u", "X-Test-Perms": "read"},
)
assert resp.status_code == 403
def test_bulk_live_empty_ids_returns_empty_body(storage):
client = _make_client(storage, coord_mgr=_build_mgr(storage))
resp = client.get("/v1/api/cluster/ws/live?ids=", headers=_ADMIN_HEADERS)
assert resp.status_code == 200
body = resp.json()
assert body == {"results": {}, "denied": [], "truncated": False}
def test_bulk_live_strips_invalid_ids(storage):
"""IDs failing the hex-regex are silently dropped; duplicates
collapse."""
client = _make_client(storage, coord_mgr=_build_mgr(storage))
# NOT-HEX is invalid; the valid id is 32 chars hex but unknown to
# storage → shows up as denied.
resp = client.get(
"/v1/api/cluster/ws/live?ids=NOT-HEX,NOT-HEX,," + ("a" * 32) + "," + ("a" * 32),
headers=_ADMIN_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
# Invalid / empty / duplicate ids trimmed; only the one valid-but-
# missing id is reported as denied.
assert body["denied"] == ["a" * 32]
assert body["results"] == {}
def test_bulk_live_caps_ids_at_50(storage):
"""Ids past the server-side cap truncate with truncated=true."""
client = _make_client(storage, coord_mgr=_build_mgr(storage))
# 60 fake ids → cap=50 keeps the first 50 (dedup preserves order).
ids = ",".join(f"{i:064x}" for i in range(60))
resp = client.get(
"/v1/api/cluster/ws/live?ids=" + ids,
headers=_ADMIN_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
assert body["truncated"] is True
# All 50 kept ids resolve to 'denied' (no storage rows) — their
# inclusion in the response proves the cap took the head 50.
assert len(body["denied"]) == 50
def test_bulk_live_admin_bypass_returns_live(storage):
"""An admin user (holds admin.users or admin.roles, not just
admin.cluster.inspect) bypasses tenancy and sees non-owned rows'
live blocks. Coordinator live-block synthesis is in-process, so
results is populated without any upstream node fetch."""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="other-user")
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
"/v1/api/cluster/ws/live?ids=" + ws.id,
headers={
"X-Test-User": "user-1",
# admin.users grants the _is_admin bypass in addition to
# admin.cluster.inspect for the endpoint itself.
"X-Test-Perms": "admin.cluster.inspect,admin.users",
},
)
assert resp.status_code == 200
body = resp.json()
assert ws.id in body["results"]
assert body["denied"] == []
def test_bulk_live_cluster_wide_visibility(storage):
"""Trusted-team visibility: any ``admin.cluster.inspect`` caller
sees every row in ``results``. ``denied`` is reserved for ids
that don't correspond to a persisted workstream (no existence
oracle for unknown ids)."""
ws_id = "b" * 32
_seed_workstream(storage, ws_id=ws_id, node_id="node-a", user_id="stranger")
client = _make_client(storage, coord_mgr=_build_mgr(storage))
resp = client.get(
f"/v1/api/cluster/ws/live?ids={ws_id}",
headers={"X-Test-User": "user-1", "X-Test-Perms": "admin.cluster.inspect"},
)
assert resp.status_code == 200
body = resp.json()
assert ws_id in body["results"]
assert body["denied"] == []
def test_bulk_live_unknown_ids_route_to_denied(storage):
"""Unknown ids (not in storage) land in ``denied`` so the endpoint
can't be used as an existence oracle."""
ws_id = "c" * 32 # not seeded
client = _make_client(storage, coord_mgr=_build_mgr(storage))
resp = client.get(
f"/v1/api/cluster/ws/live?ids={ws_id}",
headers={"X-Test-User": "user-1", "X-Test-Perms": "admin.cluster.inspect"},
)
assert resp.status_code == 200
body = resp.json()
assert body["denied"] == [ws_id]
assert body["results"] == {}
def test_bulk_live_coordinator_row_uses_manager_snapshot(storage):
"""A coordinator ws_id routes through _fetch_live_block's
coordinator branch — live is populated from the in-process manager
even though the pseudo-node has no /dashboard endpoint."""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/cluster/ws/live?ids={ws.id}",
headers=_OWNER_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
assert ws.id in body["results"]
live = body["results"][ws.id]
assert live is not None
assert "pending_approval" in live
# New field always present on the wire — None when no approval
# is pending so the JS can `key in row` without surprise.
assert "pending_approval_detail" in live
assert live["pending_approval_detail"] is None
def test_bulk_live_coordinator_row_includes_pending_approval_detail(storage):
"""When _pending_approval is set on a coord UI, the live block
surfaces the merged items + judge_verdict payload through the
coord-pseudo-node path. End-to-end equivalent of the dashboard
test in test_server_authz, but for the console live-bulk
endpoint that the coord tree UI actually consumes."""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
ws.ui._pending_approval = {
"type": "approve_request",
"items": [
{
"call_id": "c-99",
"header": "spawn_workstream",
"preview": "{...}",
"func_name": "spawn_workstream",
"approval_label": "spawn_workstream",
"needs_approval": True,
}
],
"judge_pending": False,
}
ws.ui._llm_verdicts["c-99"] = {
"recommendation": "approve",
"risk_level": "low",
"tier": "llm",
}
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/cluster/ws/live?ids={ws.id}",
headers=_OWNER_HEADERS,
)
assert resp.status_code == 200
live = resp.json()["results"][ws.id]
assert live["pending_approval"] is True # boolean derived flag
detail = live["pending_approval_detail"]
assert detail is not None
assert detail["call_id"] == "c-99"
assert detail["items"][0]["func_name"] == "spawn_workstream"
assert detail["items"][0]["judge_verdict"]["recommendation"] == "approve"
# ---------------------------------------------------------------------------
# GET /v1/api/workstreams/{ws_id}/metrics — per-coordinator health snapshot
# ---------------------------------------------------------------------------
_METRICS_HEADERS = {"X-Test-User": "user-1", "X-Test-Perms": "admin.coordinator"}
def test_metrics_requires_permission(storage):
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers={"X-Test-User": "user-1", "X-Test-Perms": "read"},
)
assert resp.status_code == 403
def test_metrics_invalid_ws_id_400(storage):
mgr = _build_mgr(storage)
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
"/v1/api/workstreams/NOT-HEX/metrics",
headers=_METRICS_HEADERS,
)
assert resp.status_code == 400
def test_metrics_any_admin_coordinator_caller_can_read(storage):
"""Trusted-team visibility: metrics are readable by any caller
with ``admin.coordinator`` regardless of the coordinator owner."""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="stranger")
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers=_METRICS_HEADERS,
)
assert resp.status_code == 200
assert resp.json()["ws_id"] == ws.id
def test_metrics_empty_coordinator_defaults(storage):
"""A freshly created coordinator with no spawns / no verdicts
returns zero / empty defaults."""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers=_METRICS_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
assert body["ws_id"] == ws.id
assert body["spawns_total"] == 0
assert body["spawns_last_hour"] == 0
assert body["child_state_counts"] == {}
assert body["judge_fallback_rate"] == 0.0
assert body["wait_completions"] == 0
assert body["wait_timeouts"] == 0
assert body["wait_avg_elapsed"] == 0.0
def test_metrics_spawns_and_state_counts(storage):
"""spawns_total counts ALL children (including closed); state
histogram groups by current state. All children share the
coordinator's owner so the non-admin tenant filter on the
aggregate queries counts them all (see next test for the
cross-tenant filter behaviour)."""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
_seed_workstream(
storage,
ws_id="aa" * 16,
node_id="node-a",
user_id="user-1",
parent_ws_id=ws.id,
state="idle",
)
_seed_workstream(
storage,
ws_id="bb" * 16,
node_id="node-a",
user_id="user-1",
parent_ws_id=ws.id,
state="running",
)
_seed_workstream(
storage,
ws_id="cc" * 16,
node_id="node-a",
user_id="user-1",
parent_ws_id=ws.id,
state="closed",
)
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers=_METRICS_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
assert body["spawns_total"] == 3
assert body["child_state_counts"] == {"idle": 1, "running": 1, "closed": 1}
def test_metrics_cluster_wide_aggregates(storage):
"""Trusted-team model: aggregates are cluster-wide across every
caller with ``admin.coordinator``. Every child under the
coordinator counts, regardless of the ``user_id`` on the row.
"""
mgr = _build_mgr(storage)
ws = mgr.create(user_id="alice")
_seed_workstream(
storage,
ws_id="aa" * 16,
node_id="node-a",
user_id="alice",
parent_ws_id=ws.id,
state="idle",
)
_seed_workstream(
storage,
ws_id="bb" * 16,
node_id="node-a",
user_id="bob",
parent_ws_id=ws.id,
state="running",
)
client = _make_client(storage, coord_mgr=mgr)
# Every admin.coordinator caller sees both children.
for caller in ("alice", "bob", "admin-1"):
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers={"X-Test-User": caller, "X-Test-Perms": "admin.coordinator"},
)
assert resp.status_code == 200, caller
body = resp.json()
assert body["spawns_total"] == 2, caller
assert body["child_state_counts"] == {"idle": 1, "running": 1}, caller
def test_metrics_judge_fallback_rate_substring_match(storage):
"""judge_fallback_rate is computed from any verdict whose ``tier``
field contains 'fallback' (case-insensitive). Supports tiers like
'llm_fallback', 'LLM_FALLBACK', 'fallback_deterministic'."""
import uuid
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
# Three verdicts, two marked fallback (one LLM_FALLBACK, one
# llm_fallback → both match case-insensitive substring).
for tier in ("llm_primary", "LLM_FALLBACK", "llm_fallback"):
storage.create_intent_verdict(
verdict_id=uuid.uuid4().hex,
ws_id=ws.id,
call_id="c-" + tier,
func_name="f",
func_args="{}",
intent_summary="",
risk_level="low",
confidence=0.9,
recommendation="allow",
reasoning="",
evidence="",
tier=tier,
judge_model="j",
latency_ms=1,
)
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers=_METRICS_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
# 2 / 3 verdicts matched → 0.667 (rounded to 3 places).
assert body["judge_fallback_rate"] == pytest.approx(0.667, abs=1e-3)
assert body["intent_verdicts_sample"] == 3
def test_metrics_spawns_last_hour_boundary(storage):
"""Only children whose created timestamp is within the last 3600s
count toward spawns_last_hour; older children count toward
spawns_total but not the hour bucket."""
import time
from datetime import UTC, datetime, timedelta
mgr = _build_mgr(storage)
ws = mgr.create(user_id="user-1")
# One child created "now" (within the window); one created 2
# hours ago (outside the window).
recent_iso = datetime.fromtimestamp(time.time(), tz=UTC).strftime("%Y-%m-%dT%H:%M:%S")
old_iso = (datetime.fromtimestamp(time.time(), tz=UTC) - timedelta(hours=2)).strftime(
"%Y-%m-%dT%H:%M:%S"
)
_seed_workstream(
storage,
ws_id="aa" * 16,
node_id="node-a",
parent_ws_id=ws.id,
state="idle",
created=recent_iso,
)
_seed_workstream(
storage,
ws_id="bb" * 16,
node_id="node-a",
parent_ws_id=ws.id,
state="closed",
created=old_iso,
)
client = _make_client(storage, coord_mgr=mgr)
resp = client.get(
f"/v1/api/workstreams/{ws.id}/metrics",
headers=_METRICS_HEADERS,
)
assert resp.status_code == 200
body = resp.json()
assert body["spawns_total"] == 2
assert body["spawns_last_hour"] == 1