| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | FALLBACK_FOCUS_MS, |
| 6 | FALLBACK_POLL_MS, |
| 7 | LIVE_DEBOUNCE_MS, |
| 8 | LIVE_HOLD_CHECK_MS, |
| 9 | LIVE_MAX_WAIT_MS, |
| 10 | LIVE_MIN_GAP_MS, |
| 11 | type RepoChange, |
| 12 | atBottom, |
| 13 | branchOf, |
| 14 | fallbackDue, |
| 15 | gate, |
| 16 | isTyping, |
| 17 | newCommits, |
| 18 | repoPages, |
| 19 | tagOf, |
| 20 | } from "./repo-live.ts"; |
| 21 | |
| 22 | const AT = "2026-10-11T10:00:00Z"; |
| 23 | const NOW = Date.parse(AT); |
| 24 | |
| 25 | function change(kind: string, more: Partial<RepoChange> = {}): RepoChange { |
| 26 | return { repo_id: "rep_1", repo: "acme/rocket", kind, at: AT, ...more }; |
| 27 | } |
| 28 | |
| 29 | const push = (branch: string, commits?: number) => change("git.push", { ref: `refs/heads/${branch}`, sha: "abc", commits: commits ?? null }); |
| 30 | |
| 31 | test("refs name their branch or tag", () => { |
| 32 | assert.equal(branchOf("refs/heads/feature/x"), "feature/x"); |
| 33 | assert.equal(branchOf("refs/tags/v1"), null); |
| 34 | assert.equal(tagOf("refs/tags/v1.2"), "v1.2"); |
| 35 | assert.equal(tagOf(null), null); |
| 36 | }); |
| 37 | |
| 38 | test("a project's overview follows what it runs, who is working on it and what may need someone, not every push", () => { |
| 39 | const overview = repoPages.overview(); |
| 40 | for (const c of [ |
| 41 | change("deployment_status.created"), |
| 42 | change("status.created", { sha: "abc" }), |
| 43 | change("release.published"), |
| 44 | change("work.claimed", { number: 3 }), |
| 45 | change("session.started"), |
| 46 | change("session.progressed"), |
| 47 | change("pull.ready", { number: 4 }), |
| 48 | change("review.completed", { number: 4 }), |
| 49 | ]) { |
| 50 | assert.equal(overview(c), true, c.kind); |
| 51 | } |
| 52 | assert.equal(overview(push("feature")), false); |
| 53 | assert.equal(overview(change("comment.created", { number: 3 })), false); |
| 54 | }); |
| 55 | |
| 56 | test("the overview hears its own commit's checks, and every pull request's finished ones", () => { |
| 57 | const overview = repoPages.overview("abc"); |
| 58 | assert.equal(overview(change("status.created", { sha: "abc" })), true); |
| 59 | assert.equal(overview(change("run.updated", { sha: "abc", run: "run_1" })), true); |
| 60 | assert.equal(overview(change("status.created", { sha: "f00d" })), false, "another commit's"); |
| 61 | assert.equal(overview(change("run.updated", { sha: "f00d", run: "run_2" })), false); |
| 62 | assert.equal(overview(change("checks.completed", { sha: "f00d", number: 4 })), true, "how often checks pass"); |
| 63 | assert.equal(overview(change("status.created")), true, "a check that names no commit may be its"); |
| 64 | }); |
| 65 | |
| 66 | test("the files page follows refs, checks, releases and deployments", () => { |
| 67 | const files = repoPages.files(); |
| 68 | for (const c of [push("main"), push("other"), change("git.deleted", { ref: "refs/heads/old" }), change("status.created", { sha: "abc" }), change("release.published"), change("deployment_status.created")]) { |
| 69 | assert.equal(files(c), true, c.kind); |
| 70 | } |
| 71 | assert.equal(files(change("issue.opened", { number: 3 })), false); |
| 72 | assert.equal(files(change("comment.created", { number: 3 })), false); |
| 73 | }); |
| 74 | |
| 75 | test("a page of files hears its own branch and its commit's checks, not every branch's", () => { |
| 76 | const files = repoPages.files({ branch: "main", branches: ["main", "feature"], sha: "abc" }); |
| 77 | assert.equal(files(push("main")), true, "its files and latest commit"); |
| 78 | assert.equal(files(push("feature")), false, "another branch moving changes nothing here"); |
| 79 | assert.equal(files(push("brand-new")), true, "a new branch: the menu and count"); |
| 80 | assert.equal(files(change("git.deleted", { ref: "refs/heads/feature" })), true, "one of the menu's gone"); |
| 81 | assert.equal(files(change("git.deleted", { ref: "refs/heads/never-listed" })), false); |
| 82 | assert.equal(files(change("git.push", { ref: "refs/tags/v2" })), true, "the About's tags"); |
| 83 | assert.equal(files(change("branch.renamed", { ref: "refs/heads/feature" })), true); |
| 84 | assert.equal(files(change("repo.default_branch_changed")), true); |
| 85 | assert.equal(files(change("status.created", { sha: "abc" })), true, "its latest commit's checks"); |
| 86 | assert.equal(files(change("check_run.completed", { sha: "f00d" })), false, "another commit's checks"); |
| 87 | assert.equal(files(change("run.updated", { run: "run_1" })), true, "a check that names no commit may be its"); |
| 88 | assert.equal(files(change("deployment_status.created")), true); |
| 89 | assert.equal(files(change("release.published")), true); |
| 90 | // A tag or a commit shown: no branch of its own moves it. |
| 91 | const tag = repoPages.files({ branch: null, branches: ["main"], sha: "abc" }); |
| 92 | assert.equal(tag(push("main")), false); |
| 93 | // Without the branches, any branch may be new: only its own and deletions count. |
| 94 | const unknown = repoPages.files({ branch: "main", branches: null, sha: "abc" }); |
| 95 | assert.equal(unknown(push("other")), false); |
| 96 | assert.equal(unknown(change("git.deleted", { ref: "refs/heads/other" })), true); |
| 97 | }); |
| 98 | |
| 99 | test("the commits page hears pushes to its own branch only", () => { |
| 100 | const commits = repoPages.commits("main"); |
| 101 | assert.equal(commits(push("main")), true); |
| 102 | assert.equal(commits(push("feature")), false); |
| 103 | assert.equal(commits(change("git.push", { ref: "refs/tags/v1" })), false); |
| 104 | assert.equal(commits(change("workflow.completed", { sha: "abc", run: "run_1" })), true, "checks on its commits"); |
| 105 | assert.equal(commits(change("pull.opened", { number: 1 })), false); |
| 106 | const shown = repoPages.commits("main", ["abc", "def"]); |
| 107 | assert.equal(shown(change("status.created", { sha: "def" })), true, "checks on a commit it lists"); |
| 108 | assert.equal(shown(change("status.created", { sha: "f00d" })), false, "not on one it does not"); |
| 109 | }); |
| 110 | |
| 111 | test("branches, tags, and the lists of pull requests and issues", () => { |
| 112 | assert.equal(repoPages.refs()(push("x")), true); |
| 113 | assert.equal(repoPages.refs()(change("pull.opened", { number: 1 })), true); |
| 114 | assert.equal(repoPages.refs()(change("issue.opened", { number: 1 })), false); |
| 115 | assert.equal(repoPages.tags()(change("git.push", { ref: "refs/tags/v2" })), true); |
| 116 | assert.equal(repoPages.tags()(push("main")), false); |
| 117 | assert.equal(repoPages.tags()(change("release.published")), true); |
| 118 | assert.equal(repoPages.pulls()(change("pull.updated", { number: 4 })), true); |
| 119 | assert.equal(repoPages.pulls()(change("review.completed", { number: 4 })), true); |
| 120 | assert.equal(repoPages.pulls()(change("issue.opened", { number: 5 })), false); |
| 121 | assert.equal(repoPages.issues()(change("issue.closed", { number: 5 })), true); |
| 122 | assert.equal(repoPages.issues()(change("comment.created", { number: 5 })), true); |
| 123 | assert.equal(repoPages.issues()(change("pull.merged", { number: 6 })), true, "a merge may close issues"); |
| 124 | assert.equal(repoPages.issues()(push("main")), false); |
| 125 | }); |
| 126 | |
| 127 | test("a pull request hears its own changes, its head branch and its head's checks", () => { |
| 128 | const pull = repoPages.pull(7, { branch: "feature", sha: "f00d" }); |
| 129 | assert.equal(pull(change("pull.updated", { number: 7 })), true); |
| 130 | assert.equal(pull(change("comment.created", { number: 7 })), true); |
| 131 | assert.equal(pull(change("review.completed", { number: 7 })), true); |
| 132 | assert.equal(pull(change("checks.completed", { number: 7, sha: "f00d" })), true); |
| 133 | assert.equal(pull(change("pull.merged", { number: 7 })), true); |
| 134 | assert.equal(pull(push("feature")), true, "new commits"); |
| 135 | assert.equal(pull(change("status.created", { sha: "f00d" })), true); |
| 136 | assert.equal(pull(change("run.updated", { run: "run_1", number: 7 })), true, "a run for it"); |
| 137 | assert.equal(pull(change("comment.created", { number: 8 })), false); |
| 138 | assert.equal(pull(push("main")), false); |
| 139 | assert.equal(pull(change("status.created", { sha: "beef" })), false); |
| 140 | }); |
| 141 | |
| 142 | test("an issue hears its own changes and its linked pull requests", () => { |
| 143 | const issue = repoPages.issue(3, [9]); |
| 144 | assert.equal(issue(change("comment.created", { number: 3 })), true); |
| 145 | assert.equal(issue(change("issue.closed", { number: 3 })), true); |
| 146 | assert.equal(issue(change("pull.merged", { number: 9 })), true); |
| 147 | assert.equal(issue(change("comment.created", { number: 4 })), false); |
| 148 | assert.equal(issue(push("main")), false); |
| 149 | }); |
| 150 | |
| 151 | test("the Work page hears claims, sessions starting and ending, what the queue finds, and merges", () => { |
| 152 | const page = repoPages.work(); |
| 153 | for (const kind of ["work.claimed", "work.claim_expired", "session.started", "session.ended", "dependency.changed", "overlap.detected", "integration.failed", "decision.recorded", "queue.changed", "pull.merged"]) { |
| 154 | assert.equal(page(change(kind, { number: 3 })), true, kind); |
| 155 | } |
| 156 | assert.equal(page(change("session.progressed")), true, "a session going on"); |
| 157 | assert.equal(page(change("session.appended", { number: 3 })), false); |
| 158 | assert.equal(page(change("comment.created", { number: 3 })), false); |
| 159 | assert.equal(page(push("main")), false); |
| 160 | // An issue hears coordination about itself and its pull requests by number. |
| 161 | assert.equal(repoPages.issue(3, [9])(change("integration.failed", { number: 9 })), true); |
| 162 | }); |
| 163 | |
| 164 | test("a session going on reaches the issue and pull request it is on, and no other", () => { |
| 165 | assert.equal(repoPages.issue(3, [9])(change("session.progressed", { number: 3 })), true); |
| 166 | assert.equal(repoPages.issue(3, [9])(change("session.progressed", { number: 9 })), true); |
| 167 | assert.equal(repoPages.issue(3)(change("session.progressed", { number: 4 })), false); |
| 168 | assert.equal(repoPages.issue(3)(change("session.progressed")), false); |
| 169 | assert.equal(repoPages.pull(9)(change("session.progressed", { number: 9 })), true); |
| 170 | assert.equal(repoPages.pull(9)(change("session.progressed", { number: 3 })), false); |
| 171 | assert.equal(repoPages.pull(9)(change("session.progressed")), false); |
| 172 | }); |
| 173 | |
| 174 | test("a session's page hears every session, claim and overlap in the repository", () => { |
| 175 | const page = repoPages.session(); |
| 176 | for (const kind of ["session.started", "session.progressed", "session.ended", "work.claimed", "work.released", "overlap.detected"]) { |
| 177 | assert.equal(page(change(kind)), true, kind); |
| 178 | } |
| 179 | assert.equal(page(change("comment.created", { number: 3 })), false); |
| 180 | assert.equal(page(change("pull.merged", { number: 3 })), false); |
| 181 | assert.equal(page(push("main")), false); |
| 182 | }); |
| 183 | |
| 184 | test("runs, one run, and security", () => { |
| 185 | assert.equal(repoPages.runs()(change("run.updated", { run: "run_1" })), true); |
| 186 | assert.equal(repoPages.runs()(change("status.created", { sha: "abc" })), true, "a new run reports pending on its commit"); |
| 187 | assert.equal(repoPages.runs()(change("issue.opened", { number: 1 })), false); |
| 188 | const run = repoPages.run("run_1", "abc"); |
| 189 | assert.equal(run(change("run.updated", { run: "run_1" })), true); |
| 190 | assert.equal(run(change("workflow.completed", { run: "run_1" })), true); |
| 191 | assert.equal(run(change("run.updated", { run: "run_2" })), false); |
| 192 | assert.equal(run(change("status.created", { sha: "abc" })), true); |
| 193 | assert.equal(repoPages.security()(change("secret_scanning_alert.created")), true); |
| 194 | assert.equal(repoPages.security()(change("code_scanning_alert.fixed", { number: 2 })), true); |
| 195 | assert.equal(repoPages.security()(change("issue.opened", { number: 1 })), false); |
| 196 | }); |
| 197 | |
| 198 | test("new commits are counted, and uncounted when a push was not", () => { |
| 199 | assert.equal(newCommits([push("main", 2), push("main", 3), push("other", 9)], "main"), 5); |
| 200 | assert.equal(newCommits([push("main", 2), push("main")], "main"), null); |
| 201 | assert.equal(newCommits([change("status.created")], "main"), 0); |
| 202 | }); |
| 203 | |
| 204 | test("a change waits a second for more, five at most, then refreshes", () => { |
| 205 | const idle = { now: NOW, visible: true, busy: false, navigating: false }; |
| 206 | assert.deepEqual(gate({ pendingSince: null, lastChangeAt: null, lastRunAt: null }, idle), { action: "idle" }); |
| 207 | assert.deepEqual(gate({ pendingSince: NOW, lastChangeAt: NOW, lastRunAt: null }, idle), { action: "wait", ms: LIVE_DEBOUNCE_MS }); |
| 208 | assert.deepEqual(gate({ pendingSince: NOW, lastChangeAt: NOW, lastRunAt: null }, { ...idle, now: NOW + LIVE_DEBOUNCE_MS }), { action: "run" }); |
| 209 | // A steady stream: refreshed after five seconds anyway. |
| 210 | const stream = { pendingSince: NOW, lastChangeAt: NOW + LIVE_MAX_WAIT_MS - 100, lastRunAt: null }; |
| 211 | assert.deepEqual(gate(stream, { ...idle, now: NOW + LIVE_MAX_WAIT_MS - 50 }), { action: "wait", ms: 50 }); |
| 212 | assert.deepEqual(gate(stream, { ...idle, now: NOW + LIVE_MAX_WAIT_MS }), { action: "run" }); |
| 213 | }); |
| 214 | |
| 215 | test("never under someone typing, a dialog, or a navigation: it waits for them", () => { |
| 216 | const due = { pendingSince: NOW - 10_000, lastChangeAt: NOW - 10_000, lastRunAt: null }; |
| 217 | for (const held of [{ busy: true, navigating: false }, { busy: false, navigating: true }]) { |
| 218 | assert.deepEqual(gate(due, { now: NOW, visible: true, ...held }), { action: "wait", ms: LIVE_HOLD_CHECK_MS }); |
| 219 | } |
| 220 | assert.deepEqual(gate(due, { now: NOW, visible: true, busy: false, navigating: false }), { action: "run" }); |
| 221 | }); |
| 222 | |
| 223 | test("a burst of changes refreshes at most every two seconds", () => { |
| 224 | const idle = { now: NOW, visible: true, busy: false, navigating: false }; |
| 225 | // Quiet for a second, but the page refreshed half a second ago: it waits out the gap. |
| 226 | const justRan = { pendingSince: NOW - LIVE_DEBOUNCE_MS, lastChangeAt: NOW - LIVE_DEBOUNCE_MS, lastRunAt: NOW - 500 }; |
| 227 | assert.deepEqual(gate(justRan, idle), { action: "wait", ms: LIVE_MIN_GAP_MS - 500 }); |
| 228 | assert.deepEqual(gate(justRan, { ...idle, now: NOW - 500 + LIVE_MIN_GAP_MS }), { action: "run" }); |
| 229 | // A steady stream after a refresh: one more at most five seconds on, never sooner than the gap. |
| 230 | const stream = { pendingSince: NOW - LIVE_MAX_WAIT_MS, lastChangeAt: NOW, lastRunAt: NOW - LIVE_MAX_WAIT_MS - 10 }; |
| 231 | assert.deepEqual(gate(stream, idle), { action: "run" }); |
| 232 | // Changes arriving every 100 ms for a minute refresh no more than once per gap. |
| 233 | let state = { pendingSince: null as number | null, lastChangeAt: null as number | null, lastRunAt: NOW }; |
| 234 | let runs = 0; |
| 235 | for (let t = NOW; t < NOW + 60_000; t += 100) { |
| 236 | state = { ...state, pendingSince: state.pendingSince ?? t, lastChangeAt: t }; |
| 237 | if (gate(state, { ...idle, now: t }).action === "run") { |
| 238 | runs++; |
| 239 | state = { pendingSince: null, lastChangeAt: null, lastRunAt: t }; |
| 240 | } |
| 241 | } |
| 242 | assert.ok(runs <= 60_000 / LIVE_MIN_GAP_MS, `${runs} runs`); |
| 243 | assert.ok(runs >= 60_000 / LIVE_MAX_WAIT_MS - 1, `${runs} runs: still kept up to date`); |
| 244 | }); |
| 245 | |
| 246 | test("a tab out of view never refreshes; back in view, it catches up at once", () => { |
| 247 | const hidden = { now: NOW, visible: false, busy: false, navigating: false }; |
| 248 | const waiting = { pendingSince: NOW - 5000, lastChangeAt: NOW - 5000, lastRunAt: NOW - 20 * 60_000 }; |
| 249 | assert.deepEqual(gate(waiting, hidden), { action: "hold" }, "no timer: coming back asks again"); |
| 250 | assert.deepEqual(gate({ pendingSince: null, lastChangeAt: null, lastRunAt: null }, hidden), { action: "idle" }); |
| 251 | assert.deepEqual(gate(waiting, { ...hidden, visible: true }), { action: "run" }); |
| 252 | }); |
| 253 | |
| 254 | test("without the socket: back in view every 30 s at most, a poll every minute in view, never with it", () => { |
| 255 | const base = { live: false, visible: true, now: NOW }; |
| 256 | assert.equal(fallbackDue({ ...base, lastRunAt: null, reason: "focus" }), true); |
| 257 | assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_FOCUS_MS + 1, reason: "focus" }), false); |
| 258 | assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_FOCUS_MS, reason: "focus" }), true); |
| 259 | assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_FOCUS_MS, reason: "poll" }), false); |
| 260 | assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_POLL_MS, reason: "poll" }), true); |
| 261 | assert.equal(fallbackDue({ ...base, visible: false, lastRunAt: null, reason: "poll" }), false, "a hidden tab is not polled"); |
| 262 | assert.equal(fallbackDue({ ...base, live: true, lastRunAt: null, reason: "focus" }), false); |
| 263 | }); |
| 264 | |
| 265 | test("typing is in text boxes, text areas and editors", () => { |
| 266 | assert.equal(isTyping({ tagName: "TEXTAREA" }), true); |
| 267 | assert.equal(isTyping({ tagName: "input", type: "text" }), true); |
| 268 | assert.equal(isTyping({ tagName: "INPUT", type: "search" }), true); |
| 269 | assert.equal(isTyping({ tagName: "INPUT" }), true, "an input is text unless it says otherwise"); |
| 270 | assert.equal(isTyping({ tagName: "DIV", isContentEditable: true }), true); |
| 271 | assert.equal(isTyping({ tagName: "INPUT", type: "checkbox" }), false); |
| 272 | assert.equal(isTyping({ tagName: "BUTTON" }), false); |
| 273 | assert.equal(isTyping({ tagName: "TEXTAREA", readOnly: true }), false); |
| 274 | assert.equal(isTyping(null), false); |
| 275 | }); |
| 276 | |
| 277 | test("the bottom of the page, with some slack", () => { |
| 278 | assert.equal(atBottom({ top: 880, height: 2000, viewport: 1000 }), true); |
| 279 | assert.equal(atBottom({ top: 500, height: 2000, viewport: 1000 }), false); |
| 280 | }); |