Skip to content

Commit 2768da9

Browse files
Merge pull request #2467 from tmdeveloper007/#2462
test : added unit tests for repo-health-insights pure helpers
2 parents f18e6f8 + 4f5f98c commit 2768da9

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

test/repo-health-insights.test.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
gradeLetter,
4+
gradeLabel,
5+
buildRadarData,
6+
buildBreakdown,
7+
generateInsights,
8+
} from "../src/lib/repo-health-insights";
9+
import type { RepoHealthSignals } from "../src/types/repo-health";
10+
11+
const BEST_SIGNALS: RepoHealthSignals = {
12+
commitFrequency: 10,
13+
prMergeRate: 1,
14+
avgPrOpenTimeHours: 0,
15+
openIssuesCount: 0,
16+
daysSinceLastCommit: 0,
17+
};
18+
19+
const WORST_SIGNALS: RepoHealthSignals = {
20+
commitFrequency: 0,
21+
prMergeRate: 0,
22+
avgPrOpenTimeHours: 9999,
23+
openIssuesCount: 9999,
24+
daysSinceLastCommit: 9999,
25+
};
26+
27+
describe("gradeLetter", () => {
28+
it("returns D for very low scores", () => {
29+
expect(gradeLetter(0)).toBe("D");
30+
expect(gradeLetter(19)).toBe("D");
31+
});
32+
33+
it("returns C / C+ for low scores", () => {
34+
expect(gradeLetter(20)).toBe("C");
35+
expect(gradeLetter(29)).toBe("C");
36+
expect(gradeLetter(30)).toBe("C+");
37+
expect(gradeLetter(39)).toBe("C+");
38+
});
39+
40+
it("returns B- / B / B+ for medium scores", () => {
41+
expect(gradeLetter(40)).toBe("B\u2212");
42+
expect(gradeLetter(49)).toBe("B\u2212");
43+
expect(gradeLetter(50)).toBe("B");
44+
expect(gradeLetter(59)).toBe("B");
45+
expect(gradeLetter(60)).toBe("B+");
46+
expect(gradeLetter(69)).toBe("B+");
47+
});
48+
49+
it("returns A- / A / A+ for high scores", () => {
50+
expect(gradeLetter(70)).toBe("A\u2212");
51+
expect(gradeLetter(79)).toBe("A\u2212");
52+
expect(gradeLetter(80)).toBe("A");
53+
expect(gradeLetter(89)).toBe("A");
54+
expect(gradeLetter(90)).toBe("A+");
55+
expect(gradeLetter(100)).toBe("A+");
56+
});
57+
});
58+
59+
describe("gradeLabel", () => {
60+
it("returns the human label for each tier", () => {
61+
expect(gradeLabel("green")).toBe("Healthy");
62+
expect(gradeLabel("yellow")).toBe("Needs Attention");
63+
expect(gradeLabel("red")).toBe("At Risk");
64+
});
65+
});
66+
67+
describe("buildRadarData", () => {
68+
it("returns 5 axes with fullMark 100 and a 0-100 integer value", () => {
69+
const data = buildRadarData(BEST_SIGNALS);
70+
expect(data).toHaveLength(5);
71+
72+
const labels = data.map((d) => d.metric);
73+
expect(labels).toEqual([
74+
"Commits",
75+
"PR Rate",
76+
"PR Speed",
77+
"Issues",
78+
"Activity",
79+
]);
80+
81+
for (const axis of data) {
82+
expect(axis.fullMark).toBe(100);
83+
expect(Number.isInteger(axis.value)).toBe(true);
84+
expect(axis.value).toBeGreaterThanOrEqual(0);
85+
expect(axis.value).toBeLessThanOrEqual(100);
86+
}
87+
});
88+
89+
it("returns 0 for all axes when the signals are at their worst", () => {
90+
const data = buildRadarData(WORST_SIGNALS);
91+
for (const axis of data) {
92+
expect(axis.value).toBe(0);
93+
}
94+
});
95+
96+
it("returns 100 for all axes when the signals are at their best", () => {
97+
const data = buildRadarData(BEST_SIGNALS);
98+
for (const axis of data) {
99+
expect(axis.value).toBe(100);
100+
}
101+
});
102+
});
103+
104+
describe("buildBreakdown", () => {
105+
it("returns 5 rows with consistent labels and weights summing to 100", () => {
106+
const rows = buildBreakdown(BEST_SIGNALS);
107+
expect(rows).toHaveLength(5);
108+
109+
const labels = rows.map((r) => r.label);
110+
expect(labels).toEqual([
111+
"Commit Frequency",
112+
"PR Merge Rate",
113+
"PR Turnaround",
114+
"Open Issues",
115+
"Recent Activity",
116+
]);
117+
118+
const totalWeight = rows.reduce((sum, r) => sum + r.weightPct, 0);
119+
expect(totalWeight).toBe(100);
120+
121+
for (const row of rows) {
122+
expect(row.earned).toBeGreaterThanOrEqual(0);
123+
expect(row.earned).toBeLessThanOrEqual(row.maxScore);
124+
expect(row.tip.length).toBeGreaterThan(0);
125+
}
126+
});
127+
128+
it("uses singular '1 commit' and '1 open issue' for count of 1", () => {
129+
const rows = buildBreakdown({
130+
...BEST_SIGNALS,
131+
commitFrequency: 1,
132+
openIssuesCount: 1,
133+
});
134+
expect(rows[0].rawValue).toBe("1 commit");
135+
expect(rows[3].rawValue).toBe("1 open issue");
136+
});
137+
138+
it("renders 'No PRs' when avgPrOpenTimeHours is 0", () => {
139+
const rows = buildBreakdown({ ...BEST_SIGNALS, avgPrOpenTimeHours: 0 });
140+
expect(rows[2].rawValue).toBe("No PRs");
141+
});
142+
143+
it("renders the PR turnaround as '<n>h avg' for non-zero hours", () => {
144+
const rows = buildBreakdown({ ...BEST_SIGNALS, avgPrOpenTimeHours: 12.4 });
145+
expect(rows[2].rawValue).toBe("12h avg");
146+
});
147+
148+
it("renders the PR merge rate as a rounded percentage", () => {
149+
const rows = buildBreakdown({ ...BEST_SIGNALS, prMergeRate: 0.654 });
150+
expect(rows[1].rawValue).toBe("65%");
151+
});
152+
153+
it("renders 'Today' for daysSinceLastCommit === 0 and 'Unknown' for sentinel 9999", () => {
154+
const today = buildBreakdown({ ...BEST_SIGNALS, daysSinceLastCommit: 0 });
155+
expect(today[4].rawValue).toBe("Today");
156+
157+
const unknown = buildBreakdown({ ...BEST_SIGNALS, daysSinceLastCommit: 9999 });
158+
expect(unknown[4].rawValue).toBe("Unknown");
159+
});
160+
});
161+
162+
describe("generateInsights", () => {
163+
it("returns an empty array when signals are at the sentinel worst values", () => {
164+
// WORST_SIGNALS still triggers at least the "no commits" and "low-merge-rate"
165+
// rules, so this assertion is intentionally narrow.
166+
const insights = generateInsights({
167+
...WORST_SIGNALS,
168+
commitFrequency: 0,
169+
prMergeRate: 0,
170+
});
171+
expect(insights.length).toBeGreaterThan(0);
172+
expect(insights.map((i) => i.id)).toContain("no-commits");
173+
});
174+
175+
it("returns a strong-activity insight for the best-case signals", () => {
176+
const insights = generateInsights(BEST_SIGNALS);
177+
const ids = insights.map((i) => i.id);
178+
expect(ids).toContain("good-commits");
179+
expect(ids).toContain("no-issues");
180+
expect(ids).toContain("active-repo");
181+
});
182+
183+
it("emits a high-issues warning when openIssuesCount >= 20", () => {
184+
const insights = generateInsights({ ...BEST_SIGNALS, openIssuesCount: 25 });
185+
const ids = insights.map((i) => i.id);
186+
expect(ids).toContain("high-issues");
187+
});
188+
189+
it("emits a slow-prs warning when avgPrOpenTimeHours > 168", () => {
190+
const insights = generateInsights({ ...BEST_SIGNALS, avgPrOpenTimeHours: 200 });
191+
const ids = insights.map((i) => i.id);
192+
expect(ids).toContain("slow-prs");
193+
});
194+
195+
it("emits a no-commit-data info insight when daysSinceLastCommit is the sentinel", () => {
196+
const insights = generateInsights({
197+
...BEST_SIGNALS,
198+
daysSinceLastCommit: 9999,
199+
});
200+
const ids = insights.map((i) => i.id);
201+
expect(ids).toContain("no-commit-data");
202+
});
203+
});

0 commit comments

Comments
 (0)