diff --git a/packages/core/src/__tests__/duplicate-intake.test.ts b/packages/core/src/__tests__/duplicate-intake.test.ts index 0b75d2e579..29d4f4a17a 100644 --- a/packages/core/src/__tests__/duplicate-intake.test.ts +++ b/packages/core/src/__tests__/duplicate-intake.test.ts @@ -180,6 +180,24 @@ describe("findSameAgentDuplicates", () => { expect(first).toBe(second); }); + it("never anchors on dates or UUIDs, and a date never outranks a file-path anchor", () => { + expect(computeCrossParentDiagnosticClaimId({ + description: "Fix nightly build failure observed on 2026-07-22 in the settings modal", + })).toBeNull(); + expect(computeCrossParentDiagnosticClaimId({ + description: "Fix failed run d3be2cd6-9221-4e1d-a27a-c5fbe04f9200 stuck in merge", + })).toBeNull(); + + const withDate = computeCrossParentDiagnosticClaimId({ + description: "Fix typecheck error in packages/core/src/store.ts since 2026-07-22", + }); + const withoutDate = computeCrossParentDiagnosticClaimId({ + description: "Fix typecheck error in packages/core/src/store.ts", + }); + expect(withDate).not.toBeNull(); + expect(withDate).toBe(withoutDate); + }); + it("does not claim ordinary work that merely names a file path", () => { expect(computeCrossParentDiagnosticClaimId({ description: "Add caching to packages/core/src/store.ts for faster board loads", diff --git a/packages/core/src/duplicate-intake.ts b/packages/core/src/duplicate-intake.ts index d7083132c8..dc6e0e7c14 100644 --- a/packages/core/src/duplicate-intake.ts +++ b/packages/core/src/duplicate-intake.ts @@ -121,8 +121,16 @@ function normalizeDiagnosticObject(value: string): string | null { return /[0-9@./_-]/.test(normalized) ? normalized : null; } +/* +FNXC:TaskCreationDeduplication 2026-07-22-15:20: +A distinctive slug must have at least one non-hex segment. Dates ("2026-07-22") and +UUIDs are hyphen-shaped but identify WHEN/WHICH RUN, not WHAT is broken: two unrelated +failure reports quoting the same date must not silently converge into one task, and a +date must never outrank a real file-path anchor in the sorted-first object pick. +*/ function isDistinctiveSlug(value: string): boolean { - return /^[a-z0-9]+(?:-[a-z0-9]+){2,}$/.test(value); + return /^[a-z0-9]+(?:-[a-z0-9]+){2,}$/.test(value) + && value.split("-").some((segment) => !/^[0-9a-f]+$/.test(segment)); } function normalizeDiagnosticPath(value: string): string { diff --git a/packages/dashboard/app/components/TaskTokenStatsPanel.tsx b/packages/dashboard/app/components/TaskTokenStatsPanel.tsx index b478316ed7..c6f5f73e20 100644 --- a/packages/dashboard/app/components/TaskTokenStatsPanel.tsx +++ b/packages/dashboard/app/components/TaskTokenStatsPanel.tsx @@ -306,13 +306,23 @@ export function TaskTokenStatsPanel({ tokenUsage, loading, task }: TaskTokenStat
{task.sourceAgentId}
) : null} + {/* + FNXC:TaskStatsProvenance 2026-07-22-15:20: + sourceMetadata is writable through the task-creation API, so issueUrl is untrusted: + only http(s) URLs may render as a link (blocks javascript:-scheme injection); + anything else renders as plain text. + */} {typeof task.sourceMetadata?.issueUrl === "string" ? (
{t("taskDetail.provenance.importedFrom", "Imported from")}
- - {task.sourceMetadata.issueUrl} - + {/^https?:\/\//i.test(task.sourceMetadata.issueUrl) ? ( + + {task.sourceMetadata.issueUrl} + + ) : ( + task.sourceMetadata.issueUrl + )}
) : null} diff --git a/packages/dashboard/app/components/__tests__/TaskTokenStatsPanel.test.tsx b/packages/dashboard/app/components/__tests__/TaskTokenStatsPanel.test.tsx index 84e38769da..8277ffb811 100644 --- a/packages/dashboard/app/components/__tests__/TaskTokenStatsPanel.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskTokenStatsPanel.test.tsx @@ -311,6 +311,16 @@ describe("TaskTokenStatsPanel", () => { .toHaveAttribute("href", "https://github.com/Runfusion/Fusion/issues/2356"); }); + it("renders a non-http issueUrl as plain text, never as a link", () => { + render(); + + expect(screen.getByText("javascript:alert(1)")).toBeInTheDocument(); + expect(screen.queryByRole("link", { name: "javascript:alert(1)" })).toBeNull(); + }); + it("omits the provenance section when the task has no recorded source", () => { render();