test(FN-4777): complete Step 4 — add dedup tracking coverage

Fusion-Task-Id: FN-4777
Fusion-Task-Lineage: ed00f639-c928-4c14-be1f-2b43db6022c7
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 12:38:22 -07:00
committed by gsxdsm
parent f60dde01fe
commit 1ca873f64e
2 changed files with 201 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import {
buildIssueSearchQueries,
DEDUP_MATCH_THRESHOLD,
extractFileScopePaths,
extractSymptomKeywords,
scoreCandidateIssue,
} from "../github-tracking-dedup.js";
describe("extractFileScopePaths", () => {
it("parses file scope block, dedupes, strips trailing globs, and caps results", () => {
const prompt = `# Task\n\n- before\n\n## File Scope\n- \`packages/dashboard/src/routes/register-session-diff-routes.ts\`\n- packages/foo/*\n- packages/foo/*\n- packages/bar/**\n- packages/a\n- packages/b\n- packages/c\n- packages/d\n- packages/e\n- packages/f\n\n## Steps\n- after`;
expect(extractFileScopePaths({ description: "desc", prompt })).toEqual([
"packages/dashboard/src/routes/register-session-diff-routes.ts",
"packages/foo",
"packages/bar",
"packages/a",
"packages/b",
"packages/c",
"packages/d",
"packages/e",
]);
});
it("returns empty when file scope section is missing", () => {
expect(extractFileScopePaths({ description: "no section" })).toEqual([]);
});
});
describe("extractSymptomKeywords", () => {
it("extracts identifiers and error names, drops stopwords, respects max", () => {
const keywords = extractSymptomKeywords({
title: "Fix rebaseMergeTruncation and ParseIssueError",
description: "Touches `registerSessionDiffRoutes` and `fusion` and `tokenizeIssueBody`",
}, { max: 3 });
expect(keywords).toEqual(["registerSessionDiffRoutes", "tokenizeIssueBody", "ParseIssueError"]);
});
});
describe("buildIssueSearchQueries", () => {
it("returns keyword query and quoted path queries (1-3 total)", () => {
const queries = buildIssueSearchQueries(
["packages/dashboard/src/routes/register-session-diff-routes.ts", "packages/dashboard/src/github.ts"],
["rebaseMerge", "registerSessionDiffRoutes", "ParseIssueError"],
);
expect(queries).toEqual([
"rebaseMerge registerSessionDiffRoutes ParseIssueError",
'"packages/dashboard/src/routes/register-session-diff-routes.ts"',
'"packages/dashboard/src/github.ts"',
]);
});
});
describe("scoreCandidateIssue", () => {
it("scores path + keyword matches and threshold gate is usable", () => {
const scored = scoreCandidateIssue(
{
title: "Rebase merge truncation in registerSessionDiffRoutes",
body: "File: packages/dashboard/src/routes/register-session-diff-routes.ts throws ParseIssueError",
},
["packages/dashboard/src/routes/register-session-diff-routes.ts"],
["registerSessionDiffRoutes", "ParseIssueError", "nonMatchKeyword"],
);
expect(scored.matchedPaths).toEqual(["packages/dashboard/src/routes/register-session-diff-routes.ts"]);
expect(scored.matchedKeywords).toEqual(["registerSessionDiffRoutes", "ParseIssueError"]);
expect(scored.score).toBe(4);
expect(scored.score).toBeGreaterThanOrEqual(DEDUP_MATCH_THRESHOLD);
});
});

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Task } from "@fusion/core";
const createIssueMock = vi.fn();
const searchIssuesMock = vi.fn();
const resolveAuthMock = vi.fn();
const summarizeTitleMock = vi.fn();
@@ -16,6 +17,7 @@ vi.mock("@fusion/core", async () => {
vi.mock("../github.js", () => ({
GitHubClient: vi.fn().mockImplementation(() => ({
createIssue: createIssueMock,
searchIssues: searchIssuesMock,
})),
}));
@@ -157,6 +159,7 @@ describe("maybeCreateTrackingIssue", () => {
createdAt: "2026-01-01T00:00:00.000Z",
});
summarizeTitleMock.mockResolvedValue(null);
searchIssuesMock.mockResolvedValue([]);
});
it("returns tracking_disabled when not enabled", async () => {
@@ -247,6 +250,131 @@ describe("maybeCreateTrackingIssue", () => {
}));
});
it("links existing issue when dedup match is found", async () => {
const linkGithubIssue = vi.fn();
const recordActivity = vi.fn();
searchIssuesMock.mockResolvedValue([
{
number: 400,
title: "Diff route truncation in packages/dashboard/src/routes/register-session-diff-routes.ts",
body: "rebase-merge path drops output",
html_url: "https://github.com/o/r/issues/400",
state: "closed",
updatedAt: "2026-05-01T00:00:00.000Z",
},
]);
const result = await maybeCreateTrackingIssue(buildTask({
title: "Fix rebase-merge truncation in registerSessionDiffRoutes",
description: "## File Scope\n- packages/dashboard/src/routes/register-session-diff-routes.ts",
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue, recordActivity } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(result).toEqual({ created: false, reason: "existing_issue_found" });
expect(createIssueMock).not.toHaveBeenCalled();
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({
owner: "o",
repo: "r",
number: 400,
url: "https://github.com/o/r/issues/400",
}));
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({ type: "github-issue-dedup-matched", number: 400 }),
}));
});
it("falls through to create issue when dedup search has no qualifying match", async () => {
searchIssuesMock.mockResolvedValue([
{
number: 401,
title: "Unrelated docs cleanup",
body: "touches readme only",
html_url: "https://github.com/o/r/issues/401",
state: "closed",
updatedAt: "2026-05-01T00:00:00.000Z",
},
]);
const result = await maybeCreateTrackingIssue(buildTask({
title: "Fix rebase-merge truncation in registerSessionDiffRoutes",
description: "## File Scope\n- packages/dashboard/src/routes/register-session-diff-routes.ts",
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(searchIssuesMock).toHaveBeenCalled();
expect(createIssueMock).toHaveBeenCalledTimes(1);
expect(result).toMatchObject({ created: true });
});
it("skips dedup search when disabled in project settings", async () => {
await maybeCreateTrackingIssue(buildTask({
title: "Fix rebase-merge truncation in registerSessionDiffRoutes",
description: "## File Scope\n- packages/dashboard/src/routes/register-session-diff-routes.ts",
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
projectSettings: { githubTrackingDedupEnabled: false } as any,
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(searchIssuesMock).not.toHaveBeenCalled();
expect(createIssueMock).toHaveBeenCalledTimes(1);
});
it("continues issue creation when dedup search errors", async () => {
const logger = { warn: vi.fn(), info: vi.fn() };
searchIssuesMock.mockRejectedValue(new Error("search failed"));
const result = await maybeCreateTrackingIssue(buildTask({
title: "Fix rebase-merge truncation in registerSessionDiffRoutes",
description: "## File Scope\n- packages/dashboard/src/routes/register-session-diff-routes.ts",
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger,
});
expect(result).toMatchObject({ created: true });
expect(createIssueMock).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("duplicate-search failed"));
});
it("skips dedup search when there is no file-scope or keyword signal", async () => {
await maybeCreateTrackingIssue(buildTask({
title: "Fix bug",
description: "tiny",
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(searchIssuesMock).not.toHaveBeenCalled();
expect(createIssueMock).toHaveBeenCalledTimes(1);
});
it("creates issue for github_import tasks when tracking is explicitly enabled", async () => {
const linkGithubIssue = vi.fn();
const recordActivity = vi.fn();