| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { |
| 4 | ASK_MARKER, |
| 5 | CUTOFF_MINOR, |
| 6 | WINDOW_DAYS, |
| 7 | highestMentionedVersion, |
| 8 | isStaleVersion, |
| 9 | parseReportedVersion, |
| 10 | releasedVersions, |
| 11 | renderAsk, |
| 12 | renderClose, |
| 13 | shouldAsk, |
| 14 | shouldClose, |
| 15 | } from "./stale-report-sweep.mjs"; |
| 16 | |
| 17 | const NOW = "2026-07-30T00:00:00Z"; |
| 18 | const daysBefore = (n) => new Date(Date.parse(NOW) - n * 86400000).toISOString(); |
| 19 | const ask = (createdAt) => ({ body: `${ASK_MARKER}\nstill reproducing?`, createdAt, authorAssociation: "OWNER" }); |
| 20 | |
| 21 | test("the reported version comes from the form field, not from prose", () => { |
| 22 | const body = "### Version line\n\nv2\n\n### Exact version\n\n1.8.1\n\n### What happened?\n\nsaw 1.17.9 mentioned in a log"; |
| 23 | assert.deepEqual(parseReportedVersion(body), { major: 1, minor: 8, patch: 1, raw: "1.8.1" }); |
| 24 | assert.equal(parseReportedVersion("### What happened?\n\n1.2.3"), null); |
| 25 | }); |
| 26 | |
| 27 | test("staleness is a version cutoff, and an unparsable version is never stale", () => { |
| 28 | assert.equal(isStaleVersion(parseReportedVersion("### Exact version\n\n1.8.1")), true); |
| 29 | assert.equal(isStaleVersion(parseReportedVersion("### Exact version\n\n1.17.21")), false); |
| 30 | assert.equal(isStaleVersion(parseReportedVersion("### Exact version\n\n1.10.0")), false); |
| 31 | assert.equal(isStaleVersion(null), false); |
| 32 | }); |
| 33 | |
| 34 | test("a version that was never released is a typo, not an old release", () => { |
| 35 | const released = releasedVersions(["desktop-v1.7.0", "desktop-v1.17.18", "desktop-v1.18.0", "npm-v1.17.18"]); |
| 36 | const typo = parseReportedVersion("### Exact version\n\n1.7.18"); // dropped digit from 1.17.18 |
| 37 | const real = parseReportedVersion("### Exact version\n\n1.7.0"); |
| 38 | assert.equal(isStaleVersion(typo, CUTOFF_MINOR, released), false); |
| 39 | assert.equal(isStaleVersion(real, CUTOFF_MINOR, released), true); |
| 40 | // without the tag list there is nothing to catch it, which is why main passes one |
| 41 | assert.equal(isStaleVersion(typo), true); |
| 42 | }); |
| 43 | |
| 44 | test("a version named elsewhere in the body outranks a wrong form field", () => { |
| 45 | const released = releasedVersions(["desktop-v1.0.0", "desktop-v1.17.13", "desktop-v1.18.0"]); |
| 46 | // real shape: the form field kept its default while the true build is in prose |
| 47 | const body = "### Exact version\n\n1.0.0\n\n### Steps to reproduce\n\nwin64 vscode, 软件版本 v1.17.13"; |
| 48 | assert.equal(parseReportedVersion(body).raw, "1.0.0"); |
| 49 | assert.equal(highestMentionedVersion(body, released).raw, "1.17.13"); |
| 50 | assert.equal(isStaleVersion(highestMentionedVersion(body, released), CUTOFF_MINOR, released), false); |
| 51 | }); |
| 52 | |
| 53 | test("unreleased numbers in the body are ignored, so Node versions cannot age a report", () => { |
| 54 | const released = releasedVersions(["desktop-v1.8.1", "desktop-v1.18.0"]); |
| 55 | const body = "### Exact version\n\n1.8.1\n\n**Node**: v26.3.1\n**Terminal size**: 65.0.0"; |
| 56 | assert.equal(highestMentionedVersion(body, released).raw, "1.8.1"); |
| 57 | assert.equal(highestMentionedVersion("no versions here", released), null); |
| 58 | }); |
| 59 | |
| 60 | test("released versions are collected across every tag series", () => { |
| 61 | const released = releasedVersions(["v1.18.0", "desktop-v1.17.21", "npm-v1.17.21", "not-a-tag", "desktop-v1.7.0"]); |
| 62 | assert.deepEqual([...released].sort(), ["1.17.21", "1.18.0", "1.7.0"]); |
| 63 | }); |
| 64 | |
| 65 | test("only defect reports are swept — a feature request cannot answer the question", () => { |
| 66 | const base = { version: { major: 1, minor: 2, patch: 0, raw: "1.2.0" }, comments: [] }; |
| 67 | assert.equal(shouldAsk({ ...base, labels: ["enhancement", "desktop"] }), false); |
| 68 | assert.equal(shouldAsk({ ...base, labels: [] }), false); |
| 69 | assert.equal(shouldAsk({ ...base, labels: ["bug", "enhancement"] }), true); |
| 70 | }); |
| 71 | |
| 72 | test("severity labels are never swept, however old the report", () => { |
| 73 | const base = { version: { major: 1, minor: 2, patch: 0, raw: "1.2.0" }, comments: [] }; |
| 74 | for (const label of ["data-loss", "security", "crash"]) { |
| 75 | assert.equal(shouldAsk({ ...base, labels: ["bug", label] }), false, label); |
| 76 | } |
| 77 | assert.equal(shouldAsk({ ...base, labels: ["bug", "windows"] }), true); |
| 78 | }); |
| 79 | |
| 80 | test("a maintainer reply takes the report out of the sweep", () => { |
| 81 | const base = { version: { major: 1, minor: 2, patch: 0, raw: "1.2.0" }, labels: ["bug"] }; |
| 82 | const reporter = { body: "me too", createdAt: daysBefore(40), authorAssociation: "NONE" }; |
| 83 | const maintainer = { body: "looking", createdAt: daysBefore(40), authorAssociation: "COLLABORATOR" }; |
| 84 | assert.equal(shouldAsk({ ...base, comments: [reporter] }), true); |
| 85 | assert.equal(shouldAsk({ ...base, comments: [maintainer] }), false); |
| 86 | }); |
| 87 | |
| 88 | test("the same issue is never asked twice", () => { |
| 89 | const issue = { |
| 90 | version: { major: 1, minor: 2, patch: 0, raw: "1.2.0" }, |
| 91 | labels: ["bug"], |
| 92 | comments: [ask(daysBefore(3))], |
| 93 | }; |
| 94 | assert.equal(shouldAsk(issue), false); |
| 95 | }); |
| 96 | |
| 97 | test("closing requires an ask that nobody answered and a window that elapsed", () => { |
| 98 | const labels = ["bug"]; |
| 99 | assert.equal(shouldClose({ labels, comments: [] }, { now: NOW }), false, "never asked"); |
| 100 | assert.equal( |
| 101 | shouldClose({ labels, comments: [ask(daysBefore(WINDOW_DAYS - 1))] }, { now: NOW }), |
| 102 | false, |
| 103 | "window not elapsed", |
| 104 | ); |
| 105 | assert.equal(shouldClose({ labels, comments: [ask(daysBefore(WINDOW_DAYS))] }, { now: NOW }), true); |
| 106 | }); |
| 107 | |
| 108 | test("any reply after the ask cancels the close, whoever wrote it", () => { |
| 109 | const labels = ["bug"]; |
| 110 | const asked = ask(daysBefore(60)); |
| 111 | for (const association of ["NONE", "CONTRIBUTOR", "COLLABORATOR"]) { |
| 112 | const reply = { body: "still broken", createdAt: daysBefore(1), authorAssociation: association }; |
| 113 | assert.equal(shouldClose({ labels, comments: [asked, reply] }, { now: NOW }), false, association); |
| 114 | } |
| 115 | // a comment predating the ask is not an answer to it |
| 116 | const older = { body: "me too", createdAt: daysBefore(90), authorAssociation: "NONE" }; |
| 117 | assert.equal(shouldClose({ labels, comments: [older, asked] }, { now: NOW }), true); |
| 118 | }); |
| 119 | |
| 120 | test("a severity label added after the ask still blocks the close", () => { |
| 121 | const comments = [ask(daysBefore(60))]; |
| 122 | assert.equal(shouldClose({ labels: ["bug"], comments }, { now: NOW }), true); |
| 123 | assert.equal(shouldClose({ labels: ["bug", "data-loss"], comments }, { now: NOW }), false); |
| 124 | }); |
| 125 | |
| 126 | test("the ask states both versions and the deadline; the close invites reopening", () => { |
| 127 | const body = renderAsk({ version: "1.8.1", current: "desktop-v1.18.0" }); |
| 128 | assert.ok(body.startsWith(ASK_MARKER)); |
| 129 | assert.match(body, /1\.8\.1/); |
| 130 | assert.match(body, /desktop-v1\.18\.0/); |
| 131 | assert.match(body, new RegExp(`${WINDOW_DAYS} days`)); |
| 132 | const closed = renderClose({}); |
| 133 | assert.match(closed, /reopens/); |
| 134 | assert.match(closed, new RegExp(`${WINDOW_DAYS} days`)); |
| 135 | // stale is a bookkeeping state; claiming a fix we never verified would be a lie |
| 136 | assert.doesNotMatch(closed, /\bfixed\b|\bresolved\b/i); |
| 137 | }); |
| 138 |