| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | type Merged, |
| 6 | placePushes, |
| 7 | historyCovers, |
| 8 | change, |
| 9 | checksFact, |
| 10 | dayKey, |
| 11 | isTestFile, |
| 12 | landedToday, |
| 13 | mergedAutomatically, |
| 14 | parseSort, |
| 15 | parseTab, |
| 16 | pullFacts, |
| 17 | reachesBack, |
| 18 | reasonFor, |
| 19 | signedPercent, |
| 20 | sortRows, |
| 21 | summaryLine, |
| 22 | pushedByPerson, |
| 23 | pushedCommits, |
| 24 | weekOf, |
| 25 | whyFor, |
| 26 | } from "./mission-control.ts"; |
| 27 | |
| 28 | const HOUR = 3_600_000; |
| 29 | const DAY = 24 * HOUR; |
| 30 | // Monday 2026-10-05, 18:00 UTC. |
| 31 | const NOW = Date.parse("2026-10-05T18:00:00Z"); |
| 32 | const repo = { namespace: "acme", name: "web" }; |
| 33 | |
| 34 | test("each kind of need gets its reason chip", () => { |
| 35 | assert.equal(reasonFor({ kind: "deploy" }), "blocking"); |
| 36 | assert.equal(reasonFor({ kind: "review" }), "needs_review"); |
| 37 | assert.equal(reasonFor({ kind: "invitation" }), "invited"); |
| 38 | assert.equal(reasonFor({ kind: "checks" }), "checks_failing"); |
| 39 | assert.equal(reasonFor({ kind: "runner" }), "stalled"); |
| 40 | }); |
| 41 | |
| 42 | test("why a need needs a person is said for every reason", () => { |
| 43 | for (const kind of ["deploy", "invitation", "review", "stuck", "checks", "ready", "conflict", "stalled", "runner"] as const) { |
| 44 | const need = { kind }; |
| 45 | assert.ok(whyFor(reasonFor(need), need).length > 15, kind); |
| 46 | } |
| 47 | }); |
| 48 | |
| 49 | test("what is known lists only what is known", () => { |
| 50 | const facts = pullFacts({ |
| 51 | checkStatus: "failed", |
| 52 | files: [ |
| 53 | { path: "src/retry.ts", additions: 40, deletions: 2 }, |
| 54 | { path: "src/retry.test.ts", additions: 60, deletions: 0 }, |
| 55 | ], |
| 56 | }); |
| 57 | const by = Object.fromEntries(facts.map((f) => [f.label, f])); |
| 58 | assert.equal(by["Required checks"].value, "Failed in the merge queue"); |
| 59 | assert.equal(by["Required checks"].tone, "bad"); |
| 60 | assert.equal(by["Files changed"].value, "2"); |
| 61 | assert.equal(by.Lines.value, "+100 −2"); |
| 62 | assert.equal(by.Tests.value, "1 file"); |
| 63 | |
| 64 | const bare = pullFacts({ checkStatus: null, files: [] }); |
| 65 | assert.deepEqual(bare, [{ label: "Required checks", value: "Not known yet", tone: null }]); |
| 66 | }); |
| 67 | |
| 68 | test("the required checks are read from the pull request's check status", () => { |
| 69 | assert.deepEqual(checksFact("running"), { label: "Required checks", value: "Running", tone: null }); |
| 70 | assert.equal(checksFact("passed").tone, "good"); |
| 71 | assert.equal(checksFact("failed").value, "Failed in the merge queue"); |
| 72 | }); |
| 73 | |
| 74 | test("test files are recognised by the names test runners use", () => { |
| 75 | assert.ok(isTestFile("src/a.test.ts")); |
| 76 | assert.ok(isTestFile("src/__tests__/a.ts")); |
| 77 | assert.ok(isTestFile("tests/api.rs")); |
| 78 | assert.ok(isTestFile("pkg/store_test.go")); |
| 79 | assert.ok(!isTestFile("src/testing-utils.ts")); |
| 80 | assert.ok(!isTestFile("src/contest.ts")); |
| 81 | assert.ok(isTestFile("test.js")); |
| 82 | assert.ok(isTestFile("lib/spec.rb")); |
| 83 | }); |
| 84 | |
| 85 | const merged = (daysAgo: number, mergedBy: string | null, number = 1): Merged => ({ |
| 86 | repo, |
| 87 | number, |
| 88 | title: `Change ${number}`, |
| 89 | mergedBy, |
| 90 | mergedAt: new Date(NOW - daysAgo * DAY).toISOString(), |
| 91 | files: [], |
| 92 | }); |
| 93 | |
| 94 | test("a push to the default branch counts a person's own commits, not merges or g1t's", () => { |
| 95 | const c = (hash: string, email: string, parents = 1) => ({ hash, author: { name: "g1t", email }, parents: Array(parents).fill("p") }); |
| 96 | // Every commit is named "g1t": the name decides nothing, the address does. |
| 97 | const history = [c("e", "chase@example.com"), c("d", "agent@g1t.sh"), c("m", "chase@example.com", 2), c("b", "chase@example.com"), c("a", "chase@example.com")]; |
| 98 | const byG1t = (commit: { author: { email: string } }) => commit.author.email === "agent@g1t.sh"; |
| 99 | assert.deepEqual(pushedCommits(history, "a", byG1t).map((x) => x.hash), ["e", "b"]); |
| 100 | // Where it pointed before was not read: everything read counts. |
| 101 | assert.deepEqual(pushedCommits(history, "zz", byG1t).map((x) => x.hash), ["e", "b", "a"]); |
| 102 | }); |
| 103 | |
| 104 | test("who pushed is the account that signed in, not the name on the commits", () => { |
| 105 | assert.ok(pushedByPerson({ actor: "usr_chase", data: {} })); |
| 106 | assert.ok(pushedByPerson({ actor: null, data: {} })); |
| 107 | assert.ok(!pushedByPerson({ actor: "usr_g1t_agent", data: {} })); |
| 108 | assert.ok(!pushedByPerson({ actor: "g1t_policy", data: {} })); |
| 109 | // A workflow job's own token is not a person either. |
| 110 | assert.ok(!pushedByPerson({ actor: "usr_chase", data: { causedByJob: "run_1" } })); |
| 111 | |
| 112 | const c = (hash: string, name: string) => ({ hash, parents: ["p"], author: { name, email: `${hash}@example.com` } }); |
| 113 | // A person whose git name is "g1t" pushed m1; g1t pushed w1 under a person's name. |
| 114 | const history = [c("w1", "Chase Pierce"), c("m1", "g1t"), c("old", "Chase Pierce")]; |
| 115 | const pushes = [ |
| 116 | { time: "2026-10-07T12:00:00Z", actor: "usr_g1t_agent", data: { after: "w1", before: "m1" } }, |
| 117 | { time: "2026-10-05T12:00:00Z", actor: "usr_chase", data: { after: "m1", before: "old" } }, |
| 118 | ]; |
| 119 | assert.deepEqual(placePushes(history, pushes), [{ hash: "m1", at: "2026-10-05T12:00:00Z" }]); |
| 120 | }); |
| 121 | |
| 122 | test("a short read of the branch is enough when it reaches the oldest push", () => { |
| 123 | const h = (...hashes: string[]) => hashes.map((hash) => ({ hash })); |
| 124 | const pushes = [ |
| 125 | { time: "2026-10-07T12:00:00Z", data: { after: "c1", before: "c2" } }, |
| 126 | { time: "2026-10-05T12:00:00Z", data: { after: "c2", before: "c3" } }, |
| 127 | ]; |
| 128 | // Holds the oldest push's before. |
| 129 | assert.ok(historyCovers(h("c1", "c2", "c3"), pushes, 3)); |
| 130 | // Full, and the oldest push reaches past it: read further. |
| 131 | assert.ok(!historyCovers(h("c1", "c2", "x"), pushes, 3)); |
| 132 | // Shorter than asked: the whole branch. |
| 133 | assert.ok(historyCovers(h("c1", "c2"), pushes, 3)); |
| 134 | // The oldest push made the branch: only the whole branch will do. |
| 135 | assert.ok(!historyCovers(h("c1", "c2", "c3"), [{ time: "", data: { after: "c1" } }], 3)); |
| 136 | }); |
| 137 | |
| 138 | test("each push to the default branch places the commits it brought, at its time, from one read of the history", () => { |
| 139 | const c = (hash: string, parents = ["p"], email = "chase@example.com") => ({ hash, parents, author: { name: "Chase Pierce", email } }); |
| 140 | // Newest first: a Wednesday push of two, a Monday push of one, a merge, and g1t's own commit. |
| 141 | const history = [c("w2"), c("w1"), c("m1"), c("merge", ["a", "b"]), c("bot", ["p"], "g1t@users.noreply.g1t.sh"), c("old")]; |
| 142 | const pushes = [ |
| 143 | { time: "2026-10-07T12:00:00Z", actor: "usr_chase", data: { after: "w2", before: "m1" } }, |
| 144 | { time: "2026-10-05T12:00:00Z", actor: "usr_chase", data: { after: "m1", before: "old" } }, |
| 145 | { time: "2026-10-01T12:00:00Z", actor: "usr_chase", data: { after: "gone", before: "older" } }, |
| 146 | ]; |
| 147 | const byG1t = (commit: { author: { email: string } }) => commit.author.email.endsWith("@users.noreply.g1t.sh"); |
| 148 | assert.deepEqual(placePushes(history, pushes, byG1t), [ |
| 149 | { hash: "m1", at: "2026-10-05T12:00:00Z" }, |
| 150 | { hash: "w2", at: "2026-10-07T12:00:00Z" }, |
| 151 | { hash: "w1", at: "2026-10-07T12:00:00Z" }, |
| 152 | ]); |
| 153 | }); |
| 154 | |
| 155 | test("a change merged automatically when g1t merged it", () => { |
| 156 | assert.ok(mergedAutomatically({ mergedBy: "g1t" })); |
| 157 | assert.ok(mergedAutomatically({ mergedBy: null })); |
| 158 | assert.ok(!mergedAutomatically({ mergedBy: "syntaqx" })); |
| 159 | }); |
| 160 | |
| 161 | test("the week is seven days of landed changes, with the week before", () => { |
| 162 | const week = weekOf( |
| 163 | [merged(0, "g1t"), merged(0, "syntaqx"), merged(0, "syntaqx", 2), merged(1, "g1t", 3), merged(1, "g1t"), merged(6, "g1t"), merged(8, "g1t"), merged(10, "alex"), merged(20, "g1t")], |
| 164 | NOW, |
| 165 | "UTC", |
| 166 | ); |
| 167 | assert.equal(week.days.length, 7); |
| 168 | assert.equal(week.days[6].key, "2026-10-05"); |
| 169 | assert.equal(week.days[6].label, "Mon"); |
| 170 | assert.deepEqual( |
| 171 | week.days.map((d) => d.count), |
| 172 | [1, 0, 0, 0, 0, 2, 3], |
| 173 | ); |
| 174 | assert.equal(week.total, 6); |
| 175 | assert.equal(week.previous, 2); |
| 176 | // Not knowing the week before is not the same as nothing in it. |
| 177 | assert.equal(weekOf([merged(0, "g1t")], NOW, "UTC", false).previous, null); |
| 178 | }); |
| 179 | |
| 180 | test("days follow the viewer's time zone", () => { |
| 181 | // 02:00 UTC on the 5th is still the 4th in Los Angeles. |
| 182 | const late = Date.parse("2026-10-05T02:00:00Z"); |
| 183 | assert.equal(dayKey(late, "America/Los_Angeles"), "2026-10-04"); |
| 184 | assert.equal(dayKey(late, "UTC"), "2026-10-05"); |
| 185 | assert.equal(dayKey(late, "Not/AZone"), "2026-10-05"); |
| 186 | const week = weekOf([{ mergedAt: new Date(late).toISOString() }], NOW, "America/Los_Angeles"); |
| 187 | assert.equal(week.days.find((d) => d.key === "2026-10-04")?.count, 1); |
| 188 | }); |
| 189 | |
| 190 | test("the change from last week, and how it reads", () => { |
| 191 | assert.equal(change(12, 10), 0.2); |
| 192 | assert.equal(change(5, 0), null); |
| 193 | assert.equal(change(5, null), null); |
| 194 | assert.equal(signedPercent(0.2), "+20%"); |
| 195 | assert.equal(signedPercent(-0.054), "−5%"); |
| 196 | assert.equal(signedPercent(0.001), "0%"); |
| 197 | }); |
| 198 | |
| 199 | test("a repository's merged list reaches back when it is not full or its oldest is old enough", () => { |
| 200 | const since = NOW - 14 * DAY; |
| 201 | const at = (days: number) => ({ mergedAt: new Date(NOW - days * DAY).toISOString(), updatedAt: new Date(NOW).toISOString() }); |
| 202 | assert.ok(reachesBack([at(1), at(2)], since, 100)); |
| 203 | assert.ok(reachesBack([at(1), at(20)], since, 2)); |
| 204 | assert.ok(!reachesBack([at(1), at(3)], since, 2)); |
| 205 | }); |
| 206 | |
| 207 | test("landed today is the viewer's calendar day, newest first", () => { |
| 208 | const rows = landedToday([merged(0.1, "g1t", 3), merged(0.5, "syntaqx", 4), merged(1, "g1t", 2)], NOW, "UTC"); |
| 209 | assert.deepEqual( |
| 210 | rows.map((r) => [r.ref, r.automatic]), |
| 211 | [ |
| 212 | ["#3", true], |
| 213 | ["#4", false], |
| 214 | ], |
| 215 | ); |
| 216 | assert.equal(rows[1].by?.name, "syntaqx"); |
| 217 | }); |
| 218 | |
| 219 | test("tabs and sorting come from the address", () => { |
| 220 | assert.equal(parseTab("landed"), "landed"); |
| 221 | assert.equal(parseTab("waiting"), null); |
| 222 | assert.equal(parseTab("nope"), null); |
| 223 | assert.equal(parseSort("newest"), "newest"); |
| 224 | assert.equal(parseSort(null), "impact"); |
| 225 | const rows = [{ at: 1 }, { at: 3 }, { at: 2 }]; |
| 226 | assert.deepEqual(sortRows(rows, "impact"), rows); |
| 227 | assert.deepEqual( |
| 228 | sortRows(rows, "newest").map((r) => r.at), |
| 229 | [3, 2, 1], |
| 230 | ); |
| 231 | }); |
| 232 | |
| 233 | test("the summary says the week plainly", () => { |
| 234 | assert.equal(summaryLine({ total: 47, needs: 0 }), "47 changes landed in the last 7 days."); |
| 235 | assert.equal(summaryLine({ total: 1, needs: 2 }), "1 change landed in the last 7 days."); |
| 236 | assert.equal(summaryLine({ total: 0, needs: 2 }), "Nothing has landed this week. What is waiting on you is below."); |
| 237 | assert.equal(summaryLine({ total: 0, needs: 0 }), "Nothing has landed this week yet."); |
| 238 | }); |
| 239 | |
| 240 | test("a job waiting for a self-hosted runner says so", () => { |
| 241 | const runner = { kind: "runner" as const }; |
| 242 | assert.equal(reasonFor(runner), "stalled"); |
| 243 | assert.match(whyFor(reasonFor(runner), runner), /self-hosted runner/); |
| 244 | }); |