feat(*): t3code-mcp connector - machines, projects, dispatch, wait, interrupt
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
from t3code_mcp.config import Machine, ModelSpec, load_machines
|
||||
from t3code_mcp.server import Registry, build_server
|
||||
|
||||
|
||||
class FakeT3:
|
||||
def __init__(
|
||||
self, label: str, projects: list[dict[str, Any]], token: str = "tok"
|
||||
) -> None:
|
||||
self.label = label
|
||||
self.token = token
|
||||
self.projects = projects
|
||||
self.threads: dict[str, dict[str, Any]] = {}
|
||||
self.commands: list[dict[str, Any]] = []
|
||||
self.polls_until_done = 2
|
||||
self.pending_input = False
|
||||
self.fail_with: str | None = None
|
||||
|
||||
def handle(self, request: httpx.Request) -> httpx.Response:
|
||||
path = request.url.path
|
||||
if path == "/.well-known/t3/environment":
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"label": self.label,
|
||||
"serverVersion": "0.0.36",
|
||||
"platform": {"os": "linux"},
|
||||
},
|
||||
)
|
||||
if request.headers.get("authorization") != f"Bearer {self.token}":
|
||||
return httpx.Response(
|
||||
401, json={"code": "auth_invalid", "reason": "missing_credential"}
|
||||
)
|
||||
if path == "/api/orchestration/shell":
|
||||
return httpx.Response(200, json=self._shell())
|
||||
if path.startswith("/api/orchestration/threads/"):
|
||||
thread_id = path.rsplit("/", 1)[1]
|
||||
if thread_id not in self.threads:
|
||||
return httpx.Response(
|
||||
404, json={"code": "not_found", "reason": "thread_not_found"}
|
||||
)
|
||||
self._tick(thread_id)
|
||||
return httpx.Response(
|
||||
200, json={"snapshotSequence": 1, "thread": self.threads[thread_id]}
|
||||
)
|
||||
if path == "/api/orchestration/dispatch":
|
||||
command = json.loads(request.content)
|
||||
self.commands.append(command)
|
||||
return self._dispatch(command)
|
||||
return httpx.Response(404, json={"code": "not_found"})
|
||||
|
||||
def _shell(self) -> dict[str, Any]:
|
||||
threads = [
|
||||
{
|
||||
k: v
|
||||
for k, v in t.items()
|
||||
if k not in ("messages", "activities", "checkpoints")
|
||||
}
|
||||
| {"hasPendingUserInput": self.pending_input, "hasPendingApprovals": False}
|
||||
for t in self.threads.values()
|
||||
]
|
||||
return {
|
||||
"snapshotSequence": 1,
|
||||
"projects": self.projects,
|
||||
"threads": threads,
|
||||
"updatedAt": "now",
|
||||
}
|
||||
|
||||
def _tick(self, thread_id: str) -> None:
|
||||
thread = self.threads[thread_id]
|
||||
if self.fail_with and thread.get("_pending_turn"):
|
||||
thread["session"] = {"status": "stopped", "lastError": self.fail_with}
|
||||
return
|
||||
if thread.get("_pending_turn"):
|
||||
thread["latestTurn"] = {
|
||||
"turnId": thread.pop("_pending_turn"),
|
||||
"state": "running",
|
||||
}
|
||||
return
|
||||
latest = thread["latestTurn"]
|
||||
if latest and latest["state"] == "running":
|
||||
latest["_polls"] = latest.get("_polls", 0) + 1
|
||||
if latest["_polls"] > self.polls_until_done:
|
||||
latest["state"] = "completed"
|
||||
thread["messages"].append(
|
||||
{"role": "assistant", "text": "done: report", "streaming": False}
|
||||
)
|
||||
thread["session"]["status"] = "ready"
|
||||
|
||||
def _dispatch(self, command: dict[str, Any]) -> httpx.Response:
|
||||
kind = command["type"]
|
||||
if kind == "thread.create":
|
||||
self.threads[command["threadId"]] = {
|
||||
"id": command["threadId"],
|
||||
"projectId": command["projectId"],
|
||||
"title": command["title"],
|
||||
"modelSelection": command["modelSelection"],
|
||||
"runtimeMode": command["runtimeMode"],
|
||||
"latestTurn": None,
|
||||
"session": {"status": "idle", "lastError": None},
|
||||
"messages": [],
|
||||
"activities": [],
|
||||
"checkpoints": [],
|
||||
"archivedAt": None,
|
||||
"updatedAt": "now",
|
||||
}
|
||||
elif kind == "thread.turn.start":
|
||||
thread = self.threads[command["threadId"]]
|
||||
thread["messages"].append(
|
||||
{"role": "user", "text": command["message"]["text"], "streaming": False}
|
||||
)
|
||||
thread["latestTurn"] = None
|
||||
thread["_pending_turn"] = "turn-" + command["commandId"][:4]
|
||||
thread["session"] = {"status": "running", "lastError": None}
|
||||
if "modelSelection" in command:
|
||||
thread["modelSelection"] = command["modelSelection"]
|
||||
elif kind == "thread.turn.interrupt":
|
||||
thread = self.threads[command["threadId"]]
|
||||
thread.pop("_pending_turn", None)
|
||||
thread["latestTurn"] = {"turnId": "t", "state": "interrupted"}
|
||||
else:
|
||||
return httpx.Response(
|
||||
400, json={"code": "invalid_request", "reason": "invalid_command"}
|
||||
)
|
||||
return httpx.Response(200, json={"sequence": len(self.commands)})
|
||||
|
||||
|
||||
PROJECTS_MAC = [
|
||||
{
|
||||
"id": "p-smoke",
|
||||
"title": "t3-smoke",
|
||||
"workspaceRoot": "/Users/h/projects/playgrounds/t3-smoke",
|
||||
"defaultModelSelection": {"instanceId": "codex", "model": "gpt-5.6-sol"},
|
||||
},
|
||||
{
|
||||
"id": "p-secret",
|
||||
"title": "secret",
|
||||
"workspaceRoot": "/Users/h/secret",
|
||||
"defaultModelSelection": None,
|
||||
},
|
||||
]
|
||||
PROJECTS_DELL = [
|
||||
{
|
||||
"id": "p-root",
|
||||
"title": "projects",
|
||||
"workspaceRoot": "/root/projects",
|
||||
"defaultModelSelection": None,
|
||||
},
|
||||
{
|
||||
"id": "p-repo",
|
||||
"title": "beaver/x",
|
||||
"workspaceRoot": "/root/projects/beaver/x",
|
||||
"defaultModelSelection": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class Router(httpx.AsyncBaseTransport):
|
||||
def __init__(self, fakes: dict[str, FakeT3]) -> None:
|
||||
self.fakes = fakes
|
||||
|
||||
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
||||
return self.fakes[request.url.host].handle(request)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fakes(monkeypatch: pytest.MonkeyPatch) -> dict[str, FakeT3]:
|
||||
monkeypatch.setenv("T3_MAC_TOKEN", "tok")
|
||||
monkeypatch.setenv("T3_DELL_TOKEN", "tok")
|
||||
return {"mac": FakeT3("mac", PROJECTS_MAC), "dell": FakeT3("dell", PROJECTS_DELL)}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry(fakes: dict[str, FakeT3]) -> Registry:
|
||||
machines = {
|
||||
"mac": Machine.model_validate(
|
||||
{
|
||||
"name": "mac",
|
||||
"url": "http://mac:3773",
|
||||
"token_env": "T3_MAC_TOKEN",
|
||||
"projects": ("t3-smoke",),
|
||||
"model": "claudeAgent/claude-opus-5",
|
||||
"options": {"effort": "high"},
|
||||
}
|
||||
),
|
||||
"dell": Machine(
|
||||
name="dell",
|
||||
url="http://dell:3773",
|
||||
token_env="T3_DELL_TOKEN",
|
||||
projects=("/root/projects", "/root/projects/*"),
|
||||
),
|
||||
}
|
||||
return Registry(machines, transport=Router(fakes))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(registry: Registry) -> Client:
|
||||
return Client(build_server(registry, poll=0.001))
|
||||
|
||||
|
||||
def data(result: Any) -> Any:
|
||||
return (
|
||||
result.structured_content["result"]
|
||||
if "result" in result.structured_content
|
||||
else result.structured_content
|
||||
)
|
||||
|
||||
|
||||
async def test_machines_and_projects(client: Client) -> None:
|
||||
async with client:
|
||||
machines = data(await client.call_tool("t3_machines"))
|
||||
assert [m["name"] for m in machines] == ["mac", "dell"]
|
||||
assert all(m["online"] for m in machines)
|
||||
projects = data(await client.call_tool("t3_projects", {"machine": "mac"}))
|
||||
assert [p["title"] for p in projects] == ["t3-smoke"]
|
||||
assert projects[0]["default_model"] == "codex/gpt-5.6-sol"
|
||||
dell = data(await client.call_tool("t3_projects", {"machine": "dell"}))
|
||||
assert [p["title"] for p in dell] == ["projects", "beaver/x"]
|
||||
|
||||
|
||||
async def test_dispatch_wait_and_thread(
|
||||
client: Client, fakes: dict[str, FakeT3]
|
||||
) -> None:
|
||||
async with client:
|
||||
started = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{
|
||||
"machine": "mac",
|
||||
"project": "t3-smoke",
|
||||
"prompt": "write README\nmore",
|
||||
},
|
||||
)
|
||||
)
|
||||
assert started["title"] == "write README"
|
||||
assert started["model"] == "claudeAgent/claude-opus-5 effort=high"
|
||||
kinds = [c["type"] for c in fakes["mac"].commands]
|
||||
assert kinds == ["thread.create", "thread.turn.start"]
|
||||
assert fakes["mac"].commands[0]["modelSelection"] == {
|
||||
"instanceId": "claudeAgent",
|
||||
"model": "claude-opus-5",
|
||||
"options": [{"id": "effort", "value": "high"}],
|
||||
}
|
||||
waited = data(
|
||||
await client.call_tool(
|
||||
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
|
||||
)
|
||||
)
|
||||
assert waited["state"] == "completed"
|
||||
assert waited["messages"][-1]["text"] == "done: report"
|
||||
assert waited["timed_out"] is False
|
||||
view = data(
|
||||
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
|
||||
)
|
||||
assert view["project"] == "t3-smoke"
|
||||
assert view["pending"] is None
|
||||
|
||||
|
||||
async def test_follow_up_uses_same_thread(
|
||||
client: Client, fakes: dict[str, FakeT3]
|
||||
) -> None:
|
||||
fakes["mac"].polls_until_done = 0
|
||||
async with client:
|
||||
started = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{"machine": "mac", "project": "t3-smoke", "prompt": "one"},
|
||||
)
|
||||
)
|
||||
await client.call_tool(
|
||||
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
|
||||
)
|
||||
again = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{
|
||||
"machine": "mac",
|
||||
"project": "t3-smoke",
|
||||
"prompt": "two",
|
||||
"thread_id": started["thread_id"],
|
||||
},
|
||||
)
|
||||
)
|
||||
assert again["thread_id"] == started["thread_id"]
|
||||
assert [c["type"] for c in fakes["mac"].commands] == [
|
||||
"thread.create",
|
||||
"thread.turn.start",
|
||||
"thread.turn.start",
|
||||
]
|
||||
|
||||
|
||||
async def test_allowlist_and_unknown_machine(client: Client) -> None:
|
||||
async with client:
|
||||
with pytest.raises(ToolError, match="not available on mac"):
|
||||
await client.call_tool(
|
||||
"t3_dispatch", {"machine": "mac", "project": "secret", "prompt": "x"}
|
||||
)
|
||||
with pytest.raises(ToolError, match="unknown machine"):
|
||||
await client.call_tool("t3_projects", {"machine": "rpi"})
|
||||
|
||||
|
||||
async def test_wait_timeout_and_interrupt(
|
||||
client: Client, fakes: dict[str, FakeT3]
|
||||
) -> None:
|
||||
fakes["dell"].polls_until_done = 10_000
|
||||
async with client:
|
||||
started = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{
|
||||
"machine": "dell",
|
||||
"project": "/root/projects",
|
||||
"prompt": "loop",
|
||||
"model": "claudeAgent/claude-sonnet-5",
|
||||
},
|
||||
)
|
||||
)
|
||||
assert started["model"] == "claudeAgent/claude-sonnet-5"
|
||||
waited = data(
|
||||
await client.call_tool(
|
||||
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
|
||||
)
|
||||
)
|
||||
assert waited["state"] == "running"
|
||||
assert waited["timed_out"] is True
|
||||
stopped = data(
|
||||
await client.call_tool("t3_interrupt", {"thread_id": started["thread_id"]})
|
||||
)
|
||||
assert stopped["state"] == "interrupting"
|
||||
assert fakes["dell"].commands[-1]["type"] == "thread.turn.interrupt"
|
||||
view = data(
|
||||
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
|
||||
)
|
||||
assert view["state"] == "interrupted"
|
||||
|
||||
|
||||
async def test_wait_returns_on_pending_input(
|
||||
client: Client, fakes: dict[str, FakeT3]
|
||||
) -> None:
|
||||
fakes["mac"].polls_until_done = 10_000
|
||||
fakes["mac"].pending_input = True
|
||||
async with client:
|
||||
started = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{"machine": "mac", "project": "t3-smoke", "prompt": "ask"},
|
||||
)
|
||||
)
|
||||
waited = data(
|
||||
await client.call_tool(
|
||||
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
|
||||
)
|
||||
)
|
||||
assert waited["pending"] == {
|
||||
"approval": False,
|
||||
"user_input": True,
|
||||
"plan": False,
|
||||
}
|
||||
assert waited["state"] == "running"
|
||||
|
||||
|
||||
async def test_locate_without_cache(
|
||||
registry: Registry, fakes: dict[str, FakeT3]
|
||||
) -> None:
|
||||
fakes["dell"].polls_until_done = 0
|
||||
async with Client(build_server(registry, poll=0.001)) as client:
|
||||
started = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{
|
||||
"machine": "dell",
|
||||
"project": "beaver/x",
|
||||
"prompt": "hi",
|
||||
"model": "claudeAgent/claude-opus-5",
|
||||
},
|
||||
)
|
||||
)
|
||||
registry.threads.clear()
|
||||
async with Client(build_server(registry, poll=0.001)) as client:
|
||||
view = data(
|
||||
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
|
||||
)
|
||||
assert view["machine"] == "dell"
|
||||
|
||||
|
||||
def test_config_parsing(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("T3_MAC_TOKEN", "abc")
|
||||
cfg = tmp_path / "t3code.toml"
|
||||
cfg.write_text(
|
||||
'[machines.mac]\nurl = "http://x"\ntoken_env = "T3_MAC_TOKEN"\nprojects = ["a"]\nmodel = "claudeAgent/claude-opus-5"\noptions = { effort = "high" }\n'
|
||||
)
|
||||
machines = load_machines(cfg)
|
||||
assert machines["mac"].model == ModelSpec(
|
||||
instance="claudeAgent", model="claude-opus-5", options={"effort": "high"}
|
||||
)
|
||||
assert machines["mac"].token == "abc"
|
||||
monkeypatch.delenv("T3_MAC_TOKEN")
|
||||
with pytest.raises(ValueError, match="T3_MAC_TOKEN is empty"):
|
||||
load_machines(cfg)
|
||||
|
||||
|
||||
async def test_wait_returns_on_runtime_failure(
|
||||
client: Client, fakes: dict[str, FakeT3]
|
||||
) -> None:
|
||||
fakes["dell"].fail_with = "Claude runtime stream failed."
|
||||
async with client:
|
||||
started = data(
|
||||
await client.call_tool(
|
||||
"t3_dispatch",
|
||||
{
|
||||
"machine": "dell",
|
||||
"project": "projects",
|
||||
"prompt": "x",
|
||||
"model": "claudeAgent/claude-opus-5",
|
||||
},
|
||||
)
|
||||
)
|
||||
waited = data(
|
||||
await client.call_tool(
|
||||
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
|
||||
)
|
||||
)
|
||||
assert waited["state"] == "starting"
|
||||
assert waited["error"] == "Claude runtime stream failed."
|
||||
assert waited["waited"] < 1
|
||||
Reference in New Issue
Block a user