mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
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).
This commit is contained in:
committed by
Patrick Buckley
parent
fbb9be27f9
commit
7d2d7db9d2
@@ -1710,6 +1710,58 @@ def test_cluster_inspect_node_backed_success(storage):
|
||||
assert live["pending_approval"] is False
|
||||
|
||||
|
||||
def test_cluster_inspect_node_backed_pending_approval_detail_passes_through(storage):
|
||||
"""Node /dashboard returns pending_approval_detail → live block carries
|
||||
it through verbatim. Regression guard for the projection allowlist
|
||||
`_CLUSTER_WS_LIVE_KEYS`: dropping the key from the tuple silently
|
||||
breaks inline approve/deny buttons on remote-node child rows even
|
||||
though coord rows still work via the in-process synthesis branch."""
|
||||
mgr = _build_mgr(storage)
|
||||
ws_id = "f0" * 16
|
||||
_seed_node_workstream(storage, ws_id=ws_id, node_id="node-a")
|
||||
detail = {
|
||||
"call_id": "c-bash",
|
||||
"judge_pending": False,
|
||||
"items": [
|
||||
{
|
||||
"call_id": "c-bash",
|
||||
"header": "bash",
|
||||
"preview": "$ rm -rf /tmp/x",
|
||||
"func_name": "bash",
|
||||
"approval_label": "bash",
|
||||
"needs_approval": True,
|
||||
"error": None,
|
||||
"heuristic_verdict": None,
|
||||
"judge_verdict": {
|
||||
"recommendation": "deny",
|
||||
"risk_level": "crit",
|
||||
"tier": "llm",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
payload = {
|
||||
"workstreams": [
|
||||
{
|
||||
"ws_id": ws_id,
|
||||
"state": "attention",
|
||||
"activity_state": "approval",
|
||||
"activity": "awaiting approval",
|
||||
"tokens": 100,
|
||||
"pending_approval_detail": detail,
|
||||
}
|
||||
]
|
||||
}
|
||||
client = _make_client(storage, coord_mgr=mgr, registry=_fake_registry())
|
||||
_install_collector_with_node(client, "node-a", "http://node-a")
|
||||
_install_proxy_client(client, httpx.MockTransport(lambda r: httpx.Response(200, json=payload)))
|
||||
resp = client.get(f"/v1/api/cluster/ws/{ws_id}/detail", headers=_CLUSTER_HEADERS)
|
||||
assert resp.status_code == 200
|
||||
live = resp.json()["live"]
|
||||
assert live["pending_approval"] is True # derived bool, existing behavior
|
||||
assert live["pending_approval_detail"] == detail # full payload, new behavior
|
||||
|
||||
|
||||
def test_cluster_inspect_node_backed_pending_approval_synthesized(storage):
|
||||
"""activity_state=='approval' from the node synthesizes pending_approval=True."""
|
||||
mgr = _build_mgr(storage)
|
||||
|
||||
@@ -228,6 +228,52 @@ def test_bulk_live_coordinator_row_uses_manager_snapshot(storage):
|
||||
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"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -607,6 +607,12 @@ _CLUSTER_WS_LIVE_KEYS = (
|
||||
"model_alias",
|
||||
"title",
|
||||
"name",
|
||||
# Carries the inline approve/deny payload (items + judge_verdict)
|
||||
# so coord live-bulk callers can render row-level UI without a
|
||||
# per-child round-trip. ``None`` when no approval is pending.
|
||||
# Cross-tenant exposure follows the trusted-team posture documented
|
||||
# on ``SessionUIBase.serialize_pending_approval_detail``.
|
||||
"pending_approval_detail",
|
||||
)
|
||||
|
||||
|
||||
@@ -717,6 +723,17 @@ def _coordinator_live_snapshot(ws: Any) -> dict[str, Any]:
|
||||
val = getattr(obj, name, "") if obj else ""
|
||||
return val if isinstance(val, str) else ""
|
||||
|
||||
# Coord rows synthesize the same ``pending_approval_detail`` shape
|
||||
# the node-side dashboard produces — single source of truth via
|
||||
# ``SessionUIBase.serialize_pending_approval_detail``. The console
|
||||
# coord LLM judge isn't wired today (``coordinator_ui.py:138``
|
||||
# hardcodes ``judge_pending=False``), so ``judge_verdict`` will
|
||||
# always be ``None`` for these rows; the coord-self stretch in
|
||||
# the plan covers that follow-up. ``ui`` may be ``None`` in
|
||||
# transient states (newly-created ws before activation); every
|
||||
# active coord UI is a ``SessionUIBase`` and supports the method.
|
||||
pending_approval_detail = ui.serialize_pending_approval_detail() if ui is not None else None
|
||||
|
||||
return {
|
||||
"state": ws.state.value if hasattr(ws.state, "value") else str(ws.state),
|
||||
"tokens": 0,
|
||||
@@ -729,6 +746,7 @@ def _coordinator_live_snapshot(ws: Any) -> dict[str, Any]:
|
||||
"title": "",
|
||||
"name": getattr(ws, "name", "") or "",
|
||||
"pending_approval": pending_approval,
|
||||
"pending_approval_detail": pending_approval_detail,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user