import assert from "node:assert/strict"; import { test } from "node:test"; import { createServer } from "node:http"; // **Every open pull request's new head is announced, however many are open** (novox/hq issue 579). A pass asked the // forge for the 20 most recently updated open pull requests of a repository, so a new head on the 21st or later was // not announced and its merge check never started. A pass now pages through the open pull requests updated since its // last look — every page on a sweep or the first pass — and stops at the first page that reaches back past the floor. const HOUR = 3600_000; const T0 = Date.parse("2026-10-12T12:00:00Z"); const sha = (n: number, v: string) => `${v}${String(n).padStart(4, "0")}`.padEnd(40, "0"); /** A forge holding `count` open pull requests of novox/hq, #n updated n hours after T0 (#count the newest), paged and * sorted the way gitea answers `sort=recentupdate`: by update time, newest first. */ function forge(count: number, heads: Record) { const asked: URLSearchParams[] = []; const server = createServer((req, res) => { const url = new URL(req.url ?? "", "http://x"); if (url.pathname !== "/api/v1/repos/novox/hq/pulls") { res.statusCode = 404; return res.end("{}"); } asked.push(url.searchParams); const limit = Number(url.searchParams.get("limit") ?? 30); const page = Number(url.searchParams.get("page") ?? 1); const all = Array.from({ length: count }, (_, i) => count - i).map((n) => ({ number: n, state: "open", base: { ref: "main" }, head: { ref: `b${n}`, sha: heads[n] ?? sha(n, "a") }, updated_at: new Date(T0 + n * HOUR).toISOString(), html_url: `h/${n}`, })); res.end(JSON.stringify(all.slice((page - 1) * limit, page * limit))); }); return { server, asked }; } async function serve(count: number, heads: Record) { const { GiteaClient } = await import("../client.ts"); const f = forge(count, heads); await new Promise((r) => f.server.listen(0, r)); const client = new GiteaClient(`http://127.0.0.1:${(f.server.address() as any).port}`, "t"); return { client, ...f }; } test("a new head beyond the newest 20 open pull requests is announced, exactly once", async () => { const { headsToAnnounce } = await import("../pulls.ts"); // 25 open, every head announced before; #3 — the 23rd most recently updated — got a new head. const announced: Record = {}; for (let n = 1; n <= 25; n++) announced[`novox/hq#${n}`] = sha(n, "a"); const { client, server, asked } = await serve(25, { 3: sha(3, "b") }); try { // The fault: the newest 20 alone do not hold it. const newest = await client.listPullRequests("novox", "hq", { state: "open", sort: "recentupdate", limit: "20" }); assert.deepEqual(headsToAnnounce("novox/hq", newest, announced), [], "the newest 20 miss #3's new head"); asked.length = 0; const said: string[] = []; for (let pass = 0; pass < 2; pass++) { // The sweep, as index.ts's pollPulls asks: no floor, every page. const open = await client.listOpenPullsSince("novox", "hq", ""); for (const p of headsToAnnounce("novox/hq", open, announced)) { said.push(`#${p.number}@${p.head_sha}`); announced[`novox/hq#${p.number}`] = String(p.head_sha); } } assert.deepEqual(said, [`#3@${sha(3, "b")}`], "the new head past the newest 20, announced once over two passes"); assert.ok(asked.every((q) => q.get("state") === "open" && q.get("sort") === "recentupdate"), "asked newest first"); } finally { server.close(); } }); test("a pass pages until it reaches back past its floor, and no further", async () => { // 130 open pull requests, #n updated n hours after T0: page 1 holds #130–#81, page 2 #80–#31, page 3 #30–#1. const { client, server, asked } = await serve(130, {}); try { // A floor between #60 and #61: page 2 reaches back past it, so page 3 is never asked. const floor = new Date(T0 + 60.5 * HOUR).toISOString(); const open = await client.listOpenPullsSince("novox", "hq", floor); assert.deepEqual(asked.map((q) => q.get("page")), ["1", "2"]); assert.equal(open.length, 100); assert.ok(open.some((p) => p.number === 61), "every one updated since the floor is read"); // A floor no page reaches past: the last short page ends it. asked.length = 0; assert.equal((await client.listOpenPullsSince("novox", "hq", "")).length, 130); assert.deepEqual(asked.map((q) => q.get("page")), ["1", "2", "3"]); // Every page holds fewer than 50 updated since a recent floor: one page. asked.length = 0; await client.listOpenPullsSince("novox", "hq", new Date(T0 + 129 * HOUR).toISOString()); assert.deepEqual(asked.map((q) => q.get("page")), ["1"]); } finally { server.close(); } }); // **One forge request never holds the others, and a file too big for an answer is not downloaded** (novox/hq issue 1577): a // request for a 4.9 MB file never ended and every read through the module timed out behind it. test("a request the forge never answers is cut at the limit and says which request", async () => { const { GiteaClient } = await import("../client.ts"); const server = createServer((req) => { if (!req.url?.includes("fast")) return; }); await new Promise((r) => server.listen(0, r)); const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t"); client.requestTimeoutMs = 150; try { const started = Date.now(); await assert.rejects(client.api("/repos/novox/hq/pulls"), /Gitea API GET \/repos\/novox\/hq\/pulls: no answer in 0\.15 s/); assert.ok(Date.now() - started < 3000); } finally { server.closeAllConnections(); await new Promise((r) => server.close(() => r())); } }); test("a file over 512 KiB is refused from its directory listing, and its body is never asked for", async () => { const { GiteaClient } = await import("../client.ts"); const asked: string[] = []; const server = createServer((req, res) => { asked.push(req.url ?? ""); res.setHeader("Content-Type", "application/json"); if (req.url?.startsWith("/api/v1/repos/novox/hq/contents/bin?")) { return res.end(JSON.stringify([{ path: "bin/big", type: "file", size: 5_000_000 }, { path: "bin/small", type: "file", size: 5 }])); } if (req.url?.startsWith("/api/v1/repos/novox/hq/contents/bin/small")) { return res.end(JSON.stringify({ type: "file", encoding: "base64", sha: "s", size: 5, content: Buffer.from("hello").toString("base64") })); } 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 { await assert.rejects(client.getFile("novox", "hq", "bin/big", "main"), /the file is 5000000 bytes, over the 512 KiB a tool answer can carry; read it from a clone/); assert.ok(!asked.some((u) => u.includes("/contents/bin/big")), asked.join("\n")); assert.equal((await client.getFile("novox", "hq", "bin/small", "main")).content, "hello"); } finally { await new Promise((r) => server.close(() => r())); } });