Files
awesome-copilot/eng/review-metrics.test.mjs
T
d82df27473 Phase 3: canvas smoke-test evidence and weekly review metrics (#4184) (#4191)
* Phase 3: canvas smoke-test evidence, safe auto-merge, and weekly review metrics (#4184)

Adds the canvas-smoke-test check with review evidence, a config-gated safe auto-merge workflow (disabled by default), and a weekly review operating metrics workflow, plus maintainer docs.

Refs github/awesome-copilot#4184

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Trigger safe auto-merge sweep after Submission Gate Writer completes

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Rename thirdParty identifier to satisfy codespell

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review on canvas smoke test, auto-merge, and metrics

- Bind canvas report writer to the triggering workflow_run PR (base/head repo, ref, SHA)
- Bound PNG inflation by IHDR-derived size and a decoded-bytes budget
- Classify file: URLs as unsafe manifest paths
- Follow CommonJS require() and literal dynamic imports; error on missing
  reachable targets and devDependency-only runtime imports
- Validate removed extension paths and orphaned plugins instead of skipping
- Warn on .py/.rb/.pl scripts
- Auto-merge: support CODEOWNERS teams (fail closed), front matter authors,
  list PRs by head SHA, only fall back to direct merge on clean status,
  drop ineffective check_run trigger
- Metrics: ignore reviews before the PR was ready for review

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Docs: note microsoft/azure-dev-tools as a portable marketplace for the review model

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Auto-merge: only trust submission-gate runs from the Submission Gate Writer

Adds trusted_checks (default submission-gate -> external_id
submission-gate-writer). Any other check run or status with that name on the
head SHA blocks arming, so a PR-defined job cannot satisfy the gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Harden canvas smoke-test checker

- String-, template-, and regex-aware comment stripping so literals containing // cannot hide imports
- Never follow symlinks or read non-regular files; bounded, contained reads
- Validate plugins whose plugin.json was deleted
- Reject unsafe extension IDs in plugin manifests before materialization

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Phase 3: remove auto-merge, isolate canvas smoke test, harden checker

- Remove safe auto-merge (config, workflow, script, tests, labels) per
  maintainer review; documented as deferred pending a security review.
- Restore validate-canvas-extensions.yml to main and move the smoke test
  to its own canvas-smoke-test.yml workflow scoped to extensions/**.
- Reject duplicate IHDR chunks in preview PNGs.
- Mask string/template/regex literal contents before extracting dynamic
  import() and require() specifiers.
- Resolve package.json imports aliases through the same classifier and
  module graph; fail closed on alias shapes that cannot be analyzed.
- Rename docs to canvas-evidence-and-metrics.md and update references.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix canvas smoke and review metrics findings

- Trigger canvas smoke tests for plugin changes while skipping expensive setup when target detection finds no canvas coverage.
- Fail reachable data: imports while preserving warnings for unreachable module data: imports.
- Use base plugin manifests for deleted bundle plugin detection and fail closed when base manifests cannot be read.
- Paginate GraphQL PR review pages before computing weekly review metrics.
- Report workflow run API collection errors as incomplete automation health instead of not found.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address follow-up canvas review findings

Fail closed on non-literal runtime imports, require canonical PNG termination, restrict comment updates to github-actions[bot], and paginate external-plugin label events.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Aaron Powell <me@aaron-powell.com>
2026-10-07 16:36:13 +11:00

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);
});