testJobsOutliveTheirServer
A background job outlives the server that started it: when tcm is replaced (a new known-good verified) and the supervisor starts the server again, the new one lists the session's jobs, follows one to its end (all it printed, and its exit code), stops one, and reads what a tcm_merge started before the restart did. An id the session never gave is said so.
import { okIn, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import { startedSession } from "#codebase/tcmts/cli/test/e2e/mcp"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; export const testJobsOutliveTheirServer = () => withChildren(async (keep) => { const dir = tempDir("mcp jobs restart"); const tcm = (...args: string[]) => okIn(dir, ...args); tcm("init"); fs.rmSync(path.join(dir, "scratch.ts"), { force: true }); fs.writeFileSync( path.join(dir, "jobs.ts"), `export const slow = async (ms: string) => {\n console.log("started");\n await new Promise((r) => setTimeout(r, Number(ms)));\n console.log("finished");\n process.exit(3);\n};\n`, ); tcm("add", "jobs.ts", "--into", "lib"); const bin = path.join(dir, "tcm-bin.mjs"); fs.writeFileSync(bin, `import ${JSON.stringify(pathToFileURL(tcmBin()).href)};\n`); const s = await startedSession(dir, keep, {}, [bin, "mcp", "--branch", "agent/a"]); const started = await s.call("tcm_run", { name: "lib.slow", args: ["4000"], background: true }); assert.match(started.text, /^started job 1: tcm run lib\.slow -- 4000/, started.text); assert.match( (await s.call("tcm_run", { name: "lib.slow", args: ["600000"], background: true })).text, /^started job 2/, ); // work to land, landed by a job the restart comes in the middle of assert.match( (await s.call("tcm_add", { code: "export const added = 1;\n", namespace: "lib" })).text, /lib\.added/, ); assert.match( (await s.call("tcm_merge", { approved: true, message: "lib.added", background: true })).text, /^started job 3: tcm_merge/, ); // tcm replaced: the supervisor starts the server again fs.writeFileSync(bin, `${fs.readFileSync(bin, "utf8")}// v2\n`); for ( const until = Date.now() + 30_000; !s.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"); const listed = (await s.call("tcm_job")).text; assert.match(listed, /^1 tcm run lib\.slow -- 4000 .*started by an earlier server$/m, listed); assert.match( listed, /^2 tcm run lib\.slow -- 600000 running for \d+s, started by an earlier server$/m, listed, ); assert.match(listed, /^3 tcm_merge /m, listed); // followed to its end: all it printed, and its exit code const one = await s.call("tcm_job", { id: 1, wait: 60 }); assert.equal(one.isError, true, one.text); assert.match( one.text, /^job 1 \(tcm run lib\.slow -- 4000\) failed \(exit 3\) after \d+s, started by an earlier server:/, one.text, ); assert.match(one.text, /started\nfinished/, one.text); // stopped, with what it runs const record = JSON.parse( fs.readFileSync(path.join(dir, ".tcm", "sessions", "agent_a", "jobs", "2.json"), "utf8"), ) as { pid: number }; const stopped = await s.call("tcm_job", { id: 2, stop: true }); assert.match( stopped.text, /^stopped job 2 \(tcm run lib\.slow -- 600000\) after \d+s/, stopped.text, ); assert.match(stopped.text, /started/); // (its process group, the command in it, gone) const gone = () => { try { process.kill(-record.pid, 0); return false; } catch { return true; } }; for (const until = Date.now() + 10_000; !gone(); await new Promise((r) => setTimeout(r, 100))) if (Date.now() > until) assert.fail("what job 2 ran is still running"); assert.match((await s.call("tcm_job", { id: 2 })).text, /^job 2 \(.*\) was stopped after \d+s/); assert.match((await s.call("tcm_job")).text, /^2 .* stopped after \d+s/m); // the merge landed all the same, and what it said is there to read const merged = await s.call("tcm_job", { id: 3, wait: 120 }); assert.equal(merged.isError, false, merged.text); assert.match(merged.text, /agent\/a is on main/, merged.text); assert.match(tcm("view", "lib.added"), /added = 1/); // an id it never gave const unknown = await s.call("tcm_job", { id: 9 }); assert.equal(unknown.isError, true); assert.match( unknown.text, /^no job 9 in this session: its jobs are numbered 1 to 3/, unknown.text, ); s.close(); })