Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions actions/setup/js/issue_title_dedup.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ function parseDeduplicateByTitle(value) {
if (value === "true") {
return { enabled: true, maxDistance: 0 };
}
if (typeof value === "string" && /^\d+$/.test(value)) {
value = Number.parseInt(value, 10);
}
if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 && value <= MAX_DEDUPLICATE_BY_TITLE_DISTANCE) {
return { enabled: true, maxDistance: value };
const numeric = typeof value === "string" && /^\d+$/.test(value) ? Number.parseInt(value, 10) : value;
if (typeof numeric === "number" && Number.isFinite(numeric) && Number.isInteger(numeric) && numeric >= 0 && numeric <= MAX_DEDUPLICATE_BY_TITLE_DISTANCE) {
return { enabled: true, maxDistance: numeric };
}
throw new Error(`deduplicate-by-title must be a boolean, a boolean-like string, or a non-negative integer (0-${MAX_DEDUPLICATE_BY_TITLE_DISTANCE})`);
}
Expand Down Expand Up @@ -59,7 +57,7 @@ function normalizeTitleForDedup(title) {
* @returns {{ title: string, distance: number } | null}
*/
function findDuplicateByTitle(normalizedTitle, candidates, maxDistance) {
/** @type {any} */
/** @type {{ title: string, distance: number } | null} */
let bestMatch = null;

for (const candidate of candidates) {
Expand Down
131 changes: 131 additions & 0 deletions actions/setup/js/issue_title_dedup.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// @ts-check
import { describe, it, expect } from "vitest";
import { createRequire } from "module";

const req = createRequire(import.meta.url);
const { parseDeduplicateByTitle, normalizeTitleForDedup, findDuplicateByTitle } = req("./issue_title_dedup.cjs");

describe("parseDeduplicateByTitle", () => {
it("returns disabled for undefined", () => {
expect(parseDeduplicateByTitle(undefined)).toEqual({ enabled: false, maxDistance: 0 });
});

it("returns disabled for null", () => {
expect(parseDeduplicateByTitle(null)).toEqual({ enabled: false, maxDistance: 0 });
});

it("returns disabled for false", () => {
expect(parseDeduplicateByTitle(false)).toEqual({ enabled: false, maxDistance: 0 });
});

it("returns disabled for string 'false'", () => {
expect(parseDeduplicateByTitle("false")).toEqual({ enabled: false, maxDistance: 0 });
});

it("returns enabled with distance 0 for true", () => {
expect(parseDeduplicateByTitle(true)).toEqual({ enabled: true, maxDistance: 0 });
});

it("returns enabled with distance 0 for string 'true'", () => {
expect(parseDeduplicateByTitle("true")).toEqual({ enabled: true, maxDistance: 0 });
});

it("returns enabled with numeric distance for integer", () => {
expect(parseDeduplicateByTitle(5)).toEqual({ enabled: true, maxDistance: 5 });
});

it("returns enabled with numeric distance for string integer", () => {
expect(parseDeduplicateByTitle("10")).toEqual({ enabled: true, maxDistance: 10 });
});

it("returns enabled with distance 0 for integer 0", () => {
expect(parseDeduplicateByTitle(0)).toEqual({ enabled: true, maxDistance: 0 });
});

it("throws for out-of-range integer (above 100)", () => {
expect(() => parseDeduplicateByTitle(101)).toThrow("deduplicate-by-title");
});

it("throws for negative integer", () => {
expect(() => parseDeduplicateByTitle(-1)).toThrow("deduplicate-by-title");
});

it("throws for float", () => {
expect(() => parseDeduplicateByTitle(1.5)).toThrow("deduplicate-by-title");
});

it("throws for non-numeric string", () => {
expect(() => parseDeduplicateByTitle("abc")).toThrow("deduplicate-by-title");
});

it("accepts max distance 100", () => {
expect(parseDeduplicateByTitle(100)).toEqual({ enabled: true, maxDistance: 100 });
});
});

describe("normalizeTitleForDedup", () => {
it("lowercases title", () => {
expect(normalizeTitleForDedup("Hello World")).toBe("hello world");
});

it("collapses multiple spaces", () => {
expect(normalizeTitleForDedup("hello world")).toBe("hello world");
});

it("trims leading/trailing whitespace", () => {
expect(normalizeTitleForDedup(" hello ")).toBe("hello");
});

it("handles empty string", () => {
expect(normalizeTitleForDedup("")).toBe("");
});

it("handles mixed case and extra whitespace together", () => {
expect(normalizeTitleForDedup(" Fix BUG ")).toBe("fix bug");
});
});

describe("findDuplicateByTitle", () => {
it("returns null when candidates is empty", () => {
expect(findDuplicateByTitle("foo", [], 0)).toBeNull();
});

it("finds exact match with maxDistance 0", () => {
const result = findDuplicateByTitle("fix bug", [{ title: "Fix Bug" }], 0);
expect(result).not.toBeNull();
expect(result?.distance).toBe(0);
expect(result?.title).toBe("Fix Bug");
});

it("returns null when no match within maxDistance", () => {
const result = findDuplicateByTitle("fix bug", [{ title: "add feature" }], 2);
expect(result).toBeNull();
});

it("finds closest match within maxDistance", () => {
const candidates = [{ title: "Fix Bug in Module A" }, { title: "fix bug" }];
const result = findDuplicateByTitle("fix bug", candidates, 5);
expect(result?.title).toBe("fix bug");
expect(result?.distance).toBe(0);
});

it("uses normalizedTitle when provided", () => {
const candidates = [{ title: "Original Title", normalizedTitle: "fix bug" }];
const result = findDuplicateByTitle("fix bug", candidates, 0);
expect(result?.title).toBe("Original Title");
expect(result?.distance).toBe(0);
});

it("returns best match (lowest distance) among multiple candidates", () => {
const candidates = [{ title: "fix bugs" }, { title: "fix bug" }];
const result = findDuplicateByTitle("fix bug", candidates, 5);
expect(result?.title).toBe("fix bug");
expect(result?.distance).toBe(0);
});

it("returns null if best match exceeds maxDistance", () => {
const candidates = [{ title: "completely different title entirely" }];
const result = findDuplicateByTitle("fix bug", candidates, 2);
expect(result).toBeNull();
});
});
Loading