Files
turnstone/tests/test_sdk_console.py
Patrick Buckley 7a06f5e8bc refactor(session): make ModelLane the provider boundary (#979) (#989)
* refactor(session): make ModelLane the provider boundary (#979)

## Summary

This closes the model-lane ownership gap left by #832: `ChatSession` no longer stores raw provider/client handles. `ResolvedModelBinding` now carries the provider, client, model, capabilities, registry generation, and backend-auth configuration as one coherent snapshot.

- Atomically rebind existing sessions after model-registry changes while pinning each in-flight send, fallback, judge, output guard, task agent, title, compaction, perception, and voice operation to its initiating principal and binding.
- Fence UI publication, canonical trajectory folds, durable writes, streams, retries, child scopes, and judge work by generation. Stop can hand off to a successor without accepting late state; cancelled tools retain typed effect receipts, and concurrent approval batches resolve by exact cycle or call.
- Make create, fork, open, close, and delete race-safe with hidden `creating` reservations, incarnation-aware state tails, and an ACL-rechecked transaction that clones checkpoint-bounded history, configuration, project/persona state, and attachment references.
- Extend REST/OpenAPI and Python/TypeScript SDK contracts for create/fork inputs, routed-create metadata, live-workstream probes, targeted approvals, and structured cancellation results.
- Update architecture, storage, authentication, judge, channel, console, API, and SDK documentation, including regenerated architecture diagrams and OpenAPI artifacts.

## Validation

- SQLite suite: 11,188 passed, 9 skipped, 10 deselected
- PostgreSQL suite: 11,195 passed, 2 skipped, 10 deselected
- Live backend: 3 passed
- SSE recovery: 6 passed; browser recovery harness passed all scenarios
- Ruff: clean; 595 files correctly formatted
- mypy: 243 source files clean
- TypeScript: typecheck/build and 35 tests passed
- OpenAPI artifacts fresh; all 14 changed diagrams reproduce byte-for-byte
- `git diff --check` and Git LFS integrity clean

Closes #979.

* fix(deps): update nanoid for GHSA-2v37-7h3g-55p8

Refresh the transitive lock entry admitted by PostCSS so the TypeScript security gate no longer resolves the vulnerable custom-generator implementation.

Validation:
- npm ci
- npm audit --audit-level=moderate: 0 vulnerabilities
- TypeScript typecheck and build
- TypeScript tests: 35 passed

* fix(test): assert canonical model registry URLs

Replace prefix checks with exact canonical base URL assertions so the tests do not model incomplete URL validation.

Validation: tests/test_model_registry.py (185 passed); Ruff check/format; mypy.
2026-08-08 16:13:35 -07:00

745 lines
26 KiB
Python

"""Tests for turnstone.sdk.console — console client with mocked HTTP transport."""
from __future__ import annotations
import json
import httpx
import pytest
from turnstone.sdk._types import TurnstoneAPIError
from turnstone.sdk.console import AsyncTurnstoneConsole
def _json_response(data: dict, status: int = 200) -> httpx.Response:
return httpx.Response(status, json=data)
def _mock_transport(
responses: dict[str, httpx.Response] | None = None,
) -> httpx.MockTransport:
table = responses or {}
def handler(request: httpx.Request) -> httpx.Response:
key = f"{request.method} {request.url.path}"
if key in table:
return table[key]
return httpx.Response(404, json={"error": "not found"})
return httpx.MockTransport(handler)
# ---------------------------------------------------------------------------
# Routing proxy — rewind / retry (#549)
# ---------------------------------------------------------------------------
@pytest.mark.anyio
async def test_route_rewind_sends_turns_body():
"""``route_rewind`` forwards ``{"turns": N}`` through the proxy."""
captured_path: list[str] = []
captured_body: list[dict] = []
def handler(request: httpx.Request) -> httpx.Response:
captured_path.append(request.url.path)
captured_body.append(json.loads(request.content) if request.content else {})
return _json_response({"status": "ok", "removed": 3})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_rewind("ws1", turns=3)
assert captured_path[0] == "/v1/api/route/workstreams/ws1/rewind"
assert captured_body[0] == {"turns": 3}
@pytest.mark.anyio
async def test_route_retry_posts_to_path_keyed_endpoint():
transport = _mock_transport(
{
"POST /v1/api/route/workstreams/ws1/retry": _json_response(
{"status": "ok", "retried": True}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.route_retry("ws1")
assert resp["status"] == "ok"
# ---------------------------------------------------------------------------
# Cluster overview
# ---------------------------------------------------------------------------
@pytest.mark.anyio
async def test_overview():
transport = _mock_transport(
{
"GET /v1/api/cluster/overview": _json_response(
{
"nodes": 2,
"workstreams": 5,
"states": {"running": 1, "idle": 4},
"aggregate": {"total_tokens": 1000, "total_tool_calls": 20},
"version_drift": False,
"versions": ["0.3.0"],
}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.overview()
assert resp.nodes == 2
assert resp.workstreams == 5
@pytest.mark.anyio
async def test_nodes():
transport = _mock_transport(
{
"GET /v1/api/cluster/nodes": _json_response(
{
"nodes": [
{
"node_id": "n1",
"server_url": "http://localhost:8080",
"ws_total": 3,
"ws_running": 1,
"ws_thinking": 0,
"ws_attention": 0,
"ws_idle": 2,
"ws_error": 0,
"total_tokens": 500,
"started": 1700000000.0,
"reachable": True,
"health": {"status": "ok"},
"version": "0.3.0",
}
],
"total": 1,
}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.nodes(sort="tokens", limit=50)
assert resp.total == 1
assert resp.nodes[0].node_id == "n1"
@pytest.mark.anyio
async def test_workstreams():
transport = _mock_transport(
{
"GET /v1/api/cluster/workstreams": _json_response(
{
"workstreams": [
{
"id": "ws1",
"name": "test",
"state": "running",
"node": "n1",
}
],
"total": 1,
"page": 1,
"per_page": 50,
"pages": 1,
}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.workstreams(state="running", page=1)
assert resp.total == 1
@pytest.mark.anyio
async def test_node_detail():
transport = _mock_transport(
{
"GET /v1/api/cluster/node/n1": _json_response(
{
"node_id": "n1",
"server_url": "http://localhost:8080",
"health": {"status": "ok"},
"workstreams": [],
"aggregate": {"total_tokens": 0, "total_tool_calls": 0},
}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.node_detail("n1")
assert resp.node_id == "n1"
@pytest.mark.anyio
async def test_create_workstream():
transport = _mock_transport(
{
"POST /v1/api/cluster/workstreams/new": _json_response(
{"status": "dispatched", "correlation_id": "abc123", "target_node": "n1"}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.create_workstream(node_id="n1", name="test")
assert resp.correlation_id == "abc123"
# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
@pytest.mark.anyio
async def test_login():
transport = _mock_transport(
{"POST /v1/api/auth/login": _json_response({"status": "ok", "role": "read"})}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.login("tok_test")
assert resp.role == "read"
# ---------------------------------------------------------------------------
# Health
# ---------------------------------------------------------------------------
@pytest.mark.anyio
async def test_health():
transport = _mock_transport(
{
"GET /health": _json_response(
{
"status": "ok",
"service": "turnstone-console",
"nodes": 2,
"workstreams": 5,
"version_drift": False,
"versions": ["0.3.0"],
}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.health()
assert resp.status == "ok"
assert resp.nodes == 2
# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------
@pytest.mark.anyio
async def test_node_not_found():
transport = _mock_transport(
{"GET /v1/api/cluster/node/bad": httpx.Response(404, json={"error": "Node not found"})}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
with pytest.raises(TurnstoneAPIError) as exc_info:
await client.node_detail("bad")
assert exc_info.value.status_code == 404
@pytest.mark.anyio
async def test_query_params_passed():
"""Verify query params are sent correctly for paginated endpoints."""
captured_url: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
captured_url.append(str(request.url))
return httpx.Response(
200,
json={
"workstreams": [],
"total": 0,
"page": 2,
"per_page": 25,
"pages": 0,
},
)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.workstreams(state="running", page=2, per_page=25)
assert "state=running" in captured_url[0]
assert "page=2" in captured_url[0]
assert "per_page=25" in captured_url[0]
# ---------------------------------------------------------------------------
# Schedules
# ---------------------------------------------------------------------------
_SCHEDULE_FIXTURE = {
"task_id": "t1",
"name": "nightly",
"description": "",
"schedule_type": "cron",
"cron_expr": "0 2 * * *",
"at_time": "",
"target_mode": "auto",
"model": "",
"initial_message": "Run nightly checks",
"auto_approve": False,
"auto_approve_tools": [],
"enabled": True,
"created_by": "u1",
"last_run": None,
"next_run": "2026-03-06T02:00:00Z",
"created": "2026-03-05T12:00:00Z",
"updated": "2026-03-05T12:00:00Z",
}
@pytest.mark.anyio
async def test_list_schedules():
transport = _mock_transport(
{"GET /v1/api/admin/schedules": _json_response({"schedules": [_SCHEDULE_FIXTURE]})}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.list_schedules()
assert len(resp.schedules) == 1
assert resp.schedules[0].task_id == "t1"
assert resp.schedules[0].name == "nightly"
@pytest.mark.anyio
async def test_create_schedule():
captured_body: list[dict] = []
def handler(request: httpx.Request) -> httpx.Response:
captured_body.append(json.loads(request.content))
return _json_response(_SCHEDULE_FIXTURE)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.create_schedule(
name="nightly",
schedule_type="cron",
initial_message="Run nightly checks",
cron_expr="0 2 * * *",
persona="researcher",
project_id="proj_1",
)
assert resp.task_id == "t1"
body = captured_body[0]
assert body["name"] == "nightly"
assert body["schedule_type"] == "cron"
assert body["cron_expr"] == "0 2 * * *"
assert body["initial_message"] == "Run nightly checks"
assert body["persona"] == "researcher"
assert body["project_id"] == "proj_1"
# Optional fields with defaults should not appear when not set
assert "description" not in body
assert "model" not in body
@pytest.mark.anyio
async def test_get_schedule():
transport = _mock_transport(
{"GET /v1/api/admin/schedules/t1": _json_response(_SCHEDULE_FIXTURE)}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.get_schedule("t1")
assert resp.task_id == "t1"
assert resp.schedule_type == "cron"
@pytest.mark.anyio
async def test_update_schedule_partial():
"""Only explicitly-passed fields should appear in the request body."""
captured_body: list[dict] = []
def handler(request: httpx.Request) -> httpx.Response:
captured_body.append(json.loads(request.content))
return _json_response({**_SCHEDULE_FIXTURE, "enabled": False})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.update_schedule("t1", enabled=False)
assert resp.enabled is False
body = captured_body[0]
assert body == {"enabled": False}
@pytest.mark.anyio
async def test_delete_schedule():
transport = _mock_transport(
{"DELETE /v1/api/admin/schedules/t1": _json_response({"status": "ok"})}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.delete_schedule("t1")
assert resp.status == "ok"
@pytest.mark.anyio
async def test_list_schedule_runs():
transport = _mock_transport(
{
"GET /v1/api/admin/schedules/t1/runs": _json_response(
{
"runs": [
{
"run_id": "r1",
"task_id": "t1",
"node_id": "n1",
"ws_id": "ws1",
"correlation_id": "c1",
"started": "2026-03-05T02:00:00Z",
"status": "dispatched",
"error": "",
}
]
}
)
}
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.list_schedule_runs("t1", limit=10)
assert len(resp.runs) == 1
assert resp.runs[0].run_id == "r1"
assert resp.runs[0].status == "dispatched"
# ---------------------------------------------------------------------------
# create_workstream extended params
# ---------------------------------------------------------------------------
def test_cluster_create_sdk_signatures_match_live_schema():
"""The console SDK must not advertise fields the handler silently ignores."""
import inspect
from turnstone.api.console_schemas import ConsoleCreateWsRequest
from turnstone.sdk.console import TurnstoneConsole
expected = set(ConsoleCreateWsRequest.model_fields)
async_params = set(inspect.signature(AsyncTurnstoneConsole.create_workstream).parameters)
sync_params = set(inspect.signature(TurnstoneConsole.create_workstream).parameters)
assert async_params - {"self"} == expected
assert sync_params - {"self"} == expected
@pytest.mark.anyio
async def test_create_workstream_contract_params():
"""Cluster create exposes every optional field accepted by its schema."""
captured_body: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured_body.update(json.loads(request.content))
return _json_response(
{"status": "dispatched", "correlation_id": "abc", "target_node": "n1"}
)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.create_workstream(
node_id="n1",
name="ext",
project_id="project-42",
judge_model="judge-fast",
)
assert captured_body["node_id"] == "n1"
assert captured_body["name"] == "ext"
assert captured_body["project_id"] == "project-42"
assert captured_body["judge_model"] == "judge-fast"
@pytest.mark.anyio
async def test_create_workstream_omits_empty_new_params():
"""Default-valued new params should not appear in JSON body."""
captured_body: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured_body.update(json.loads(request.content))
return _json_response(
{"status": "dispatched", "correlation_id": "abc", "target_node": "n1"}
)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.create_workstream(name="min")
assert captured_body == {"name": "min"}
assert "project_id" not in captured_body
assert "judge_model" not in captured_body
# ---------------------------------------------------------------------------
# Route methods
# ---------------------------------------------------------------------------
@pytest.mark.anyio
async def test_route_create_workstream():
captured_body: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured_body.update(json.loads(request.content))
return _json_response(
{
"ws_id": "ws1",
"name": "routed",
"node_url": "http://n1:8080",
"node_id": "n1",
"routing_strategy": "target_node",
}
)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.route_create_workstream(
name="routed",
model="gpt-5",
auto_approve=True,
project_id="project-42",
judge_model="judge-fast",
target_node="n1",
user_id="u1",
client_type="scheduled",
notify_targets=[{"channel_type": "slack", "channel_id": "C123"}],
)
assert resp.ws_id == "ws1"
assert resp.node_url == "http://n1:8080"
assert resp.routing_strategy == "target_node"
assert captured_body["name"] == "routed"
assert captured_body["model"] == "gpt-5"
assert captured_body["auto_approve"] is True
assert captured_body["project_id"] == "project-42"
assert captured_body["judge_model"] == "judge-fast"
assert captured_body["target_node"] == "n1"
assert captured_body["user_id"] == "u1"
assert captured_body["client_type"] == "scheduled"
assert captured_body["notify_targets"] == [{"channel_type": "slack", "channel_id": "C123"}]
@pytest.mark.anyio
async def test_route_create_workstream_rejects_attachments_with_target_node():
"""Regression: target_node has no effect on multipart route_create
(which routes by ?ws_id=) — refuse the combination at the SDK boundary
instead of silently routing to the wrong node.
"""
from turnstone.sdk._types import AttachmentUpload
transport = httpx.MockTransport(
lambda req: _json_response({"error": "should not be called"}, status=500)
)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
with pytest.raises(ValueError, match="target_node"):
await client.route_create_workstream(
name="x",
target_node="n1",
attachments=[AttachmentUpload(filename="a.txt", data=b"hi")],
)
@pytest.mark.anyio
async def test_route_create_workstream_omits_defaults():
captured_body: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured_body.update(json.loads(request.content))
return _json_response(
{
"ws_id": "ws1",
"name": "bare",
"node_url": "http://n1:8080",
"node_id": "n1",
"routing_strategy": "rendezvous",
}
)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_create_workstream(name="bare")
assert captured_body == {"name": "bare"}
@pytest.mark.anyio
async def test_route_send():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.route_send("Hello", "ws1")
assert resp["status"] == "ok"
assert captured["path"] == "/v1/api/route/workstreams/ws1/send"
assert captured["body"] == {"message": "Hello"}
@pytest.mark.anyio
async def test_route_approve():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_approve(ws_id="ws1", approved=False, feedback="no", always=True)
assert captured["path"] == "/v1/api/route/workstreams/ws1/approve"
assert captured["body"] == {
"approved": False,
"feedback": "no",
"always": True,
}
@pytest.mark.anyio
async def test_route_approve_omits_defaults():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_approve(ws_id="ws1", approved=True)
assert captured["path"] == "/v1/api/route/workstreams/ws1/approve"
assert captured["body"] == {"approved": True}
@pytest.mark.anyio
async def test_route_close():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.route_close("ws1")
assert resp["status"] == "ok"
assert captured["path"] == "/v1/api/route/workstreams/ws1/close"
assert captured["body"] == {}
@pytest.mark.anyio
async def test_route_cancel():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_cancel("ws1", force=True)
assert captured["path"] == "/v1/api/route/workstreams/ws1/cancel"
assert captured["body"] == {"force": True}
@pytest.mark.anyio
async def test_route_cancel_omits_force_when_false():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_cancel("ws1")
assert captured["path"] == "/v1/api/route/workstreams/ws1/cancel"
assert captured["body"] == {}
@pytest.mark.anyio
async def test_route_command():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return _json_response({"status": "ok"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
await client.route_command(ws_id="ws1", command="/clear")
assert captured["path"] == "/v1/api/route/command"
assert captured["body"] == {"ws_id": "ws1", "command": "/clear"}
@pytest.mark.anyio
async def test_route_lookup():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["url"] = str(request.url)
return _json_response({"node_url": "http://n1:8080", "node_id": "n1"})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.route_lookup("ws1")
assert resp["node_url"] == "http://n1:8080"
assert resp["node_id"] == "n1"
assert captured["path"] == "/v1/api/route"
assert "ws_id=ws1" in captured["url"]
@pytest.mark.anyio
async def test_route_workstream_live():
captured: dict = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
return _json_response({"ws_id": "ws1", "live": True})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc:
client = AsyncTurnstoneConsole(httpx_client=hc)
resp = await client.route_workstream_live("ws1")
assert resp.ws_id == "ws1"
assert resp.live is True
assert captured["path"] == "/v1/api/route/workstreams/ws1/live"