| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { mkdtempSync, rmSync, existsSync } from "node:fs"; |
| 4 | import { tmpdir } from "node:os"; |
| 5 | import { join, resolve } from "node:path"; |
| 6 | import { execFile } from "node:child_process"; |
| 7 | import { promisify } from "node:util"; |
| 8 | import { startLocal } from "./coordination-local.mjs"; |
| 9 | const exec = promisify(execFile); |
| 10 | const binary = resolve(`target/debug/g1t${process.platform === "win32" ? ".exe" : ""}`); |
| 11 | const built = ["apps/api/build/index.js", "services/work/build/index.js", binary].every(existsSync); |
| 12 | // A release check must not pass by skipping: G1T_REQUIRE_LOCAL_WORKERS=1 turns a missing build into a failure. |
| 13 | if (!built && process.env.G1T_REQUIRE_LOCAL_WORKERS === "1") |
| 14 | throw new Error("Build API/Work Workers (node scripts/deploy.mjs build --only work,api) and the g1t CLI (cargo build -p g1t) first"); |
| 15 | |
| 16 | test( |
| 17 | "real Workers: CLI and MCP share durable updates, receipts and ownership rules", |
| 18 | { skip: !built && "Build API/Work Workers and the g1t CLI first", timeout: 60000 }, |
| 19 | async () => { |
| 20 | const dir = mkdtempSync(join(tmpdir(), "g1t-local-coordination-")); |
| 21 | let local; |
| 22 | try { |
| 23 | local = await startLocal({ port: 0, persist: dir }); |
| 24 | const rest = async (path, body, token = "local-rehearsal") => { |
| 25 | const response = await fetch(`${local.url}/repos/tester/rehearsal/${path}`, { |
| 26 | method: body ? "POST" : "GET", |
| 27 | headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, |
| 28 | body: body ? JSON.stringify(body) : undefined, |
| 29 | }); |
| 30 | const result = await response.json(); |
| 31 | assert.equal(response.status, 200, JSON.stringify(result)); |
| 32 | return result; |
| 33 | }; |
| 34 | const mcp = async (args, token = "local-rehearsal") => { |
| 35 | const response = await fetch(`${local.url}/mcp`, { |
| 36 | method: "POST", |
| 37 | headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, |
| 38 | body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "session", arguments: { repo: "tester/rehearsal", ...args } } }), |
| 39 | }); |
| 40 | const text = await response.text(); |
| 41 | assert.equal(response.status, 200, text); |
| 42 | return JSON.parse(text).result; |
| 43 | }; |
| 44 | const unwrap = (result) => { |
| 45 | assert.equal(result.isError, false, JSON.stringify(result)); |
| 46 | return JSON.parse(result.content[0].text); |
| 47 | }; |
| 48 | const a = await rest("sessions", { harness: "codex", harness_session_id: "codex-test" }); |
| 49 | const b = unwrap(await mcp({ action: "start", harness: "claude_code", harness_session_id: "claude-test" })); |
| 50 | await rest(`sessions/${a.id}/entries`, { |
| 51 | entries: [{ kind: "note", text: "Use fetchUser; implementation belongs to Codex", key: "plan" }], |
| 52 | touched: [{ kind: "file", value: "src/contract.ts" }], |
| 53 | }); |
| 54 | unwrap( |
| 55 | await mcp({ |
| 56 | action: "record", |
| 57 | id: b.id, |
| 58 | entries: [{ kind: "note", text: "Claude owns caller tests", key: "plan" }], |
| 59 | touched: [{ kind: "file", value: "src/contract.ts" }], |
| 60 | }), |
| 61 | ); |
| 62 | let detail = await rest(`sessions/${a.id}`); |
| 63 | const overlap = detail.updates.find((u) => u.kind === "overlap.detected"); |
| 64 | assert.ok(overlap); |
| 65 | const bOverlap = unwrap(await mcp({ action: "get", id: b.id })).updates.find((u) => u.kind === "overlap.detected"); |
| 66 | assert.ok(bOverlap); |
| 67 | assert.notEqual(bOverlap.id, overlap.id); |
| 68 | assert.ok( |
| 69 | (await rest(`sessions/${a.id}`)).updates.some((u) => u.id === overlap.id), |
| 70 | "reading never acknowledges", |
| 71 | ); |
| 72 | const foreign = await mcp({ action: "record", id: a.id, acknowledge: [overlap.id] }, "local-other"); |
| 73 | assert.equal(foreign.isError, true, "another actor cannot acknowledge this session"); |
| 74 | // The same person's other session cannot clear it either, and is told so. |
| 75 | const sibling = unwrap(await mcp({ action: "record", id: b.id, acknowledge: [overlap.id] })); |
| 76 | assert.equal(sibling.acknowledged, 0, "an ID from another session is not counted"); |
| 77 | assert.ok( |
| 78 | (await rest(`sessions/${a.id}`)).updates.some((u) => u.id === overlap.id), |
| 79 | "a sibling session cannot clear this inbox", |
| 80 | ); |
| 81 | const args = [ |
| 82 | "-R", |
| 83 | "tester/rehearsal", |
| 84 | "session", |
| 85 | "acknowledge", |
| 86 | overlap.id, |
| 87 | "--session", |
| 88 | a.id, |
| 89 | "--note", |
| 90 | "Separate scopes; agree the interface before editing", |
| 91 | "--json", |
| 92 | ]; |
| 93 | for (let n = 0; n < 2; n++) |
| 94 | await exec(binary, args, { env: { ...process.env, G1T_TOKEN: "local-rehearsal", G1T_API_URL: local.url, G1T_SESSION: b.id }, windowsHide: true }); |
| 95 | detail = await rest(`sessions/${a.id}`); |
| 96 | assert.ok(!detail.updates.some((u) => u.id === overlap.id)); |
| 97 | assert.equal(detail.entries.filter((e) => e.text.includes("Separate scopes;")).length, 1, "a retry records one decision"); |
| 98 | assert.ok( |
| 99 | unwrap(await mcp({ action: "get", id: b.id })).updates.some((u) => u.id === bOverlap.id), |
| 100 | "one harness cannot clear the other inbox", |
| 101 | ); |
| 102 | await local.mf.dispose(); |
| 103 | local = await startLocal({ port: 0, persist: dir }); |
| 104 | assert.ok(!(await rest(`sessions/${a.id}`)).updates.some((u) => u.id === overlap.id), "receipt survives restart"); |
| 105 | assert.ok( |
| 106 | unwrap(await mcp({ action: "get", id: b.id })).updates.some((u) => u.id === bOverlap.id), |
| 107 | "pending update survives restart", |
| 108 | ); |
| 109 | const resumed = await rest("sessions", { harness: "codex", harness_session_id: "codex-test" }); |
| 110 | assert.equal(resumed.id, a.id, "a restarted harness recovers its session"); |
| 111 | assert.equal( |
| 112 | unwrap(await mcp({ action: "record", id: b.id, entries: [{ kind: "note", text: "Handoff considered", key: "ack" }], acknowledge: [bOverlap.id] })) |
| 113 | .acknowledged, |
| 114 | 1, |
| 115 | ); |
| 116 | assert.ok(!unwrap(await mcp({ action: "get", id: b.id })).updates.some((u) => u.id === bOverlap.id)); |
| 117 | } finally { |
| 118 | if (local) await local.mf.dispose(); |
| 119 | rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); |
| 120 | } |
| 121 | }, |
| 122 | ); |