From 7c3c89c9e7c8e48ba1889d0f00b2c6672400c67f Mon Sep 17 00:00:00 2001 From: rsnetworkinginc Date: Wed, 29 Jul 2026 05:09:02 +0300 Subject: [PATCH] fix(engine): drop opportunity-metadata's blanket v8-ignore directives and branch-cover the scoring logic they hid opportunity-metadata.ts carried v8-ignore suppressions across nearly all of its decision logic -- computeMetadataPotential wholesale, the feasibility clock guard and title tiers, titlesOverlap, all five computeMetadataDupRisk branches, and six bare unexplained directives -- in a file the engine Codecov flag is supposed to grade. The rationales were self-refuting ("exercised in ranker tests": if true the directive is redundant, if false it conceals an untested branch), and stripping them proves the point: four partial branches surface under the existing ranker suites alone. Delete every directive except the opportunityMetadataInternals test-only export block's legitimate pair. No production logic changes -- the diff to the source file is 27 pure comment-line deletions; every formula, threshold, label list, and return expression is byte-for-byte identical. Direct unit tests now pin each previously-suppressed branch: label tiers and normalization, the non-finite clock guard, title-length tiers, titlesOverlap's emptiness/equality/containment- threshold/swap arms, dup-risk's self/cross-repo/overlap filters, the competition-context and timestamp ?? arms, the targetability filter, and the defensive re-parse and clamp01 guards (exercised by making the underlying primitive disagree, the only way without a logic change). Regression tests land in BOTH graded suites: the engine package's own node:test suite (the `engine` Codecov flag's coverage run) and the root vitest unit suite (the `backend` flag). Under each flag's own lcov the file now reports zero uncovered lines and zero partial branches outside the retained internals block. Closes #9616 --- .../src/opportunity-metadata.ts | 27 --- .../test/opportunity-metadata.test.ts | 208 ++++++++++++++++++ test/unit/opportunity-metadata.test.ts | 197 +++++++++++++++++ 3 files changed, 405 insertions(+), 27 deletions(-) create mode 100755 packages/loopover-engine/test/opportunity-metadata.test.ts create mode 100755 test/unit/opportunity-metadata.test.ts diff --git a/packages/loopover-engine/src/opportunity-metadata.ts b/packages/loopover-engine/src/opportunity-metadata.ts index 5e99aea9dc..6e2c2bece7 100644 --- a/packages/loopover-engine/src/opportunity-metadata.ts +++ b/packages/loopover-engine/src/opportunity-metadata.ts @@ -43,18 +43,15 @@ const NEGATIVE_LABELS = Object.freeze([ ]); function clamp01(value: number): number { - /* v8 ignore next -- Defensive guard for malformed adapter input; scores are always finite in practice. */ if (!Number.isFinite(value)) return 0; return Math.min(1, Math.max(0, value)); } function finiteNonNegativeInt(value: number): number { - /* v8 ignore next -- Defensive guard for malformed adapter input; counts are normalized before scoring. */ if (!Number.isFinite(value)) return 0; return Math.max(0, Math.trunc(value)); } -/* v8 ignore start -- Label/title normalization helpers are covered through exported ranker entrypoints. */ function normalizeLabels(labels: readonly string[]): string[] { return labels .filter((label): label is string => typeof label === "string") @@ -74,11 +71,9 @@ function resolveGoalSpec(repoFullName: string, context: MetadataRankContext): Mi } return DEFAULT_MINER_GOAL_SPEC; } -/* v8 ignore stop */ const STALE_AGE_DAYS = 9999; -/* v8 ignore start -- Internal timestamp helpers mirror freshness semantics; exercised via exported ranker paths. */ function pickMetadataTimestamp(issue: MetadataCandidateIssue): string { // Mirror freshness semantics (opportunity-freshness's pickTimestamp): only commit to a timestamp that actually // parses. Without the guard, a present-but-unparseable updatedAt shadows a valid createdAt, so issueAgeDays @@ -101,51 +96,40 @@ function issueAgeDays(issue: MetadataCandidateIssue, nowMs: number): number { if (!Number.isFinite(parsed)) return STALE_AGE_DAYS; return Math.max(0, Math.floor((nowMs - parsed) / 86_400_000)); } -/* v8 ignore stop */ /** * Estimate reward potential from issue labels alone. Explicitly negative labels collapse the score; common * contribution labels raise it; everything else keeps a neutral baseline. */ -/* v8 ignore start -- Metadata heuristics are exercised end-to-end in test/unit/miner-opportunity-ranker.test.ts. */ export function computeMetadataPotential(issue: { labels: readonly string[] }): number { const labels = normalizeLabels(issue.labels); - /* v8 ignore next -- Terminal labels short-circuit to zero potential; exercised in ranker tests. */ if (labels.some((label) => NEGATIVE_LABELS.includes(label))) return 0; let score = 0.45; - /* v8 ignore next -- Neutral metadata keeps the baseline when no contribution labels are present. */ if (labels.some((label) => POSITIVE_LABELS.includes(label))) score += 0.35; - /* v8 ignore next -- Bug/refactor bonuses are additive; neutral-only labels keep the baseline score. */ if (labels.includes("bug")) score += 0.1; - /* v8 ignore next */ if (labels.includes("refactor")) score += 0.05; return clamp01(score); } -/* v8 ignore stop */ /** * Estimate achievability from metadata-only cues: lower discussion load and fresher issues score higher. */ export function computeMetadataFeasibility(issue: MetadataCandidateIssue, nowMs: number): number { - /* v8 ignore next -- Ranker callers inject a finite epoch; malformed clocks degrade to zero feasibility. */ if (!Number.isFinite(nowMs)) return 0; const comments = finiteNonNegativeInt(issue.commentsCount); const commentScore = clamp01(1 - comments / 25); const ageDays = issueAgeDays(issue, nowMs); const ageScore = clamp01(Math.exp(-ageDays / 45)); const titleLength = normalizeTitle(issue.title).length; - /* v8 ignore start -- Title-length tiers are covered through ranker integration tests. */ let titleScore = 0.4; if (titleLength >= 8) { titleScore = 1; } else if (titleLength >= 4) { titleScore = 0.7; } - /* v8 ignore stop */ return clamp01(commentScore * 0.45 + ageScore * 0.35 + titleScore * 0.2); } -/* v8 ignore start -- Title overlap helper is exercised through computeMetadataDupRisk. */ function titlesOverlap(left: string, right: string): boolean { if (!left || !right) return false; if (left === right) return true; @@ -157,7 +141,6 @@ function titlesOverlap(left: string, right: string): boolean { } return longer.includes(shorter) && shorter.length >= 12; } -/* v8 ignore stop */ /* v8 ignore start -- Test-only export surface for branch coverage. */ export const opportunityMetadataInternals = { @@ -177,18 +160,13 @@ export function computeMetadataDupRisk( peers: readonly MetadataCandidateIssue[], ): number { const normalized = normalizeTitle(issue.title); - /* v8 ignore next -- Blank titles are treated as maximum dup risk. */ if (!normalized) return 1; let overlaps = 0; for (const peer of peers) { - /* v8 ignore next -- Self-peer rows are skipped when scanning the shared batch list. */ if (peer.issueNumber === issue.issueNumber && peer.repoFullName.trim().toLowerCase() === issue.repoFullName.trim().toLowerCase()) continue; - /* v8 ignore next -- Cross-repo peers are ignored when scanning for overlap inside a batch. */ if (peer.repoFullName.trim().toLowerCase() !== issue.repoFullName.trim().toLowerCase()) continue; - /* v8 ignore next -- Overlap hits are counted only for same-repo peers with shared title segments. */ if (titlesOverlap(normalized, normalizeTitle(peer.title))) overlaps += 1; } - /* v8 ignore next -- No overlaps keeps dup risk at zero for unique titles. */ if (overlaps === 0) return 0; return clamp01(overlaps / (overlaps + 1)); } @@ -201,9 +179,7 @@ export function buildMetadataRankInput( ): OpportunityRankInput { const goalSpec = resolveGoalSpec(issue.repoFullName, context); const repoCompetition = computeOpportunityCompetition( - /* v8 ignore next */ context.highRiskDuplicateClusters ?? 0, - /* v8 ignore next */ context.openPullRequests ?? 0, ); const batchDupRisk = computeMetadataDupRisk(issue, peers); @@ -212,11 +188,9 @@ export function buildMetadataRankInput( feasibility: computeMetadataFeasibility(issue, context.nowMs), laneFit: computeMetadataLaneFit(issue, goalSpec), freshness: computeOpportunityFreshness( - /* v8 ignore next */ [{ state: "open", updatedAt: issue.updatedAt ?? null, createdAt: issue.createdAt ?? null }], context.nowMs, ), - /* v8 ignore next */ dupRisk: clamp01(Math.max(batchDupRisk, repoCompetition)), }; } @@ -233,6 +207,5 @@ export function rankMetadataOpportunities( ...candidate, ...buildMetadataRankInput(candidate, targetableCandidates, context), })); - /* v8 ignore next */ return rankOpportunities(annotated) as Array; } diff --git a/packages/loopover-engine/test/opportunity-metadata.test.ts b/packages/loopover-engine/test/opportunity-metadata.test.ts new file mode 100755 index 0000000000..638bd24730 --- /dev/null +++ b/packages/loopover-engine/test/opportunity-metadata.test.ts @@ -0,0 +1,208 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + DEFAULT_MINER_GOAL_SPEC, + buildMetadataRankInput, + computeMetadataDupRisk, + computeMetadataFeasibility, + computeMetadataPotential, + rankMetadataOpportunities, + type MetadataCandidateIssue, + type MetadataRankContext, +} from "../dist/index.js"; +import { opportunityMetadataInternals } from "../dist/opportunity-metadata.js"; + +// #9616: direct unit tests for every branch the file's blanket `v8 ignore` directives used to hide. +// The scoring formulas themselves are untouched -- these tests pin the thresholds, label bonuses, and +// filters so a future change to any of them fails a test instead of vanishing behind a suppression. + +const NOW_ISO = "2026-07-29T00:00:00Z"; +const NOW = Date.parse(NOW_ISO); + +function issueFor(overrides: Partial = {}): MetadataCandidateIssue { + return { + repoFullName: "acme/widgets", + issueNumber: 1, + title: "improve the flaky retry logic in ci", + labels: [], + commentsCount: 0, + createdAt: NOW_ISO, + updatedAt: NOW_ISO, + ...overrides, + }; +} + +function assertClose(actual: number, expected: number): void { + assert.ok(Math.abs(actual - expected) < 1e-9, `${actual} !~ ${expected}`); +} + +test("computeMetadataPotential: label tiers -- negative collapse, positive/bug/refactor bonuses, neutral baseline", () => { + // A negative label collapses to 0, even alongside positive ones (short-circuit). + assert.equal(computeMetadataPotential({ labels: ["blocked"] }), 0); + assert.equal(computeMetadataPotential({ labels: ["help wanted", "wontfix"] }), 0); + // Neutral-only labels keep the 0.45 baseline. + assertClose(computeMetadataPotential({ labels: ["chore"] }), 0.45); + assertClose(computeMetadataPotential({ labels: [] }), 0.45); + // Positive label bonus. + assertClose(computeMetadataPotential({ labels: ["help wanted"] }), 0.8); + // Bug and refactor bonuses, separately and together. + assertClose(computeMetadataPotential({ labels: ["bug"] }), 0.55); + assertClose(computeMetadataPotential({ labels: ["refactor"] }), 0.5); + assertClose(computeMetadataPotential({ labels: ["bug", "refactor"] }), 0.6); + assertClose(computeMetadataPotential({ labels: ["good first issue", "bug", "refactor"] }), 0.95); +}); + +test("computeMetadataPotential: labels are normalized -- trims, lowercases, drops non-strings and blanks", () => { + assertClose(computeMetadataPotential({ labels: [" BUG "] }), 0.55); + assertClose(computeMetadataPotential({ labels: ["", " ", 7, null] as never }), 0.45); +}); + +test("computeMetadataFeasibility: a non-finite clock degrades to zero feasibility", () => { + assert.equal(computeMetadataFeasibility(issueFor(), Number.NaN), 0); + assert.equal(computeMetadataFeasibility(issueFor(), Number.POSITIVE_INFINITY), 0); +}); + +test("computeMetadataFeasibility: title-length tiers at >= 8, 4-7, and < 4 (normalized length)", () => { + // Fresh issue, zero comments: commentScore 1 and ageScore 1, so the tier is the only variable. + assertClose(computeMetadataFeasibility(issueFor({ title: "abcdefgh" }), NOW), 1); + assertClose(computeMetadataFeasibility(issueFor({ title: "abcd" }), NOW), 0.94); + assertClose(computeMetadataFeasibility(issueFor({ title: "abc" }), NOW), 0.88); + // normalizeTitle collapses interior whitespace before measuring: " a b " -> "a b" (length 3). + assertClose(computeMetadataFeasibility(issueFor({ title: " a b " }), NOW), 0.88); +}); + +test("computeMetadataFeasibility: comment load and timestamp fallbacks", () => { + // 25+ comments zero out the comment score. + assertClose(computeMetadataFeasibility(issueFor({ commentsCount: 25 }), NOW), 0.35 + 0.2); + // A non-finite comment count is normalized to 0, not propagated. + assert.equal( + computeMetadataFeasibility(issueFor({ commentsCount: Number.NaN }), NOW), + computeMetadataFeasibility(issueFor({ commentsCount: 0 }), NOW), + ); + assert.equal( + computeMetadataFeasibility(issueFor({ commentsCount: -3 }), NOW), + computeMetadataFeasibility(issueFor({ commentsCount: 0 }), NOW), + ); + // No parseable timestamp at all: the stale-age sentinel zeroes the age term. + assertClose(computeMetadataFeasibility(issueFor({ updatedAt: null, createdAt: null }), NOW), 0.65); + // An unparseable updatedAt falls back to a valid createdAt instead of shadowing it. + assertClose(computeMetadataFeasibility(issueFor({ updatedAt: "not a date", createdAt: NOW_ISO }), NOW), 1); +}); + +test("computeMetadataFeasibility: a validated timestamp that later fails to re-parse hits the stale sentinel (defensive guard)", () => { + // pickMetadataTimestamp validates with the same parser issueAgeDays re-parses with, so the re-parse + // guard can only be exercised by making the parser itself disagree between the two calls. + const realParse = Date.parse; + let calls = 0; + Date.parse = ((value: string) => { + calls += 1; + return calls === 1 ? realParse(value) : Number.NaN; + }) as typeof Date.parse; + try { + const reparseFailed = computeMetadataFeasibility(issueFor(), NOW); + const noTimestamps = computeMetadataFeasibility(issueFor({ updatedAt: null, createdAt: null }), NOW); + assert.equal(reparseFailed, noTimestamps); + assert.equal(calls, 2); + } finally { + Date.parse = realParse; + } +}); + +test("computeMetadataFeasibility: a non-finite intermediate score clamps to zero (clamp01 defensive guard)", () => { + // Every natural input reaching clamp01 is finite, so the guard is exercised by making the age + // exponential itself misbehave. + const realExp = Math.exp; + Math.exp = (() => Number.NaN) as typeof Math.exp; + try { + assertClose(computeMetadataFeasibility(issueFor(), NOW), 0.45 + 0.2); + } finally { + Math.exp = realExp; + } +}); + +test("titlesOverlap: emptiness, equality, containment threshold, and swap arms", () => { + const { titlesOverlap } = opportunityMetadataInternals; + assert.equal(titlesOverlap("", "anything"), false); + assert.equal(titlesOverlap("anything", ""), false); + assert.equal(titlesOverlap("same words", "same words"), true); + // Containment counts only when the shorter title is >= 12 chars. + assert.equal(titlesOverlap("fix the retry logic", "please fix the retry logic now"), true); + assert.equal(titlesOverlap("short one", "short one plus context"), false); + // Swap arm: left longer than right. + assert.equal(titlesOverlap("please fix the retry logic now", "fix the retry logic"), true); + // Long enough on both sides but no containment. + assert.equal(titlesOverlap("abcdefghijklmnop", "qrstuvwxyz0123456789"), false); +}); + +test("normalizeLabels / pickMetadataTimestamp / resolveGoalSpec internals cover their normalization arms", () => { + const { normalizeLabels, pickMetadataTimestamp, resolveGoalSpec } = opportunityMetadataInternals; + assert.deepEqual(normalizeLabels([" Bug ", "", 7, null] as never), ["bug"]); + + assert.equal(pickMetadataTimestamp(issueFor()), NOW_ISO); + assert.equal(pickMetadataTimestamp(issueFor({ updatedAt: ` ${NOW_ISO} ` })), NOW_ISO); + assert.equal(pickMetadataTimestamp(issueFor({ updatedAt: "not a date", createdAt: NOW_ISO })), NOW_ISO); + assert.equal(pickMetadataTimestamp(issueFor({ updatedAt: " ", createdAt: " " })), ""); + assert.equal(pickMetadataTimestamp(issueFor({ updatedAt: null, createdAt: "garbage" })), ""); + assert.equal(pickMetadataTimestamp(issueFor({ updatedAt: null, createdAt: null })), ""); + + const custom = { ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false }; + const context: MetadataRankContext = { nowMs: NOW, goalSpecsByRepo: { " ACME/Widgets ": custom } }; + assert.equal(resolveGoalSpec("acme/widgets", context), custom); + assert.equal(resolveGoalSpec("other/repo", context), DEFAULT_MINER_GOAL_SPEC); + assert.equal(resolveGoalSpec("acme/widgets", { nowMs: NOW }), DEFAULT_MINER_GOAL_SPEC); +}); + +test("computeMetadataDupRisk: blank titles, peer filters, and overlap accumulation", () => { + // A blank (or whitespace-only) title is maximum dup risk. + assert.equal(computeMetadataDupRisk(issueFor({ title: "" }), []), 1); + assert.equal(computeMetadataDupRisk(issueFor({ title: " " }), []), 1); + // The issue's own batch row is skipped, including with repo-name case/whitespace variance. + assert.equal(computeMetadataDupRisk(issueFor(), [issueFor({ repoFullName: " ACME/Widgets " })]), 0); + // Same issue number in a DIFFERENT repo is not self -- it falls through to the cross-repo skip. + assert.equal(computeMetadataDupRisk(issueFor(), [issueFor({ repoFullName: "other/repo" })]), 0); + // Cross-repo peers never count, even with identical titles. + assert.equal(computeMetadataDupRisk(issueFor(), [issueFor({ repoFullName: "other/repo", issueNumber: 9 })]), 0); + // A same-repo peer with an overlapping title raises the risk: 1 overlap -> 1/2. + const overlapping = issueFor({ issueNumber: 2, title: "Improve the flaky retry logic in CI for the queue" }); + assertClose(computeMetadataDupRisk(issueFor(), [overlapping]), 0.5); + // Two overlapping peers -> 2/3. + const second = issueFor({ issueNumber: 3, title: "improve the flaky retry logic in ci again" }); + assertClose(computeMetadataDupRisk(issueFor(), [overlapping, second]), 2 / 3); + // Same-repo peers with unrelated titles keep the risk at zero. + assert.equal(computeMetadataDupRisk(issueFor(), [issueFor({ issueNumber: 4, title: "completely unrelated words" })]), 0); +}); + +test("buildMetadataRankInput: competition context present and absent, timestamps present and absent", () => { + const withContext = buildMetadataRankInput(issueFor({ labels: ["bug"] }), [], { + nowMs: NOW, + highRiskDuplicateClusters: 2, + openPullRequests: 4, + }); + assertClose(withContext.potential, 0.55); + assertClose(withContext.feasibility, 1); + // dupRisk = max(batch 0, competition 2/4). + assertClose(withContext.dupRisk, 0.5); + assert.ok(withContext.laneFit >= 0 && withContext.laneFit <= 1); + assert.ok(withContext.freshness >= 0 && withContext.freshness <= 1); + + const bare = buildMetadataRankInput(issueFor({ updatedAt: null, createdAt: null }), [], { nowMs: NOW }); + assert.equal(bare.dupRisk, 0); + assert.ok(bare.freshness >= 0 && bare.freshness <= 1); +}); + +test("rankMetadataOpportunities: ranks targetable candidates and drops repos whose goal spec opts out", () => { + const strong = issueFor({ issueNumber: 10, title: "add retry backoff to the queue worker", labels: ["help wanted", "bug"] }); + const weak = issueFor({ issueNumber: 11, title: "abc", labels: ["chore"], commentsCount: 40, updatedAt: null, createdAt: null }); + const ranked = rankMetadataOpportunities([strong, weak], { nowMs: NOW }); + assert.equal(ranked.length, 2); + assert.equal(typeof ranked[0]?.rankScore, "number"); + assert.ok((ranked[0]?.rankScore ?? 0) >= (ranked[1]?.rankScore ?? 0)); + assert.equal(ranked[0]?.issueNumber, 10); + + const optedOut = rankMetadataOpportunities([strong, issueFor({ repoFullName: "other/repo", issueNumber: 12 })], { + nowMs: NOW, + goalSpecsByRepo: { "acme/widgets": { ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false } }, + }); + assert.deepEqual(optedOut.map((candidate) => candidate.issueNumber), [12]); +}); diff --git a/test/unit/opportunity-metadata.test.ts b/test/unit/opportunity-metadata.test.ts new file mode 100755 index 0000000000..1998f5d301 --- /dev/null +++ b/test/unit/opportunity-metadata.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from "vitest"; + +import { + DEFAULT_MINER_GOAL_SPEC, + buildMetadataRankInput, + computeMetadataDupRisk, + computeMetadataFeasibility, + computeMetadataPotential, + rankMetadataOpportunities, + type MetadataCandidateIssue, + type MetadataRankContext, +} from "../../packages/loopover-engine/src/index"; +import { opportunityMetadataInternals } from "../../packages/loopover-engine/src/opportunity-metadata"; + +// #9616: direct unit tests for every branch opportunity-metadata.ts's blanket `v8 ignore` directives +// used to hide, mirrored from packages/loopover-engine/test/opportunity-metadata.test.ts so the +// backend flag's vitest run grades the same arms the engine flag's node:test suite does. + +const NOW_ISO = "2026-07-29T00:00:00Z"; +const NOW = Date.parse(NOW_ISO); + +function issueFor(overrides: Partial = {}): MetadataCandidateIssue { + return { + repoFullName: "acme/widgets", + issueNumber: 1, + title: "improve the flaky retry logic in ci", + labels: [], + commentsCount: 0, + createdAt: NOW_ISO, + updatedAt: NOW_ISO, + ...overrides, + }; +} + +describe("computeMetadataPotential (#9616)", () => { + it("collapses on a negative label, even alongside positive ones", () => { + expect(computeMetadataPotential({ labels: ["blocked"] })).toBe(0); + expect(computeMetadataPotential({ labels: ["help wanted", "wontfix"] })).toBe(0); + }); + + it("applies positive/bug/refactor bonuses over the neutral 0.45 baseline", () => { + expect(computeMetadataPotential({ labels: ["chore"] })).toBeCloseTo(0.45, 9); + expect(computeMetadataPotential({ labels: [] })).toBeCloseTo(0.45, 9); + expect(computeMetadataPotential({ labels: ["help wanted"] })).toBeCloseTo(0.8, 9); + expect(computeMetadataPotential({ labels: ["bug"] })).toBeCloseTo(0.55, 9); + expect(computeMetadataPotential({ labels: ["refactor"] })).toBeCloseTo(0.5, 9); + expect(computeMetadataPotential({ labels: ["bug", "refactor"] })).toBeCloseTo(0.6, 9); + expect(computeMetadataPotential({ labels: ["good first issue", "bug", "refactor"] })).toBeCloseTo(0.95, 9); + }); + + it("normalizes labels: trims, lowercases, drops non-strings and blanks", () => { + expect(computeMetadataPotential({ labels: [" BUG "] })).toBeCloseTo(0.55, 9); + expect(computeMetadataPotential({ labels: ["", " ", 7, null] as never })).toBeCloseTo(0.45, 9); + }); +}); + +describe("computeMetadataFeasibility (#9616)", () => { + it("degrades to zero on a non-finite clock", () => { + expect(computeMetadataFeasibility(issueFor(), Number.NaN)).toBe(0); + expect(computeMetadataFeasibility(issueFor(), Number.POSITIVE_INFINITY)).toBe(0); + }); + + it("applies the title-length tiers at >= 8, 4-7, and < 4 on the normalized title", () => { + expect(computeMetadataFeasibility(issueFor({ title: "abcdefgh" }), NOW)).toBeCloseTo(1, 9); + expect(computeMetadataFeasibility(issueFor({ title: "abcd" }), NOW)).toBeCloseTo(0.94, 9); + expect(computeMetadataFeasibility(issueFor({ title: "abc" }), NOW)).toBeCloseTo(0.88, 9); + expect(computeMetadataFeasibility(issueFor({ title: " a b " }), NOW)).toBeCloseTo(0.88, 9); + }); + + it("handles comment load, malformed counts, and timestamp fallbacks", () => { + expect(computeMetadataFeasibility(issueFor({ commentsCount: 25 }), NOW)).toBeCloseTo(0.55, 9); + expect(computeMetadataFeasibility(issueFor({ commentsCount: Number.NaN }), NOW)).toBe( + computeMetadataFeasibility(issueFor({ commentsCount: 0 }), NOW), + ); + expect(computeMetadataFeasibility(issueFor({ commentsCount: -3 }), NOW)).toBe( + computeMetadataFeasibility(issueFor({ commentsCount: 0 }), NOW), + ); + expect(computeMetadataFeasibility(issueFor({ updatedAt: null, createdAt: null }), NOW)).toBeCloseTo(0.65, 9); + expect(computeMetadataFeasibility(issueFor({ updatedAt: "not a date", createdAt: NOW_ISO }), NOW)).toBeCloseTo(1, 9); + }); + + it("hits the stale sentinel when a validated timestamp later fails to re-parse (defensive guard)", () => { + // pickMetadataTimestamp validates with the same parser issueAgeDays re-parses with, so the + // re-parse guard can only be exercised by making the parser disagree between the two calls. + const realParse = Date.parse; + let calls = 0; + Date.parse = ((value: string) => { + calls += 1; + return calls === 1 ? realParse(value) : Number.NaN; + }) as typeof Date.parse; + try { + const reparseFailed = computeMetadataFeasibility(issueFor(), NOW); + const noTimestamps = computeMetadataFeasibility(issueFor({ updatedAt: null, createdAt: null }), NOW); + expect(reparseFailed).toBe(noTimestamps); + expect(calls).toBe(2); + } finally { + Date.parse = realParse; + } + }); + + it("clamps a non-finite intermediate score to zero (clamp01 defensive guard)", () => { + const realExp = Math.exp; + Math.exp = (() => Number.NaN) as typeof Math.exp; + try { + expect(computeMetadataFeasibility(issueFor(), NOW)).toBeCloseTo(0.65, 9); + } finally { + Math.exp = realExp; + } + }); +}); + +describe("opportunityMetadataInternals (#9616)", () => { + it("titlesOverlap: emptiness, equality, containment threshold, and swap arms", () => { + const { titlesOverlap } = opportunityMetadataInternals; + expect(titlesOverlap("", "anything")).toBe(false); + expect(titlesOverlap("anything", "")).toBe(false); + expect(titlesOverlap("same words", "same words")).toBe(true); + expect(titlesOverlap("fix the retry logic", "please fix the retry logic now")).toBe(true); + expect(titlesOverlap("short one", "short one plus context")).toBe(false); + expect(titlesOverlap("please fix the retry logic now", "fix the retry logic")).toBe(true); + expect(titlesOverlap("abcdefghijklmnop", "qrstuvwxyz0123456789")).toBe(false); + }); + + it("normalizeLabels, pickMetadataTimestamp, and resolveGoalSpec cover their normalization arms", () => { + const { normalizeLabels, pickMetadataTimestamp, resolveGoalSpec } = opportunityMetadataInternals; + expect(normalizeLabels([" Bug ", "", 7, null] as never)).toEqual(["bug"]); + + expect(pickMetadataTimestamp(issueFor())).toBe(NOW_ISO); + expect(pickMetadataTimestamp(issueFor({ updatedAt: ` ${NOW_ISO} ` }))).toBe(NOW_ISO); + expect(pickMetadataTimestamp(issueFor({ updatedAt: "not a date", createdAt: NOW_ISO }))).toBe(NOW_ISO); + expect(pickMetadataTimestamp(issueFor({ updatedAt: " ", createdAt: " " }))).toBe(""); + expect(pickMetadataTimestamp(issueFor({ updatedAt: null, createdAt: "garbage" }))).toBe(""); + expect(pickMetadataTimestamp(issueFor({ updatedAt: null, createdAt: null }))).toBe(""); + + const custom = { ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false }; + const context: MetadataRankContext = { nowMs: NOW, goalSpecsByRepo: { " ACME/Widgets ": custom } }; + expect(resolveGoalSpec("acme/widgets", context)).toBe(custom); + expect(resolveGoalSpec("other/repo", context)).toBe(DEFAULT_MINER_GOAL_SPEC); + expect(resolveGoalSpec("acme/widgets", { nowMs: NOW })).toBe(DEFAULT_MINER_GOAL_SPEC); + }); +}); + +describe("computeMetadataDupRisk (#9616)", () => { + it("treats a blank title as maximum dup risk", () => { + expect(computeMetadataDupRisk(issueFor({ title: "" }), [])).toBe(1); + expect(computeMetadataDupRisk(issueFor({ title: " " }), [])).toBe(1); + }); + + it("skips the self row and cross-repo peers, counts same-repo overlaps only", () => { + expect(computeMetadataDupRisk(issueFor(), [issueFor({ repoFullName: " ACME/Widgets " })])).toBe(0); + expect(computeMetadataDupRisk(issueFor(), [issueFor({ repoFullName: "other/repo" })])).toBe(0); + expect(computeMetadataDupRisk(issueFor(), [issueFor({ repoFullName: "other/repo", issueNumber: 9 })])).toBe(0); + const overlapping = issueFor({ issueNumber: 2, title: "Improve the flaky retry logic in CI for the queue" }); + expect(computeMetadataDupRisk(issueFor(), [overlapping])).toBeCloseTo(0.5, 9); + const second = issueFor({ issueNumber: 3, title: "improve the flaky retry logic in ci again" }); + expect(computeMetadataDupRisk(issueFor(), [overlapping, second])).toBeCloseTo(2 / 3, 9); + expect(computeMetadataDupRisk(issueFor(), [issueFor({ issueNumber: 4, title: "completely unrelated words" })])).toBe(0); + }); +}); + +describe("buildMetadataRankInput / rankMetadataOpportunities (#9616)", () => { + it("builds the five inputs with competition context present and absent, timestamps present and absent", () => { + const withContext = buildMetadataRankInput(issueFor({ labels: ["bug"] }), [], { + nowMs: NOW, + highRiskDuplicateClusters: 2, + openPullRequests: 4, + }); + expect(withContext.potential).toBeCloseTo(0.55, 9); + expect(withContext.feasibility).toBeCloseTo(1, 9); + expect(withContext.dupRisk).toBeCloseTo(0.5, 9); + expect(withContext.laneFit).toBeGreaterThanOrEqual(0); + expect(withContext.laneFit).toBeLessThanOrEqual(1); + expect(withContext.freshness).toBeGreaterThanOrEqual(0); + expect(withContext.freshness).toBeLessThanOrEqual(1); + + const bare = buildMetadataRankInput(issueFor({ updatedAt: null, createdAt: null }), [], { nowMs: NOW }); + expect(bare.dupRisk).toBe(0); + expect(bare.freshness).toBeGreaterThanOrEqual(0); + expect(bare.freshness).toBeLessThanOrEqual(1); + }); + + it("ranks targetable candidates and drops repos whose goal spec opts out", () => { + const strong = issueFor({ issueNumber: 10, title: "add retry backoff to the queue worker", labels: ["help wanted", "bug"] }); + const weak = issueFor({ issueNumber: 11, title: "abc", labels: ["chore"], commentsCount: 40, updatedAt: null, createdAt: null }); + const ranked = rankMetadataOpportunities([strong, weak], { nowMs: NOW }); + expect(ranked).toHaveLength(2); + expect(typeof ranked[0]?.rankScore).toBe("number"); + expect(ranked[0]?.rankScore ?? 0).toBeGreaterThanOrEqual(ranked[1]?.rankScore ?? 0); + expect(ranked[0]?.issueNumber).toBe(10); + + const optedOut = rankMetadataOpportunities([strong, issueFor({ repoFullName: "other/repo", issueNumber: 12 })], { + nowMs: NOW, + goalSpecsByRepo: { "acme/widgets": { ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false } }, + }); + expect(optedOut.map((candidate) => candidate.issueNumber)).toEqual([12]); + }); +});