

















1-import { readFileSync } from "node:fs";
1+import { execFileSync } from "node:child_process";
2+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+import os from "node:os";
4+import path from "node:path";
25import { describe, expect, it } from "vitest";
3-import { findForbiddenChangelogThanks } from "../../scripts/check-changelog-attributions.mjs";
6+import {
7+findForbiddenChangelogThanks,
8+isForbiddenChangelogThanksHandle,
9+requiresExplicitHumanChangelogThanks,
10+} from "../../scripts/check-changelog-attributions.mjs";
11+12+const changelogScriptPath = path.join(process.cwd(), "scripts", "pr-lib", "changelog.sh");
13+14+function run(cwd: string, command: string, args: string[], env?: NodeJS.ProcessEnv): string {
15+return execFileSync(command, args, {
16+ cwd,
17+encoding: "utf8",
18+env: env ? { ...process.env, ...env } : process.env,
19+}).trim();
20+}
21+22+function createRepoWithPrChangelogDiff(entry: string): string {
23+const repo = mkdtempSync(path.join(os.tmpdir(), "openclaw-changelog-credit-"));
24+run(repo, "git", ["init", "-q", "--initial-branch=main"]);
25+run(repo, "git", ["config", "user.email", "test@example.com"]);
26+run(repo, "git", ["config", "user.name", "Test User"]);
27+writeFileSync(repo + "/CHANGELOG.md", "# Changelog\n\n## Unreleased\n\n### Fixes\n\n", "utf8");
28+run(repo, "git", ["add", "CHANGELOG.md"]);
29+run(repo, "git", ["commit", "-qm", "seed"]);
30+const baseSha = run(repo, "git", ["rev-parse", "HEAD"]);
31+// validate_changelog_entry_for_pr reads origin/main...HEAD, so the test
32+// fixture needs a real base ref plus a feature-branch changelog diff.
33+run(repo, "git", ["update-ref", "refs/remotes/origin/main", baseSha]);
34+run(repo, "git", ["checkout", "-qb", "feature"]);
35+writeFileSync(
36+repo + "/CHANGELOG.md",
37+`# Changelog\n\n## Unreleased\n\n### Fixes\n\n${entry}\n`,
38+"utf8",
39+);
40+run(repo, "git", ["add", "CHANGELOG.md"]);
41+run(repo, "git", ["commit", "-qm", "add changelog entry"]);
42+return repo;
43+}
44+45+function validateChangelogEntry(repo: string, contrib: string): string {
46+return run(
47+repo,
48+"bash",
49+[
50+"-c",
51+'source "$OPENCLAW_PR_CHANGELOG_SH"; validate_changelog_entry_for_pr 123 "$OPENCLAW_TEST_CONTRIB"',
52+],
53+{
54+OPENCLAW_PR_CHANGELOG_SH: changelogScriptPath,
55+OPENCLAW_TEST_CONTRIB: contrib,
56+},
57+);
58+}
459560describe("check-changelog-attributions", () => {
661it("flags forbidden bot, org, and maintainer thanks attributions", () => {
@@ -9,13 +64,19 @@ describe("check-changelog-attributions", () => {
964"- Org-owned fix. Thanks @openclaw.",
1065"- Maintainer-owned fix. Thanks @steipete.",
1166"- Mixed credit. Thanks @contributor and @OpenClaw.",
67+"- Bot repair. Thanks @clawsweeper[bot].",
68+"- Dependency bump. Thanks @dependabot[bot].",
69+"- App repair. Thanks @app/clawsweeper.",
1270].join("\n");
13711472expect(findForbiddenChangelogThanks(content)).toEqual([
1573{ line: 1, handle: "codex", text: "- Internal cleanup. Thanks @codex." },
1674{ line: 2, handle: "openclaw", text: "- Org-owned fix. Thanks @openclaw." },
1775{ line: 3, handle: "steipete", text: "- Maintainer-owned fix. Thanks @steipete." },
1876{ line: 4, handle: "openclaw", text: "- Mixed credit. Thanks @contributor and @OpenClaw." },
77+{ line: 5, handle: "clawsweeper[bot]", text: "- Bot repair. Thanks @clawsweeper[bot]." },
78+{ line: 6, handle: "dependabot[bot]", text: "- Dependency bump. Thanks @dependabot[bot]." },
79+{ line: 7, handle: "app/clawsweeper", text: "- App repair. Thanks @app/clawsweeper." },
1980]);
2081});
2182@@ -27,6 +88,82 @@ describe("check-changelog-attributions", () => {
2788).toStrictEqual([]);
2889});
299091+it("checks every thanked handle on a changelog line", () => {
92+expect(
93+findForbiddenChangelogThanks("- Mixed credit (#123). Thanks @openclaw and @alice."),
94+).toEqual([
95+{
96+line: 1,
97+handle: "openclaw",
98+text: "- Mixed credit (#123). Thanks @openclaw and @alice.",
99+},
100+]);
101+});
102+103+it("uses one attribution predicate for scanner and shell checks", () => {
104+expect(isForbiddenChangelogThanksHandle("")).toBe(true);
105+expect(isForbiddenChangelogThanksHandle("null")).toBe(true);
106+expect(isForbiddenChangelogThanksHandle("app/any-bot")).toBe(true);
107+expect(isForbiddenChangelogThanksHandle("codex")).toBe(true);
108+expect(isForbiddenChangelogThanksHandle("openclaw")).toBe(true);
109+expect(isForbiddenChangelogThanksHandle("steipete")).toBe(true);
110+expect(isForbiddenChangelogThanksHandle("app/clawsweeper")).toBe(true);
111+expect(isForbiddenChangelogThanksHandle("clawsweeper")).toBe(true);
112+expect(isForbiddenChangelogThanksHandle("clawsweeper[bot]")).toBe(true);
113+expect(isForbiddenChangelogThanksHandle("openclaw-clawsweeper")).toBe(true);
114+expect(isForbiddenChangelogThanksHandle("openclaw-clawsweeper[bot]")).toBe(true);
115+expect(isForbiddenChangelogThanksHandle("dependabot[bot]")).toBe(true);
116+expect(isForbiddenChangelogThanksHandle("dependabot[bot]", { strictBotHandle: true })).toBe(
117+true,
118+);
119+expect(isForbiddenChangelogThanksHandle("alice")).toBe(false);
120+expect(isForbiddenChangelogThanksHandle("human-clawsweeper-fan")).toBe(false);
121+expect(
122+isForbiddenChangelogThanksHandle("human-clawsweeper-fan", { strictBotHandle: true }),
123+).toBe(false);
124+125+expect(requiresExplicitHumanChangelogThanks("clawsweeper")).toBe(true);
126+expect(requiresExplicitHumanChangelogThanks("clawsweeper[bot]")).toBe(true);
127+expect(requiresExplicitHumanChangelogThanks("dependabot[bot]")).toBe(true);
128+expect(requiresExplicitHumanChangelogThanks("app/clawsweeper")).toBe(true);
129+expect(requiresExplicitHumanChangelogThanks("human-clawsweeper-fan")).toBe(false);
130+expect(requiresExplicitHumanChangelogThanks("steipete")).toBe(false);
131+expect(requiresExplicitHumanChangelogThanks("")).toBe(false);
132+});
133+134+it("requires explicit human thanks for bot PR changelog entries", () => {
135+const repo = createRepoWithPrChangelogDiff("- Bot repair (#123).");
136+try {
137+let output = "";
138+try {
139+validateChangelogEntry(repo, "dependabot[bot]");
140+} catch (error) {
141+output = String((error as { stdout?: unknown }).stdout ?? error);
142+}
143+expect(output).toContain("must include an explicit human Thanks @handle");
144+} finally {
145+rmSync(repo, { recursive: true, force: true });
146+}
147+});
148+149+it("accepts explicit human thanks for bot PR changelog entries", () => {
150+const repo = createRepoWithPrChangelogDiff("- Bot repair (#123). Thanks @alice.");
151+try {
152+expect(validateChangelogEntry(repo, "dependabot[bot]")).toContain("explicit thanks");
153+} finally {
154+rmSync(repo, { recursive: true, force: true });
155+}
156+});
157+158+it("keeps non-bot forbidden contributors on the no-thanks fallback", () => {
159+const repo = createRepoWithPrChangelogDiff("- Maintainer repair (#123).");
160+try {
161+expect(validateChangelogEntry(repo, "steipete")).toContain("skipping thanks check");
162+} finally {
163+rmSync(repo, { recursive: true, force: true });
164+}
165+});
166+30167it("keeps PR changelog gates on the same attribution policy", () => {
31168const commonLib = readFileSync("scripts/pr-lib/common.sh", "utf8");
32169const changelogLib = readFileSync("scripts/pr-lib/changelog.sh", "utf8");
@@ -36,10 +173,12 @@ describe("check-changelog-attributions", () => {
3617337174expect(commonLib).toContain("pr_contributor_allows_human_trailers");
38175expect(commonLib).toContain("resolve_contributor_coauthor_email");
39-expect(changelogLib).toContain("node scripts/check-changelog-attributions.mjs CHANGELOG.md");
176+expect(changelogLib).toContain("changelog_attribution_script");
177+expect(changelogLib).toContain("--is-forbidden-handle");
178+expect(changelogLib).toContain("--requires-explicit-human-thanks");
40179expect(changelogLib).toContain("changelog_thanks_required_for_contributor");
41-expect(changelogLib).toContain('"app/"*');
42-expect(changelogLib).toContain('"clawsweeper"');
180+expect(changelogLib).toContain("changelog_explicit_human_thanks_required_for_contributor");
181+expect(changelogLib).toContain("Choose the credited original contributor");
43182expect(gates).toContain("validate_changelog_attribution_policy");
44183expect(prepareCore).toContain("resolve_contributor_coauthor_email");
45184expect(mergeLib).toContain("pr_contributor_allows_human_trailers");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。