import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { agentToken, BASE, body, call, MODERN, patClient, readSse, seedToken, seedWorld, teardown, toolText, } from "./mcp-helpers"; import type { RpcReply, World } from "./mcp-helpers"; let world: World; beforeAll(async () => { world = await seedWorld(); }, 120_000); afterAll(async () => { await teardown(world); }); describe("multi-agent coordination", () => { test("two concurrent agents claiming overlapping files see each other's claims", async () => { const [tokenA, tokenB] = await Promise.all([ agentToken(world, "agent-a", { contents: "write" }), agentToken(world, "agent-b", { contents: "write" }), ]); const a = patClient(tokenA, "agent-a"); const b = patClient(tokenB, "agent-b"); await Promise.all([a.client.connect(a.transport), b.client.connect(b.transport)]); const [resultA, resultB] = await Promise.all([ call(a.client, "claim_files", { owner: world.owner, repo: world.repo, paths: ["src/"], ttl_seconds: 120 }), call(b.client, "claim_files", { owner: world.owner, repo: world.repo, paths: ["src/app.ts", "docs/guide.md"], ttl_seconds: 120, }), ]); type Claim = { granted: Array<{ key: string; agent: string }>; conflicts: Array<{ key: string; held_by: { agent: string } }>; }; const claimA = resultA.structuredContent as Claim; const claimB = resultB.structuredContent as Claim; const srcWinners = [ ...claimA.granted.filter((lease) => lease.key === "src/").map(() => "agent-a"), ...claimB.granted.filter((lease) => lease.key === "src/app.ts").map(() => "agent-b"), ]; expect(srcWinners).toHaveLength(1); expect(claimB.granted.map((lease) => lease.key)).toContain("docs/guide.md"); const loser = srcWinners[0] === "agent-a" ? claimB : claimA; expect(loser.conflicts[0]?.held_by.agent).toBe(srcWinners[0] === "agent-a" ? "agent-a" : "agent-b"); const listed = await call(a.client, "list_claims", { owner: world.owner, repo: world.repo }); const agents = new Set((listed.structuredContent?.claims as Array<{ agent: string }>).map((claim) => claim.agent)); expect(agents).toEqual(new Set(["agent-a", "agent-b"])); const commitB = await call(b.client, "commit_files", { owner: world.owner, repo: world.repo, branch: "main", message: "Touch a file agent A claimed", files: [{ path: "src/other.ts", content: "export {};\n" }], respect_claims: srcWinners[0] === "agent-a", }); if (srcWinners[0] === "agent-a") { expect(commitB.isError).toBe(true); expect(toolText(commitB)).toContain("claimed by other agents"); } await call(a.client, "release_claims", { owner: world.owner, repo: world.repo }); await call(b.client, "release_claims", { owner: world.owner, repo: world.repo }); await Promise.all([a.client.close(), b.client.close()]); }, 30_000); }); describe("sessions, hibernation and resume", () => { test("a 2025-era session replays missed events after reconnecting with Last-Event-ID", async () => { const token = seedToken(world, "e2e legacy", { contents: "write" }, [world.repoId]); const headers = { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json, text/event-stream", }; const init = await fetch(`${BASE}/mcp`, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "legacy-agent", version: "1" } }, }), }); const sessionId = init.headers.get("mcp-session-id") ?? ""; expect(sessionId).not.toBe(""); const withSession = { ...headers, "mcp-session-id": sessionId, "mcp-protocol-version": "2025-11-25" }; const rpc = async (id: number, method: string, params: Record) => body( await fetch(`${BASE}/mcp`, { method: "POST", headers: withSession, body: JSON.stringify({ jsonrpc: "2.0", id, method, params }), }), ); await fetch(`${BASE}/mcp`, { method: "POST", headers: withSession, body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), }); const subscribed = await rpc(2, "tools/call", { name: "subscribe_events", arguments: { owner: world.owner, repo: world.repo, events: ["claims"] }, }); expect(subscribed.result?.structuredContent?.pushed).toBe(true); // The client is disconnected (no GET stream open) while another agent claims a task. const other = patClient(seedToken(world, "e2e other", { contents: "write" }, [world.repoId]), "agent-c"); await other.client.connect(other.transport); await call(other.client, "claim_task", { owner: world.owner, repo: world.repo, task: `issue-${world.suffix}`, ttl_seconds: 60, }); const resumed = await fetch(`${BASE}/mcp`, { headers: { ...withSession, accept: "text/event-stream", "last-event-id": "get-0" }, }); expect(resumed.headers.get("content-type")).toContain("text/event-stream"); const frames = await readSse(resumed, (data) => data.includes(`issue-${world.suffix}`), 10_000); const replayed = frames.find((frame) => frame.data.includes(`issue-${world.suffix}`)); expect(replayed?.id).toMatch(/^get-\d+$/); // Resuming from the last seen id does not redeliver it. const again = await fetch(`${BASE}/mcp`, { headers: { ...withSession, accept: "text/event-stream", "last-event-id": replayed?.id ?? "" }, }); const later = await readSse(again, () => false, 1500); expect(later.some((frame) => frame.data.includes(`issue-${world.suffix}`))).toBe(false); const tools = await rpc(3, "tools/list", {}); expect(tools.result?.tools?.length).toBeGreaterThan(10); const closed = await fetch(`${BASE}/mcp`, { method: "DELETE", headers: withSession }); expect(closed.status).toBe(204); const gone = await rpc(4, "tools/list", {}); expect(gone.error?.code).toBe(-32600); await call(other.client, "release_claims", { owner: world.owner, repo: world.repo }); await other.client.close(); }, 30_000); test("a session survives a real Durable Object eviction and replays its log afterwards", async () => { const token = await agentToken(world, `evict-${world.suffix}`, { contents: "write" }); const headers = { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json, text/event-stream", }; const init = await fetch(`${BASE}/mcp`, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "evict-agent", version: "1" } }, }), }); const withSession = { ...headers, "mcp-session-id": init.headers.get("mcp-session-id") ?? "" }; const rpc = async (id: number, method: string, params: Record) => body( await fetch(`${BASE}/mcp`, { method: "POST", headers: withSession, body: JSON.stringify({ jsonrpc: "2.0", id, method, params }), }), ); await rpc(2, "tools/call", { name: "subscribe_events", arguments: { owner: world.owner, repo: world.repo, events: ["claims"] }, }); const other = patClient(seedToken(world, "e2e evict other", { contents: "write" }, [world.repoId]), "agent-e"); await other.client.connect(other.transport); await call(other.client, "claim_task", { owner: world.owner, repo: world.repo, task: `evict-${world.suffix}`, ttl_seconds: 60, }); const evicted = await fetch(`${BASE}/mcp/__test/evict`, { method: "POST", headers: withSession }); expect(evicted.status).toBe(200); const probe = await body<{ before: { instance: string }; after: { instance: string; streams: number } }>(evicted); expect(probe.after.instance).not.toBe(probe.before.instance); expect(probe.after.streams).toBe(0); const tools = await rpc(3, "tools/list", {}); expect(tools.result?.tools?.length).toBeGreaterThan(10); const resumed = await fetch(`${BASE}/mcp`, { headers: { ...withSession, accept: "text/event-stream", "last-event-id": "get-0" }, }); const frames = await readSse(resumed, (data) => data.includes(`evict-${world.suffix}`), 10_000); expect(frames.some((frame) => frame.data.includes(`evict-${world.suffix}`))).toBe(true); await call(other.client, "release_claims", { owner: world.owner, repo: world.repo }); await other.client.close(); }, 30_000); test("a hibernatable WebSocket replays the log and receives live events", async () => { const token = seedToken(world, "e2e ws", { contents: "write" }, [world.repoId]); const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" }; const init = await fetch(`${BASE}/mcp`, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "ws-agent", version: "1" } }, }), }); const sessionId = init.headers.get("mcp-session-id") ?? ""; const subscribe = await fetch(`${BASE}/mcp`, { method: "POST", headers: { ...headers, "mcp-session-id": sessionId }, body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "resources/subscribe", params: { uri: `gitfor-events://${world.owner}/${world.repo}` }, }), }); expect((await body(subscribe)).result).toEqual({}); await fetch(`${BASE}/mcp`, { method: "POST", headers: { ...headers, "mcp-session-id": sessionId }, body: JSON.stringify({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "subscribe_events", arguments: { owner: world.owner, repo: world.repo, events: ["claims"] } }, }), }); const messages: string[] = []; const socket = new WebSocket(`${BASE.replace(/^http/, "ws")}/mcp?session=${sessionId}&last_event_id=get-0`, { headers: { authorization: `Bearer ${token}` }, } as never); await new Promise((resolve, reject) => { socket.addEventListener("open", () => resolve()); socket.addEventListener("error", () => reject(new Error("websocket failed"))); }); socket.addEventListener("message", (event) => messages.push(String(event.data))); const other = patClient(seedToken(world, "e2e ws other", { contents: "write" }, [world.repoId]), "agent-d"); await other.client.connect(other.transport); await call(other.client, "claim_task", { owner: world.owner, repo: world.repo, task: `ws-${world.suffix}`, ttl_seconds: 60, }); const deadline = Date.now() + 5000; while (Date.now() < deadline && !messages.some((message) => message.includes(`ws-${world.suffix}`))) { await Bun.sleep(100); } expect(messages.some((message) => message.includes("notifications/resources/updated"))).toBe(true); expect(messages.some((message) => message.includes(`ws-${world.suffix}`))).toBe(true); socket.close(); await call(other.client, "release_claims", { owner: world.owner, repo: world.repo }); await other.client.close(); }, 30_000); test("2026-era requests are stateless and validate routing headers", async () => { const token = seedToken(world, "e2e modern", { contents: "read" }, [world.repoId]); const request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "list_claims", arguments: { owner: world.owner, repo: world.repo }, _meta: { "io.modelcontextprotocol/protocolVersion": MODERN }, }, }; const mismatched = await fetch(`${BASE}/mcp`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "mcp-protocol-version": MODERN, "mcp-method": "tools/call", "mcp-name": "get_repo", }, body: JSON.stringify(request), }); expect(mismatched.status).toBe(400); expect((await body(mismatched)).error?.code).toBe(-32020); const ok = await fetch(`${BASE}/mcp`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "mcp-protocol-version": MODERN, "mcp-method": "tools/call", "mcp-name": "list_claims", }, body: JSON.stringify(request), }); const reply = await body(ok); expect(reply.result?.resultType).toBe("complete"); expect(reply.result?._meta?.["io.modelcontextprotocol/serverInfo"]?.name).toBe("gitfor"); }); });