import assert from "node:assert/strict"; import { test } from "node:test"; import { createServer } from "node:http"; // **A pull request opened after its push is still checked** (novox/hq issue 347). The announcer read only the // repositories whose update time moved since its last pass, less a minute's margin; opening, reopening or // retargeting a pull request does not move a repository's update time, so a pull request opened more than a // minute or so after its push was never announced, never checked, and nothing said so. const repo = (full: string, updated: string) => { const [owner, name] = full.split("/"); return { full_name: full, owner, name, private: false, html_url: "", updated_at: updated }; }; test("a pull request opened after the margin is looked at, by its own update time", async () => { const { reposToLook } = await import("../pulls.ts"); const pushed = "2026-10-09T10:00:00Z"; // the branch pushed: the repository's update time // The pull request opened two minutes later; the last pass was at 10:02:30, so the floor is 10:01:30. const floor = "2026-10-09T10:01:30Z"; const repos = [repo("novox/mesh-sdk", pushed), repo("novox/hq", "2026-10-09T09:00:00Z")]; const opened = [{ owner: "novox", name: "mesh-sdk", updated_at: "2026-10-09T10:02:00Z" }]; assert.deepEqual(reposToLook(repos, floor, opened, false).map((r) => r.full_name), ["novox/mesh-sdk"], "the repository of a pull request updated since the floor, though the repository itself did not move"); assert.deepEqual(reposToLook(repos, floor, [], false).map((r) => r.full_name), [], "without the pull request's own time nothing is looked at — the fault"); assert.deepEqual(reposToLook(repos, floor, [], true).map((r) => r.full_name).sort(), ["novox/hq", "novox/mesh-sdk"], "a sweep looks at every repository"); assert.deepEqual(reposToLook(repos, "", [], false).length, 2, "the first pass looks at every repository"); }); test("every open pull request is read with what its base requires and what its head has (gitea_open_pulls)", async () => { const { GiteaClient } = await import("../client.ts"); const asked: string[] = []; const server = createServer((req, res) => { asked.push(req.url ?? ""); const url = req.url ?? ""; const send = (v: unknown) => res.end(JSON.stringify(v)); if (url.startsWith("/api/v1/repos/issues/search")) { return send(url.includes("page=1") ? [ { number: 243, repository: { owner: "novox", name: "hq", full_name: "novox/hq" }, updated_at: "2026-10-09T11:00:00Z" }, { number: 14, repository: { owner: "novox", name: "mesh-sdk", full_name: "novox/mesh-sdk" } }, ] : []); } if (url.startsWith("/api/v1/repos/novox/hq/pulls/243")) { return send({ number: 243, state: "open", base: { ref: "main" }, head: { ref: "x", sha: "aaa1111" }, html_url: "h" }); } if (url.startsWith("/api/v1/repos/novox/mesh-sdk/pulls/14")) { return send({ number: 14, state: "open", base: { ref: "main" }, head: { ref: "y", sha: "bbb2222" }, html_url: "s" }); } if (url.startsWith("/api/v1/repos/novox/hq/branch_protections")) { return send([{ rule_name: "main", enable_status_check: true, status_check_contexts: ["mesh/repo-check"] }]); } if (url.startsWith("/api/v1/repos/novox/mesh-sdk/branch_protections")) { return send([{ rule_name: "main", enable_status_check: true, status_check_contexts: ["mesh/merge-gate", "mesh/repo-check"] }]); } if (url.includes("/commits/aaa1111/status")) return send({ statuses: [] }); if (url.includes("/commits/bbb2222/status")) return send({ statuses: [{ context: "mesh/merge-gate", status: "pending", description: "the mesh is mapping this head onto its module graph", updated_at: "2026-10-11T00:11:39Z" }] }); res.statusCode = 404; res.end("{}"); }); await new Promise((r) => server.listen(0, r)); const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t"); try { const open = await client.openPulls(); assert.deepEqual(open.map((p) => [p.owner, p.repo, p.number, p.base, p.head_sha, p.required, p.statuses]), [ ["novox", "hq", 243, "main", "aaa1111", ["mesh/repo-check"], []], ["novox", "mesh-sdk", 14, "main", "bbb2222", ["mesh/merge-gate", "mesh/repo-check"], ["mesh/merge-gate"]], ]); // Each status with its state and time (novox/hq issue 438): a check pending is told from one that answered. assert.deepEqual(open.map((p) => p.checks), [{}, { "mesh/merge-gate": { state: "pending", description: "the mesh is mapping this head onto its module graph", updated_at: "2026-10-11T00:11:39Z" } }]); } finally { server.close(); } }); test("a pull request or issue number that is not one is refused before the forge is asked", async () => { const { GiteaClient } = await import("../client.ts"); let asked = 0; const server = createServer((_req, res) => { asked++; res.end("{}"); }); await new Promise((r) => server.listen(0, r)); const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t"); try { await assert.rejects(client.getPullRequest("novox", "hq", Number(undefined)), /whole number/); await assert.rejects(client.addComment("novox", "hq", 0, "x"), /whole number/); await assert.rejects(client.listPullFiles("novox", "hq", 1.5), /whole number/); assert.equal(asked, 0, "the forge was asked about pulls/NaN"); } finally { server.close(); } }); // **A merge names the branch it came from, also once that branch is deleted** (novox/hq issue 578). On 2026-10-12 // the merge tool merged mesh-controller #271 and mesh-catalog #251, one delivery group, and deleted each branch. The // forge then read each pull request's head.ref as `refs/pull//head`, and `pull.merged` named that as the head: the // controller asked mesh-delivery for a group of that name, found none, and cut #251's walk at once, before #271's, // which the group's order put first. test("a merged pull request whose branch was deleted is announced with its branch, not its pull ref", async () => { const { GiteaClient, headBranchOf } = await import("../client.ts"); const group = "feat/341-a-merge-check-judges-the-merge"; // As the forge answered for #251 after its merge. const merged = { number: 251, title: "A verdict goes out of date when its trunk moves", state: "closed", merged: true, merged_at: "2026-10-12T16:11:29Z", merge_commit_sha: "23ea0f431faf91aa83e7b0297e0a9e442180f0c7", head: { label: group, ref: "refs/pull/251/head", sha: "41b6e10077efdef694b519879fcbaeaf69d472b1" }, base: { label: "main", ref: "main" }, html_url: "http://fake/novox/mesh-catalog/pulls/251", }; const server = createServer((req, res) => { const url = req.url ?? ""; const send = (v: unknown) => res.end(JSON.stringify(v)); if (url.startsWith("/api/v1/repos/novox/mesh-catalog/pulls/251")) return send(merged); if (url.startsWith("/api/v1/repos/novox/mesh-catalog/pulls?")) return send([merged]); res.statusCode = 404; res.end("{}"); }); await new Promise((r) => server.listen(0, r)); const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t"); try { // What the announcer reads each pass, and what `pull.merged` carries as its head. const listed = await client.listPullRequests("novox", "mesh-catalog", { state: "closed", sort: "recentupdate", limit: "20" }); assert.deepEqual(listed.map((p) => p.head), [group], "the pass names the group's branch, never refs/pull/251/head"); assert.equal((await client.getPullRequest("novox", "mesh-catalog", 251)).head, group); } finally { server.close(); } // While the branch exists the forge says it as ref, and that is what is named; a label alone is the branch too. assert.equal(headBranchOf({ label: "feat/x", ref: "feat/x" }), "feat/x"); assert.equal(headBranchOf({ label: "feat/x" }), "feat/x"); assert.equal(headBranchOf({ ref: "feat/x" }), "feat/x"); // A branch whose own name only looks like a pull ref's start is still a branch. assert.equal(headBranchOf({ label: "y", ref: "refs/pulled" }), "refs/pulled"); // No label to read: the pull ref is all the forge said, and it is kept rather than made up. assert.equal(headBranchOf({ ref: "refs/pull/7/head" }), "refs/pull/7/head"); assert.equal(headBranchOf(undefined), undefined); });