paperclip/scripts/release-registry-versions.t...

259 lines
7.9 KiB
JavaScript

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
const repoRoot = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
const scriptPath = join(repoRoot, "scripts", "release-registry-versions.mjs");
function writeExecutable(path, body) {
writeFileSync(path, body, { mode: 0o755 });
}
function makeFixture() {
const fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-release-registry-"));
const binDir = join(fixtureDir, "bin");
const callLog = join(fixtureDir, "calls.log");
mkdirSync(binDir);
writeFileSync(callLog, "");
writeExecutable(
join(binDir, "npm"),
`#!/usr/bin/env bash
set -euo pipefail
printf 'npm %s\\n' "$*" >> "$FAKE_CALL_LOG"
target="$2"
case "$target" in
"@paperclipai/present@"*)
printf '%s\\n' "\${target##*@}"
;;
"@paperclipai/absent@"*)
exit 1
;;
"@paperclipai/present")
echo '["1.0.0","2026.707.0","2026.707.1","2026.707.1-canary.4"]'
;;
*)
exit 1
;;
esac
`,
);
return { fixtureDir, binDir, callLog };
}
function runScript(args, { binDir, callLog }, extraEnv = {}) {
let status = 0;
let stdout = "";
let stderr = "";
try {
stdout = execFileSync("node", [scriptPath, ...args], {
encoding: "utf8",
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
FAKE_CALL_LOG: callLog,
...extraEnv,
},
stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) {
status = error.status ?? 1;
stdout = error.stdout ?? "";
stderr = error.stderr ?? "";
}
return { status, stdout, stderr, calls: readFileSync(callLog, "utf8") };
}
function runReleaseLibHelper(fnCall, { binDir, callLog }, extraEnv = {}) {
const script = `
set -euo pipefail
source "${repoRoot}/scripts/release-lib.sh"
${fnCall}
`;
let status = 0;
let output = "";
try {
output = execFileSync("bash", ["-c", script], {
encoding: "utf8",
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
FAKE_CALL_LOG: callLog,
REPO_ROOT: repoRoot,
...extraEnv,
},
stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) {
status = error.status ?? 1;
output = `${error.stdout ?? ""}${error.stderr ?? ""}`;
}
return { status, output, calls: readFileSync(callLog, "utf8") };
}
test("fetch prints a JSON version map and treats missing packages as empty", () => {
const fixture = makeFixture();
const result = runScript(["fetch", "@paperclipai/present", "@paperclipai/missing"], fixture);
assert.equal(result.status, 0);
const map = JSON.parse(result.stdout);
assert.deepEqual(map["@paperclipai/present"], [
"1.0.0",
"2026.707.0",
"2026.707.1",
"2026.707.1-canary.4",
]);
assert.deepEqual(map["@paperclipai/missing"], []);
assert.match(result.calls, /^npm view @paperclipai\/present versions --json$/m);
assert.match(result.calls, /^npm view @paperclipai\/missing versions --json$/m);
});
test("assert-absent succeeds when no package has the version", () => {
const fixture = makeFixture();
const result = runScript(
["assert-absent", "2026.707.2", "@paperclipai/absent", "@paperclipai/absent"],
fixture,
);
assert.equal(result.status, 0);
assert.match(result.calls, /^npm view @paperclipai\/absent@2026\.707\.2 version$/m);
});
test("assert-absent fails and names packages that already have the version", () => {
const fixture = makeFixture();
const result = runScript(
["assert-absent", "2026.707.2", "@paperclipai/present", "@paperclipai/absent"],
fixture,
);
assert.equal(result.status, 1);
assert.match(result.stderr, /npm version @paperclipai\/present@2026\.707\.2 already exists\./);
assert.doesNotMatch(result.stderr, /@paperclipai\/absent@/);
});
test("invalid concurrency fails instead of skipping registry checks", () => {
const fixture = makeFixture();
const result = runScript(["assert-absent", "2026.707.2", "@paperclipai/present"], fixture, {
RELEASE_REGISTRY_CONCURRENCY: "0",
});
assert.equal(result.status, 2);
assert.match(result.stderr, /RELEASE_REGISTRY_CONCURRENCY must be a positive integer\./);
assert.equal(result.calls, "");
});
test("next_stable_version reads RELEASE_PACKAGE_VERSIONS_FILE without calling npm", () => {
const fixture = makeFixture();
const versionsFile = join(fixture.fixtureDir, "versions.json");
writeFileSync(
versionsFile,
JSON.stringify({
"@paperclipai/a": ["2026.707.0", "2026.707.1", "2026.707.1-canary.4"],
"@paperclipai/b": [],
}),
);
const result = runReleaseLibHelper(
'next_stable_version 2026-07-07 "@paperclipai/a" "@paperclipai/b"',
fixture,
{ RELEASE_PACKAGE_VERSIONS_FILE: versionsFile },
);
assert.equal(result.status, 0);
assert.equal(result.output, "2026.707.2");
assert.doesNotMatch(result.calls, /npm view/);
});
test("next_canary_version reads RELEASE_PACKAGE_VERSIONS_FILE without calling npm", () => {
const fixture = makeFixture();
const versionsFile = join(fixture.fixtureDir, "versions.json");
writeFileSync(
versionsFile,
JSON.stringify({
"@paperclipai/a": ["2026.707.0", "2026.707.1", "2026.707.1-canary.4"],
}),
);
const result = runReleaseLibHelper('next_canary_version 2026.707.1 "@paperclipai/a"', fixture, {
RELEASE_PACKAGE_VERSIONS_FILE: versionsFile,
});
assert.equal(result.status, 0);
assert.equal(result.output, "2026.707.1-canary.5");
assert.doesNotMatch(result.calls, /npm view/);
});
test("next_stable_version falls back to npm view without a versions file", () => {
const fixture = makeFixture();
const result = runReleaseLibHelper('next_stable_version 2026-07-07 "@paperclipai/present"', fixture);
assert.equal(result.status, 0);
assert.equal(result.output, "2026.707.2");
assert.match(result.calls, /^npm view @paperclipai\/present versions --json$/m);
});
test("next_prerelease_version counts per channel so nightly numbering ignores canaries", () => {
const fixture = makeFixture();
const versionsFile = join(fixture.fixtureDir, "versions.json");
writeFileSync(
versionsFile,
JSON.stringify({
"@paperclipai/a": ["2026.707.1-canary.4", "2026.707.1-nightly.0", "2026.707.1-nightly.1"],
}),
);
const result = runReleaseLibHelper(
'next_prerelease_version nightly 2026.707.1 "@paperclipai/a"',
fixture,
{ RELEASE_PACKAGE_VERSIONS_FILE: versionsFile },
);
assert.equal(result.status, 0);
assert.equal(result.output, "2026.707.1-nightly.2");
assert.doesNotMatch(result.calls, /npm view/);
});
test("next_prerelease_version counts beta numbering independently of other channels", () => {
const fixture = makeFixture();
const versionsFile = join(fixture.fixtureDir, "versions.json");
writeFileSync(
versionsFile,
JSON.stringify({
"@paperclipai/a": ["2026.707.1-canary.4", "2026.707.1-nightly.3", "2026.707.1-beta.0"],
}),
);
const result = runReleaseLibHelper(
'next_prerelease_version beta 2026.707.1 "@paperclipai/a"',
fixture,
{ RELEASE_PACKAGE_VERSIONS_FILE: versionsFile },
);
assert.equal(result.status, 0);
assert.equal(result.output, "2026.707.1-beta.1");
assert.doesNotMatch(result.calls, /npm view/);
});
test("next_prerelease_version rejects unknown channels", () => {
const fixture = makeFixture();
const result = runReleaseLibHelper(
'next_prerelease_version weekly 2026.707.1 "@paperclipai/a"',
fixture,
);
assert.equal(result.status, 1);
assert.match(result.output, /unknown prerelease channel: weekly/);
});
test("prerelease_tag_name namespaces tags by channel", () => {
const fixture = makeFixture();
const result = runReleaseLibHelper("prerelease_tag_name nightly 2026.707.1-nightly.2", fixture);
assert.equal(result.status, 0);
assert.equal(result.output.trim(), "nightly/v2026.707.1-nightly.2");
});