openclaw/test/scripts/openclaw-npm-extended-stable-release.test.ts
Kevin Lin 9eeebf7cb1
feat(release): preflight all extended-stable npm packages (#101757)
* feat(release): preflight all extended-stable npm packages

* fix(release): install packed ai runtime in smoke

* fix(release): install sibling tarballs before core

* fix(release): verify unpublished tarballs locally

* fix(release): declare local preflight dependencies

* fix(release): pack workspace dependencies for smoke

* fix(release): accept pnpm pack artifact path

* fix(release): seed ai in sdk smoke

* fix(release): preserve prepublish verifier compatibility
2026-07-08 08:01:00 -07:00

481 lines
16 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { describe, expect, it, vi } from "vitest";
import {
capturePriorExtendedStableSelector,
extendedStableSelectorRepairCommand,
parseExtendedStableGuardBypass,
parsePriorExtendedStableSelector,
validateFullReleaseValidationManifest,
validateNpmPublishBoundary,
validateExtendedStableNpmReleaseRequest,
validateExtendedStableRunIdentity,
verifyExtendedStableRegistryReadback,
} from "../../scripts/openclaw-npm-extended-stable-release.mjs";
const sha = "a".repeat(40);
const branch = "extended-stable/2026.6.33";
describe("npm extended-stable publication boundary", () => {
it("parses only explicit boolean extended-stable guard values", () => {
expect(parseExtendedStableGuardBypass()).toBe(false);
expect(parseExtendedStableGuardBypass("")).toBe(false);
expect(parseExtendedStableGuardBypass("false")).toBe(false);
expect(parseExtendedStableGuardBypass("true")).toBe(true);
expect(() => parseExtendedStableGuardBypass("1")).toThrow(/must be "true" or "false"/u);
});
it.each([
["2026.6.11-alpha.1", "alpha"],
["2026.6.11-beta.1", "beta"],
["2026.6.11", "alpha"],
["2026.6.11", "beta"],
["2026.6.11", "latest"],
["2026.6.11-1", "alpha"],
["2026.6.11-1", "beta"],
["2026.6.11-1", "latest"],
["2026.6.33", "extended-stable"],
["2026.6.34", "extended-stable"],
])("accepts %s on %s", (version, distTag) => {
expect(() => validateNpmPublishBoundary(version, distTag)).not.toThrow();
});
it.each([
["2026.6.11", "extended-stable"],
["2026.6.11-alpha.1", "beta"],
["2026.6.11-alpha.1", "extended-stable"],
["2026.6.11-beta.1", "latest"],
["2026.6.11-beta.1", "extended-stable"],
["2026.6.33", "alpha"],
["2026.6.33", "beta"],
["2026.6.33", "latest"],
["2026.6.33-1", "alpha"],
["2026.6.33-1", "beta"],
["2026.6.33-1", "latest"],
["2026.6.33-1", "extended-stable"],
["2026.6.33", "stable"],
["2026.6.33", "nightly"],
])("rejects %s on %s", (version, distTag) => {
expect(() => validateNpmPublishBoundary(version, distTag)).toThrow();
});
it("prints exactly channel then publish tag from the dependency-free CLI", () => {
const result = spawnSync(
process.execPath,
["scripts/openclaw-npm-extended-stable-release.mjs", "publish-plan"],
{
encoding: "utf8",
env: {
...process.env,
PACKAGE_VERSION: "2026.6.33",
REQUESTED_PUBLISH_TAG: "extended-stable",
},
},
);
expect(result.status).toBe(0);
expect(result.stdout).toBe("stable\nextended-stable\n");
expect(result.stderr).toBe("");
});
it("allows a pre-.33 final extended-stable version only with the explicit bypass", () => {
expect(() =>
validateNpmPublishBoundary("2026.6.11", "extended-stable", {
bypassExtendedStableGuard: true,
}),
).not.toThrow();
expect(() => validateNpmPublishBoundary("2026.6.11", "extended-stable")).toThrow(
/patch 33 or above/u,
);
});
it.each(["alpha", "beta", "latest"])(
"rejects extended-stable guard bypass with the %s dist-tag",
(distTag) => {
expect(() =>
validateNpmPublishBoundary("2026.6.11", distTag, {
bypassExtendedStableGuard: true,
}),
).toThrow(/only be used with the extended-stable npm dist-tag/u);
},
);
it("preserves the unknown dist-tag rejection when bypass is requested", () => {
expect(() =>
validateNpmPublishBoundary("2026.6.11", "nightly", {
bypassExtendedStableGuard: true,
}),
).toThrow('Unsupported npm dist-tag "nightly"');
});
it.each([
["malformed bypass", "extended-stable", "sometimes", /must be "true" or "false"/u],
[
"non-extended-stable bypass",
"beta",
"true",
/only be used with the extended-stable npm dist-tag/u,
],
])("rejects %s in the dependency-free CLI", (_label, distTag, bypass, error) => {
const result = spawnSync(
process.execPath,
["scripts/openclaw-npm-extended-stable-release.mjs", "publish-plan"],
{
encoding: "utf8",
env: {
...process.env,
BYPASS_EXTENDED_STABLE_GUARD: bypass,
PACKAGE_VERSION: "2026.6.11",
REQUESTED_PUBLISH_TAG: distTag,
},
},
);
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(error);
});
});
describe("extended-stable npm release request", () => {
const valid = {
npmDistTag: "extended-stable",
releaseTag: "v2026.6.33",
npmWorkflowRef: "refs/heads/extended-stable/2026.6.33",
checkoutSha: sha,
tagSha: sha,
extendedStableBranchSha: sha,
packageVersion: "2026.6.33",
mainPackageVersion: "2026.7.2",
};
it("accepts .33, later patches, and any later protected-main calendar month", () => {
expect(validateExtendedStableNpmReleaseRequest(valid)).toEqual({
extendedStable: true,
releaseVersion: "2026.6.33",
extendedStableBranch: "extended-stable/2026.6.33",
});
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
releaseTag: "v2026.6.34",
packageVersion: "2026.6.34",
}),
).toMatchObject({ extendedStable: true, releaseVersion: "2026.6.34" });
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
releaseTag: "v2026.12.33",
npmWorkflowRef: "refs/heads/extended-stable/2026.12.33",
packageVersion: "2026.12.33",
mainPackageVersion: "2027.1.1",
}),
).toMatchObject({
extendedStable: true,
extendedStableBranch: "extended-stable/2026.12.33",
});
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion: "2026.8.1" }),
).not.toThrow();
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion: "2027.1.1" }),
).not.toThrow();
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion: "2028.12.32" }),
).not.toThrow();
});
it.each([
["patch below 33", { releaseTag: "v2026.6.32", packageVersion: "2026.6.32" }],
["beta prerelease", { releaseTag: "v2026.6.33-beta.1", packageVersion: "2026.6.33-beta.1" }],
["alpha prerelease", { releaseTag: "v2026.6.33-alpha.1", packageVersion: "2026.6.33-alpha.1" }],
["correction suffix", { releaseTag: "v2026.6.33-1", packageVersion: "2026.6.33-1" }],
["wrong branch", { npmWorkflowRef: "refs/heads/extended-stable/2026.6.34" }],
["checkout mismatch", { checkoutSha: "b".repeat(40) }],
["tag mismatch", { tagSha: "b".repeat(40) }],
["branch tip mismatch", { extendedStableBranchSha: "b".repeat(40) }],
["package mismatch", { packageVersion: "2026.6.34" }],
["main same month", { mainPackageVersion: "2026.6.1" }],
["main earlier month", { mainPackageVersion: "2026.5.32" }],
["main earlier year", { mainPackageVersion: "2025.12.32" }],
["main patch at monthly boundary", { mainPackageVersion: "2026.7.33" }],
])("rejects %s", (_label, changes) => {
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, ...changes })).toThrow();
});
it("preserves SHA-only regular preflight requests", () => {
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
npmDistTag: "beta",
releaseTag: sha,
}),
).toEqual({ extendedStable: false });
});
it("accepts a SHA-only extended-stable preflight from an arbitrary workflow ref", () => {
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: sha,
npmWorkflowRef: "refs/heads/main",
extendedStableBranchSha: "",
}),
).toEqual({
extendedStable: true,
releaseVersion: "2026.6.33",
extendedStableBranch: "extended-stable/2026.6.33",
});
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: "b".repeat(40),
npmWorkflowRef: "refs/heads/dev/preflight-candidate",
extendedStableBranchSha: "",
}),
).toThrow(/must match the checked-out commit/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: sha,
checkoutSha: "",
}),
).toThrow(/requires the full checked-out commit SHA/u);
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, releaseTag: sha })).toThrow(
/exact final vYYYY\.M\.P release tag/u,
);
});
it("bypasses patch and protected-main policy while preserving canonical branch identity", () => {
const bypassed = {
...valid,
bypassExtendedStableGuard: true,
releaseTag: "v2026.6.11",
packageVersion: "2026.6.11",
mainPackageVersion: "",
};
expect(validateExtendedStableNpmReleaseRequest(bypassed)).toEqual({
extendedStable: true,
releaseVersion: "2026.6.11",
extendedStableBranch: "extended-stable/2026.6.33",
bypassExtendedStableGuard: true,
});
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...bypassed, packageVersion: "2026.6.12" }),
).toThrow(/package version mismatch/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...bypassed,
npmWorkflowRef: "refs/heads/dev/extended-stable-publish-test",
}),
).toThrow(/workflow ref mismatch/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...bypassed,
extendedStableBranchSha: "b".repeat(40),
}),
).toThrow(/branch tip SHAs must match/u);
});
it("rejects bypass on a regular npm release request", () => {
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
bypassExtendedStableGuard: true,
npmDistTag: "beta",
releaseTag: sha,
}),
).toThrow(/only be used with the extended-stable npm dist-tag/u);
});
});
describe("extended-stable npm run identity", () => {
const validPreflight = {
workflowName: "OpenClaw NPM Release",
event: "workflow_dispatch",
conclusion: "success",
headBranch: branch,
headSha: sha,
};
it("accepts exact extended-stable preflight and validation runs", () => {
expect(
validateExtendedStableRunIdentity({
run: validPreflight,
kind: "preflight",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toBe(validPreflight);
expect(() =>
validateExtendedStableRunIdentity({
run: {
...validPreflight,
workflowName: "Full Release Validation",
status: "completed",
},
kind: "validation",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).not.toThrow();
});
it("accepts only a completed successful Plugin NPM Release run on the exact branch and SHA", () => {
const pluginRun = {
workflowName: "Plugin NPM Release",
displayTitle: `Plugin NPM Release [extended-stable] ${sha}`,
event: "workflow_dispatch",
status: "completed",
conclusion: "success",
headBranch: branch,
headSha: sha,
};
expect(
validateExtendedStableRunIdentity({
run: pluginRun,
kind: "plugin",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toBe(pluginRun);
for (const changes of [
{ workflowName: "OpenClaw NPM Release" },
{ displayTitle: `Plugin NPM Release [default] ${sha}` },
{ displayTitle: `Plugin NPM Release [extended-stable] ${"b".repeat(40)}` },
{ status: "in_progress" },
{ conclusion: "failure" },
{ headBranch: "main" },
{ headSha: "b".repeat(40) },
]) {
expect(() =>
validateExtendedStableRunIdentity({
run: { ...pluginRun, ...changes },
kind: "plugin",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toThrow();
}
});
it.each([
["wrong branch", { headBranch: "main" }],
["missing branch", { headBranch: undefined }],
["wrong SHA", { headSha: "b".repeat(40) }],
["missing SHA", { headSha: undefined }],
])("rejects %s", (_label, changes) => {
expect(() =>
validateExtendedStableRunIdentity({
run: { ...validPreflight, ...changes },
kind: "preflight",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toThrow(/headBranch=.*headSha=/u);
});
});
describe("Full Validation manifest identity", () => {
const valid = { workflowName: "Full Release Validation", workflowRef: branch, targetSha: sha };
it("accepts the exact branch and target SHA", () => {
expect(
validateFullReleaseValidationManifest({
manifest: valid,
npmDistTag: "extended-stable",
expectedWorkflowRef: branch,
expectedSha: sha,
}),
).toBe(valid);
});
it.each([
["wrong workflow ref", { workflowRef: "main" }],
["missing workflow ref", { workflowRef: undefined }],
["wrong target SHA", { targetSha: "b".repeat(40) }],
["missing target SHA", { targetSha: undefined }],
])("rejects %s", (_label, changes) => {
expect(() =>
validateFullReleaseValidationManifest({
manifest: { ...valid, ...changes },
npmDistTag: "extended-stable",
expectedWorkflowRef: branch,
expectedSha: sha,
}),
).toThrow();
});
});
describe("extended-stable selector capture", () => {
it("distinguishes bootstrap absence from an existing selector", () => {
expect(parsePriorExtendedStableSelector('{"latest":"2026.7.1"}')).toBe("absent");
expect(parsePriorExtendedStableSelector('{"extended-stable":"2026.6.33"}')).toBe("2026.6.33");
});
it.each(["not json", "null", "[]", '"2026.6.33"'])("rejects invalid result %s", (value) => {
expect(() => parsePriorExtendedStableSelector(value)).toThrow();
});
it("rejects command failure rather than treating it as bootstrap", () => {
expect(() =>
capturePriorExtendedStableSelector({ query: () => ({ status: 1, stdout: "" }) }),
).toThrow(/query failed/u);
});
});
describe("extended-stable registry readback", () => {
it("accepts eventual convergence and sleeps 10 seconds between attempts", async () => {
let attempt = 0;
const sleep = vi.fn(async () => {});
const result = await verifyExtendedStableRegistryReadback({
expectedVersion: "2026.6.33",
query: async (target: string) => {
if (target === "openclaw@2026.6.33") {
attempt += 1;
}
return { status: 0, stdout: attempt >= 2 ? "2026.6.33\n" : "2026.6.32\n" };
},
sleep,
});
expect(result).toEqual({
exactVersion: "2026.6.33",
extendedStableSelector: "2026.6.33",
attemptsUsed: 2,
});
expect(sleep).toHaveBeenCalledOnce();
expect(sleep).toHaveBeenCalledWith(10_000);
});
it("exhausts exactly 12 dual-query attempts on mismatch or failure", async () => {
const query = vi.fn(async () => ({ status: 1, stdout: "" }));
const sleep = vi.fn(async () => {});
await expect(
verifyExtendedStableRegistryReadback({ expectedVersion: "2026.6.33", query, sleep }),
).rejects.toThrow(/after 12 attempts/u);
expect(query).toHaveBeenCalledTimes(24);
expect(sleep).toHaveBeenCalledTimes(11);
expect(sleep.mock.calls.every(([delay]) => delay === 10_000)).toBe(true);
});
});
describe("extended-stable selector repair", () => {
it("points the selector at the expected published version", () => {
expect(extendedStableSelectorRepairCommand("v2026.6.33")).toBe(
"npm dist-tag add openclaw@2026.6.33 extended-stable",
);
});
it.each([undefined, "absent", "2026.6.33-beta.1", "2026.6.33-1"])(
"rejects an invalid expected version: %s",
(expectedVersion) => {
expect(() => extendedStableSelectorRepairCommand(expectedVersion)).toThrow(
"Extended-stable selector repair requires an exact final YYYY.M.P version.",
);
},
);
});