Files
hive/scripts/auto-close-duplicates.test.ts
T
mishrapravin114 05dde7414f fix(workflow): prevent circular duplicate closure in auto-close script
- 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>
2026-01-29 00:22:23 +05:30

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);
});
});