testSessionsWorkApart
Agents in sessions (tcm mcp --branch) work in one codebase at once, each on its own branch with its own scratch
files, out of the mirror's files and each other's way, until the user has tcm_merge land their work on main (and
only then): main merged in, the tests it reaches passing, then onto main, which the files follow. A branch an
earlier session left is seen by a new one, and can be landed from it.
import { okIn, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import { session } from "#codebase/tcmts/cli/test/e2e/mcp"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; export const testSessionsWorkApart = () => withChildren(async (keep) => { const root = tempDir("sessions"); const proj = path.join(root, "proj"); const dir = path.join(root, "cb"); for (const d of ["src", "test"]) fs.mkdirSync(path.join(proj, d), { recursive: true }); fs.mkdirSync(dir); fs.writeFileSync(path.join(proj, "package.json"), JSON.stringify({ type: "module" })); fs.writeFileSync( path.join(proj, "src", "math.ts"), "export const square = (x: number): number => x * x;\n", ); fs.writeFileSync( path.join(proj, "test", "math.ts"), `import assert from "node:assert/strict";\nimport { square } from "../src/math.ts";\n\nexport const testSquare = () => assert.equal(square(3), 9);\n`, ); const tcm = (...args: string[]) => okIn(dir, ...args); const source = () => fs.readFileSync(path.join(proj, "src", "math.ts"), "utf8"); tcm("init"); fs.rmSync(path.join(dir, "scratch.ts")); tcm("import", proj, "--into", "lib", "--tests", "--mirror"); const a = session(dir, keep, "--branch", "agent/a"); const b = session(dir, keep, "--branch", "agent/b"); const init = await a.request("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "0" }, }); assert.match( String(init.result!.instructions), // (landed only when the user says to: until then it stays on its branch) /a session on its own branch, agent\/a[\s\S]*ask the user whether to land it[\s\S]*tcm_merge/, ); // a changes square, in its own scratch file, on its own branch const edited = await a.call("tcm_edit", { names: ["lib.math.square"] }); assert.match(edited.text, /sessions\/agent_a\/scratch\.ts/); const file = path.join(dir, ".tcm", "sessions", "agent_a", "scratch.ts"); fs.writeFileSync(file, fs.readFileSync(file, "utf8").replace("x * x", "x ** 2")); assert.match((await a.call("tcm_update")).text, /~ update +lib\.math\.square/); // b, main and the files don't see it assert.match((await b.call("tcm_view", { names: ["lib.math.square"] })).text, /x \* x/); assert.match(tcm("view", "lib.math.square"), /x \* x/); assert.match(source(), /x \* x/); // and it stays off main assert.match( (await a.call("tcm_cli", { args: ["switch", "main"] })).text, /main is the codebase's own branch: a session's work lands there with tcm_merge/, ); // b's scratch files are its own too; it adds a test that fails assert.match( (await b.call("tcm_edit", { names: ["lib.math.square"] })).text, /sessions\/agent_b\/scratch\.ts/, ); fs.writeFileSync( path.join(dir, ".tcm", "sessions", "agent_b", "scratch.ts"), `// tcm: namespace lib.math\nexport const testCube = () => {\n throw new Error("not yet");\n};\n`, ); assert.match((await b.call("tcm_update")).text, /\+ new +lib\.math\.testCube/); // not told to by the user, nothing lands: a's work stays on its branch const unasked = await a.call("tcm_merge"); assert.equal(unasked.isError, true, unasked.text); assert.match( unasked.text, /Not landed: tcm_merge lands a branch only when the user has told you to/, ); assert.match(tcm("view", "lib.math.square"), /x \* x/); // told to, but not what it's for: main's history would say nothing of it const unsaid = await a.call("tcm_merge", { approved: true }); assert.equal(unsaid.isError, true, unsaid.text); assert.match(unsaid.text, /Not landed: say what agent\/a's work is for in `message`/); // told to: its change is on main, and in the files, and main's history says what it's for const landed = await a.call("tcm_merge", { approved: true, message: "lib.math.square: x ** 2\n\nSquares read as powers.", }); assert.equal(landed.isError, false, landed.text); assert.match( tcm("history", "1"), // (a's one step, landed with nothing on main since: the same change from the same point, so the same entry) /^#\w{8} +0 .* lib\.math\.square: x \*\* 2\n {4}Squares read as powers\.\n {4}update lib\.math\.square\n/, ); assert.match(landed.text, /agent\/a is on main/); // (what landed by name, not its code again: a made it) assert.match(landed.text, /edited: lib\.math\.square/); assert.doesNotMatch(landed.text, /x \*\* 2/); assert.match(tcm("view", "lib.math.square"), /x \*\* 2/); assert.match(source(), /x \*\* 2/); // b brings main in, but its test fails: nothing lands const refused = await b.call("tcm_merge", { approved: true, message: "lib.math: testCube" }); assert.equal(refused.isError, true, refused.text); assert.match(refused.text, /testCube[\s\S]*nothing landed on main/); // (only what failed) assert.doesNotMatch(refused.text, /✓/); assert.doesNotMatch(tcm("ls", "lib.math"), /testCube/); // with no flags, each server is a session on a new branch of its own const c = session(dir, keep); const cInit = await c.request("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "0" }, }); const cBranch = /own branch, (session\/\d{4}-\d{2}-\d{2}-[0-9a-f]{6}):/.exec( String(cInit.result!.instructions), )?.[1]; assert.ok(cBranch, String(cInit.result!.instructions)); assert.match(tcm("branch"), new RegExp(cBranch)); // b fixes its test, then stops (a reconnect, say): its work is on its branch, which a new session sees, and lands fs.writeFileSync( path.join(dir, ".tcm", "sessions", "agent_b", "scratch.ts"), `// tcm: namespace lib.math\nexport const testCube = () => {};\n`, ); assert.match((await b.call("tcm_update")).text, /~ update +lib\.math\.testCube/); b.close(); assert.match( (await c.call("tcm_status")).text, /branches with work main doesn't have[\s\S]*agent\/b/, ); // (in the background: the tests it runs can outlast a call, and tcm_job says how it's going) const started = await c.call("tcm_merge", { approved: true, branch: "agent/b", message: "lib.math: testCube", background: true, }); const job = /started job (\d+): tcm_merge/.exec(started.text)?.[1]; assert.ok(job, started.text); const resumed = await c.call("tcm_job", { id: Number(job), wait: 300 }); assert.equal(resumed.isError, false, resumed.text); assert.match(resumed.text, /\(tcm_merge\) ended[\s\S]*agent\/b is on main/); assert.match(tcm("ls", "lib.math"), /testCube/); c.close(); // c changed nothing: its branch goes with it; a's and b's, named, stay for (const until = Date.now() + 20_000; new RegExp(cBranch).test(tcm("branch"));) { if (Date.now() > until) assert.fail(`${cBranch} wasn't deleted: ${tcm("branch")}`); await new Promise((r) => setTimeout(r, 200)); } assert.match(tcm("branch"), /agent\/a[\s\S]*agent\/b|agent\/b[\s\S]*agent\/a/); })