testSemverChecks

tcmts.cli.test.e2e.releases.testSemverChecks · #cbdfsthd · term

semver checks: each kind of API change, the lint it trips and the version it calls for

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

export const testSemverChecks = () => {
  const a = codebase("lints");
  const before: Record<string, string> = {
    opt: `export const f = (x: number): number => x;`,
    req: `export const f = (x: number): number => x;`,
    wide: `export const f = (x: number): string => String(x);`,
    ret: `export const f = (x: number): number => x;`,
    union: `export type Shape = "circle" | "square";`,
    prop: `export interface Options { size: number }`,
    gone: `export interface Options { size: number; label: string }`,
    impl: `export const f = (x: number): number => x * 2;`,
    rename: `export const f = 1;`,
  };
  const after: Record<string, string> = {
    opt: `export const f = (x: number, y?: number): number => x + (y ?? 0);`,
    req: `export const f = (x: number, y: number): number => x + y;`,
    wide: `export const f = (x: number | string): string => String(x);`,
    ret: `export const f = (x: number): number | undefined => x || undefined;`,
    union: `export type Shape = "circle" | "square" | "triangle";`,
    prop: `export interface Options { size: number; label?: string }`,
    gone: `export interface Options { size: number }`,
    impl: `export const f = (x: number): number => x + x;`,
  };
  for (const [ns, text] of Object.entries(before)) a.save(`// tcm: namespace ${ns}\nexport const manifest = { name: "${ns}" };\n${text}\n`, "add");
  for (const ns of Object.keys(before)) a.tcm("release", `${ns}@1.0.0`);
  for (const [ns, text] of Object.entries(after)) a.save(`// tcm: namespace ${ns}\n${text}\n`);
  a.tcm("mv", "rename.f", "rename.g");
  const check = (ns: string) => a.tcm("release", "check", ns);
  assert.match(check("opt"), /1\.1\.0 .*: additions[\s\S]*minor {2}opt\.f takes a new optional parameter: y\n {9}function_optional_parameter_added/);
  assert.match(check("req"), /2\.0\.0 .*: breaking changes[\s\S]*major {2}req\.f takes a required parameter it didn't: y: number\n {9}function_required_parameter_added/);
  assert.match(check("wide"), /1\.1\.0[\s\S]*minor {2}wide\.f accepts more for a parameter: x: number → string \| number/);
  assert.match(check("ret"), /2\.0\.0[\s\S]*major {2}ret\.f can return more kinds of value: number → number \| undefined\n {9}function_return_type_widened: callers may get a value they don't handle/);
  assert.match(check("union"), /2\.0\.0[\s\S]*major {2}union\.Shape has a new case: "triangle"\n {9}type_union_member_added: code handling every case no longer does/);
  assert.match(check("prop"), /1\.1\.0[\s\S]*minor {2}prop\.Options has a new optional property: label/);
  assert.match(check("gone"), /2\.0\.0[\s\S]*major {2}gone\.Options lost a property: label/);
  assert.match(check("impl"), /1\.0\.1 .*: changes with the same API[\s\S]*patch {2}impl\.f changed, with the same API/);
  assert.match(check("rename"), /2\.0\.0[\s\S]*major {2}rename\.f renamed: rename\.g\n {9}definition_renamed: code that imports it by its old name no longer compiles/);
}

uses