mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
d73eb23b29
* fix(release): report seeded changelog provenance truthfully * fix(release): reject legacy provenance formatting * fix(release): enforce complete provenance records
433 lines
11 KiB
TypeScript
433 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
contributionRecordFor,
|
|
ledgerChecks,
|
|
ledgerFor,
|
|
renderContributionRecordEntry,
|
|
} from "../../.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs";
|
|
|
|
const targetSha = "a".repeat(40);
|
|
|
|
function contributionLedger({
|
|
nodes,
|
|
seededPullRequests = [],
|
|
sourcePullRequests = [],
|
|
sourceReferences = [],
|
|
}: {
|
|
nodes: Map<number, Record<string, unknown>>;
|
|
seededPullRequests?: number[];
|
|
sourcePullRequests?: number[];
|
|
sourceReferences?: number[];
|
|
}) {
|
|
return ledgerFor(
|
|
"v2026.7.2-beta.7",
|
|
targetSha,
|
|
[...nodes.keys()],
|
|
nodes,
|
|
new Map(),
|
|
new Map(),
|
|
{ issuesByPullRequest: new Map() },
|
|
{
|
|
legacyIssues: new Map(),
|
|
pullRequests: new Map(
|
|
seededPullRequests.map((number) => [
|
|
number,
|
|
{ externalReferences: [], references: [], thanks: [] },
|
|
]),
|
|
),
|
|
},
|
|
new Set(sourcePullRequests),
|
|
sourceReferences,
|
|
[],
|
|
[],
|
|
new Set(),
|
|
[],
|
|
Date.parse("2026-08-05T00:00:00Z"),
|
|
) as ReturnType<typeof ledgerFor> & {
|
|
provenance: {
|
|
inRangePullRequests: number;
|
|
retainedSeedOnlyPullRequests: number;
|
|
uniquePullRequests: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
describe("renderContributionRecordEntry", () => {
|
|
it("keeps external and linked issue references without repeating PR title references", () => {
|
|
expect(
|
|
renderContributionRecordEntry({
|
|
number: 123,
|
|
title: "Fix local openclaw/openclaw#45 and openclaw/imsg#141",
|
|
linkedIssues: [{ number: 45 }, { number: 67 }],
|
|
thanks: ["alice", "bob"],
|
|
}),
|
|
).toBe("- **PR #123** Related #45, openclaw/imsg#141, #67. Thanks @alice and @bob.");
|
|
});
|
|
|
|
it("deduplicates resolved issues and retains seeded cross-repository references", () => {
|
|
expect(
|
|
renderContributionRecordEntry({
|
|
number: 124,
|
|
title: "Fix #45, #45, and OpenClaw/imsg#141",
|
|
externalReferences: ["openclaw/imsg#141"],
|
|
priorReferences: [67],
|
|
linkedIssues: [{ number: 45 }, { number: 67 }],
|
|
thanks: [],
|
|
}),
|
|
).toBe("- **PR #124** Related #45, OpenClaw/imsg#141, #67.");
|
|
});
|
|
|
|
it("renders every source PR even without issue references or credits", () => {
|
|
expect(
|
|
renderContributionRecordEntry({
|
|
number: 456,
|
|
title: "Internal cleanup",
|
|
linkedIssues: [],
|
|
thanks: [],
|
|
}),
|
|
).toBe("- **PR #456**");
|
|
});
|
|
|
|
it("retains references and credits when a compact record is seeded again", () => {
|
|
const line = "- **PR #125** Related #45, openclaw/imsg#141. Thanks @alice and @bob.";
|
|
const record = contributionRecordFor({
|
|
source: [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
line,
|
|
].join("\n"),
|
|
});
|
|
const seeded = record.pullRequests.get(125);
|
|
|
|
expect(seeded).toEqual({
|
|
externalReferences: ["openclaw/imsg#141"],
|
|
references: [45],
|
|
thanks: ["alice", "bob"],
|
|
});
|
|
expect(
|
|
renderContributionRecordEntry({
|
|
number: 125,
|
|
title: "Title changed after release",
|
|
priorReferences: seeded?.references,
|
|
externalReferences: seeded?.externalReferences,
|
|
linkedIssues: [{ number: 45 }],
|
|
thanks: seeded?.thanks ?? [],
|
|
}),
|
|
).toBe(line);
|
|
});
|
|
|
|
it("retains seeded credits when the production ledger is rebuilt", () => {
|
|
const priorRecord = contributionRecordFor({
|
|
source: [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
"- **PR #125** Thanks @alice and @bob.",
|
|
].join("\n"),
|
|
});
|
|
const nodes = new Map([
|
|
[
|
|
125,
|
|
{
|
|
__typename: "PullRequest",
|
|
author: { __typename: "User", login: "carol" },
|
|
closingIssuesReferences: { nodes: [] },
|
|
mergedAt: "2026-07-08T00:00:00Z",
|
|
title: "fix: keep release credits",
|
|
},
|
|
],
|
|
]);
|
|
|
|
const result = ledgerFor(
|
|
"v2026.6.11",
|
|
targetSha,
|
|
[125],
|
|
nodes,
|
|
new Map(),
|
|
new Map(),
|
|
{ issuesByPullRequest: new Map() },
|
|
priorRecord,
|
|
new Set([125]),
|
|
new Set(),
|
|
new Set(),
|
|
new Set(),
|
|
new Set(),
|
|
[],
|
|
Date.parse("2026-07-09T00:00:00Z"),
|
|
);
|
|
|
|
expect(result.ledger).toContain("- **PR #125** Thanks @carol and @alice and @bob.");
|
|
});
|
|
|
|
it("counts associated and PR-typed source refs before retained seed-only rows", () => {
|
|
const nodes = new Map(
|
|
[1, 2, 3].map((number) => [
|
|
number,
|
|
{
|
|
__typename: "PullRequest",
|
|
closingIssuesReferences: { nodes: [] },
|
|
mergedAt: "2026-08-04T00:00:00Z",
|
|
title: `fix: contribution ${number}`,
|
|
},
|
|
]),
|
|
);
|
|
const result = contributionLedger({
|
|
nodes,
|
|
seededPullRequests: [1, 3],
|
|
sourcePullRequests: [1],
|
|
sourceReferences: [2],
|
|
});
|
|
|
|
expect(result.provenance).toEqual({
|
|
inRangePullRequests: 2,
|
|
retainedSeedOnlyPullRequests: 1,
|
|
uniquePullRequests: 3,
|
|
});
|
|
expect(result.ledger).toContain("2 in-range PRs + 1 retained seed-only PR = 3 unique PRs.");
|
|
});
|
|
|
|
it("reports zero retained seed-only PRs when every row is in range", () => {
|
|
const nodes = new Map([
|
|
[
|
|
1,
|
|
{
|
|
__typename: "PullRequest",
|
|
closingIssuesReferences: { nodes: [] },
|
|
mergedAt: "2026-08-04T00:00:00Z",
|
|
title: "fix: in-range contribution",
|
|
},
|
|
],
|
|
]);
|
|
const result = contributionLedger({ nodes, sourcePullRequests: [1] });
|
|
|
|
expect(result.provenance).toMatchObject({
|
|
inRangePullRequests: 1,
|
|
retainedSeedOnlyPullRequests: 0,
|
|
uniquePullRequests: 1,
|
|
});
|
|
});
|
|
|
|
it("reports all rows as retained seed-only when the release range has no PRs", () => {
|
|
const nodes = new Map(
|
|
[1, 2].map((number) => [
|
|
number,
|
|
{
|
|
__typename: "PullRequest",
|
|
closingIssuesReferences: { nodes: [] },
|
|
mergedAt: "2026-08-04T00:00:00Z",
|
|
title: `fix: seeded contribution ${number}`,
|
|
},
|
|
]),
|
|
);
|
|
const result = contributionLedger({ nodes, seededPullRequests: [1, 2] });
|
|
|
|
expect(result.provenance).toMatchObject({
|
|
inRangePullRequests: 0,
|
|
retainedSeedOnlyPullRequests: 2,
|
|
uniquePullRequests: 2,
|
|
});
|
|
});
|
|
|
|
it("rejects a forged canonical range and seed partition", () => {
|
|
const source = [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Highlights",
|
|
"",
|
|
"- Highlight one.",
|
|
"- Highlight two.",
|
|
"- Highlight three.",
|
|
"- Highlight four.",
|
|
"- Highlight five.",
|
|
"",
|
|
"### Changes",
|
|
"",
|
|
"### Fixes",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
`This audited record covers the complete base..${targetSha} history: 0 in-range PRs + 1 retained seed-only PR = 1 unique PR.`,
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
"- **PR #456**",
|
|
].join("\n");
|
|
const entry = {
|
|
number: 456,
|
|
title: "fix: example",
|
|
editorialEligible: true,
|
|
priorReferences: [],
|
|
externalReferences: [],
|
|
linkedIssues: [],
|
|
thanks: [],
|
|
};
|
|
|
|
expect(
|
|
ledgerChecks(
|
|
{
|
|
source,
|
|
expectedProvenance: {
|
|
inRangePullRequests: 1,
|
|
retainedSeedOnlyPullRequests: 0,
|
|
uniquePullRequests: 1,
|
|
},
|
|
},
|
|
[entry],
|
|
new Map([[456, { __typename: "PullRequest" }]]),
|
|
[],
|
|
),
|
|
).toContain("contribution record provenance partition does not match generated inventory");
|
|
});
|
|
|
|
it("retains references from a verbose record when the source title changes", () => {
|
|
const record = contributionRecordFor({
|
|
source: [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
"- **PR #126** Fix #46 and openclaw/imsg#142. Related #68. Thanks @alice.",
|
|
].join("\n"),
|
|
});
|
|
const seeded = record.pullRequests.get(126);
|
|
|
|
expect(seeded).toEqual({
|
|
externalReferences: ["openclaw/imsg#142"],
|
|
references: [46, 68],
|
|
thanks: ["alice"],
|
|
});
|
|
});
|
|
|
|
it("requires complete resolved issue tokens rather than matching substrings", () => {
|
|
const source = [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Highlights",
|
|
"",
|
|
"- Highlight one.",
|
|
"- Highlight two.",
|
|
"- Highlight three.",
|
|
"- Highlight four.",
|
|
"- Highlight five.",
|
|
"",
|
|
"### Changes",
|
|
"",
|
|
"### Fixes",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
"- **PR #456** Related openclaw/imsg#141.",
|
|
].join("\n");
|
|
const entry = {
|
|
number: 456,
|
|
title: "Internal cleanup",
|
|
editorialEligible: false,
|
|
priorReferences: [],
|
|
externalReferences: [],
|
|
linkedIssues: [{ number: 45 }, { number: 141 }],
|
|
thanks: [],
|
|
};
|
|
|
|
expect(
|
|
ledgerChecks({ source }, [entry], new Map([[456, { __typename: "PullRequest" }]]), []),
|
|
).toEqual([
|
|
"missing #45 on contribution record for PR #456",
|
|
"missing #141 on contribution record for PR #456",
|
|
]);
|
|
});
|
|
|
|
it("does not require an out-of-range PR referenced by a PR title", () => {
|
|
const line = "- **PR #456**";
|
|
const source = [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Highlights",
|
|
"",
|
|
"- Highlight one.",
|
|
"- Highlight two.",
|
|
"- Highlight three.",
|
|
"- Highlight four.",
|
|
"- Highlight five.",
|
|
"",
|
|
"### Changes",
|
|
"",
|
|
"### Fixes",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
line,
|
|
].join("\n");
|
|
const entry = {
|
|
number: 456,
|
|
title: "docs: align backup rules (#123)",
|
|
editorialEligible: false,
|
|
priorReferences: [123],
|
|
externalReferences: [],
|
|
linkedIssues: [],
|
|
thanks: [],
|
|
};
|
|
|
|
expect(renderContributionRecordEntry(entry)).toBe(line);
|
|
expect(
|
|
ledgerChecks({ source }, [entry], new Map([[456, { __typename: "PullRequest" }]]), []),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("accepts case-only differences in cross-repository references", () => {
|
|
const line = "- **PR #127** Related OpenClaw/imsg#143.";
|
|
const source = [
|
|
"## 2026.7.1",
|
|
"",
|
|
"### Highlights",
|
|
"",
|
|
"- Highlight one.",
|
|
"- Highlight two.",
|
|
"- Highlight three.",
|
|
"- Highlight four.",
|
|
"- Highlight five.",
|
|
"",
|
|
"### Changes",
|
|
"",
|
|
"### Fixes",
|
|
"",
|
|
"### Complete contribution record",
|
|
"",
|
|
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
|
|
"",
|
|
"#### Pull requests",
|
|
"",
|
|
line,
|
|
].join("\n");
|
|
const entry = {
|
|
number: 127,
|
|
title: "Internal cleanup",
|
|
editorialEligible: false,
|
|
priorReferences: [],
|
|
externalReferences: ["openclaw/imsg#143"],
|
|
linkedIssues: [],
|
|
thanks: [],
|
|
};
|
|
|
|
expect(
|
|
ledgerChecks({ source }, [entry], new Map([[127, { __typename: "PullRequest" }]]), []),
|
|
).toEqual([]);
|
|
});
|
|
});
|