testLibraryUpgrades

tcmts.cli.test.e2e.libraries.testLibraryUpgrades · #lw557lnb · term

a library moves to another release as one update, its API's changes said first: code using it follows, what no longer typechecks is to do, and code using what the new release doesn't have keeps it from moving

import { codebase } from "#codebase/tcmts/cli/test/e2e/releases";
import assert from "node:assert/strict";

export const testLibraryUpgrades = () => {
  const alice = codebase("alice upgrades");
  alice.save(
    `// tcm: namespace geo\nexport const manifest = { name: "geo" };\n/** A square's area. */\nexport const area = (side: number) => side * side;\nexport const double = (side: number) => 2 * area(side);\n`,
    "add",
  );
  alice.tcm("release", "geo@1.0.0");
  const me = codebase("me upgrades");
  me.save(`// tcm: namespace app\nexport const greeting = "hi";\n`, "add");
  me.tcm("lib", "add", alice.dir, "geo");
  me.save(
    `// tcm: namespace app\nimport { area, double } from "#codebase/app/lib/geo";\nexport const total = () => area(2) + double(3);\n`,
    "add",
  );

  // a new implementation and an addition: said, and checked, before anything changes
  alice.save(
    `// tcm: namespace geo\n/** A square's area. */\nexport const area = (side: number) => side * side * 10;\n/** A cube. */\nexport const cube = (side: number) => side ** 3;\n`,
  );
  alice.tcm("release", "geo");
  const check = me.tcm("lib", "upgrade", "geo", "--check");
  assert.match(check, /app\.lib\.geo: geo@1\.0\.0 → geo@1\.1\.0: additions/);
  assert.match(check, /minor {2}cube added/);
  assert.match(check, /patch {2}area changed, with the same API/);
  assert.match(check, /nothing changed \(--check\)/);
  assert.match(me.tcm("lib"), /geo@1\.0\.0/);
  // then made, as one history entry: code using it follows
  assert.match(
    me.tcm("lib", "upgrade", "geo"),
    /upgraded app\.lib\.geo to geo@1\.1\.0 \(2 replaced, 1 added, 0 gone\)\n.*app\.total \(dependent\)/,
  );
  assert.match(
    me.tcm("history", "1"),
    /lib upgrade app\.lib\.geo\n(.*\n)*\s+app\.lib\.geo\.cube +\+/,
  );
  assert.equal(me.tcm("run", "app.total").trim(), "220");
  assert.match(me.tcm("lib"), /app\.lib\.geo {2}geo@1\.1\.0/);
  assert.doesNotMatch(me.tcm("lib"), /aren't the namespace/);
  assert.match(me.tcm("lib", "upgrade", "geo"), /is geo@1\.1\.0 already/);

  // a breaking change: what no longer typechecks is to do, fixed as any is
  alice.save(
    `// tcm: namespace geo\n/** A square's area. */\nexport const area = (s: { side: number }) => s.side * s.side;\nexport const double = (side: number) => 2 * area({ side });\n`,
  );
  alice.tcm("release", "geo");
  const breaking = me.tcm("lib", "upgrade", "geo");
  assert.match(breaking, /geo@1\.1\.0 → geo@2\.0\.0: breaking changes/);
  assert.match(breaking, /major {2}area accepts less for a parameter/);
  assert.match(breaking, /1 dependent definition\(s\) no longer typecheck/);
  assert.match(me.tcm("todo"), /app\.total: Argument of type 'number' is not assignable/);
  me.tcm("patch", "app.total", "--old", "area(2)", "--new", "area({ side: 2 })");
  assert.match(me.tcm("todo"), /nothing to do/);

  // what the new release doesn't have, and code uses, keeps it where it is
  alice.tcm("rm", "geo.double");
  alice.tcm("release", "geo");
  const refused = me.run("lib", "upgrade", "geo");
  assert.notEqual(refused.status, 0);
  assert.match(refused.out, /major {2}double removed/);
  assert.match(refused.out, /geo@3\.0\.0 doesn't have what app\.total uses/);
  assert.match(me.tcm("lib"), /geo@2\.0\.0/);
  // (a library is named as `tcm lib` lists it)
  assert.match(me.run("lib", "upgrade", "nope").out, /no library 'nope'/);
}

uses