testMcpSessionsAndUpgrades
An agent works on a branch of its own, as work on a git branch is, unless it's told to work on the codebase's
own (--direct). And when tcm is replaced (a new version, a new known-good build), the server starts again: the client is
told its tools changed, and keeps working.
import { plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import { session, sessionOf } from "#codebase/tcmts/cli/test/e2e/mcp"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; export const testMcpSessionsAndUpgrades = () => withChildren(async (keep) => { const dir = tempDir("mcp upgrade"); assert.equal( spawnSync(process.execPath, [tcmBin(), "init"], { cwd: dir, env: plainEnv() }).status, 0, ); // (something that takes a while to answer) fs.writeFileSync( path.join(dir, "scratch.ts"), `// tcm: namespace lib\nexport const slow = async () => {\n await new Promise((r) => setTimeout(r, 12000));\n console.log("slow done");\n};\n`, ); assert.equal( spawnSync(process.execPath, [tcmBin(), "add"], { cwd: dir, env: plainEnv() }).status, 0, ); // tcm run through a program of its own, which "upgrading" replaces const bin = path.join(dir, "tcm-bin.mjs"); fs.writeFileSync(bin, `import ${JSON.stringify(pathToFileURL(tcmBin()).href)};\n`); const hello = { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "0" }, }; const a = sessionOf(dir, keep, [bin, "mcp", "--direct"]); assert.match( String((await a.request("initialize", hello)).result!.instructions), /works on the codebase's own branch directly/, ); const b = session(dir, keep); assert.match( String((await b.request("initialize", hello)).result!.instructions), /This is a session on its own branch, session\//, ); b.close(); // replaced while a call is still being answered: started again at once (that call doesn't hold it back), the // client told, the old server still answering the call, and the new one the rest let slowDone = false; const slow = a.call("tcm_run", { name: "lib.slow" }).finally(() => (slowDone = true)); await new Promise((r) => setTimeout(r, 500)); fs.writeFileSync(bin, `${fs.readFileSync(bin, "utf8")}// v2\n`); for ( const until = Date.now() + 30_000; !a.notes.some((n) => n.method === "notifications/tools/list_changed"); await new Promise((r) => setTimeout(r, 100)) ) { if (Date.now() > until) assert.fail("the server wasn't started again"); } assert.equal(slowDone, false, "the restart waited for the call in flight"); assert.match((await slow).text, /slow done/); assert.match((await a.call("tcm_status")).text, /\b1 named definitions?\b[\s\S]*lib {2}1/); a.close(); })