Skip to content

Commit b091240

Browse files
authored
test(signals): fold reward-risk-engine-branch-coverage.test.ts into its siblings (#8578) (#8582)
reward-risk-engine-branch-coverage.test.ts was a #2281 Codecov bolt-on testing buildRepoRewardRisk / rewardRiskFreshnessInternals / buildContributorRewardRiskStrategy from its own disconnected file. Move each case to the sibling that already owns the function under test: - bestFitLabels tie-break -> reward-risk-freshness.test.ts's existing "bestFitLabels keyword anchoring" describe (reused its pick() idiom) - reviewChurnRisk (high/medium) + maintainer-cut readiness -> reward-risk-freshness.test.ts, new describe block (buildRepoRewardRisk) - buildContributorRewardRiskStrategy tie-break -> reward-risk-reports.test.ts, new describe block (the only reward-risk sibling besides the signals-v2/ signals-coverage family, which stays out of scope per #8574/#8576) Verified zero coverage regression: packages/loopover-engine/src/reward-risk.ts (259/303 branches, 152/164 lines) and packages/loopover-engine/src/signals/engine.ts (441/1973 branches, 437/1351 lines) are identical before and after.
1 parent 08c2a36 commit b091240

3 files changed

Lines changed: 193 additions & 172 deletions

File tree

test/unit/reward-risk-engine-branch-coverage.test.ts

Lines changed: 0 additions & 171 deletions
This file was deleted.

test/unit/reward-risk-freshness.test.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
type FreshnessIssue,
55
} from "../../packages/loopover-engine/src/opportunity-freshness";
66
import {
7+
buildContributorFit,
78
buildContributorOutcomeHistory,
89
buildContributorProfile,
10+
buildContributorScoringProfile,
911
} from "../../src/signals/engine";
1012
import { buildRepoRewardRisk, rewardRiskFreshnessInternals } from "../../src/signals/reward-risk";
11-
import type { IssueRecord, RepositoryRecord, ScoringModelSnapshotRecord } from "../../src/types";
13+
import type { IssueRecord, PullRequestRecord, RepositoryRecord, ScoringModelSnapshotRecord } from "../../src/types";
1214

1315
function scoringSnapshot(): ScoringModelSnapshotRecord {
1416
return {
@@ -71,6 +73,22 @@ function toFreshnessIssues(issues: IssueRecord[]): FreshnessIssue[] {
7173
}));
7274
}
7375

76+
function pr(repoFullName: string, number: number, title: string, overrides: Partial<PullRequestRecord> = {}): PullRequestRecord {
77+
return {
78+
repoFullName,
79+
number,
80+
title,
81+
state: "open",
82+
authorLogin: "dev",
83+
authorAssociation: "NONE",
84+
labels: [],
85+
linkedIssues: [],
86+
body: "",
87+
updatedAt: new Date().toISOString(),
88+
...overrides,
89+
};
90+
}
91+
7492
describe("reward-risk freshness parity with loopover-engine", () => {
7593
const collab = repo("owner/collab-repo");
7694
const profile = buildContributorProfile("dev", { login: "dev", topLanguages: [], source: "github" }, [], []);
@@ -174,6 +192,11 @@ describe("bestFitLabels keyword anchoring", () => {
174192
expect(rewardRiskFreshnessInternals.bestFitLabels(null)).toEqual([]);
175193
});
176194

195+
it("breaks an equal-multiplier tie by label name", () => {
196+
// Two labels with the SAME multiplier force the sort comparator's `|| localeCompare` fallback.
197+
expect(pick({ zebra: 1.5, alpha: 1.5 })).toEqual(["alpha"]);
198+
});
199+
177200
it("aligns its meta-label exclusion set to engine.ts's canonical suspicious-label matcher (#7251)", () => {
178201
// Keywords the canonical audit excludes that the old divergent copy MISSED are now excluded here too.
179202
for (const key of ["state", "bot", "loopover", "reward", "score", "miner"]) {
@@ -184,3 +207,92 @@ describe("bestFitLabels keyword anchoring", () => {
184207
expect(pick({ "contributor:top-tier": 5, bug: 2 })).toEqual(["contributor:top-tier"]);
185208
});
186209
});
210+
211+
describe("buildRepoRewardRisk — reviewChurnRisk and maintainer-cut readiness branch coverage (#2281)", () => {
212+
it("reviewChurnRisk reports high risk when the repo-specific closed-PR rate is high", () => {
213+
const profile = buildContributorProfile("dev", { login: "dev", topLanguages: ["TypeScript"], source: "github" }, [], []);
214+
const churnRepo = repo("owner/churn");
215+
// Two closed + one merged PR => closedPullRequestRate ~0.67 => reviewChurnRisk risk >= 45 => "high".
216+
const outcomeHistory = buildContributorOutcomeHistory({
217+
login: "dev",
218+
profile,
219+
repositories: [churnRepo],
220+
pullRequests: [
221+
pr(churnRepo.fullName, 30, "Closed one", { state: "closed" }),
222+
pr(churnRepo.fullName, 31, "Closed two", { state: "closed" }),
223+
pr(churnRepo.fullName, 32, "Merged", { state: "merged", mergedAt: "2026-05-20T00:00:00.000Z" }),
224+
],
225+
issues: [],
226+
repoStats: [],
227+
});
228+
const fit = buildContributorFit(profile, [churnRepo], [], [], [], []);
229+
const scoringProfile = buildContributorScoringProfile({ login: "dev", fit, scoringSnapshot: scoringSnapshot() });
230+
const analysis = buildRepoRewardRisk({
231+
login: "dev",
232+
repo: churnRepo,
233+
repoFullName: churnRepo.fullName,
234+
profile,
235+
outcomeHistory,
236+
scoringSnapshot: scoringSnapshot(),
237+
scoringProfile,
238+
issues: [],
239+
pullRequests: [],
240+
});
241+
expect(analysis.riskBreakdown.reviewChurnRisk).toBe("high");
242+
});
243+
244+
it("reviewChurnRisk reports medium risk for a moderate closed-PR rate", () => {
245+
const profile = buildContributorProfile("dev", { login: "dev", topLanguages: ["TypeScript"], source: "github" }, [], []);
246+
const churnRepo = repo("owner/churn-mid");
247+
// One closed + two merged => closedPullRequestRate ~0.33 => risk in [20, 45) => "medium".
248+
const outcomeHistory = buildContributorOutcomeHistory({
249+
login: "dev",
250+
profile,
251+
repositories: [churnRepo],
252+
pullRequests: [
253+
pr(churnRepo.fullName, 40, "Closed one", { state: "closed" }),
254+
pr(churnRepo.fullName, 41, "Merged one", { state: "merged", mergedAt: "2026-05-20T00:00:00.000Z" }),
255+
pr(churnRepo.fullName, 42, "Merged two", { state: "merged", mergedAt: "2026-05-21T00:00:00.000Z" }),
256+
],
257+
issues: [],
258+
repoStats: [],
259+
});
260+
const fit = buildContributorFit(profile, [churnRepo], [], [], [], []);
261+
const scoringProfile = buildContributorScoringProfile({ login: "dev", fit, scoringSnapshot: scoringSnapshot() });
262+
const analysis = buildRepoRewardRisk({
263+
login: "dev",
264+
repo: churnRepo,
265+
repoFullName: churnRepo.fullName,
266+
profile,
267+
outcomeHistory,
268+
scoringSnapshot: scoringSnapshot(),
269+
scoringProfile,
270+
issues: [],
271+
pullRequests: [],
272+
});
273+
expect(analysis.riskBreakdown.reviewChurnRisk).toBe("medium");
274+
});
275+
276+
it("maintainer-cut readiness scores without the low-queue bonus when the owned repo's queue is not low", () => {
277+
// Owner === login => maintainer lane; a heavily loaded queue keeps queueHealth.level above "low",
278+
// exercising the `level === "low" ? 20 : 0` false branch.
279+
const ownedRepo = repo("dev/owned");
280+
const busyPrs = Array.from({ length: 14 }, (_, i) => pr(ownedRepo.fullName, i + 1, `Open work ${i}`, { authorLogin: `other${i}` }));
281+
const profile = buildContributorProfile("dev", { login: "dev", topLanguages: ["TypeScript"], source: "github" }, [], []);
282+
const fit = buildContributorFit(profile, [ownedRepo], [], [], [], []);
283+
const scoringProfile = buildContributorScoringProfile({ login: "dev", fit, scoringSnapshot: scoringSnapshot() });
284+
const analysis = buildRepoRewardRisk({
285+
login: "dev",
286+
repo: ownedRepo,
287+
repoFullName: ownedRepo.fullName,
288+
profile,
289+
outcomeHistory: buildContributorOutcomeHistory({ login: "dev", profile, repositories: [ownedRepo], pullRequests: busyPrs, issues: [], repoStats: [] }),
290+
scoringSnapshot: scoringSnapshot(),
291+
scoringProfile,
292+
issues: [],
293+
pullRequests: busyPrs,
294+
});
295+
expect(analysis.roleContext.maintainerLane).toBe(true);
296+
expect(analysis.actions.some((a) => a.actionKind === "maintainer_cut_readiness")).toBe(true);
297+
});
298+
});

0 commit comments

Comments
 (0)