05dde7414f
- Skip closing an issue as duplicate of another that is already closed (avoids circular closure when bot and human close in opposite order). - Skip when duplicate target is self (same issue number). - Extract testable helpers: isDupeComment, isDupeCommentOldEnough, authorDisagreedWithDupe, getLastDupeComment, decideAutoClose. - Add 23 unit tests (Bun) and run them in CI before auto-close step. - Add scripts/AUTO_CLOSE_DUPLICATES_CROSS_VERIFY.md for impact summary. Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
7.7 KiB
TypeScript
262 lines
7.7 KiB
TypeScript
/**
|
|
* Tests for auto-close-duplicates script: comment filter, 12h check,
|
|
* author reaction, extractDuplicateIssueNumber, and decideAutoClose
|
|
* (circular-dup and self-ref prevention).
|
|
*/
|
|
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
authorDisagreedWithDupe,
|
|
decideAutoClose,
|
|
extractDuplicateIssueNumber,
|
|
getLastDupeComment,
|
|
isDupeComment,
|
|
isDupeCommentOldEnough,
|
|
type GitHubComment,
|
|
type GitHubIssue,
|
|
type GitHubReaction,
|
|
} from "./auto-close-duplicates";
|
|
|
|
describe("extractDuplicateIssueNumber", () => {
|
|
test("extracts #123 format", () => {
|
|
expect(
|
|
extractDuplicateIssueNumber("Found a possible duplicate of #1275: ...")
|
|
).toBe(1275);
|
|
expect(extractDuplicateIssueNumber("Duplicate of #1")).toBe(1);
|
|
expect(extractDuplicateIssueNumber("See #1000")).toBe(1000);
|
|
});
|
|
|
|
test("extracts first #N when multiple present", () => {
|
|
expect(
|
|
extractDuplicateIssueNumber("Duplicate of #1000 and also #1275")
|
|
).toBe(1000);
|
|
});
|
|
|
|
test("extracts GitHub issue URL format", () => {
|
|
expect(
|
|
extractDuplicateIssueNumber(
|
|
"Duplicate of https://github.com/adenhq/hive/issues/42"
|
|
)
|
|
).toBe(42);
|
|
});
|
|
|
|
test("returns null when no issue number", () => {
|
|
expect(extractDuplicateIssueNumber("No number here")).toBe(null);
|
|
expect(extractDuplicateIssueNumber("")).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("isDupeComment", () => {
|
|
test("true when body has 'possible duplicate' and user is Bot", () => {
|
|
expect(
|
|
isDupeComment({
|
|
id: 1,
|
|
body: "Found a possible duplicate of #1000: same bug",
|
|
created_at: "",
|
|
user: { type: "Bot", id: 2 },
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isDupeComment({
|
|
id: 1,
|
|
body: "Possible duplicate of #1275",
|
|
created_at: "",
|
|
user: { type: "Bot", id: 2 },
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
test("false when body lacks 'possible duplicate'", () => {
|
|
expect(
|
|
isDupeComment({
|
|
id: 1,
|
|
body: "Not a duplicate",
|
|
created_at: "",
|
|
user: { type: "Bot", id: 2 },
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
test("false when user is not Bot", () => {
|
|
expect(
|
|
isDupeComment({
|
|
id: 1,
|
|
body: "Found a possible duplicate of #1000",
|
|
created_at: "",
|
|
user: { type: "User", id: 2 },
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isDupeCommentOldEnough", () => {
|
|
test("true when comment date is before twelveHoursAgo", () => {
|
|
const twelveHoursAgo = new Date("2025-01-28T12:00:00Z");
|
|
const oldComment = new Date("2025-01-28T00:00:00Z");
|
|
expect(isDupeCommentOldEnough(oldComment, twelveHoursAgo)).toBe(true);
|
|
});
|
|
|
|
test("true when comment date equals twelveHoursAgo", () => {
|
|
const twelveHoursAgo = new Date("2025-01-28T12:00:00Z");
|
|
expect(isDupeCommentOldEnough(twelveHoursAgo, twelveHoursAgo)).toBe(true);
|
|
});
|
|
|
|
test("false when comment is after twelveHoursAgo (too recent)", () => {
|
|
const twelveHoursAgo = new Date("2025-01-28T12:00:00Z");
|
|
const recentComment = new Date("2025-01-28T18:00:00Z");
|
|
expect(isDupeCommentOldEnough(recentComment, twelveHoursAgo)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("authorDisagreedWithDupe", () => {
|
|
test("true when issue author gave thumbs down", () => {
|
|
const issue = { number: 1275, title: "", state: "open", user: { id: 42 }, created_at: "" };
|
|
const reactions: GitHubReaction[] = [
|
|
{ user: { id: 42 }, content: "-1" },
|
|
];
|
|
expect(authorDisagreedWithDupe(reactions, issue)).toBe(true);
|
|
});
|
|
|
|
test("false when only other users reacted", () => {
|
|
const issue = { number: 1275, title: "", state: "open", user: { id: 42 }, created_at: "" };
|
|
const reactions: GitHubReaction[] = [
|
|
{ user: { id: 99 }, content: "-1" },
|
|
{ user: { id: 1 }, content: "+1" },
|
|
];
|
|
expect(authorDisagreedWithDupe(reactions, issue)).toBe(false);
|
|
});
|
|
|
|
test("false when author gave +1 or other reaction", () => {
|
|
const issue = { number: 1275, title: "", state: "open", user: { id: 42 }, created_at: "" };
|
|
expect(authorDisagreedWithDupe([{ user: { id: 42 }, content: "+1" }], issue)).toBe(false);
|
|
expect(authorDisagreedWithDupe([{ user: { id: 42 }, content: "eyes" }], issue)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getLastDupeComment", () => {
|
|
test("returns null when no dupe comments", () => {
|
|
expect(
|
|
getLastDupeComment([
|
|
{ id: 1, body: "Not a duplicate", created_at: "", user: { type: "User", id: 1 } },
|
|
])
|
|
).toBe(null);
|
|
});
|
|
|
|
test("returns the only dupe comment when one exists", () => {
|
|
const c: GitHubComment = {
|
|
id: 1,
|
|
body: "Found a possible duplicate of #1000",
|
|
created_at: "",
|
|
user: { type: "Bot", id: 2 },
|
|
};
|
|
expect(getLastDupeComment([c])).toBe(c);
|
|
});
|
|
|
|
test("returns the last dupe comment when multiple exist", () => {
|
|
const c1: GitHubComment = {
|
|
id: 1,
|
|
body: "Found a possible duplicate of #1000",
|
|
created_at: "",
|
|
user: { type: "Bot", id: 2 },
|
|
};
|
|
const c2: GitHubComment = {
|
|
id: 2,
|
|
body: "Found a possible duplicate of #1275",
|
|
created_at: "",
|
|
user: { type: "Bot", id: 2 },
|
|
};
|
|
const other: GitHubComment = {
|
|
id: 3,
|
|
body: "Some other comment",
|
|
created_at: "",
|
|
user: { type: "User", id: 3 },
|
|
};
|
|
expect(getLastDupeComment([other, c1, c2])).toBe(c2);
|
|
});
|
|
});
|
|
|
|
function issue(num: number, state = "open"): GitHubIssue {
|
|
return {
|
|
number: num,
|
|
title: `Issue ${num}`,
|
|
state,
|
|
user: { id: 1 },
|
|
created_at: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function comment(body: string): GitHubComment {
|
|
return {
|
|
id: 1,
|
|
body,
|
|
created_at: new Date().toISOString(),
|
|
user: { type: "Bot", id: 2 },
|
|
};
|
|
}
|
|
|
|
describe("decideAutoClose", () => {
|
|
test("returns null when comment has no extractable issue number", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Possible duplicate of something else"),
|
|
async () => ({ state: "open" })
|
|
);
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
test("returns null when duplicate target is self (same issue number)", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Found a possible duplicate of #1275: same issue"),
|
|
async () => ({ state: "open" })
|
|
);
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
test("returns null when target issue is closed (avoids circular closure)", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Found a possible duplicate of #1000"),
|
|
async (num) => (num === 1000 ? { state: "closed" } : { state: "open" })
|
|
);
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
test("returns null when getTargetIssue returns null", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Found a possible duplicate of #1000"),
|
|
async () => null
|
|
);
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
test("returns null when getTargetIssue throws", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Found a possible duplicate of #1000"),
|
|
async () => {
|
|
throw new Error("API error");
|
|
}
|
|
);
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
test("returns duplicateOf number when target is open (should close)", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Found a possible duplicate of #1000: same bug"),
|
|
async (num) => (num === 1000 ? { state: "open" } : { state: "closed" })
|
|
);
|
|
expect(result).toBe(1000);
|
|
});
|
|
|
|
test("returns null when target state is not exactly 'open' (e.g. uppercase)", async () => {
|
|
const result = await decideAutoClose(
|
|
issue(1275),
|
|
comment("Found a possible duplicate of #1000"),
|
|
async () => ({ state: "OPEN" } as { state: string })
|
|
);
|
|
expect(result).toBe(null);
|
|
});
|
|
});
|