clawhub/scripts/github/barnacle-auto-response.test.mjs

503 lines
14 KiB
JavaScript

/* @vitest-environment node */
import { describe, expect, it } from "vitest";
import {
candidateLabels,
classifyPullRequestCandidateLabels,
managedLabelSpecs,
runBarnacleAutoResponse,
} from "./barnacle-auto-response.mjs";
const blankTemplateBody = [
"## Summary",
"",
"Describe the problem and fix in 2-5 bullets:",
"",
"- Problem:",
"- Why it matters:",
"- What changed:",
"- What did NOT change:",
"",
"## Linked Issue/PR",
"",
"- Closes #",
"- Related #",
"",
"## Root Cause",
"",
"- Root cause:",
"",
"## Test Plan",
"",
"- Target test or file:",
].join("\n");
function pr(title, body = blankTemplateBody) {
return {
title,
body,
};
}
function file(filename, status = "modified") {
return {
filename,
status,
};
}
function barnacleContext(pullRequest, labels = [], options = {}) {
return {
repo: {
owner: "openclaw",
repo: "clawhub",
},
payload: {
action: options.action ?? "opened",
label: options.label,
sender: options.sender,
pull_request: {
number: 123,
title: "Cleanup docs",
body: blankTemplateBody,
author_association: "CONTRIBUTOR",
user: {
login: "contributor",
},
labels: labels.map((name) => ({ name })),
...pullRequest,
},
},
};
}
function barnacleIssueContext(issue, labels = [], options = {}) {
return {
repo: {
owner: "openclaw",
repo: "clawhub",
},
payload: {
action: options.action ?? "opened",
label: options.label,
sender: options.sender,
issue: {
number: 456,
title: "ClawHub issue",
body: "",
state: "open",
user: {
login: "reporter",
},
labels: labels.map((name) => ({ name })),
...issue,
},
},
};
}
function barnacleGithub(files) {
const calls = {
addLabels: [],
createComment: [],
createLabel: [],
lock: [],
removeLabel: [],
update: [],
updateLabel: [],
};
const github = {
paginate: async () => files,
rest: {
issues: {
addLabels: async (params) => {
calls.addLabels.push(params);
},
createComment: async (params) => {
calls.createComment.push(params);
},
createLabel: async (params) => {
calls.createLabel.push(params);
},
getLabel: async (params) => ({
data: {
color: managedLabelSpecs[params.name]?.color ?? "C5DEF5",
description: managedLabelSpecs[params.name]?.description ?? "",
},
}),
lock: async (params) => {
calls.lock.push(params);
},
removeLabel: async (params) => {
calls.removeLabel.push(params);
},
update: async (params) => {
calls.update.push(params);
},
updateLabel: async (params) => {
calls.updateLabel.push(params);
},
},
pulls: {
listFiles: async () => files,
},
repos: {
getCollaboratorPermissionLevel: async () => ({
data: {
role_name: "read",
},
}),
},
teams: {
getMembershipForUserInOrg: async () => {
const error = new Error("not found");
error.status = 404;
throw error;
},
},
},
};
return { calls, github };
}
describe("barnacle-auto-response", () => {
it("keeps Barnacle-owned labels documented for ClawHub", () => {
expect(managedLabelSpecs["r: support"].description).toContain("support requests");
expect(managedLabelSpecs["r: direct-skill-content"].description).toContain("published");
expect(managedLabelSpecs["r: third-party-skill-issue"].description).toContain("publisher");
expect(managedLabelSpecs["r: paid-skill"].description).toContain("paid skills");
for (const label of Object.values(candidateLabels)) {
expect(managedLabelSpecs[label]).toBeDefined();
expect(managedLabelSpecs[label].description).toMatch(/^Candidate:/);
}
});
it("labels low-signal docs without closing on bot-applied labels", async () => {
const labels = classifyPullRequestCandidateLabels(pr("Update README translation"), [
file("README.md"),
]);
expect(labels).toEqual(
expect.arrayContaining([candidateLabels.blankTemplate, candidateLabels.lowSignalDocs]),
);
const { calls, github } = barnacleGithub([file("README.md")]);
await runBarnacleAutoResponse({
github,
context: barnacleContext({}, [candidateLabels.lowSignalDocs], {
action: "labeled",
label: { name: candidateLabels.lowSignalDocs },
sender: { login: "github-actions[bot]", type: "Bot" },
}),
core: {
info: () => undefined,
},
});
expect(calls.update).toEqual([]);
expect(calls.createComment).toEqual([]);
});
it("labels direct skill content as a ClawHub publishing bypass", () => {
const labels = classifyPullRequestCandidateLabels(pr("Add useful browser skill"), [
file("skills/browser/SKILL.md", "added"),
]);
expect(labels).toContain(candidateLabels.directSkillContent);
});
it("does not treat repo-local developer skills as published skill content", () => {
const labels = classifyPullRequestCandidateLabels(pr("Update Convex helper skill"), [
file(".agents/skills/convex/SKILL.md", "modified"),
]);
expect(labels).not.toContain(candidateLabels.directSkillContent);
});
it("ignores unchecked PR template checklist entries when classifying refactors", () => {
const body = [
"## Summary",
"- Adds GET /api/v1/stars and a list-stars CLI command.",
"",
"## Type of change",
"- [x] New feature",
"- [ ] Refactor",
"- [ ] Documentation",
"",
"## Test Plan",
"- bun run test",
].join("\n");
const labels = classifyPullRequestCandidateLabels(pr("feat: add stars API", body), [
file("src/routes/api/v1/stars.ts"),
file("packages/clawhub/src/commands/list-stars.ts"),
]);
expect(labels).not.toContain(candidateLabels.refactorOnly);
});
it("uses linked issues as context and suppresses low-signal docs labels", () => {
const labels = classifyPullRequestCandidateLabels(
pr("Update docs", `${blankTemplateBody}\n\nRelated #123`),
[file("docs/skill-format.md")],
);
expect(labels).not.toContain(candidateLabels.lowSignalDocs);
});
it("warns on broad high-surface PRs instead of auto-closing them immediately", async () => {
const { calls, github } = barnacleGithub([
file("src/routes/index.tsx"),
file("convex/skills.ts"),
file("server/routes/og/skill.png.tsx"),
file("docs/skill-format.md"),
]);
await runBarnacleAutoResponse({
github,
context: barnacleContext({}),
core: {
info: () => undefined,
},
});
expect(calls.addLabels).toContainEqual(
expect.objectContaining({
labels: expect.arrayContaining([candidateLabels.dirtyCandidate]),
}),
);
expect(calls.createComment).toEqual([]);
expect(calls.update).toEqual([]);
});
it("does not add candidate labels to maintainer-authored PRs", async () => {
const { calls, github } = barnacleGithub([
file("src/routes/index.tsx"),
file("convex/skills.ts"),
file("server/routes/og/skill.png.tsx"),
file("docs/skill-format.md"),
]);
await runBarnacleAutoResponse({
github,
context: barnacleContext({
author_association: "OWNER",
user: {
login: "maintainer",
},
}),
core: {
info: () => undefined,
},
});
expect(calls.addLabels).toEqual([]);
});
it("actions manually applied candidate labels", async () => {
const { calls, github } = barnacleGithub([file("skills/browser/SKILL.md", "added")]);
await runBarnacleAutoResponse({
github,
context: barnacleContext({}, [candidateLabels.directSkillContent], {
action: "labeled",
label: { name: candidateLabels.directSkillContent },
sender: { login: "maintainer", type: "User" },
}),
core: {
info: () => undefined,
},
});
expect(calls.createComment).toContainEqual(
expect.objectContaining({
body: expect.stringContaining("published through ClawHub"),
}),
);
expect(calls.update).toContainEqual(expect.objectContaining({ state: "closed" }));
});
it("closes explicit support-labeled issues", async () => {
const { calls, github } = barnacleGithub([]);
await runBarnacleAutoResponse({
github,
context: {
repo: {
owner: "openclaw",
repo: "clawhub",
},
payload: {
action: "labeled",
issue: {
number: 456,
title: "Need help installing a skill",
body: "",
user: {
login: "user",
},
labels: [{ name: "r: support" }],
},
},
},
core: {
info: () => undefined,
},
});
expect(calls.createComment).toContainEqual(
expect.objectContaining({
issue_number: 456,
body: expect.stringContaining("community support server"),
}),
);
expect(calls.update).toContainEqual(
expect.objectContaining({
issue_number: 456,
state: "closed",
state_reason: "not_planned",
}),
);
});
it("closes third-party skill issues when maintainers apply the manual label", async () => {
const { calls, github } = barnacleGithub([]);
await runBarnacleAutoResponse({
github,
context: barnacleIssueContext(
{
title: "CamelCamelCamel Alerts Skill",
body: "This published skill still needs publisher-side cleanup.",
},
["r: third-party-skill-issue"],
{
action: "labeled",
label: { name: "r: third-party-skill-issue" },
sender: { login: "maintainer", type: "User" },
},
),
core: {
info: () => undefined,
},
});
expect(calls.createComment).toContainEqual(
expect.objectContaining({
issue_number: 456,
body: expect.stringContaining("third-party skill"),
}),
);
expect(calls.update).toContainEqual(
expect.objectContaining({
issue_number: 456,
state: "closed",
state_reason: "not_planned",
}),
);
});
it("routes direct skill submission issues through the publishing guidance rule", async () => {
const { calls, github } = barnacleGithub([]);
await runBarnacleAutoResponse({
github,
context: barnacleIssueContext({
title: "Awesome-Gaussian-Skills: Universal AI Agent Skill Pack",
body: [
"**Can this skill be added?**",
"",
"Project URL: https://github.com/example/awesome-gaussian-skills",
"",
"Please give it a try and star the repo.",
].join("\n"),
}),
core: {
info: () => undefined,
},
});
expect(calls.addLabels).toContainEqual(
expect.objectContaining({
labels: expect.arrayContaining(["r: direct-skill-content"]),
}),
);
expect(calls.createComment).toContainEqual(
expect.objectContaining({
body: expect.stringContaining("published through ClawHub"),
}),
);
expect(calls.update).toContainEqual(expect.objectContaining({ state: "closed" }));
});
it("labels rescan review requests for the dedicated rescan guidance workflow", async () => {
const { calls, github } = barnacleGithub([]);
await runBarnacleAutoResponse({
github,
context: barnacleIssueContext({
title: "[Skill Review Request] claw-calendar marked as suspicious.llm_suspicious",
body: [
"Please re-review this ClawHub skill after metadata fixes.",
"The skill is still flagged suspicious by the scanner.",
"Skill URL: https://clawhub.ai/openclaw/claw-calendar",
].join("\n"),
}),
core: {
info: () => undefined,
},
});
expect(calls.addLabels).toContainEqual(
expect.objectContaining({
labels: expect.arrayContaining(["r: rescan-guidance"]),
}),
);
expect(calls.createComment).toEqual([]);
expect(calls.update).toEqual([]);
});
it("labels obvious security reports even when security is not in the title", async () => {
const { calls, github } = barnacleGithub([]);
await runBarnacleAutoResponse({
github,
context: barnacleIssueContext({
title: "Skill package exposes plaintext credentials",
body: "A published skill appears to hardcode a live API token in the package.",
}),
core: {
info: () => undefined,
},
});
expect(calls.addLabels).toContainEqual(
expect.objectContaining({
labels: expect.arrayContaining(["security"]),
}),
);
expect(calls.createComment).toEqual([]);
expect(calls.update).toEqual([]);
});
it("keeps the existing title-based security issue label behavior", async () => {
const { calls, github } = barnacleGithub([]);
await runBarnacleAutoResponse({
github,
context: barnacleIssueContext({
title: "Security report for published skill",
body: "Please review.",
}),
core: {
info: () => undefined,
},
});
expect(calls.addLabels).toContainEqual(
expect.objectContaining({
labels: expect.arrayContaining(["security"]),
}),
);
});
});