mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-23 15:37:44 +00:00
* test: remove low-value tests (batch d397) Retain independent test and fixture cleanup while preserving main's test file inventory, CI metadata, and distinct authorization/lifecycle proof. Repair fixture callback types and retain a portable WebSocket mock contract. Retained campaign changes, with current-main conflict and review exclusions: af1067dcd12debe2947c34cd20d89a8600d25964 test(gateway): deslop y0054 tests cc8aaaddb712d5a31c0998408585abe80dd88289 test(scripts): deslop y0055 tests 3f124d71f12a0c9123f7d6db1faad13ccdc10715 test(gateway): deslop y0057 tests 6b486a8191a8bd5c77076e2f37018177fd3ed679 test(ui): deslop y0056 tests 407d44c63a987b8001c2d6706b30681c0697bfae test(infra): deslop y0058 tests b4339db99e4eda03669e32841d7e86fb214f8794 test(ai): deslop y0059 tests 72dee977a7a5ce88b481a3997d3cc0ed951b596b test(agents): deslop y0061 tests b64b50b13f41d9530c77d524f665e11f9843e818 test(codex): deslop y0062 tests b317b9a6f7f87ab5b0865ba80e3d50dac48b67bc test(scripts): deslop y0064 tests Plain changed-file formatting/lint and diff checks pass. Runtime/type/build proof is delegated to hosted CI under the operator's CI-only compute budget. * test(gateway): preserve async publication effect contract * test: withdraw UI cleanup from batch d397 Preserve main UI tests unchanged after an unproven UI unit timeout. The newer scheduled main UI shard passed, so no inherited-failure exception is claimed. Retain only the independently reviewed non-UI deslop changes.
399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { writeFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { splitChangelog } from "../../scripts/lib/release-changelog.mjs";
|
|
import {
|
|
GITHUB_RELEASE_BODY_MAX_BYTES,
|
|
GITHUB_RELEASE_BODY_MAX_CHARACTERS,
|
|
extractChangelogSection,
|
|
formatContributionRecordProvenance,
|
|
formatShippedBaselineExclusions,
|
|
parseContributionRecordProvenance,
|
|
parseShippedBaselineExclusions,
|
|
releaseNotesVersionForTag,
|
|
renderGithubReleaseNotes,
|
|
verifyGithubReleaseNotes,
|
|
} from "../../scripts/render-github-release-notes.mts";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const repository = "openclaw/openclaw";
|
|
const tag = "v2026.7.1-beta.3";
|
|
const version = "2026.7.1";
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
function changelogFor(record: string): string {
|
|
return [
|
|
"# Changelog",
|
|
`## ${version}`,
|
|
"### Highlights\n\n- A grouped user-facing highlight.",
|
|
"### Fixes\n\n- A grouped user-facing fix.",
|
|
`### Complete contribution record\n\n${record}`,
|
|
"## 2026.6.11\n\n- Previous release.\n",
|
|
].join("\n\n");
|
|
}
|
|
|
|
function renderNotes(
|
|
options: Pick<Parameters<typeof renderGithubReleaseNotes>[0], "changelog"> &
|
|
Partial<Parameters<typeof renderGithubReleaseNotes>[0]>,
|
|
) {
|
|
return renderGithubReleaseNotes({ version, tag, repository, ...options });
|
|
}
|
|
|
|
function releaseRepo() {
|
|
const rootDir = tempDirs.make("openclaw-release-render-");
|
|
const git = (...args: string[]) =>
|
|
execFileSync("git", args, { cwd: rootDir, encoding: "utf8" }).trim();
|
|
git("init", "-q");
|
|
git("config", "user.name", "Release Fixture");
|
|
git("config", "user.email", "release-fixture@openclaw.invalid");
|
|
git("config", "commit.gpgsign", "false");
|
|
const render = (ref: string, releaseTag = tag, extra: string[] = []) =>
|
|
execFileSync(
|
|
process.execPath,
|
|
[
|
|
resolve("scripts/render-github-release-notes.mts"),
|
|
"--root",
|
|
rootDir,
|
|
"--ref",
|
|
ref,
|
|
"--tag",
|
|
releaseTag,
|
|
"--repository",
|
|
repository,
|
|
...extra,
|
|
],
|
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
return { rootDir, git, render };
|
|
}
|
|
|
|
describe("GitHub release-note rendering", () => {
|
|
it("refuses docs-mirror source in the initial renderer", () => {
|
|
expect(() =>
|
|
renderNotes({
|
|
changelog: `## ${version}\n\n<!-- openclaw-docs-mirror-v1 {} -->\n\nPublished reader prose.\n`,
|
|
}),
|
|
).toThrow("docs-publication renderer");
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"renders pinned legacy and split sources through the CLI (compact=%s)",
|
|
(compact) => {
|
|
const { rootDir, git, render } = releaseRepo();
|
|
const changelog = changelogFor(
|
|
`- **PR #123** ${compact ? "record ".repeat(20_000) : "fix: example."}`,
|
|
);
|
|
writeFileSync(join(rootDir, "CHANGELOG.md"), changelog);
|
|
git("add", ".");
|
|
git("commit", "-qm", "legacy release");
|
|
const legacy = git("rev-parse", "HEAD");
|
|
splitChangelog({ rootDir });
|
|
git("add", ".");
|
|
git("commit", "-qm", "split release");
|
|
const split = git("rev-parse", "HEAD");
|
|
writeFileSync(
|
|
join(rootDir, `CHANGELOG/${version}.md`),
|
|
`## ${version}\n\nUncommitted drift.\n`,
|
|
);
|
|
const legacyBody = render(legacy);
|
|
const splitBody = render(split);
|
|
const bodyPath = join(rootDir, "release-body.md");
|
|
writeFileSync(bodyPath, splitBody);
|
|
const verify = () => render(split, tag, ["--verify-body", bodyPath]);
|
|
expect(verify()).toBe("");
|
|
writeFileSync(bodyPath, `${splitBody}\nUnapproved appended prose.\n`);
|
|
expect(verify).toThrow("Release body does not match canonical release notes.");
|
|
expect(legacyBody).not.toContain("Uncommitted drift");
|
|
expect(splitBody).not.toContain("Uncommitted drift");
|
|
if (compact) {
|
|
expect(legacyBody).toContain(`/blob/${tag}/CHANGELOG.md#complete-contribution-record`);
|
|
expect(splitBody).toContain(
|
|
`/blob/${tag}/CHANGELOG/records/${version}.md#complete-contribution-record`,
|
|
);
|
|
expect(splitBody).toBe(
|
|
legacyBody.replace("/CHANGELOG.md#", `/CHANGELOG/records/${version}.md#`),
|
|
);
|
|
} else {
|
|
expect(splitBody).toBe(legacyBody);
|
|
expect(splitBody).toBe(extractChangelogSection(changelog, version));
|
|
}
|
|
},
|
|
);
|
|
|
|
it("renders and verifies a pinned beta delta instead of cumulative stable notes", () => {
|
|
const { rootDir, git, render } = releaseRepo();
|
|
const delta = `## ${tag.slice(1)}\n\n### Fixes\n\n- New beta-only fix.`;
|
|
writeFileSync(
|
|
join(rootDir, "CHANGELOG.md"),
|
|
`${delta}\n\n## ${version}\n\n- Cumulative stable feature.\n`,
|
|
);
|
|
splitChangelog({ rootDir });
|
|
git("add", ".");
|
|
git("commit", "-qm", "beta delta");
|
|
const ref = git("rev-parse", "HEAD");
|
|
writeFileSync(join(rootDir, `CHANGELOG/${tag.slice(1)}.md`), `${delta}\nUncommitted drift.\n`);
|
|
expect(render(ref)).toBe(delta);
|
|
expect(render(ref, `v${version}`)).toBe(`## ${version}\n\n- Cumulative stable feature.`);
|
|
const bodyPath = join(rootDir, "body.md");
|
|
writeFileSync(bodyPath, delta);
|
|
expect(render(ref, tag, ["--version", tag.slice(1), "--verify-body", bodyPath])).toBe("");
|
|
});
|
|
|
|
it("round-trips canonical contribution provenance and accepts published legacy lines", () => {
|
|
const target = "a".repeat(40);
|
|
const canonical = {
|
|
base: "v2026.7.2-beta.7",
|
|
target,
|
|
inRangePullRequests: 1,
|
|
retainedSeedOnlyPullRequests: 0,
|
|
uniquePullRequests: 1,
|
|
};
|
|
const singular = formatContributionRecordProvenance(canonical);
|
|
const commaSeparated = formatContributionRecordProvenance({
|
|
...canonical,
|
|
inRangePullRequests: 1_234,
|
|
retainedSeedOnlyPullRequests: 56,
|
|
uniquePullRequests: 1_290,
|
|
});
|
|
|
|
expect(singular).toContain("1 in-range PR + 0 retained seed-only PRs = 1 unique PR.");
|
|
expect(commaSeparated).toContain(
|
|
"1,234 in-range PRs + 56 retained seed-only PRs = 1,290 unique PRs.",
|
|
);
|
|
expect(
|
|
parseContributionRecordProvenance(
|
|
[singular, "", "#### Pull requests", "", "- **PR #123** fix: canonical example."].join(
|
|
"\n",
|
|
),
|
|
),
|
|
).toEqual(canonical);
|
|
const legacy = parseContributionRecordProvenance(
|
|
[
|
|
`This audited record covers the complete v2026.7.2-beta.6..02d06caeb0febe7ec3c0df1454b85c38f3fb27d1 history: 1 merged PR. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.`,
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
"- **PR #123** fix: legacy example.",
|
|
].join("\n"),
|
|
);
|
|
expect(legacy).toMatchObject({ uniquePullRequests: 1 });
|
|
expect(() => formatContributionRecordProvenance(legacy!)).toThrow("requires split PR counts");
|
|
for (const [line, error] of [
|
|
[
|
|
commaSeparated.replace("= 1,290 unique PRs", "= 1,291 unique PRs"),
|
|
"provenance arithmetic is invalid",
|
|
],
|
|
[commaSeparated.replace("1,234", "1234"), "provenance is malformed"],
|
|
[singular.replace("1 in-range", "001 in-range"), "provenance is malformed"],
|
|
] as const) {
|
|
expect(() => parseContributionRecordProvenance(line)).toThrow(error);
|
|
}
|
|
expect(() => parseContributionRecordProvenance(singular)).toThrow(
|
|
"positive contribution record requires a Pull requests section",
|
|
);
|
|
});
|
|
|
|
it("prefixes extended-stable notes with immutable regular-stable context", () => {
|
|
const extendedVersion = "2026.8.35";
|
|
const extendedTag = `v${extendedVersion}`;
|
|
const regularStableVersion = "2026.9.5";
|
|
const changelog = changelogFor("- **PR #123** fix: example.").replaceAll(
|
|
version,
|
|
extendedVersion,
|
|
);
|
|
const rendered = renderNotes({
|
|
changelog,
|
|
version: extendedVersion,
|
|
tag: extendedTag,
|
|
regularStableVersion,
|
|
});
|
|
|
|
expect(
|
|
rendered.body.startsWith(
|
|
"This is a gateway-only `extended-stable` release, which is our current equivalent to LTS. " +
|
|
"This release is OpenClaw from the end of August 2026, plus critical security updates, " +
|
|
"reliability and performance fixes, and features like new model support. " +
|
|
"The latest version of OpenClaw at the time of this release is " +
|
|
"[2026.9.5](https://github.com/openclaw/openclaw/releases#release-v2026.9.5)\n\n" +
|
|
"## 2026.8.35",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
verifyGithubReleaseNotes({
|
|
body: rendered.body,
|
|
changelog,
|
|
version: extendedVersion,
|
|
tag: extendedTag,
|
|
repository,
|
|
regularStableVersion,
|
|
}).matches,
|
|
).toBe(true);
|
|
expect(() =>
|
|
renderNotes({
|
|
changelog,
|
|
version: extendedVersion,
|
|
tag: extendedTag,
|
|
}),
|
|
).toThrow("regular stable version must be a string");
|
|
});
|
|
|
|
it("uses the full form at exactly 125,000 bytes and compacts at 125,001", () => {
|
|
const seed = renderNotes({
|
|
changelog: changelogFor("x"),
|
|
});
|
|
const exactRecordLength =
|
|
GITHUB_RELEASE_BODY_MAX_BYTES - seed.size.bytes + Buffer.byteLength("x");
|
|
const exact = renderNotes({
|
|
changelog: changelogFor("x".repeat(exactRecordLength)),
|
|
});
|
|
const over = renderNotes({
|
|
changelog: changelogFor("x".repeat(exactRecordLength + 1)),
|
|
});
|
|
|
|
expect(exact.mode).toBe("full");
|
|
expect(exact.size).toEqual({
|
|
characters: GITHUB_RELEASE_BODY_MAX_CHARACTERS,
|
|
bytes: GITHUB_RELEASE_BODY_MAX_BYTES,
|
|
});
|
|
expect(over.mode).toBe("compact");
|
|
});
|
|
|
|
it("normalizes correction tags to the stable changelog section", () => {
|
|
expect(releaseNotesVersionForTag("v2026.7.1-2")).toBe("2026.7.1");
|
|
const rendered = renderNotes({
|
|
changelog: changelogFor("- **PR #123** fix: correction."),
|
|
tag: "v2026.7.1-2",
|
|
});
|
|
|
|
expect(rendered.body).toContain("## 2026.7.1");
|
|
});
|
|
|
|
it("round-trips canonical shipped baseline exclusions and rejects malformed metadata", () => {
|
|
const line = formatShippedBaselineExclusions([
|
|
{ ref: "v2026.6.11", count: 2, pullRequests: [108, 101] },
|
|
{ ref: "v2026.6.10-beta.2", count: 0, pullRequests: [] },
|
|
]);
|
|
|
|
expect(line).toBe(
|
|
"Shipped baseline exclusions: v2026.6.10-beta.2 (0 PRs); v2026.6.11 (2 PRs: #101, #108).",
|
|
);
|
|
expect(parseShippedBaselineExclusions(line)).toEqual([
|
|
{ ref: "v2026.6.10-beta.2", count: 0, pullRequests: [] },
|
|
{ ref: "v2026.6.11", count: 2, pullRequests: [101, 108] },
|
|
]);
|
|
expect(() =>
|
|
parseShippedBaselineExclusions("Shipped baseline exclusion: v2026.6.11 (8 PRs)."),
|
|
).toThrow("malformed shipped baseline exclusion");
|
|
expect(() =>
|
|
parseShippedBaselineExclusions("Shipped baseline exclusions: v2026.6.11 (2 PRs: #101)."),
|
|
).toThrow("invalid shipped baseline exclusion count");
|
|
});
|
|
|
|
it("rejects tag/version drift and legacy oversized ledger anchors", () => {
|
|
expect(() =>
|
|
renderNotes({
|
|
changelog: changelogFor("- **PR #123** fix: example."),
|
|
tag: "v2026.7.2-beta.1",
|
|
}),
|
|
).toThrow("requires CHANGELOG.md version 2026.7.2");
|
|
|
|
expect(() =>
|
|
renderNotes({
|
|
changelog: changelogFor(
|
|
`### Complete contribution ledger\n\n${"legacy ".repeat(20_000)}`,
|
|
).replace("### Complete contribution record\n\n", ""),
|
|
}),
|
|
).toThrow("cannot be compacted without a complete contribution record");
|
|
});
|
|
|
|
it("prefers an alpha tag's exact changelog heading over the Unreleased fallback", () => {
|
|
// Shipped alpha tags carry their own heading with no base section,
|
|
// matching the tagged CHANGELOG.md shape of v2026.6.20-alpha.1.
|
|
const changelog = [
|
|
"# Changelog",
|
|
"## 2026.7.1-alpha.2\n\n- Alpha-only fix.",
|
|
"## 2026.6.11\n\n- Previous release.\n",
|
|
].join("\n\n");
|
|
const rendered = renderNotes({
|
|
changelog,
|
|
tag: "v2026.7.1-alpha.2",
|
|
});
|
|
|
|
expect(rendered.body).toContain("## 2026.7.1-alpha.2");
|
|
expect(rendered.body).toContain("Alpha-only fix.");
|
|
expect(rendered.body).not.toContain("Previous release.");
|
|
});
|
|
|
|
it("permits the Unreleased fallback only for alpha tags", () => {
|
|
const changelog = changelogFor("- **PR #123** fix: example.").replace(
|
|
"## 2026.7.1",
|
|
"## Unreleased",
|
|
);
|
|
const rendered = renderNotes({
|
|
changelog,
|
|
tag: "v2026.7.1-alpha.1",
|
|
});
|
|
|
|
expect(rendered.body).toContain("## 2026.7.1");
|
|
expect(rendered.body).not.toContain("## Unreleased");
|
|
expect(() => renderNotes({ changelog })).toThrow("CHANGELOG.md does not contain ## 2026.7.1");
|
|
expect(() =>
|
|
renderNotes({
|
|
changelog,
|
|
version: "foo",
|
|
tag: "vfoo-alpha.1",
|
|
}),
|
|
).toThrow("invalid release tag");
|
|
});
|
|
|
|
it("compacts at the real contribution record instead of a fenced pseudo-heading", () => {
|
|
const changelog = changelogFor(`- **PR #123** ${"record-only-detail ".repeat(9_000)}`).replace(
|
|
"### Fixes",
|
|
["```md", "### Complete contribution record", "```", "", "### Fixes"].join("\n"),
|
|
);
|
|
const rendered = renderNotes({ changelog });
|
|
|
|
expect(rendered.mode).toBe("compact");
|
|
expect(rendered.body).toContain("```md\n### Complete contribution record\n```");
|
|
expect(rendered.body).toContain("- A grouped user-facing fix.");
|
|
expect(rendered.body).not.toContain("record-only-detail");
|
|
});
|
|
|
|
it("verifies the exact generated compact body and optional proof tail", () => {
|
|
const changelog = changelogFor(`- **PR #123** ${"z".repeat(130_000)}`);
|
|
const verification = "### Release verification\n\n- release SHA: `abc123`";
|
|
const rendered = renderNotes({
|
|
changelog,
|
|
verification,
|
|
});
|
|
|
|
expect(
|
|
verifyGithubReleaseNotes({
|
|
body: rendered.body,
|
|
changelog,
|
|
version,
|
|
tag,
|
|
repository,
|
|
}),
|
|
).toMatchObject({ matches: true, mode: "compact" });
|
|
expect(
|
|
verifyGithubReleaseNotes({
|
|
body: rendered.body.replace("tag-pinned", "mutable"),
|
|
changelog,
|
|
version,
|
|
tag,
|
|
repository,
|
|
}).matches,
|
|
).toBe(false);
|
|
expect(
|
|
verifyGithubReleaseNotes({
|
|
body: rendered.body,
|
|
changelog,
|
|
version,
|
|
tag: "v2026.7.1-beta.2",
|
|
repository,
|
|
}).matches,
|
|
).toBe(false);
|
|
});
|
|
});
|