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"} elif kind == "thread.user-input.respond": self.threads[command["threadId"]]["activities"].append( { "kind": "user-input.resolved", "summary": "User input submitted", "tone": "info", "payload": { "requestId": command["requestId"], "answers": command["answers"], }, } ) 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 def shell_thread( tid: str, state: str | None, *, user_input: bool = False, error: str | None = None ) -> dict[str, Any]: return { "id": tid, "latestTurn": {"turnId": "t", "state": state} if state else None, "session": {"status": "stopped" if error else "running", "lastError": error}, "hasPendingUserInput": user_input, "hasPendingApprovals": 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"}} ) fakes["mac"].threads[tid]["activities"].append( { "kind": "user-input.requested", "summary": "User input requested", "tone": "info", "payload": { "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"] == "thread.user-input.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", )