Files
awesome-copilot/eng/review-metrics.test.mjs
T

297 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import {
businessDaysBetween,
collectData,
computeMetrics,
concentration,
loadMetricsConfig,
normalizeConfig,
parseArgs,
percentile,
renderReport,
renderTrackingBody,
TRACKING_MARKER,
} from "./review-metrics.mjs";
test("business days skip weekends and count partial weekdays", () => {
// 2025-01-03 is a Friday.
assert.equal(businessDaysBetween("2025-01-03T00:00:00Z", "2025-01-06T00:00:00Z"), 1);
assert.equal(businessDaysBetween("2025-01-04T00:00:00Z", "2025-01-06T00:00:00Z"), 0);
assert.equal(businessDaysBetween("2025-01-03T12:00:00Z", "2025-01-06T12:00:00Z"), 1);
assert.equal(businessDaysBetween("2025-01-06T00:00:00Z", "2025-01-13T00:00:00Z"), 5);
assert.equal(businessDaysBetween("2025-01-06T00:00:00Z", "2025-01-06T06:00:00Z"), 0.25);
assert.equal(businessDaysBetween("2025-01-06T00:00:00Z", "2025-01-05T00:00:00Z"), 0);
});
test("percentiles interpolate linearly and ignore invalid values", () => {
assert.equal(percentile([], 50), null);
assert.equal(percentile([5], 90), 5);
assert.equal(percentile([1, 2, 3, 4], 50), 2.5);
assert.equal(percentile([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 90), 9.1);
assert.equal(percentile([3, NaN, 1, 2], 50), 2);
});
test("reviewer concentration reports top share and HHI", () => {
assert.deepEqual(concentration({}), { total: 0, reviewers: 0, top_share: null, hhi: null, effective_reviewers: null });
const single = concentration({ a: 4 });
assert.equal(single.hhi, 10000);
assert.equal(single.top_share, 1);
const even = concentration({ a: 5, b: 5, c: 5, d: 5 });
assert.equal(even.hhi, 2500);
assert.equal(even.top_share, 0.25);
assert.equal(even.effective_reviewers, 4);
});
const config = normalizeConfig({ automation_workflows: ["a.yml", "missing.yml"] });
const now = "2025-01-10T12:00:00Z"; // Friday
const maintainer = (login, submittedAt, state = "APPROVED") => ({ state, submittedAt, authorCanPushToRepository: true, author: { login, type: "User" } });
function fixture() {
return {
openPrs: [
{ number: 1, title: "Waiting a while", url: "u1", createdAt: "2025-01-03T12:00:00Z", isDraft: false, author: { login: "alice", type: "User" }, labels: ["ready-for-review", "merge-risk:low"], reviews: [] },
{ number: 2, title: "Needs fixes", url: "u2", createdAt: "2025-01-02T12:00:00Z", isDraft: false, author: { login: "bob", type: "User" }, labels: ["requires-submitter-fixes"], reviews: [] },
{ number: 3, title: "Reviewed", url: "u3", createdAt: "2025-01-02T12:00:00Z", isDraft: false, author: { login: "carol", type: "User" }, labels: [], reviews: [maintainer("m1", "2025-01-06T12:00:00Z", "COMMENTED")] },
{ number: 4, title: "Draft", url: "u4", createdAt: "2025-01-09T12:00:00Z", isDraft: true, author: { login: "dave", type: "User" }, labels: [], reviews: [] },
{ number: 5, title: "Bot PR", url: "u5", createdAt: "2025-01-09T12:00:00Z", isDraft: false, author: { login: "github-actions", type: "Bot" }, labels: [], reviews: [] },
{ number: 6, title: "Fresh", url: "u6", createdAt: "2025-01-09T12:00:00Z", isDraft: false, author: { login: "erin", type: "User" }, labels: ["merge-risk:high"], reviews: [] },
],
windowPrs: [
{ number: 10, createdAt: "2025-01-06T00:00:00Z", mergedAt: "2025-01-07T00:00:00Z", author: { login: "x", type: "User" }, labels: [], reviews: [maintainer("m1", "2025-01-06T02:00:00Z"), maintainer("m1", "2025-01-06T05:00:00Z")] },
{ number: 11, createdAt: "2025-01-06T00:00:00Z", mergedAt: "2025-01-09T00:00:00Z", author: { login: "y", type: "User" }, labels: [], reviews: [maintainer("m2", "2025-01-06T10:00:00Z"), maintainer("y", "2025-01-06T01:00:00Z"), { ...maintainer("bot", "2025-01-06T00:30:00Z"), author: { login: "copilot", type: "Bot" } }] },
{ number: 12, createdAt: "2024-12-01T00:00:00Z", mergedAt: null, author: { login: "z", type: "User" }, labels: [], reviews: [maintainer("m1", "2024-12-02T00:00:00Z")] },
{ number: 13, createdAt: "2025-01-08T00:00:00Z", mergedAt: null, author: { login: "w", type: "User" }, labels: [], reviews: [{ ...maintainer("drive-by", "2025-01-08T01:00:00Z"), authorCanPushToRepository: false }] },
],
externalPluginIssues: [
{ number: 20, title: "Plugin", url: "i20", createdAt: "2024-12-20T00:00:00Z", readyAt: "2025-01-03T00:00:00Z", labels: ["external-plugin", "ready-for-review"] },
{ number: 21, title: "Plugin fixes", url: "i21", createdAt: "2024-12-20T00:00:00Z", labels: ["external-plugin", "requires-submitter-fixes"] },
],
workflowRuns: [
{
file: "a.yml",
found: true,
name: "A",
runs: [
{ status: "completed", conclusion: "success", created_at: "2025-01-08T00:00:00Z" },
{ status: "completed", conclusion: "failure", created_at: "2025-01-08T00:00:00Z" },
{ status: "completed", conclusion: "startup_failure", created_at: "2025-01-08T00:00:00Z" },
{ status: "completed", conclusion: "cancelled", created_at: "2025-01-08T00:00:00Z" },
{ status: "completed", conclusion: "skipped", created_at: "2025-01-08T00:00:00Z" },
{ status: "in_progress", conclusion: null, created_at: "2025-01-08T00:00:00Z" },
{ status: "completed", conclusion: "failure", created_at: "2024-12-01T00:00:00Z" },
],
},
{ file: "missing.yml", found: false, runs: [] },
],
};
}
test("computes open contribution breakdowns with graceful label fallbacks", () => {
const metrics = computeMetrics(fixture(), config, { now });
const prs = metrics.open.pull_requests;
assert.equal(prs.total, 5, "bot PRs are excluded");
assert.equal(prs.drafts, 1);
assert.equal(prs.automation_authored_excluded, 1);
assert.equal(prs.by_state["ready-for-review"], 1);
assert.equal(prs.by_state["requires-submitter-fixes"], 1);
assert.equal(prs.by_state.unlabeled, 2);
assert.equal(prs.by_risk["merge-risk:low"], 1);
assert.equal(prs.by_risk["merge-risk:high"], 1);
assert.equal(prs.by_risk.unclassified, 2);
assert.equal(metrics.open.external_plugin_issues.by_state["ready-for-review"], 1);
});
test("computes review speed, maintainer load, and concentration", () => {
const metrics = computeMetrics(fixture(), config, { now });
// First maintainer reviews in window: #10 after 2h, #11 after 10h. Author, bot,
// and non-maintainer reviews (#13) are ignored.
assert.equal(metrics.time_to_first_review.count, 2);
assert.equal(metrics.time_to_first_review.median_hours, 6);
assert.equal(metrics.time_to_merge.count, 2);
assert.equal(metrics.time_to_merge.median_hours, 48);
assert.deepEqual(
metrics.reviews_per_maintainer.map((item) => [item.login, item.pull_requests, item.reviews]),
[["m1", 1, 2], ["m2", 1, 1]],
);
assert.equal(metrics.reviewer_concentration.top_share, 0.5);
assert.equal(metrics.reviewer_concentration.hhi, 5000);
});
test("flags items past business-day targets", () => {
const metrics = computeMetrics(fixture(), config, { now });
// #1 waits 5 business days, #6 waits 1, issue #20 waits 5.5; #2 needs submitter fixes; #3 was reviewed.
assert.deepEqual(metrics.overdue["2"].items.map((item) => `${item.kind}#${item.number}`), ["issue#20", "pr#1"]);
assert.equal(metrics.overdue["4"].count, 2);
});
test("computes automation failure rate over completed, non-cancelled runs", () => {
const metrics = computeMetrics(fixture(), config, { now });
assert.equal(metrics.automation.runs, 3);
assert.equal(metrics.automation.failed, 2);
assert.equal(metrics.automation.failure_rate, 0.667);
assert.equal(metrics.automation.workflows[1].found, false);
});
test("automation API errors are reported as incomplete instead of not found", () => {
const data = fixture();
data.workflowRuns.push({ file: "error.yml", found: true, runs: [], error: "server unavailable" });
const metrics = computeMetrics(data, config, { now });
assert.equal(metrics.automation.incomplete, true);
assert.deepEqual(metrics.automation.errors, [{ workflow: "error.yml", error: "server unavailable" }]);
assert.equal(metrics.automation.runs, 3);
assert.equal(metrics.automation.failed, 2);
assert.equal(metrics.automation.failure_rate, 0.667);
const report = renderReport(metrics, {});
assert.match(report, /Failure rate: \*\*66\.7% \(incomplete\)\*\*/);
assert.match(report, /`error\.yml` \| – \| – \| collection error: server unavailable/);
});
test("collectData paginates PR reviews beyond the first 100", async () => {
const pageOneReviews = Array.from({ length: 100 }, (_, index) => maintainer(`m${index}`, "2025-01-06T01:00:00Z"));
const pageTwoReview = maintainer("last", "2025-01-06T02:00:00Z");
const client = {
graphql: async (query) => {
if (query.includes("search(")) {
return {
search: {
pageInfo: { hasNextPage: false, endCursor: null },
nodes: [{
id: "PR_1",
number: 1,
title: "Many reviews",
url: "https://example.test/pull/1",
createdAt: "2025-01-06T00:00:00Z",
mergedAt: null,
isDraft: false,
author: { login: "author", __typename: "User" },
labels: { nodes: [] },
reviews: { pageInfo: { hasNextPage: true, endCursor: "page-1" }, nodes: [...pageOneReviews] },
timelineItems: { nodes: [] },
}],
},
};
}
assert.match(query, /node\(id: \$id\)/);
return {
node: {
reviews: { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [pageTwoReview] },
},
};
},
request: async () => { throw new Error("request should not be called"); },
paginate: async () => { throw new Error("paginate should not be called"); },
};
const data = await collectData(client, { owner: "o", repo: "r" }, normalizeConfig({ automation_workflows: [], external_plugin_label: "" }), { now });
assert.equal(data.openPrs[0].reviews.length, 101);
assert.equal(data.windowPrs[0].reviews.length, 101);
assert.equal(data.windowPrs[0].reviews[100].author.login, "last");
});
test("collectData paginates external plugin issue label events", async () => {
let searchCalls = 0;
const client = {
graphql: async (query, variables) => {
if (query.includes("search(")) {
searchCalls++;
if (searchCalls <= 2) {
return { search: { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] } };
}
return {
search: {
pageInfo: { hasNextPage: false, endCursor: null },
nodes: [{
id: "ISSUE_1",
number: 42,
title: "Long-lived plugin",
url: "https://example.test/issues/42",
createdAt: "2024-01-01T00:00:00Z",
labels: { nodes: [{ name: "external-plugin" }, { name: "ready-for-review" }] },
timelineItems: {
pageInfo: { hasPreviousPage: true, startCursor: "recent-page" },
nodes: [{ createdAt: "2025-01-09T00:00:00Z", label: { name: "triage" } }],
},
}],
},
};
}
assert.equal(variables.id, "ISSUE_1");
assert.equal(variables.cursor, "recent-page");
return {
node: {
timelineItems: {
pageInfo: { hasPreviousPage: false, startCursor: null },
nodes: [{ createdAt: "2025-01-03T00:00:00Z", label: { name: "ready-for-review" } }],
},
},
};
},
request: async () => { throw new Error("request should not be called"); },
paginate: async () => { throw new Error("paginate should not be called"); },
};
const data = await collectData(
client,
{ owner: "o", repo: "r" },
normalizeConfig({ automation_workflows: [], external_plugin_label: "external-plugin" }),
{ now },
);
assert.equal(data.externalPluginIssues[0].readyAt, "2025-01-03T00:00:00Z");
});
test("handles an empty repository without errors", () => {
const metrics = computeMetrics({}, config, { now });
assert.equal(metrics.time_to_first_review.median_hours, null);
assert.equal(metrics.automation.failure_rate, null);
const report = renderReport(metrics, {});
assert.match(report, /No maintainer reviews/);
assert.match(report, /n\/a/);
});
test("renders a report and tracking issue body", () => {
const metrics = computeMetrics(fixture(), config, { now });
const report = renderReport(metrics, { repository: "o/r", runUrl: "https://example.test/run" });
assert.match(report, /Review operating metrics — week ending 2025-01-10/);
assert.match(report, /Time to first review \| 2 \| 6 h/);
assert.match(report, /HHI: \*\*5000\*\*/);
assert.match(report, /`missing.yml` \| – \| – \| not found/);
assert.match(report, /@\u200bm1/, "mentions are neutralized");
assert.ok(renderTrackingBody(report).startsWith(TRACKING_MARKER));
});
test("config and CLI parsing", () => {
const loaded = loadMetricsConfig();
assert.equal(loaded.window_days, 7);
assert.deepEqual(loaded.targets_business_days, [2, 4]);
assert.ok(loaded.automation_workflows.includes("canvas-smoke-test.yml"));
assert.equal(loadMetricsConfig("does-not-exist.yml").tracking_issue.label, "review-metrics");
const options = parseArgs(["--repo", "o/r", "--dry-run", "--now", "2025-01-10T00:00:00Z", "--window-days", "14"]);
assert.equal(options.dryRun, true);
assert.equal(options.windowDays, 14);
assert.equal(options.now.toISOString(), "2025-01-10T00:00:00.000Z");
assert.throws(() => parseArgs(["--now", "nope"]), /ISO date/);
assert.throws(() => parseArgs(["--bogus"]), /Unknown argument/);
});
test("time to first review ignores reviews submitted before the PR was ready", () => {
const data = {
openPrs: [],
externalPluginIssues: [],
workflowRuns: [],
windowPrs: [
{
number: 30,
createdAt: "2025-01-06T00:00:00Z",
readyForReviewAt: "2025-01-07T00:00:00Z",
mergedAt: null,
author: { login: "x", type: "User" },
labels: [],
reviews: [maintainer("m1", "2025-01-06T01:00:00Z", "COMMENTED"), maintainer("m2", "2025-01-07T04:00:00Z")],
},
],
};
const metrics = computeMetrics(data, config, { now });
assert.equal(metrics.time_to_first_review.count, 1);
assert.equal(metrics.time_to_first_review.median_hours, 4);
});