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"]})