testTodoIsEditedAgainstWhatReplacedItsUses

tcmts.cli.test.e2e.editing.testTodoIsEditedAgainstWhatReplacedItsUses · #ramibrpq · term

A definition in todo is edited against what replaced the versions it used, imported by name, even where that's a version from before (a change undone: Ctx given jobs, then not, so Tool hashes as it first did): tcm edit and tcm patch say so, and a scratch file in a directory of its own (a session's) checks. A version imported by its hash from a module that isn't there is said plainly, without what follows from it.

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

export const testTodoIsEditedAgainstWhatReplacedItsUses = () => {
  const dir = tempDir("todo edited by name");
  const ok = (...args: string[]) => okIn(dir, ...args);
  ok("init");
  const write = (f: string, text: string) => {
    fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true });
    fs.writeFileSync(path.join(dir, f), text);
  };
  write(
    "lib.ts",
    `// tcm: namespace lib\n` +
      `export interface Ctx { root: string }\n` +
      `export interface Tool { name: string; run: (ctx: Ctx) => string }\n`,
  );
  ok("add", "lib.ts");
  ok("patch", "lib.Ctx", "--old", "root: string;", "--new", "root: string;\n  jobs: number[];");
  write(
    "tools.ts",
    `// tcm: namespace lib\n` +
      `import type { Tool } from "#codebase/lib";\n` +
      `export const TOOLS: Tool[] = [{ name: "jobs", run: (ctx) => ctx.jobs.join(",") }];\n`,
  );
  ok("add", "tools.ts");
  assert.match(
    ok("patch", "lib.Ctx", "--old", "\n  jobs: number[];", "--new", ""),
    /no longer typecheck/,
  );

  fs.mkdirSync(path.join(dir, "sub"), { recursive: true });
  const edited = ok("edit", "lib.TOOLS", "--file", "sub/t.ts");
  const replaced = /Tool: imported as lib\.Tool \(the version it used, #(\w+), is replaced\)/.exec(
    edited,
  );
  assert.ok(replaced, edited);
  const text = fs.readFileSync(path.join(dir, "sub", "t.ts"), "utf8");
  assert.match(text, /import type \{ Tool \} from "#codebase\/lib"/, text);
  assert.doesNotMatch(text, /\.tcm\/cache/, text);
  // checked against the current Tool: the real problem
  const checked = tcmIn(dir, "check", "sub/t.ts");
  assert.notEqual(checked.status, 0);
  assert.match(checked.out, /Property 'jobs' does not exist on type 'Ctx'/, checked.out);

  const patched = ok("patch", "lib.TOOLS", "--old", `ctx.jobs.join(",")`, "--new", "ctx.root");
  assert.match(patched, /Tool: imported as lib\.Tool/, patched);
  assert.match(ok("todo"), /nothing to do/);

  // the old version by its hash, from where its module isn't (the project's cache, written from another directory)
  const cache = path.join(dir, ".tcm", "cache");
  const old = fs
    .readdirSync(cache)
    .find((f) => f.startsWith(replaced[1]!))!
    .replace(/\.tsx?$/, "");
  write(
    "sub/m.ts",
    `// tcm: namespace lib\n` +
      `import type { h_${old} as Tool } from "./.tcm/cache/${old}.ts";\n` +
      `export const MORE: Tool[] = [{ name: "more", run: (ctx) => ctx.root }];\n`,
  );
  const missing = tcmIn(dir, "check", "sub/m.ts");
  assert.notEqual(missing.status, 0);
  assert.match(
    missing.out,
    new RegExp(
      `uses #${replaced[1]}, a version of lib\\.Tool nothing names now: import the current one by its name, from "#codebase/lib"`,
    ),
    missing.out,
  );
  assert.doesNotMatch(missing.out, /Cannot find module|implicitly has an 'any' type/, missing.out);
}

uses