diff --git a/packages/loopover-miner/lib/opportunity-ranker.ts b/packages/loopover-miner/lib/opportunity-ranker.ts index 61dd2aaf8e..91f4a09864 100644 --- a/packages/loopover-miner/lib/opportunity-ranker.ts +++ b/packages/loopover-miner/lib/opportunity-ranker.ts @@ -54,6 +54,14 @@ function normalizeCandidate(candidate: Record) { .filter((label) => typeof label === "string" && label.trim()) .map((label) => label.trim()) : []; + // #9330: RankedCandidateIssue's `RawCandidateIssue &` type promises `assignees`, but this function + // dropped it. Mirror `labels`'s array-of-strings validation; no `.trim()` because the producer + // (opportunity-fanout.ts assigneeLogins) already emits clean GitHub logins filtered to length > 0. + const assignees = Array.isArray(candidate.assignees) + ? candidate.assignees.filter( + (assignee): assignee is string => typeof assignee === "string" && assignee.length > 0, + ) + : []; return { owner, repo, @@ -61,6 +69,7 @@ function normalizeCandidate(candidate: Record) { issueNumber, title, labels, + assignees, commentsCount: Number.isFinite(candidate.commentsCount) ? (candidate.commentsCount as number) : 0, createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : null, updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null, diff --git a/test/unit/miner-opportunity-ranker.test.ts b/test/unit/miner-opportunity-ranker.test.ts index 0182ea858d..f2da90a5ea 100644 --- a/test/unit/miner-opportunity-ranker.test.ts +++ b/test/unit/miner-opportunity-ranker.test.ts @@ -296,3 +296,28 @@ describe("rankCandidateIssues (#2302 follow-up)", () => { vi.useRealTimers(); }); }); + +describe("rankCandidateIssues carries assignees through normalizeCandidate (#9330)", () => { + it("preserves a populated assignees array in the ranked output", () => { + const ranked = rankCandidateIssues( + [rawIssue({ assignees: ["octocat", "hubot"] })], + { nowMs: NOW }, + ); + expect(ranked[0]?.assignees).toEqual(["octocat", "hubot"]); + }); + + it("normalizes a missing or malformed assignees field to [] without throwing", () => { + const ranked = rankCandidateIssues( + [ + rawIssue({ issueNumber: 1, assignees: undefined }), + rawIssue({ issueNumber: 2, assignees: "not-an-array" as unknown as string[] }), + rawIssue({ issueNumber: 3, assignees: [42, "", "keep"] as unknown as string[] }), + ], + { nowMs: NOW }, + ); + const byNumber = new Map(ranked.map((entry) => [entry.issueNumber, entry.assignees])); + expect(byNumber.get(1)).toEqual([]); + expect(byNumber.get(2)).toEqual([]); + expect(byNumber.get(3)).toEqual(["keep"]); + }); +});