Files
t3code-mcp/tests/test_server.py
T

687 lines
24 KiB
Python

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
from t3code_mcp.t3 import T3Error
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.1.0",
"platform": {"os": "linux"},
},
)
if request.headers.get("authorization") != f"Bearer {self.token}":
return httpx.Response(
401, json={"code": "auth_invalid", "reason": "missing_credential"}
)
if request.headers.get("x-t3-orchestration-protocol") != "2":
return httpx.Response(400, json={"code": "invalid_request"})
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, "projection": self.threads[thread_id]}
)
return httpx.Response(404, json={"code": "not_found"})
async def rpc(self, tag: str, payload: dict[str, Any]) -> Any:
assert tag == "orchestration.dispatchCommand"
self.commands.append(payload)
return self._dispatch(payload)
def _shell(self) -> dict[str, Any]:
threads = []
for projection in self.threads.values():
thread = projection["thread"]
runs = projection["runs"]
pending = [
r for r in projection["runtimeRequests"] if r["status"] == "pending"
]
threads.append(
{
"id": thread["id"],
"projectId": thread["projectId"],
"title": thread["title"],
"archivedAt": None,
"status": runs[-1]["status"] if runs else "idle",
"lastError": self.fail_with,
"pendingRuntimeRequest": (
{"id": pending[-1]["id"], "kind": pending[-1]["kind"]}
if pending
else None
),
"hasActionableProposedPlan": False,
}
)
return {"snapshotSequence": 1, "projects": self.projects, "threads": threads}
def _tick(self, thread_id: str) -> None:
projection = self.threads[thread_id]
if self.pending_input and not projection["runtimeRequests"]:
projection["runtimeRequests"].append(
{"id": "req-0", "kind": "user_input", "status": "pending"}
)
if not projection["runs"]:
return
run = projection["runs"][-1]
if self.fail_with and run["status"] == "running":
run["status"] = "failed"
projection["turnItems"].append(
{
"type": "error",
"runId": run["id"],
"status": "failed",
"failure": {"message": self.fail_with},
}
)
return
if run["status"] == "running":
run["_polls"] = run.get("_polls", 0) + 1
if run["_polls"] > self.polls_until_done:
run["status"] = "completed"
projection["messages"].append(
{
"role": "assistant",
"text": "done: report",
"streaming": False,
"runId": run["id"],
}
)
def _dispatch(self, command: dict[str, Any]) -> dict[str, Any]:
kind = command["type"]
if kind == "thread.create":
self.threads[command["threadId"]] = {
"thread": {
"id": command["threadId"],
"projectId": command["projectId"],
"title": command["title"],
"modelSelection": command["modelSelection"],
"runtimeMode": command["runtimeMode"],
"archivedAt": None,
},
"runs": [],
"messages": [],
"turnItems": [],
"runtimeRequests": [],
"checkpoints": [],
"providerSessions": [],
"providerTurns": [],
"plans": [],
}
elif kind == "message.dispatch":
assert command["dispatchMode"] == {"type": "start_immediately"}
projection = self.threads[command["threadId"]]
run_id = f"run-{len(projection['runs']) + 1}"
projection["runs"].append(
{
"id": run_id,
"ordinal": len(projection["runs"]) + 1,
"status": "running",
}
)
projection["messages"].append(
{
"role": "user",
"text": command["text"],
"streaming": False,
"runId": run_id,
}
)
if "modelSelection" in command:
projection["thread"]["modelSelection"] = command["modelSelection"]
elif kind == "run.interrupt":
run = self.threads[command["threadId"]]["runs"][-1]
assert run["id"] == command["runId"]
run["status"] = "interrupted"
elif kind == "runtime-request.respond":
for request in self.threads[command["threadId"]]["runtimeRequests"]:
if request["id"] == command["requestId"]:
request["status"] = "resolved"
else:
raise T3Error(0, {"_tag": "OrchestrationV2DispatchCommandError"})
return {"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), rpc=lambda name: fakes[name].rpc)
@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", "message.dispatch"]
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",
"message.dispatch",
"message.dispatch",
]
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"] == "run.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"] == "failed"
assert waited["error"] == "Claude runtime stream failed."
assert waited["waited"] < 1
def shell_thread(
tid: str, state: str | None, *, user_input: bool = False, error: str | None = None
) -> dict[str, Any]:
return {
"id": tid,
"status": state or "idle",
"lastError": error,
"pendingRuntimeRequest": (
{"id": "req-1", "kind": "user_input"} if user_input else None
),
"hasActionableProposedPlan": False,
}
def test_tracker_transitions(tmp_path) -> None:
from t3code_mcp.watch import Tracker
tracker = Tracker(tmp_path / "state.json")
tracker.track("t1", "dell", "projects", "do x")
assert tracker.apply("dell", shell_thread("t1", None)) == []
assert tracker.apply("dell", shell_thread("t1", "running")) == []
assert [
e.kind
for e in tracker.apply("dell", shell_thread("t1", "running", user_input=True))
] == ["question"]
assert tracker.apply("dell", shell_thread("t1", "running", user_input=True)) == []
assert [e.kind for e in tracker.apply("dell", shell_thread("t1", "completed"))] == [
"completed"
]
assert tracker.apply("dell", shell_thread("t1", "completed")) == []
assert tracker.apply("mac", shell_thread("t1", "running")) == []
assert tracker.apply("dell", shell_thread("untracked", "completed")) == []
tracker.track("t2", "dell", "projects", "fail")
assert [
e.kind
for e in tracker.apply(
"dell", shell_thread("t2", None, error="Claude runtime stream failed.")
)
] == ["failed"]
reloaded = Tracker(tmp_path / "state.json")
assert reloaded.threads["t1"].state == "completed"
assert reloaded.threads["t2"].error == "Claude runtime stream failed."
async def test_hook_outbox_retries(tmp_path) -> None:
from t3code_mcp.watch import Hook, Tracker
attempts: list[dict[str, Any]] = []
fail_first = {"n": 1}
def handler(request: httpx.Request) -> httpx.Response:
assert request.headers["authorization"] == "Bearer gw"
attempts.append(json.loads(request.content))
if fail_first["n"]:
fail_first["n"] -= 1
return httpx.Response(503, text="down")
return httpx.Response(202, json={"job": 1})
tracker = Tracker(tmp_path / "state.json")
hook = Hook(
"http://gateway/hooks/t3code",
"gw",
tracker,
transport=httpx.MockTransport(handler),
)
hook.send({"event": "completed", "thread_id": "t1"})
assert Tracker(tmp_path / "state.json").outbox == [
{"event": "completed", "thread_id": "t1"}
]
assert await hook.deliver_once() is False
assert await hook.deliver_once() is True
assert tracker.outbox == [] and len(attempts) == 2
await hook.aclose()
async def test_answer_pending_question(
client: Client, fakes: dict[str, FakeT3]
) -> None:
fakes["mac"].polls_until_done = 10_000
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{"machine": "mac", "project": "t3-smoke", "prompt": "ask"},
)
)
tid = started["thread_id"]
with pytest.raises(ToolError, match="no pending question"):
await client.call_tool(
"t3_answer", {"thread_id": tid, "answers": {"q1": "yes"}}
)
projection = fakes["mac"].threads[tid]
projection["runtimeRequests"].append(
{"id": "req-1", "kind": "user_input", "status": "pending"}
)
projection["turnItems"].append(
{
"type": "user_input_request",
"runId": "run-1",
"status": "waiting",
"requestId": "req-1",
"questions": [
{
"id": "q1",
"header": "Deploy",
"question": "Deploy now?",
"options": [
{"label": "yes", "description": "go"},
{"label": "no", "description": "wait"},
],
}
],
}
)
view = data(await client.call_tool("t3_thread", {"thread_id": tid}))
assert view["question"]["request_id"] == "req-1"
assert view["question"]["questions"][0]["options"][0]["label"] == "yes"
with pytest.raises(ToolError, match="unknown question ids"):
await client.call_tool(
"t3_answer", {"thread_id": tid, "answers": {"zz": "yes"}}
)
answered = data(
await client.call_tool(
"t3_answer", {"thread_id": tid, "answers": {"q1": "yes"}}
)
)
assert answered["answered"] == ["q1"]
cmd = fakes["mac"].commands[-1]
assert (
cmd["type"] == "runtime-request.respond"
and cmd["requestId"] == "req-1"
and cmd["answers"] == {"q1": "yes"}
)
async def test_dispatch_tracks_thread(client: Client, registry: Registry) -> None:
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{"machine": "mac", "project": "t3-smoke", "prompt": "track me"},
)
)
tracked = registry.tracker.threads[started["thread_id"]]
assert (tracked.machine, tracked.project, tracked.title) == (
"mac",
"t3-smoke",
"track me",
)
def test_tracker_watch_flag(tmp_path) -> None:
from t3code_mcp.watch import Tracker
tracker = Tracker(tmp_path / "state.json")
tracker.track("t1", "dell", "projects", "quiet", watch=False)
tracker.apply("dell", shell_thread("t1", "running"))
assert tracker.apply("dell", shell_thread("t1", "completed")) == []
assert tracker.threads["t1"].state == "completed"
tracker.set_watch("t1", enabled=True)
tracker.apply("dell", shell_thread("t1", "running"))
assert [e.kind for e in tracker.apply("dell", shell_thread("t1", "completed"))] == [
"completed"
]
assert Tracker(tmp_path / "state.json").threads["t1"].watch is True
async def test_watch_tool_lists_and_toggles(client: Client) -> None:
async with client:
loud = data(
await client.call_tool(
"t3_dispatch",
{"machine": "mac", "project": "t3-smoke", "prompt": "tell me"},
)
)
quiet = data(
await client.call_tool(
"t3_dispatch",
{
"machine": "mac",
"project": "t3-smoke",
"prompt": "leave me alone",
"watch": False,
},
)
)
assert (loud["watch"], quiet["watch"]) == (True, False)
view = data(await client.call_tool("t3_watch"))
assert [t["thread_id"] for t in view["watched"]] == [loud["thread_id"]]
assert [t["thread_id"] for t in view["unwatched"]] == [quiet["thread_id"]]
view = data(
await client.call_tool(
"t3_watch", {"thread_id": loud["thread_id"], "enabled": False}
)
)
assert view["watched"] == []
assert {t["thread_id"] for t in view["unwatched"]} == {
loud["thread_id"],
quiet["thread_id"],
}
view = data(
await client.call_tool(
"t3_watch", {"thread_id": quiet["thread_id"], "enabled": True}
)
)
assert [t["thread_id"] for t in view["watched"]] == [quiet["thread_id"]]
with pytest.raises(ToolError, match="nothing to unwatch"):
await client.call_tool("t3_watch", {"thread_id": "nope", "enabled": False})
with pytest.raises(ToolError, match="enabled=true or enabled=false"):
await client.call_tool("t3_watch", {"thread_id": loud["thread_id"]})