Files
openclaw/test/scripts/prepare-github-release-notes.test.ts
2026-07-10 02:33:23 +01:00

183 lines
6.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
appendGitHubReleaseVerification,
GITHUB_RELEASE_VERIFICATION_RESERVE_CHARACTERS,
GITHUB_RELEASE_VERIFICATION_RESERVE_UTF8_BYTES,
MAX_GITHUB_RELEASE_NOTES_CHARACTERS,
MAX_GITHUB_RELEASE_NOTES_UTF8_BYTES,
MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS,
MAX_GITHUB_RELEASE_SOURCE_NOTES_UTF8_BYTES,
prepareGitHubReleaseNotes,
} from "../../scripts/prepare-github-release-notes.mjs";
describe("prepareGitHubReleaseNotes", () => {
it("preserves the stable-base heading for prerelease notes", () => {
const changelog = [
"## Unreleased",
"",
"Future work.",
"",
"## 2026.7.1",
"",
"Release work.",
"",
"## 2026.6.11",
"",
"Older work.",
].join("\n");
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toBe(
"## 2026.7.1\n\nRelease work.\n",
);
});
it("uses an exact alpha heading before the Unreleased fallback", () => {
const changelog = [
"## Unreleased",
"",
"Future nightly work.",
"",
"## 2026.7.2-alpha.3",
"",
"Nightly release work.",
"",
"## 2026.7.1",
"",
"Stable work.",
].join("\n");
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.2-alpha.3")).toBe(
"## 2026.7.2-alpha.3\n\nNightly release work.\n",
);
});
it("keeps the Unreleased fallback for alpha branches without an exact heading", () => {
const changelog = "## Unreleased\n\nNightly release work.\n\n## 2026.7.1\n\nStable work.\n";
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.2-alpha.3")).toBe(
"## Unreleased\n\nNightly release work.\n",
);
});
it("prefers an exact numeric correction heading", () => {
const changelog = [
"## 2026.7.1-2",
"",
"Correction-specific work.",
"",
"## 2026.7.1",
"",
"Base release work.",
].join("\n");
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-2")).toBe(
"## 2026.7.1-2\n\nCorrection-specific work.\n",
);
});
it("falls back to the base heading for numeric correction tags", () => {
const changelog = "## 2026.7.1\n\nBase release work.\n\n## 2026.6.11\n\nOlder work.\n";
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-2")).toBe(
"## 2026.7.1\n\nBase release work.\n",
);
});
it("requires the stable-base section for beta and stable releases", () => {
const changelog = "## Unreleased\n\nCandidate work.\n\n## 2026.6.11\n\nOlder work.\n";
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
"does not contain release notes for 2026.7.1",
);
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1")).toThrow(
"does not contain release notes for 2026.7.1",
);
});
it("rejects a heading without release-note content", () => {
const changelog = "## 2026.7.1\n\n## 2026.6.11\n\nOlder work.\n";
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
"does not contain release-note content",
);
});
it("accepts multibyte source sections below the character and byte budgets", () => {
const changelog = `## 2026.7.1\n\n${"é".repeat(59_000)}\n`;
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toHaveLength(59_014);
});
it("rejects multibyte source sections that exceed the UTF-8 byte budget", () => {
const changelog = `## 2026.7.1\n\n${"é".repeat(70_000)}\n`;
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
`complete source section exceeds the ${MAX_GITHUB_RELEASE_SOURCE_NOTES_UTF8_BYTES}-byte source safety budget`,
);
});
it("accepts a source body exactly at the proof-reserving limit", () => {
const prefix = "## 2026.7.1\n\n";
const content = "x".repeat(MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS - prefix.length - 1);
expect(prepareGitHubReleaseNotes(`${prefix}${content}\n`, "v2026.7.1-beta.3")).toHaveLength(
MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS,
);
});
it("rejects source sections that consume the required proof reserve", () => {
const prefix = "## 2026.7.1\n\n";
const content = "x".repeat(MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS - prefix.length);
const changelog = `${prefix}${content}\n`;
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
"complete source section exceeds the 120000-character source budget",
);
});
it("appends and replaces canonical release verification within the reserve", () => {
const notes = "## 2026.7.1\n\nRelease work.\n\n### Release verification\n\n- stale proof\n";
const proof = "### Release verification\n\n- current proof";
expect(appendGitHubReleaseVerification(notes, proof)).toBe(
"## 2026.7.1\n\nRelease work.\n\n### Release verification\n\n- current proof\n",
);
});
it("rejects proof that exceeds its reserved capacity", () => {
const proof = `### Release verification\n\n${"x".repeat(
GITHUB_RELEASE_VERIFICATION_RESERVE_CHARACTERS,
)}`;
expect(() => appendGitHubReleaseVerification("## 2026.7.1\n\nRelease work.\n", proof)).toThrow(
"exceeds the reserved 5000-character budget",
);
});
it("rejects multibyte proof that exceeds its reserved byte capacity", () => {
const proof = `### Release verification\n\n${"é".repeat(
GITHUB_RELEASE_VERIFICATION_RESERVE_UTF8_BYTES / 2,
)}`;
expect(() => appendGitHubReleaseVerification("## 2026.7.1\n\nRelease work.\n", proof)).toThrow(
"exceeds the reserved 5000-byte safety budget",
);
});
it("keeps the combined body under GitHub's hard limit", () => {
const prefix = "## 2026.7.1\n\n";
const content = "x".repeat(MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS - prefix.length - 1);
const notes = prepareGitHubReleaseNotes(`${prefix}${content}\n`, "v2026.7.1");
const proof = `### Release verification\n\n${"x".repeat(
GITHUB_RELEASE_VERIFICATION_RESERVE_CHARACTERS - 40,
)}`;
expect(appendGitHubReleaseVerification(notes, proof).length).toBeLessThanOrEqual(
MAX_GITHUB_RELEASE_NOTES_CHARACTERS,
);
expect(
Buffer.byteLength(appendGitHubReleaseVerification(notes, proof), "utf8"),
).toBeLessThanOrEqual(MAX_GITHUB_RELEASE_NOTES_UTF8_BYTES);
});
});