Files
servers/scripts/prepare-python-release.test.mjs
cliffhallandClaude Opus 5.5 6a1d753914 fix: stamp on a license change; keep the Python release PR on v2/main and its body current
- A license file ships in both distributions, so a change to one now counts
  as a shipped change.
- prepare-python-release.yml refuses a dispatch from any ref but v2/main,
  since the dispatch ref becomes the PR's base.
- A same-day rerun rewrites the open PR's body to match the force-updated
  branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-10-01 09:45:14 -04:00

217 lines
6.8 KiB
JavaScript

// Tests for the Python CalVer stamping script (#4472). The pure helpers are
// driven with inline manifests; `main` runs against a throwaway tree with git
// and uv replaced by a seam, so the decision (stamp or skip) is covered without
// either tool, and once against a real git repository so the two git queries
// the decision rests on are exercised as written.
// Run via `npm run test:scripts`.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import {
TOOLS,
calver,
main,
readProject,
shipsInPackage,
stampVersion,
} from "./prepare-python-release.mjs";
const manifest = (name, version) =>
[
"[project]",
`name = "${name}"`,
`version = "${version}"`,
'description = "x"',
"",
"[tool.hatch.version]",
'version = "not-this-one"',
"",
].join("\n");
test("calver is the UTC date without zero padding", () => {
assert.equal(calver(new Date("2026-08-01T12:00:00Z")), "2026.8.1");
assert.equal(calver(new Date("2026-12-31T23:59:59Z")), "2026.12.31");
// 23:30 on the 1st in UTC-5 is already the 2nd in UTC.
assert.equal(calver(new Date("2026-08-01T23:30:00-05:00")), "2026.8.2");
});
test("shipsInPackage counts what a user installs, not the tests", () => {
for (const file of [
"src/mcp_server_git/server.py",
"src/mcp_server_git/py.typed",
"README.md",
"pyproject.toml",
"LICENSE",
"LICENSE.txt",
"licence",
])
assert.equal(shipsInPackage(file), true, file);
for (const file of [
"tests/test_server.py",
"test/time_server_test.py",
"uv.lock",
"Dockerfile",
"src/mcp_server_git/licenses.json",
".python-version",
])
assert.equal(shipsInPackage(file), false, file);
});
test("readProject reads the [project] table's name and version", () => {
assert.deepEqual(readProject(manifest("mcp-server-time", "0.6.2")), {
name: "mcp-server-time",
version: "0.6.2",
});
});
test("readProject fails loudly on a manifest it cannot read", () => {
assert.throws(
() => readProject('[tool.x]\nversion = "1"\n'),
/no \[project\]/,
);
assert.throws(
() => readProject('[project]\nname = "x"\ndynamic = ["version"]\n'),
/no `version = "…"` line/,
);
});
test("stampVersion changes only the [project] version line", () => {
const before = manifest("mcp-server-time", "0.6.2");
const after = stampVersion(before, "2026.8.1");
assert.equal(after, before.replace('"0.6.2"', '"2026.8.1"'));
assert.match(after, /version = "not-this-one"/);
});
test("stampVersion does not reach into a later table for a version", () => {
assert.throws(
() =>
stampVersion('[project]\nname = "x"\n\n[tool.y]\nversion = "1"\n', "2"),
/no `version = "…"` line/,
);
});
function withTree(packages, fn) {
const root = mkdtempSync(path.join(tmpdir(), "prepare-py-"));
try {
for (const [dir, text] of Object.entries(packages)) {
mkdirSync(path.join(root, "src", dir), { recursive: true });
writeFileSync(path.join(root, "src", dir, "pyproject.toml"), text);
}
return fn(root);
} finally {
rmSync(root, { recursive: true, force: true });
}
}
const versionOf = (root, dir) =>
readProject(
readFileSync(path.join(root, "src", dir, "pyproject.toml"), "utf8"),
).version;
/** Runs `main` with console output captured rather than printed. */
function runMain(t, options) {
const out = t.mock.method(console, "log", () => {});
t.mock.method(console, "error", () => {});
const code = main(options);
return { code, stdout: out.mock.calls.map((c) => c.arguments[0]) };
}
const today = new Date("2026-08-01T00:00:00Z");
test("main stamps a package with a shipped change and locks it", (t) => {
withTree(
{
changed: manifest("mcp-server-changed", "2026.7.4"),
"tests-only": manifest("mcp-server-tests-only", "2026.7.4"),
current: manifest("mcp-server-current", "2026.8.1"),
"not-python": "",
},
(root) => {
rmSync(path.join(root, "src", "not-python", "pyproject.toml"));
const locked = [];
const tools = {
lastVersionBump: () => "abc123",
changedSince: (dir) =>
path.basename(dir) === "changed"
? ["src/mcp_server_changed/server.py"]
: ["tests/test_server.py", "uv.lock"],
lock: (dir) => locked.push(path.basename(dir)),
};
const { code, stdout } = runMain(t, { root, today, tools });
assert.equal(code, 0);
assert.deepEqual(stdout, ["mcp-server-changed: 2026.7.4 -> 2026.8.1"]);
assert.deepEqual(locked, ["changed"]);
assert.equal(versionOf(root, "changed"), "2026.8.1");
assert.equal(versionOf(root, "tests-only"), "2026.7.4");
assert.equal(versionOf(root, "current"), "2026.8.1");
},
);
});
test("main stamps a package whose version was never bumped in history", (t) => {
withTree({ fresh: manifest("mcp-server-fresh", "0.1.0") }, (root) => {
const tools = {
lastVersionBump: () => "",
changedSince: () => assert.fail("nothing to diff against"),
lock: () => {},
};
const { stdout } = runMain(t, { root, today, tools });
assert.deepEqual(stdout, ["mcp-server-fresh: 0.1.0 -> 2026.8.1"]);
});
});
test("main fails when it finds no Python package at all", (t) => {
withTree({}, (root) => {
mkdirSync(path.join(root, "src"), { recursive: true });
assert.equal(runMain(t, { root, today }).code, 1);
});
});
test("the git queries find the last bump and what changed since", () => {
withTree({ pkg: manifest("mcp-server-pkg", "2026.7.4") }, (root) => {
const dir = path.join(root, "src", "pkg");
const git = (...args) => {
const res = spawnSync(
"git",
[
"-c",
"user.name=t",
"-c",
"user.email=t@example.com",
"-c",
"commit.gpgsign=false",
...args,
],
{ cwd: root, encoding: "utf8" },
);
assert.equal(res.status, 0, res.stderr);
return res.stdout.trim();
};
git("init", "-q");
git("add", "-A");
git("commit", "-q", "-m", "bump");
const bump = git("rev-parse", "HEAD");
assert.equal(TOOLS.lastVersionBump(dir), bump);
assert.deepEqual(TOOLS.changedSince(dir, bump), []);
writeFileSync(path.join(dir, "server.py"), "x = 1\n");
// A file outside the package must not be attributed to it.
writeFileSync(path.join(root, "src", "other.py"), "y = 1\n");
git("add", "-A");
git("commit", "-q", "-m", "change");
// The later commit did not touch the version line, so the bump is unmoved.
assert.equal(TOOLS.lastVersionBump(dir), bump);
assert.deepEqual(TOOLS.changedSince(dir, bump), ["server.py"]);
});
});