// Release Candidate Checklist tests cover release candidate checklist script behavior. import { describe, expect, it, vi } from "vitest"; import { buildPublishCommand, candidateParallelsArgs, candidateParallelsShellCommand, githubApi, parseArgs, parseRunIdFromDispatchOutput, resolveArtifactName, validateFullManifest, validateWindowsSourceRelease, } from "../../scripts/release-candidate-checklist.mjs"; describe("release candidate checklist", () => { 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", ), ).toContain("'--target-tarball' '.artifacts/preflight/openclaw candidate.tgz'"); }); 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("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("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", "release/2026.5.14", "--full-release-run", "111", "--npm-preflight-run", "222", "--skip-dispatch", ]), workflowRef: "release/2026.5.14", }; expect(buildPublishCommand(options)).toContain("'full_release_validation_run_id=111'"); expect(buildPublishCommand(options)).toContain("'preflight_run_id=222'"); expect(buildPublishCommand(options)).toContain("'tag=v2026.5.14-beta.3'"); expect(buildPublishCommand(options)).toContain("'plugin_publish_scope=all-publishable'"); expect(buildPublishCommand(options)).not.toContain("windows_node_tag="); }); 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", "release/2026.5.14", ]), workflowRef: "release/2026.5.14", 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 () => ({ ok: true, json: async () => ({ 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 () => ({ ok: true, json: async () => ({ 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", "release/2026.5.14", "--full-release-run", "111", "--npm-preflight-run", "222", "--skip-dispatch", ]), workflowRef: "release/2026.5.14", 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("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 { ok: true, json: async () => ({ 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("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", ); }); });