testSessionsReadOtherBranches

tcmts.cli.test.e2e.mcp.testSessionsReadOtherBranches · #7yurw3bv · term

A session reads another session's branch with the reading tools' branch option, as that branch has it, staying on its own: its files untouched, main too. The tools that change things have no such option, and the CLI's --branch refuses them.

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 testSessionsReadOtherBranches = () =>
  withChildren(async (keep) => {
    const dir = tempDir("read other branches");
    const tcm = (...args: string[]) => okIn(dir, ...args);
    tcm("init");
    fs.writeFileSync(
      path.join(dir, "scratch.ts"),
      `// tcm: namespace lib\nexport const square = (x: number): number => x * x;\n`,
    );
    tcm("add");
    const a = session(dir, keep, "--branch", "agent/a");
    const b = session(dir, keep, "--branch", "agent/b");
    const init = await b.request("initialize", {
      protocolVersion: "2025-06-18",
      capabilities: {},
      clientInfo: { name: "test", version: "0" },
    });
    assert.match(String(init.result!.instructions), /To look at another branch[\s\S]*`branch`/);

    // a's work: a change, and a new definition
    assert.equal(
      (await a.call("tcm_patch", { name: "lib.square", edits: [{ old: "x * x", new: "x ** 2" }] }))
        .isError,
      false,
    );
    assert.equal(
      (
        await a.call("tcm_add", {
          namespace: "lib",
          code: "export const cube = (x: number): number => x ** 3;\n",
        })
      ).isError,
      false,
    );
    // b, with a scratch file and `#codebase` files of its own
    assert.match((await b.call("tcm_edit", { names: ["lib.square"] })).text, /x \* x/);
    assert.equal((await b.call("tcm_check")).isError, false);
    const own = path.join(dir, ".tcm", "sessions", "agent_b");
    const shim = path.join(own, "codebase", "lib.ts");
    const files = () => [
      fs.readFileSync(path.join(own, "scratch.ts"), "utf8"),
      fs.readFileSync(shim, "utf8"),
    ];
    const before = files();
    assert.match(before[1]!, /square/);

    // b reads a's branch
    const theirs = { branch: "agent/a" };
    assert.match(
      (await b.call("tcm_view", { names: ["lib.square", "lib.cube"], ...theirs })).text,
      /x \*\* 2[\s\S]*x \*\* 3/,
    );
    assert.match((await b.call("tcm_search", { text: "x ** 3", ...theirs })).text, /lib\.cube/);
    assert.match((await b.call("tcm_find", { query: "cube", ...theirs })).text, /lib\.cube/);
    // a line of its view (its own, rendered as it reads there: nothing's written for a read)
    const view = path.join(".tcm", "sessions", "agent_a", "src", "lib", "square.ts");
    assert.match((await b.call("tcm_at", { place: `${view}:2`, ...theirs })).text, /^lib\.square /);
    assert.match((await b.call("tcm_at", { place: `${view}:2` })).text, /--branch agent\/a/);
    assert.match((await b.call("tcm_history", { ...theirs })).text, /lib\.cube/);
    assert.match((await b.call("tcm_todo", { ...theirs })).text, /nothing to do/);
    assert.match(
      (await b.call("tcm_status", { ...theirs })).text,
      /branch\s+agent\/a\s+\(read with --branch/,
    );

    // tools that change things have no `branch`; the CLI refuses --branch for them
    const tools = (await b.request("tools/list")).result as {
      tools: { name: string; inputSchema: { properties: Record<string, unknown> } }[];
    };
    const takes = (name: string) =>
      "branch" in tools.tools.find((t) => t.name === name)!.inputSchema.properties;
    assert.ok(takes("tcm_view") && takes("tcm_history") && takes("tcm_status") && takes("tcm_at"));
    assert.ok(!takes("tcm_patch") && !takes("tcm_update") && !takes("tcm_edit"));
    const patched = await b.call("tcm_patch", {
      name: "lib.square",
      edits: [{ old: "x ** 2", new: "x" }],
      ...theirs,
    });
    assert.equal(patched.isError, true);
    assert.match(patched.text, /tcm_patch has no `branch` option/);
    const cli = await b.call("tcm_cli", { args: ["rm", "lib.cube", "--branch", "agent/a"] });
    assert.equal(cli.isError, true);
    assert.match(cli.text, /--branch reads another branch, changing nothing/);

    // b is still on its own branch, its files as they were; a's branch and main are as they were
    assert.match((await b.call("tcm_view", { names: ["lib.square"] })).text, /x \* x/);
    assert.match((await b.call("tcm_status")).text, /branch\s+agent\/b/);
    assert.deepEqual(files(), before);
    assert.doesNotMatch(fs.readFileSync(shim, "utf8"), /cube/);
    assert.match((await a.call("tcm_view", { names: ["lib.cube"] })).text, /x \*\* 3/);
    assert.doesNotMatch(tcm("ls", "lib"), /lib\.cube/);
    a.close();
    b.close();
  })

uses