Files
openclaw-openclaw/test/scripts/release-candidate-checklist.test.ts
Vincent Koc c47ceb0f3d improve(release): reuse exact-SHA validation evidence (#104162)
* perf(release): share changelog verification snapshots

* perf(release): reuse exact-SHA validation evidence

* feat(release): checkpoint candidate workflow state

* feat(release): watch CI transitions compactly

* fix(testbox): rotate stale reusable leases

* refactor(release): move CI verifier into scripts

* fix(release): preserve verifier executable mode

* fix(testbox): force noninteractive remote hydration

* perf(testbox): skip sync for proven clean heads

* fix(testbox): keep changed gates synchronized

* fix(testbox): isolate git state probes

* fix(testbox): isolate wrapper git commands

* fix(testbox): preserve git command contracts

* fix(release): validate reused SHA evidence

* fix(release): resume serialized plugin selections

* fix(testbox): sync source on every lease reuse

* fix(release): verify from trusted workflow checkout

* fix(release): gate evidence reuse on trusted lineage

* fix(release): support legacy verifier checkouts

* fix(testbox): export CI across shell snippets

* fix(release): revalidate reused evidence before publish

* fix(release): reject untrusted reuse before lookup

* fix(release): reuse SHA-pinned root evidence

* fix(ci): allow unreleased notes in QA packages

* fix(release): satisfy script lint contracts

* fix(release): handle Unicode workflow refs safely
2026-07-11 12:48:27 +08:00

1029 lines
32 KiB
TypeScript

// Release Candidate Checklist tests cover release candidate checklist script behavior.
import { readFileSync } from "node:fs";
import { describe, expect, it, vi } from "vitest";
import { parse } from "yaml";
import {
buildReleaseCandidateState,
buildPublishCommand,
candidateCumulativeShippedPullRequests,
candidateParallelsArgs,
candidateParallelsShellCommand,
githubApi,
parseArgs,
parseRunIdFromDispatchOutput,
reconcileReleaseCandidateState,
resolveArtifactName,
requireRunIdFromDispatchOutput,
run,
validateCandidateChangelogProvenance,
validateCandidateCheckout,
validateCandidateReleaseNotes,
validateFullManifest,
validatePreflightManifest,
validateWindowsSourceRelease,
} from "../../scripts/release-candidate-checklist.mjs";
function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(body), init);
}
async function withGithubApiTimeoutEnv<T>(value: string, fn: () => Promise<T>): Promise<T> {
const previous = process.env.OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS;
process.env.OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS = value;
try {
return await fn();
} finally {
if (previous === undefined) {
delete process.env.OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS;
} else {
process.env.OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS = previous;
}
}
}
describe("release candidate checklist", () => {
it("resumes exact workflow runs from matching release candidate state", () => {
const options = parseArgs(["--tag", "v2026.7.1-beta.4"]);
const expected = buildReleaseCandidateState(options, {
targetSha: "a".repeat(40),
toolingSha: "b".repeat(40),
});
const resumed = reconcileReleaseCandidateState(
JSON.parse(
JSON.stringify({
...expected,
phase: "waiting",
fullReleaseRunId: "111",
npmPreflightRunId: "222",
}),
),
expected,
);
expect(resumed).toMatchObject({
phase: "waiting",
fullReleaseRunId: "111",
npmPreflightRunId: "222",
});
});
it("rejects stale or conflicting release candidate state", () => {
const options = parseArgs(["--tag", "v2026.7.1-beta.4"]);
const expected = buildReleaseCandidateState(options, {
targetSha: "a".repeat(40),
toolingSha: "b".repeat(40),
});
expect(() =>
reconcileReleaseCandidateState({ ...expected, targetSha: "c".repeat(40) }, expected),
).toThrow("state mismatch for targetSha");
expect(() =>
reconcileReleaseCandidateState(
{ ...expected, fullReleaseRunId: "111" },
{ ...expected, fullReleaseRunId: "333" },
),
).toThrow("state mismatch for fullReleaseRunId");
});
it("captures changelogs larger than the Node spawnSync default buffer", () => {
const output = run(
process.execPath,
["-e", "process.stdout.write('x'.repeat(2 * 1024 * 1024))"],
{ capture: true },
);
expect(output).toHaveLength(2 * 1024 * 1024);
});
it("keeps the frozen release target separate from clean trusted workflow tooling", () => {
expect(
validateCandidateCheckout({
targetSha: "a".repeat(40),
targetHeadSha: "a".repeat(40),
targetTrackedStatus: "",
toolingSha: "b".repeat(40),
trustedToolingSha: "b".repeat(40),
toolingTrackedStatus: "",
workflowRef: "main",
}),
).toEqual({
status: "passed",
targetSha: "a".repeat(40),
toolingSha: "b".repeat(40),
workflowRef: "main",
});
expect(() =>
validateCandidateCheckout({
targetSha: "a".repeat(40),
targetHeadSha: "c".repeat(40),
targetTrackedStatus: "",
toolingSha: "b".repeat(40),
trustedToolingSha: "b".repeat(40),
toolingTrackedStatus: "",
workflowRef: "main",
}),
).toThrow("target worktree HEAD");
expect(() =>
validateCandidateCheckout({
targetSha: "a".repeat(40),
targetHeadSha: "a".repeat(40),
targetTrackedStatus: " M package.json",
toolingSha: "b".repeat(40),
trustedToolingSha: "b".repeat(40),
toolingTrackedStatus: "",
workflowRef: "main",
}),
).toThrow("clean tracked target worktree");
expect(() =>
validateCandidateCheckout({
targetSha: "a".repeat(40),
targetHeadSha: "a".repeat(40),
targetTrackedStatus: "",
toolingSha: "b".repeat(40),
trustedToolingSha: "c".repeat(40),
toolingTrackedStatus: "",
workflowRef: "main",
}),
).toThrow("does not match trusted main");
expect(() =>
validateCandidateCheckout({
targetSha: "a".repeat(40),
targetHeadSha: "a".repeat(40),
targetTrackedStatus: "",
toolingSha: "b".repeat(40),
trustedToolingSha: "b".repeat(40),
toolingTrackedStatus: " M scripts/release-candidate-checklist.mjs",
workflowRef: "main",
}),
).toThrow("clean tracked tooling checkout");
const source = readFileSync("scripts/release-candidate-checklist.mjs", "utf8");
expect(source).toContain('const TOOLING_ROOT = fileURLToPath(new URL("../", import.meta.url))');
expect(source).toContain("`+refs/heads/${workflowRef}:${remoteRef}`");
expect(source).toContain('"worktree", "add", "--detach", toolingRoot, trustedToolingSha');
expect(source).toContain(
'[join(toolingRoot, "scripts/release-candidate-checklist.mjs"), ...argv]',
);
expect(source).toContain("cwd: targetRoot");
expect(source).toContain('"worktree", "remove", "--force", toolingRoot');
expect(source).toContain(
"const trustedToolingSha = fetchTrustedWorkflowSha(options.workflowRef, TOOLING_ROOT)",
);
expect(source).toContain('targetHeadSha: gitRevParse("HEAD", targetRoot)');
expect(source).toContain("toolingTrackedStatus: gitTrackedStatus(TOOLING_ROOT)");
});
it("validates the exact tag changelog before dispatching the release matrix", () => {
const check = validateCandidateReleaseNotes({
changelog: [
"# Changelog",
"",
"## 2026.7.1",
"",
"### Highlights",
"",
"- User-facing notes.",
"",
"### Complete contribution record",
"",
`- **PR #123** ${"record ".repeat(20_000)}`,
].join("\n"),
repository: "openclaw/openclaw",
tag: "v2026.7.1-beta.3",
});
const source = readFileSync("scripts/release-candidate-checklist.mjs", "utf8");
const validationIndex = source.indexOf(
"const releaseNotesCheck = validateCandidateReleaseNotes",
);
const fullMatrixDispatchIndex = source.indexOf(
"if (!options.fullReleaseRunId && !options.skipDispatch)",
);
expect(check).toMatchObject({ status: "passed", mode: "compact" });
expect(validationIndex).toBeGreaterThanOrEqual(0);
expect(fullMatrixDispatchIndex).toBeGreaterThan(validationIndex);
expect(source).toContain('run("git", ["show", `${targetSha}:CHANGELOG.md`]');
});
it("rejects contribution-record provenance outside the release tag history", () => {
const base = "v2026.6.11";
const recordedTarget = "a".repeat(40);
const targetSha = "b".repeat(40);
const changelog = [
"# Changelog",
"",
"## 2026.7.1",
"",
"### Highlights",
"",
"- User-facing notes.",
"",
"### Complete contribution record",
"",
`This audited record covers the complete ${base}..${recordedTarget} history: 1 merged PR.`,
"",
"#### Pull requests",
"",
"- **PR #123** fix: example.",
].join("\n");
const reachable = vi.fn((ancestor: string, target: string) => {
return ancestor === base && target === recordedTarget;
});
expect(() =>
validateCandidateChangelogProvenance({
changelog,
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha,
isAncestor: reachable,
}),
).toThrow(`contribution record target ${recordedTarget} is not reachable`);
expect(reachable).toHaveBeenCalledWith(base, recordedTarget);
expect(reachable).toHaveBeenCalledWith(recordedTarget, targetSha);
});
it("rejects duplicate contribution record rows even when the declared count matches", () => {
const targetSha = "b".repeat(40);
const changelog = [
"# Changelog",
"",
"## 2026.7.1",
"",
"### Highlights",
"",
"- User-facing notes.",
"",
"### Complete contribution record",
"",
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
"",
"#### Pull requests",
"",
"- **PR #123** fix: example.",
"- **PR #123** fix: duplicate.",
].join("\n");
expect(() =>
validateCandidateChangelogProvenance({
changelog,
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha,
isAncestor: () => true,
}),
).toThrow("duplicate contribution record PR rows: #123");
});
it("uses numbered historical record rows and skips Unreleased baseline rows", () => {
const changelog = [
"# Changelog",
"",
"## Unreleased",
"",
"### Complete contribution record",
"",
"This audited record covers the complete base..HEAD history: 99 merged PRs.",
"",
"#### Pull requests",
"",
"- **PR #1** fix: not shipped.",
"",
"## 2026.6.11",
"",
"### Complete contribution record",
"",
"This audited record covers the complete base..HEAD history: 0 merged PRs.",
"",
"#### Pull requests",
"",
"- **PR #2** fix: shipped.",
].join("\n");
expect([...candidateCumulativeShippedPullRequests(changelog, "test baseline")]).toEqual([2]);
});
it("validates cumulative shipped baseline exclusion metadata", () => {
const base = "66e676d29b92d040716376a75aca32bad655cfac";
const recordedTarget = "a".repeat(40);
const changelog = [
"# Changelog",
"",
"## 2026.7.1",
"",
"### Highlights",
"",
"- User-facing notes.",
"",
"### Complete contribution record",
"",
`This audited record covers the complete ${base}..${recordedTarget} history: 1 merged PR.`,
"",
"Shipped baseline exclusions: v2026.6.11 (8 PRs: #101, #102, #103, #104, #105, #106, #107, #108).",
"",
"#### Pull requests",
"",
"- **PR #123** fix: example.",
].join("\n");
const shippedPullRequests = new Set([101, 102, 103, 104, 105, 106, 107, 108]);
const loadShippedBaseline = vi.fn(() => ({
ref: "v2026.6.11",
pullRequests: shippedPullRequests,
}));
expect(
validateCandidateChangelogProvenance({
changelog,
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha: recordedTarget,
isAncestor: () => true,
loadShippedBaseline,
}),
).toEqual({
status: "passed",
base,
target: recordedTarget,
shippedBaselines: [
{
ref: "v2026.6.11",
count: 8,
pullRequests: [101, 102, 103, 104, 105, 106, 107, 108],
},
],
});
expect(loadShippedBaseline).toHaveBeenCalledWith("v2026.6.11");
expect(() =>
validateCandidateChangelogProvenance({
changelog: changelog.replace("8 PRs:", "8 pull requests:"),
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha: recordedTarget,
isAncestor: () => true,
loadShippedBaseline,
}),
).toThrow("malformed shipped baseline exclusion");
expect(() =>
validateCandidateChangelogProvenance({
changelog,
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha: recordedTarget,
isAncestor: () => true,
loadShippedBaseline: () => ({
ref: "v2026.6.11",
pullRequests: new Set([...shippedPullRequests].slice(1)),
}),
}),
).toThrow("lists PRs absent from shipped baseline v2026.6.11: #101");
expect(() =>
validateCandidateChangelogProvenance({
changelog: changelog.replace(
"- **PR #123** fix: example.",
"- **PR #101** fix: already shipped.",
),
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha: recordedTarget,
isAncestor: () => true,
loadShippedBaseline,
}),
).toThrow("still contains shipped PRs from v2026.6.11: #101");
});
it("requires contribution records for beta candidates but permits alpha Unreleased fallback", () => {
const betaChangelog = [
"# Changelog",
"",
"## 2026.7.1",
"",
"### Highlights",
"",
"- User-facing notes.",
].join("\n");
expect(() =>
validateCandidateChangelogProvenance({
changelog: betaChangelog,
version: "2026.7.1",
tag: "v2026.7.1-beta.3",
targetSha: "a".repeat(40),
}),
).toThrow("missing ### Complete contribution record");
const alpha = validateCandidateChangelogProvenance({
changelog: betaChangelog.replace("## 2026.7.1", "## Unreleased"),
version: "2026.7.1",
tag: "v2026.7.1-alpha.1",
targetSha: "a".repeat(40),
});
expect(alpha).toEqual({
status: "skipped",
reason: "alpha release uses the explicit Unreleased fallback",
shippedBaselines: [],
});
});
it("infers validation profiles from candidate tags", () => {
expect(parseArgs(["--tag", "v2026.5.14-beta.3"]).releaseProfile).toBe("beta");
expect(parseArgs(["--tag", "v2026.5.14", "--windows-node-tag", "v0.6.3"]).releaseProfile).toBe(
"stable",
);
expect(
parseArgs([
"--tag",
"v2026.5.14",
"--windows-node-tag",
"v0.6.3",
"--release-profile",
"full",
]).releaseProfile,
).toBe("full");
});
it("runs Parallels against the exact prepared candidate tarball", () => {
expect(candidateParallelsArgs(".artifacts/preflight/openclaw.tgz")).toEqual([
"test:parallels:npm-update",
"--",
"--target-tarball",
".artifacts/preflight/openclaw.tgz",
"--json",
]);
expect(
candidateParallelsShellCommand(
".artifacts/preflight/openclaw candidate.tgz",
"/opt/homebrew/bin/gtimeout",
),
).toContain(
"set -a; source \"$HOME/.profile\" >/dev/null 2>&1 || true; set +a; exec '/opt/homebrew/bin/gtimeout' --foreground 150m pnpm",
);
expect(
candidateParallelsShellCommand(
".artifacts/preflight/openclaw candidate.tgz",
"/opt/homebrew/bin/gtimeout",
[".artifacts/preflight/openclaw-ai candidate.tgz"],
),
).toContain("'--target-tarball' '.artifacts/preflight/openclaw candidate.tgz'");
expect(
candidateParallelsArgs(".artifacts/preflight/openclaw.tgz", [
".artifacts/preflight/openclaw-ai.tgz",
]),
).toEqual([
"test:parallels:npm-update",
"--",
"--target-tarball",
".artifacts/preflight/openclaw.tgz",
"--dependency-tarball",
".artifacts/preflight/openclaw-ai.tgz",
"--json",
]);
});
it("requires exact dependency tarball metadata in npm preflight manifests", () => {
const manifest = {
releaseTag: "v2026.7.1-beta.3",
releaseSha: "candidate-sha",
npmDistTag: "beta",
tarballName: "openclaw-2026.7.1-beta.3.tgz",
tarballSha256: "root-sha",
dependencyTarballs: [
{
packageName: "@openclaw/ai",
packageVersion: "2026.7.1-beta.3",
tarballName: "openclaw-ai-2026.7.1-beta.3.tgz",
tarballSha256: "ai-sha",
},
],
};
const params = {
tag: "v2026.7.1-beta.3",
targetSha: "candidate-sha",
npmDistTag: "beta",
};
expect(() => validatePreflightManifest(manifest, params)).not.toThrow();
expect(() =>
validatePreflightManifest({ ...manifest, dependencyTarballs: undefined }, params),
).toThrow("missing dependency tarball metadata");
expect(() =>
validatePreflightManifest(
{
...manifest,
dependencyTarballs: [
{
...manifest.dependencyTarballs[0],
tarballName: "../openclaw-ai.tgz",
},
],
},
params,
),
).toThrow("invalid dependency tarball metadata");
});
it("requires run ids when dispatch is disabled", () => {
expect(() => parseArgs(["--tag", "v2026.5.14-beta.3", "--skip-dispatch"])).toThrow(
"--skip-dispatch requires --full-release-run and --npm-preflight-run",
);
});
it("uses trusted main for regular release workflow tooling", () => {
expect(parseArgs(["--tag", "v2026.5.14-beta.3"]).workflowRef).toBe("main");
expect(() =>
parseArgs(["--tag", "v2026.5.14-beta.3", "--workflow-ref", "release/2026.5.14"]),
).toThrow("--workflow-ref must be main");
});
it("preserves the matching Tideclaw alpha workflow source", () => {
const workflowRef = "tideclaw/alpha/2026-07-10-1200Z";
const options = parseArgs([
"--tag",
"v2026.7.1-alpha.3",
"--workflow-ref",
workflowRef,
"--npm-dist-tag",
"alpha",
]);
expect(options.workflowRef).toBe(workflowRef);
expect(buildPublishCommand(options)).toContain(`'--ref' '${workflowRef}'`);
expect(() => parseArgs(["--tag", "v2026.7.1-alpha.3"])).toThrow(
"--workflow-ref must be the matching tideclaw/alpha/",
);
});
it("rejects duplicate release candidate CLI options", () => {
const requiredArgs = ["--tag", "v2026.5.14-beta.3"];
const duplicateOption = (
flag: string,
firstValue: string,
secondValue: string,
prefix = requiredArgs,
): [string, string[]] => [flag, [...prefix, flag, firstValue, flag, secondValue]];
const duplicateFlag = (flag: string): [string, string[]] => [
flag,
[...requiredArgs, flag, flag],
];
const duplicateCases = [
duplicateOption("--tag", "v2026.5.14-beta.3", "v2026.5.14-beta.4", []),
duplicateOption("--workflow-ref", "release/a", "release/b"),
duplicateOption("--repo", "openclaw/openclaw", "fork/openclaw"),
duplicateOption("--full-release-run", "111", "222"),
duplicateOption("--npm-preflight-run", "111", "222"),
duplicateOption("--windows-node-tag", "v0.6.3", "v0.6.4"),
duplicateFlag("--skip-dispatch"),
duplicateFlag("--skip-local-generated-check"),
duplicateFlag("--skip-parallels"),
duplicateFlag("--skip-telegram"),
duplicateOption("--telegram-provider-mode", "mock-openai", "live-frontier"),
duplicateOption("--provider", "blacksmith-testbox", "crabbox"),
duplicateOption("--mode", "fresh", "upgrade"),
duplicateOption("--release-profile", "beta", "stable"),
duplicateOption("--npm-dist-tag", "beta", "latest"),
duplicateOption("--plugin-publish-scope", "all-publishable", "selected"),
duplicateOption("--plugins", "telegram", "discord"),
duplicateOption("--output-dir", ".artifacts/a", ".artifacts/b"),
] satisfies Array<[string, string[]]>;
for (const [flag, args] of duplicateCases) {
expect(() => parseArgs(args), flag).toThrow(`${flag} was provided more than once`);
}
});
it("requires stable validation evidence to include soak and blocking performance", () => {
const stableManifest = {
workflowName: "Full Release Validation",
targetSha: "candidate-sha",
releaseProfile: "stable",
rerunGroup: "all",
runReleaseSoak: "true",
controls: { performanceBlocking: true },
};
expect(() =>
validateFullManifest(stableManifest, {
targetSha: "candidate-sha",
releaseProfile: "stable",
}),
).not.toThrow();
expect(() =>
validateFullManifest(
{
...stableManifest,
runReleaseSoak: "false",
},
{
targetSha: "candidate-sha",
releaseProfile: "stable",
},
),
).toThrow("runReleaseSoak=true");
expect(() =>
validateFullManifest(
{
...stableManifest,
controls: { performanceBlocking: false },
},
{
targetSha: "candidate-sha",
releaseProfile: "stable",
},
),
).toThrow("blocking product performance");
});
it("binds SHA-pinned full validation evidence through its manifest", () => {
const source = readFileSync("scripts/release-candidate-checklist.mjs", "utf8");
expect(source).toContain("allowShaPinnedWorkflowRef: true");
expect(source).toContain(
"const fullValidationEvidence = validateFullReleaseValidationEvidence({",
);
expect(source).toContain("runStrictReleaseEvidenceValidation({ repository, runId })");
expect(source).toContain("refs/heads/main:refs/remotes/origin/main");
expect(source).toContain(
'fullValidationEvidence.source === "direct" && fullRun.headSha !== targetSha',
);
});
it("stops parsing options after the argument terminator", () => {
const options = parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
"--",
"--plugin-publish-scope",
"selected",
]);
expect(options.pluginPublishScope).toBe("all-publishable");
});
it("accepts package-manager argument separators before script options", () => {
const options = parseArgs([
"--",
"--tag",
"v2026.5.14-beta.3",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
"--skip-parallels",
]);
expect(options.tag).toBe("v2026.5.14-beta.3");
expect(options.skipParallels).toBe(true);
});
it("builds the gated release publish command from green evidence inputs", () => {
const options = {
...parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--workflow-ref",
"main",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
]),
workflowRef: "main",
fullReleaseRunAttempt: 2,
};
const command = buildPublishCommand(options);
expect(command).toContain("'full_release_validation_run_id=111'");
expect(command).toContain("'full_release_validation_run_attempt=2'");
expect(command).toContain("'preflight_run_id=222'");
expect(command).toContain("'tag=v2026.5.14-beta.3'");
expect(command).toContain("'plugin_publish_scope=all-publishable'");
expect(command).toContain("'--ref' 'main'");
expect(command).not.toContain("windows_node_tag=");
const workflow = parse(
readFileSync(".github/workflows/openclaw-release-publish.yml", "utf8"),
) as {
on: { workflow_dispatch: { inputs: Record<string, unknown> } };
};
const emittedInputs = [...command.matchAll(/'-f' '([^=']+)=/gu)].map((match) => match[1]);
for (const input of emittedInputs) {
expect(workflow.on.workflow_dispatch.inputs).toHaveProperty(input);
}
});
it("requires and carries an exact Windows Node tag for stable release candidates", () => {
expect(() => parseArgs(["--tag", "v2026.5.14"])).toThrow(
"stable release candidates require --windows-node-tag",
);
expect(() => parseArgs(["--tag", "v2026.5.14", "--windows-node-tag", "latest"])).toThrow(
"--windows-node-tag must be an explicit version tag, not latest",
);
const options = {
...parseArgs([
"--tag",
"v2026.5.14",
"--windows-node-tag",
"v0.6.3",
"--workflow-ref",
"main",
]),
workflowRef: "main",
windowsNodeInstallerDigests: JSON.stringify({
"OpenClawCompanion-Setup-x64.exe": `sha256:${"a".repeat(64)}`,
"OpenClawCompanion-Setup-arm64.exe": `sha256:${"b".repeat(64)}`,
}),
};
expect(buildPublishCommand(options)).toContain("'windows_node_tag=v0.6.3'");
expect(buildPublishCommand(options)).toContain(
`'windows_node_installer_digests={"OpenClawCompanion-Setup-x64.exe":"sha256:${"a".repeat(64)}","OpenClawCompanion-Setup-arm64.exe":"sha256:${"b".repeat(64)}"}'`,
);
});
it("validates the stable Windows source release and immutable installer digests", async () => {
const assets = [
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"a".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-arm64.exe",
digest: `sha256:${"b".repeat(64)}`,
},
];
const fetchImpl = vi.fn(async () => {
return jsonResponse({
tag_name: "v0.6.3",
draft: false,
prerelease: false,
html_url: "https://github.com/openclaw/openclaw-windows-node/releases/tag/v0.6.3",
assets,
});
});
await expect(
validateWindowsSourceRelease("v0.6.3", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).resolves.toEqual({
tag: "v0.6.3",
url: "https://github.com/openclaw/openclaw-windows-node/releases/tag/v0.6.3",
assets,
});
});
it.each([
[{ draft: true }, "must be published"],
[{ prerelease: true }, "must not be a prerelease"],
[{ tag_name: "v0.6.4" }, "Windows source release tag mismatch: expected v0.6.3, got v0.6.4"],
[
{ assets: [] },
"must contain exactly one required asset OpenClawCompanion-Setup-x64.exe; found 0",
],
[
{
assets: [
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"a".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"c".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-arm64.exe",
digest: `sha256:${"b".repeat(64)}`,
},
],
},
"must contain exactly one required asset OpenClawCompanion-Setup-x64.exe; found 2",
],
[
{
assets: [
{ name: "OpenClawCompanion-Setup-x64.exe", digest: "" },
{ name: "OpenClawCompanion-Setup-arm64.exe", digest: `sha256:${"b".repeat(64)}` },
],
},
"asset OpenClawCompanion-Setup-x64.exe is missing its SHA-256 digest",
],
])("rejects an invalid stable Windows source release", async (override, message) => {
const fetchImpl = vi.fn(async () => {
return jsonResponse({
tag_name: "v0.6.3",
draft: false,
prerelease: false,
html_url: "https://github.com/openclaw/openclaw-windows-node/releases/tag/v0.6.3",
assets: [
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"a".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-arm64.exe",
digest: `sha256:${"b".repeat(64)}`,
},
],
...override,
});
});
await expect(
validateWindowsSourceRelease("v0.6.3", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).rejects.toThrow(message);
});
it("carries the Telegram proof run into the publish command when available", () => {
const options = {
...parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--workflow-ref",
"main",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
]),
workflowRef: "main",
npmTelegramRunId: "333",
};
expect(buildPublishCommand(options)).toContain("'npm_telegram_run_id=333'");
});
it("requires explicit plugin names for selected plugin publish scope", () => {
expect(() =>
parseArgs(["--tag", "v2026.5.14-beta.3", "--plugin-publish-scope", "selected"]),
).toThrow("--plugin-publish-scope selected requires --plugins");
});
it("rejects selected plugin publish scope for release candidates", () => {
expect(() =>
parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--plugin-publish-scope",
"selected",
"--plugins",
"@openclaw/diffs",
]),
).toThrow("release candidates publish OpenClaw with --plugin-publish-scope all-publishable");
});
it("extracts a workflow run id from gh dispatch output", () => {
expect(
parseRunIdFromDispatchOutput(
"https://github.com/openclaw/openclaw/actions/runs/25922042055\n",
),
).toBe("25922042055");
});
it("fails closed when gh dispatch output does not include the run url", () => {
expect(() =>
requireRunIdFromDispatchOutput(
"Created workflow_dispatch event for full-release-validation.yml",
"full-release-validation.yml",
),
).toThrow("refusing to guess from recent workflow_dispatch runs");
});
it("falls back to a single compatible artifact from the same run", () => {
expect(
resolveArtifactName(
[{ name: "openclaw-npm-preflight-dba00", expired: false }],
"openclaw-npm-preflight-v2026.5.16-beta.2",
"openclaw-npm-preflight-",
),
).toBe("openclaw-npm-preflight-dba00");
});
it("bounds GitHub API requests with a timeout signal", async () => {
const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(init?.headers).toMatchObject({
Accept: "application/vnd.github+json",
Authorization: "Bearer test-token",
"X-GitHub-Api-Version": "2022-11-28",
});
return jsonResponse({ workflow_runs: [] });
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).resolves.toEqual({ workflow_runs: [] });
expect(fetchImpl).toHaveBeenCalledWith(
"https://api.github.com/repos/openclaw/openclaw/actions/runs",
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});
it("uses a positive integer GitHub API timeout env", async () => {
const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
return jsonResponse({ workflow_runs: [] });
});
await withGithubApiTimeoutEnv("2500", async () => {
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
token: "test-token",
}),
).resolves.toEqual({ workflow_runs: [] });
});
expect(fetchImpl).toHaveBeenCalledOnce();
});
it.each(["1e3", "10.5", "0", "soon"])(
"rejects malformed GitHub API timeout env %s",
async (raw) => {
const fetchImpl = vi.fn();
await withGithubApiTimeoutEnv(raw, async () => {
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
token: "test-token",
}),
).rejects.toThrow(
"OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS must be a positive integer",
);
});
expect(fetchImpl).not.toHaveBeenCalled();
},
);
it("bounds GitHub API error bodies", async () => {
const fetchImpl = vi.fn(async () => {
return new Response("x".repeat(65), {
headers: { "content-length": "65" },
status: 500,
});
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
maxBodyBytes: 64,
timeoutMs: 1234,
token: "test-token",
}),
).rejects.toThrow(
"GitHub API repos/openclaw/openclaw/actions/runs response body exceeded 64 bytes",
);
});
it("keeps GitHub API timeouts active while reading response bodies", async () => {
const fetchImpl = vi.fn(async () => {
return new Response(new ReadableStream<Uint8Array>({ start() {} }), {
status: 200,
});
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
timeoutMs: 25,
token: "test-token",
}),
).rejects.toThrow("GitHub API repos/openclaw/openclaw/actions/runs timed out after 25ms");
});
it("includes the GitHub API path when a request times out", async () => {
const fetchImpl = vi.fn(async () => {
throw new DOMException("request timed out", "TimeoutError");
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs/123/jobs", {
fetchImpl,
timeoutMs: 5,
token: "test-token",
}),
).rejects.toThrow(
"GitHub API repos/openclaw/openclaw/actions/runs/123/jobs timed out after 5ms",
);
});
});