testEachBranchHasItsOwnViews

tcmts.cli.test.e2e.debug.testEachBranchHasItsOwnViews · #n6xc4u47 · term

Each branch has its own views (its definitions as files), as each git worktree has its own checkout, and nothing reads another's: the codebase's own branch's are in .tcm/src, a session's in its directory (written as something needs them, a stack trace say), and a branch only read (--branch) is rendered, nothing written. What a session changed, code and a comment alone, is what search, view and at with --branch see from outside it; stack traces of the same code point at each branch's own views; and main's views, as an editor reads them, are untouched.

import { okIn, plainEnv, strip, tcmBin, tcmIn, tempDir } from "#codebase/tcmts/cli/test/e2e/harness";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

export const testEachBranchHasItsOwnViews = () => {
  const dir = tempDir("views per branch");
  const tcm = (...args: string[]) => okIn(dir, ...args);
  const on = (...args: string[]) => {
    const r = spawnSync(process.execPath, [tcmBin(), ...args], {
      cwd: dir,
      encoding: "utf8",
      env: { ...plainEnv(), TCM_BRANCH: "session/s" },
    });
    return { status: r.status, out: strip(r.stdout + r.stderr) };
  };
  const okOn = (...args: string[]) => {
    const r = on(...args);
    if (r.status !== 0)
      throw new Error(`tcm ${args.join(" ")} (on session/s) exited ${r.status}:\n${r.out}`);
    return r.out;
  };
  const write = (file: string, text: string) => fs.writeFileSync(path.join(dir, file), text);
  const main = path.join(dir, ".tcm", "src");
  const own = path.join(dir, ".tcm", "sessions", "session_s", "src");
  const files = (d: string) =>
    fs.existsSync(d)
      ? (fs.readdirSync(d, { recursive: true }) as string[])
          .filter((f) => fs.statSync(path.join(d, f)).isFile())
          .sort()
          .map((f) => [f, fs.readFileSync(path.join(d, f), "utf8")])
      : [];
  tcm("init");
  fs.rmSync(path.join(dir, "scratch.ts"));
  write(
    "lib.ts",
    `// tcm: namespace lib
export const square = (x: number): number => x * x;
export const area = (side: number): number => square(side);
/** Halves. */
export function half(n: number): number {
  // a number, halved
  return n / 2;
}
export function boom(): number {
  throw new Error("boom");
}
`,
  );
  tcm("add", "lib.ts");
  tcm("branch", "session/s");
  const mainViews = files(main);
  assert.match(fs.readFileSync(path.join(main, "lib", "area.ts"), "utf8"), /square\(side\)/);

  // the session: code (area uses a new definition) and a comment alone (half reads differently, the same hash)
  write(
    "s.ts",
    `// tcm: namespace lib
export const cube = (x: number): number => x * x * x;
export const area = (side: number): number => cube(side) / side;
/** Halves. */
export function half(n: number): number {
  // the other half's gone
  return n / 2;
}
`,
  );
  okOn("update", "./s.ts");
  assert.match(okOn("history", "1"), /lib\.half +#[a-z2-7]+ reads differently/);

  const ownViews = files(own);

  // read from outside the session (the plain CLI, on main): the session's text, never main's files
  const theirs = ["--branch", "session/s"];
  assert.match(tcm("search", "cube", ...theirs), /lib\.area/);
  assert.match(tcm("search", "other half", ...theirs), /lib\.half/);
  assert.doesNotMatch(tcm("search", "a number, halved", ...theirs), /lib\.half/);
  assert.match(
    tcm("view", "lib.area", "lib.half", ...theirs),
    /cube\(side\)[\s\S]*the other half's gone/,
  );
  const sessionView = path.join(".tcm", "sessions", "session_s", "src", "lib", "half.ts");
  assert.match(tcm("at", `${sessionView}:3`, ...theirs), /^lib\.half /m);
  assert.match(
    tcm("at", path.join(".tcm", "sessions", "session_s", "src", "lib", "cube.ts"), ...theirs),
    /^lib\.cube /m,
  );
  // another branch's view is that branch's to say
  const elsewhere = tcmIn(dir, "at", sessionView);
  assert.notEqual(elsewhere.status, 0);
  assert.match(elsewhere.out, /a view of session\/s's definitions[\s\S]*--branch session\/s/);
  const mains = tcmIn(dir, "at", path.join(".tcm", "src", "lib", "half.ts"), ...theirs);
  assert.notEqual(mains.status, 0);
  assert.match(mains.out, /a view of main's definitions, not session\/s's/);
  // and reading wrote nothing of the session's
  assert.deepEqual(files(own), ownViews);

  // a stack trace on the session points at its own views, written for it; main's, of the same code, at main's
  const trace = (out: string, views: string) =>
    assert.ok(out.includes(`at boom (${path.join(views, "lib", "boom.ts")}:`), out);
  trace(on("run", "lib.boom").out, own);
  assert.match(
    fs.readFileSync(path.join(own, "lib", "boom.ts"), "utf8"),
    /throw new Error\("boom"\)/,
  );
  trace(tcmIn(dir, "run", "lib.boom").out, main);
  trace(on("run", "lib.boom").out, own);

  // main as it was: its own text, and its views as the editor reads them
  assert.doesNotMatch(tcm("search", "cube"), /lib\.area/);
  assert.match(tcm("search", "a number, halved"), /lib\.half/);
  assert.match(tcm("at", path.join(".tcm", "src", "lib", "half.ts")), /^lib\.half /m);
  assert.deepEqual(files(main), mainViews);
  assert.match(fs.readFileSync(path.join(main, "lib", "half.ts"), "utf8"), /a number, halved/);
  assert.ok(!fs.existsSync(path.join(main, "lib", "cube.ts")));
}

uses