testLibraryUpgrades
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'/); }