mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
c328bebecd
A scheduled task could pin the model and skill of the workstream each
firing creates; it can now also pin its persona and project, so a
schedule can run under, e.g., the researcher persona attached to a
specific project's memory bucket.
The two values live on scheduled_tasks (migration 066, Text NOT NULL
default '') and are passed verbatim to create_workstream at dispatch,
where the node resolves the persona for the workstream kind and gates
the project attach. Empty means "kind-default persona / no project",
resolved late at each firing (mirrors how empty model/skill already
behave) -- existing schedules keep byte-identical dispatch behaviour,
so there is no backfill.
Also fixes a latent bug this feature depends on: admin_create_schedule
read created_by from request.state.user_id, which AuthMiddleware never
sets, so every scheduled task stored created_by=''. It now reads
auth_result.user_id like every other console endpoint. This is now
load-bearing -- the scheduler dispatches under created_by and the node
gates the project attach against it. admin_update_schedule adopts the
editing admin as owner when a project is assigned to a pre-fix orphaned
('') schedule, and re-validates persona/project only when they change
so a since-disabled persona or lost membership does not block unrelated
edits (the node re-checks at dispatch either way).
Wired through: schema + migration (up/down + parity tested), both
storage backends, API schemas, SDK create_workstream and console
create_schedule/update_schedule, scheduler dispatch, and the admin
schedule shelf (persona + project pickers, current value preserved so
an edit cannot silently clear a filtered-out selection).
691 lines
24 KiB
Python
691 lines
24 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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_create_workstream_extended_params():
|
|
"""New optional params appear in JSON body only when non-empty."""
|
|
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",
|
|
auto_approve=True,
|
|
auto_approve_tools="read_file",
|
|
user_id="u42",
|
|
)
|
|
assert captured_body["node_id"] == "n1"
|
|
assert captured_body["name"] == "ext"
|
|
assert captured_body["auto_approve"] is True
|
|
assert captured_body["auto_approve_tools"] == "read_file"
|
|
assert captured_body["user_id"] == "u42"
|
|
|
|
|
|
@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 "auto_approve" not in captured_body
|
|
assert "auto_approve_tools" not in captured_body
|
|
assert "user_id" 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", "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_create_workstream(
|
|
name="routed",
|
|
model="gpt-5",
|
|
auto_approve=True,
|
|
target_node="n1",
|
|
user_id="u1",
|
|
)
|
|
assert resp["ws_id"] == "ws1"
|
|
assert resp["node_url"] == "http://n1:8080"
|
|
assert captured_body["name"] == "routed"
|
|
assert captured_body["model"] == "gpt-5"
|
|
assert captured_body["auto_approve"] is True
|
|
assert captured_body["target_node"] == "n1"
|
|
assert captured_body["user_id"] == "u1"
|
|
|
|
|
|
@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", "node_url": "http://n1:8080"})
|
|
|
|
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"]
|