import assert from "node:assert/strict"; import { test } from "node:test"; import { createServer } from "node:http"; // **The forge's 405 "Please try again later" on a merge is asked again briefly** (novox/hq issue 1275, 2026-10-26 13:40): // mesh-delivery#73's merge was answered so a few seconds after its base moved, and counted as refused at once. The forge // says it while it works out whether the pull request merges; the merge is asked again after a short wait, and every other // refusal is answered at once. A pull request's own mergeable is carried, so a group can be read whole before it merges. /** A forge whose merge of novox/hq#7 answers each of `answers` in turn (the last for ever), and whose pull request says * `mergeable`. */ async function serve(answers: { status: number; body: string }[], mergeable?: boolean) { const { GiteaClient } = await import("../client.ts"); let merges = 0; const server = createServer((req, res) => { const url = new URL(req.url ?? "", "http://x"); if (url.pathname === "/api/v1/repos/novox/hq/pulls/7/merge" && req.method === "POST") { const a = answers[Math.min(merges++, answers.length - 1)]; res.statusCode = a.status; return res.end(a.body); } if (url.pathname === "/api/v1/repos/novox/hq/pulls/7") { return res.end(JSON.stringify({ number: 7, state: "open", merged: false, base: { ref: "main" }, head: { ref: "b", sha: "a".repeat(40) }, html_url: "h/7", ...(mergeable === undefined ? {} : { mergeable }) })); } 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"); GiteaClient.mergeRetryAfter = [1, 1, 1]; return { client, server, merges: () => merges }; } const later = { status: 405, body: '{"message":"Please try again later"}' }; const ok = { status: 200, body: "" }; test("a merge answered 'try again later' while the forge works out its mergeability, then merged", async (t) => { const cases: { name: string; answers: { status: number; body: string }[]; merged: boolean; asked: number }[] = [ { name: "merged on the third ask", answers: [later, later, ok], merged: true, asked: 3 }, { name: "still 'later' after the last wait: refused, with the forge's words", answers: [later], merged: false, asked: 4 }, { name: "a refusal of another kind is answered at once", answers: [{ status: 409, body: '{"message":"head moved"}' }], merged: false, asked: 1 }, ]; for (const c of cases) { await t.test(c.name, async () => { const { client, server, merges } = await serve(c.answers); try { const done = client.mergePullRequest("novox", "hq", 7, "merge", false, "a".repeat(40)); if (c.merged) await done; else await assert.rejects(done, (err: Error) => /40[59]/.test(err.message)); assert.equal(merges(), c.asked); } finally { server.close(); } }); } }); test("a pull request carries the forge's mergeable, and none when the forge did not say", async () => { for (const mergeable of [true, false, undefined]) { const { client, server } = await serve([ok], mergeable); try { assert.equal((await client.getPullRequest("novox", "hq", 7)).mergeable, mergeable); } finally { server.close(); } } });