// What the forge's tools refuse (guard.ts; novox/hq ADR 0266): an agent reaches the forge through these tools // with a token minted for the forge's administrator, so the tools — not the forge — are what keeps it from // dropping a branch's protection, writing to main, setting the merge check's status, or merging past the // checks. Each route a review found is reproduced here against a fake forge, and each is refused with nothing // sent; the operator-approved merge of a pull request whose checks passed still goes through. import { test, after, beforeEach } from "node:test"; import assert from "node:assert/strict"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { GiteaClient } from "../dist/client.js"; import { ConfiguredToken } from "../dist/token.js"; import { getGiteaTools } from "../dist/tools/index.js"; import { mergeTool } from "../dist/merge.js"; import { commitStatusTool } from "../dist/delivery.js"; import { GateWriter } from "../dist/gate.js"; import { judgeApiCall, judgeProtectionChange, ruleCovers, routedPath } from "../dist/guard.js"; interface Sent { method: string; path: string; body: any } const state = { sent: [] as Sent[], asked: [] as string[], statuses: [] as { context: string; state: string; description?: string; updated_at?: string }[], // The trunk's head (novox/hq ADR 0323): what the merge tool compares the verdict's trunk commit with. trunk: "", // What the tools said on the bus. emitted: [] as { type: string; body: any }[], rules: [] as any[], lastCommit: "2026-01-01T00:00:00Z", // The pull request's head; the head the merge commit joined (the same, unless a test says otherwise) — the forge's // record of the pull request keeps saying `head` either way, as it would in the race issue 381 is about; and the // merge commit's own sha. head: "", landed: "", merged: false, mergeCommit: "", // When set, the merge commit's parents as the forge reports them, or the status it answers them with. parents: null as string[] | null, commitsFail: 0, }; const REVIEWED = "abc1234def5678901234567890abcdef12345678"; const TRUNK = "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b"; const MOVED = "3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"; // A verdict judged against a trunk commit, as both statuses say it (novox/hq ADR 0323 §3). const judgedAt = (trunk: string, state = "success") => [ { context: "mesh/merge-gate", state, description: `head ${REVIEWED.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: x` }, { context: "mesh/repo-check", state, description: `head ${REVIEWED.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: y` }]; const protectedMain = () => ({ rule_name: "main", enable_push: false, enable_status_check: true, status_check_contexts: ["mesh/merge-gate", "mesh/repo-check"], block_admin_merge_override: true }); beforeEach(() => { state.sent = []; state.asked = []; state.statuses = judgedAt(TRUNK); state.trunk = TRUNK; state.emitted = []; state.rules = [protectedMain()]; state.head = REVIEWED; state.landed = ""; state.merged = false; state.mergeCommit = "feedface00000000000000000000000000000000"; state.parents = null; state.commitsFail = 0; }); const json = (res: ServerResponse, status: number, body: unknown): void => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(body === null ? "" : JSON.stringify(body)); }; const read = (req: IncomingMessage): Promise => new Promise((resolve) => { let text = ""; req.on("data", (c) => (text += c)); req.on("end", () => resolve(text ? JSON.parse(text) : null)); }); const server = createServer(async (req, res) => { const url = new URL(req.url ?? "/", "http://fake"); const path = url.pathname.replace(/^\/api\/v1/, ""); const method = req.method ?? "GET"; const body = await read(req); state.asked.push(`${method} ${path}${url.search}`); if (method !== "GET") state.sent.push({ method, path, body }); const repo = "/repos/novox/mesh-catalog"; if (method === "GET" && path === repo) return json(res, 200, { full_name: "novox/mesh-catalog", default_branch: "main" }); if (method === "GET" && path === `${repo}/branch_protections`) return json(res, 200, state.rules); if (method === "GET" && path === `${repo}/branches/main`) return json(res, 200, { name: "main", commit: { id: state.trunk } }); if (method === "GET" && path === `${repo}/pulls/7/files`) { return json(res, 200, url.searchParams.get("page") === "1" ? [{ filename: "modules/gitea/x.ts", status: "modified" }] : []); } if (method === "POST" && path === `${repo}/statuses/${state.head}`) { // Kept as the forge keeps them: the newest of a context first. state.statuses = [{ ...body, updated_at: new Date().toISOString() }, ...state.statuses.filter((s) => s.context !== body.context)]; return json(res, 201, body); } if (method === "GET" && path === "/user") return json(res, 200, { login: "mesh-admin" }); if (method === "GET" && path === "/repos/mesh-admin/scratch/branch_protections") return json(res, 200, []); if (method === "GET" && /^\/repos\/[^/]+\/[^/]+\/commits$/.test(path)) { return json(res, 200, [{ commit: { committer: { date: state.lastCommit } } }]); } if (method === "GET" && path === `${repo}/pulls/7`) { return json(res, 200, { number: 7, title: "x", state: state.merged ? "closed" : "open", merged: state.merged, head: { ref: "feat/x", sha: state.head }, base: { ref: "main" }, html_url: "http://fake/7", merge_commit_sha: state.merged ? state.mergeCommit : null }); } if (method === "GET" && path === `${repo}/git/commits/${state.mergeCommit}`) { if (state.commitsFail) return json(res, state.commitsFail, { message: "the forge is not well" }); const parents = state.parents ?? ["8a53532d89b20f0fe4fd0fbc814f2e86b61cbc6b", state.landed || state.head]; return json(res, 200, { sha: state.mergeCommit, parents: parents.map((sha) => ({ sha })) }); } if (method === "GET" && path === `${repo}/commits/${state.head}/status`) { return json(res, 200, { sha: state.head, state: "success", statuses: state.statuses }); } if (method === "POST" && path === `${repo}/pulls/7/merge`) { state.merged = true; return json(res, 200, null); } return json(res, method === "GET" ? 200 : 201, {}); }); await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); after(() => new Promise((r) => server.close(() => r()))); const { port } = server.address() as { port: number }; const client = new GiteaClient(`http://127.0.0.1:${port}`, new ConfiguredToken("t")); // gitea_commit_status is served from the watcher's bundle, beside the merge check's one writer (novox/hq issue 456). const stateDir = mkdtempSync(join(tmpdir(), "gitea-tools-")); // The merge check's one writer, as the watcher's bundle holds it: what the merge tool's pending goes through. const writer = new GateWriter({ setCommitStatus: (o, r, sha, s) => client.setCommitStatus(o, r, sha, s), gateOf: async () => undefined, gateHistory: async () => [], headChecks: async () => ({}) }); const throughTheWriter: string[] = []; const gates = { rechecked: (o: string, r: string, sha: string, s: any) => { throughTheWriter.push(`${sha.slice(0, 12)} ${s.context} ${s.state}`); return writer.rechecked(o, r, sha, s); } }; const tools = [...getGiteaTools(client), commitStatusTool({ setCommitStatus: (o, r, sha, s) => client.setCommitStatus(o, r, sha, s), gates: writer, }), mergeTool(client, { gates, stateDir, emit: async (type, body) => { state.emitted.push({ type, body }); }, now: () => new Date(2026, 9, 12, 15, 1) })]; const tool = (name: string) => tools.find((t) => t.name === name)!; const writes = () => state.sent.map((s) => `${s.method} ${s.path}`); test("gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing", async () => { for (const [method, path, body] of [ ["PATCH", "/admin/users/agent", { admin: true }], ["DELETE", "/repos/novox/mesh-catalog/branch_protections/main", undefined], ["PATCH", "/repos/novox/mesh-catalog/branch_protections/main", { enable_status_check: false }], ["POST", "/repos/novox/mesh-catalog/statuses/abc1234", { context: "mesh/merge-gate", state: "success" }], ["POST", "/repos/novox/mesh-catalog/pulls/7/merge", { Do: "merge", force_merge: true }], ["PUT", "/repos/novox/mesh-catalog/collaborators/agent", { permission: "admin" }], ["POST", "/repos/novox/mesh-catalog/hooks", { type: "gitea" }], ["POST", "/repos/novox/mesh-catalog/keys", { key: "ssh-ed25519 AAAA" }], ["PATCH", "/repos/novox/mesh-catalog", { default_branch: "agent" }], ["DELETE", "/repos/novox/mesh-catalog", undefined], ["POST", "/repos/novox/mesh-catalog/transfer", { new_owner: "agent" }], ["POST", "/users/mesh-admin/tokens", { name: "mine", scopes: ["all"] }], ["PUT", "/orgs/novox/members/agent", undefined], ["PATCH", "/repos/novox/mesh-catalog/issues/1/../../branch_protections/main", { enable_push: true }], ["PATCH", "/repos/novox/mesh-catalog/issues/1/%2e%2e/%2e%2e/branch_protections/main", { enable_push: true }], ] as const) { await assert.rejects(tool("gitea_api").run({ path, method, ...(body ? { body } : {}) }), /not a write gitea_api makes|not taken|administration/, `${method} ${path}`); } await assert.rejects(tool("gitea_api").run({ path: "/admin/users" }), /administration/); assert.deepEqual(writes(), []); }); test("gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own", async () => { const put = (body: Record) => tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/contents/modules/sudo/module.json", method: "PUT", body }); await assert.rejects(put({ content: "e30=", sha: "x" }), /default branch/); await assert.rejects(put({ content: "e30=", sha: "x", branch: "main" }), /default branch/); state.rules.push({ rule_name: "release/*" }); await assert.rejects(put({ content: "e30=", sha: "x", branch: "release/1" }), /protection rule release\/\*/); assert.deepEqual(writes(), []); await put({ content: "e30=", sha: "x", branch: "main", new_branch: "feat/sudo-docs" }); assert.deepEqual(writes(), ["PUT /repos/novox/mesh-catalog/contents/modules/sudo/module.json"]); }); test("a protection rule covers a branch as the forge decides it: classes, alternatives, a plain name in any case (issue 539)", async () => { // One table with the forge bundle (cmd/forge/rules.go): `forge` is the forge's answer; `guard` is where this // guard deliberately covers more — a rule Go's regexp reads otherwise than JavaScript's — and never less. const cases = JSON.parse(readFileSync(new URL("./rule-cases.json", import.meta.url), "utf8")) as Array<{ rule: string; branch: string; forge: boolean; guard?: boolean }>; assert.ok(cases.length >= 100); for (const c of cases) { assert.ok(c.guard === undefined || (c.guard && !c.forge), `${c.rule}: the guard may only cover more than the forge`); assert.equal(ruleCovers(c.rule, c.branch), c.guard ?? c.forge, `${c.rule} covers ${c.branch}`); } const put = (branch: string) => tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/contents/README.md", method: "PUT", body: { content: "e30=", sha: "x", branch } }); state.rules.push({ rule_name: "release-[ab]" }, { rule_name: "{stable,lts}/*" }); await assert.rejects(put("release-a"), /protection rule release-\[ab\]/); await assert.rejects(put("lts/2"), /protection rule \{stable,lts\}\/\*/); await assert.rejects(put("MAIN"), /protection rule main/); assert.deepEqual(writes(), []); }); test("gitea_api still writes what an author writes", async () => { await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/issues/3/comments", method: "POST", body: { body: "hi" } }); await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/pulls/7/reviews", method: "POST", body: { event: "COMMENT" } }); await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/branch_protections" }); assert.deepEqual(writes(), ["POST /repos/novox/mesh-catalog/issues/3/comments", "POST /repos/novox/mesh-catalog/pulls/7/reviews"]); }); test("gitea_branch_protection_set only strengthens", async () => { const set = (args: Record) => tool("gitea_branch_protection_set").run({ owner: "novox", repo: "mesh-catalog", branch: "main", ...args }); await assert.rejects(set({ status_checks: "" }), /drops the required status mesh\/merge-gate, mesh\/repo-check/); await assert.rejects(set({ status_checks: "mesh/merge-gate" }), /drops the required status mesh\/repo-check/); await assert.rejects(set({ status_checks: "mesh/merge-gate,mesh/repo-check", push: true }), /push to the branch directly/); await assert.rejects(set({ status_checks: "mesh/merge-gate,mesh/repo-check", block_admin_override: false }), /merge past a status/); assert.deepEqual(writes(), []); await set({ status_checks: "mesh/merge-gate,mesh/repo-check,mesh/delivery" }); assert.deepEqual(writes(), ["PATCH /repos/novox/mesh-catalog/branch_protections/main"]); }); test("gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head", async () => { const merge = () => tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED }); state.statuses = [judgedAt(TRUNK)[0], judgedAt(TRUNK, "failure")[1]]; await assert.rejects(merge(), /not merged: .*mesh\/repo-check \(failure\)/); state.statuses = [judgedAt(TRUNK)[0]]; await assert.rejects(merge(), /mesh\/repo-check \(missing\)/); await assert.rejects(tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED, method: "manually-merged" }), /not a merge method/); assert.deepEqual(writes(), []); // The operator said yes and the checks passed: the flow that stays. state.statuses = judgedAt(TRUNK); await merge(); const sent = state.sent.find((s) => s.path.endsWith("/pulls/7/merge"))!; assert.equal(sent.body.force_merge, false); assert.equal(sent.body.head_commit_id, REVIEWED); }); // **The merge tool refuses a verdict whose trunk commit is not the trunk's head, and asks the check itself** (novox/hq // ADR 0323 §5, issue 341): node-engine #66 merged at 00:54 on a verdict of 00:49 that judged its head alone, after #65 // had moved the trunk. Both commits are named; one check is asked, the announcement a new head makes, and not a second // while it is on its way; the refusal says how long the last check of the repository took; and once the verdict judges // the trunk's head and passes, the merge goes through — asked again, never by itself. test("gitea_merge_pull_request refuses a verdict judged against an earlier trunk, and asks the check once", async () => { const merge = () => tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED }); const { keepTook, tookFile } = await import("../dist/trunk.js"); keepTook(tookFile(stateDir), "novox/mesh-catalog", { took: "4m31s", at: "2026-10-12T14:49:00Z" }); state.trunk = MOVED; // another merge moved the trunk after the verdict await assert.rejects(merge(), (err: Error) => { assert.match(err.message, /is not merged: its verdict was judged against trunk 7a8b9c0d1e2f, and main is now 3c4d5e6f7a8b/); assert.match(err.message, /check of head abc1234def56 merged into trunk 3c4d5e6f7a8b was asked/); assert.match(err.message, /the last check of novox\/mesh-catalog took 4m31s/); assert.match(err.message, /never merges by itself/); return true; }); assert.equal(state.merged, false); assert.equal(state.emitted.length, 1, "one check asked"); assert.equal(state.emitted[0].type, "pull.updated"); assert.equal(state.emitted[0].body.head_sha, REVIEWED); assert.equal(state.emitted[0].body.base, "main"); assert.deepEqual(state.emitted[0].body.paths, ["modules/gitea/x.ts"]); // The announcement names the trunk's head, for a verdict no build judges (ADR 0323 §3). assert.equal(state.emitted[0].body.base_sha, MOVED); // Its pending went through the merge check's one writer (novox/hq issue 456), not around it. assert.deepEqual(throughTheWriter.splice(0), ["abc1234def56 mesh/merge-gate pending", "abc1234def56 mesh/repo-check pending"]); const gate = state.statuses.find((s) => s.context === "mesh/merge-gate")!; assert.equal(gate.state, "pending"); assert.match(gate.description!, /^waiting in the build queue since 15:01: asked by the merge tool against trunk 3c4d5e6f7a8b/); assert.equal(state.statuses.find((s) => s.context === "mesh/repo-check")!.state, "pending"); // Asked again while that check is on its way: refused, and nothing is asked a second time. await assert.rejects(merge(), /already asked and not yet answered .*not asked again; the last check of novox\/mesh-catalog took 4m31s.*mesh-delivery\.recheck/); assert.equal(state.emitted.length, 1, "no second check while one is on its way"); state.statuses = [{ ...gate, description: "building on ace since 15:02" }, ...state.statuses.slice(1)]; await assert.rejects(merge(), /not asked again/); assert.equal(state.emitted.length, 1); // A verdict put out of date by the trunk's move is asked too, once. state.statuses = judgedAt(TRUNK).map((s) => ({ ...s, state: "pending", description: "out of date: judged against trunk 7a8b9c0d1e2f, the trunk is now 3c4d5e6f7a8b; judged again on a recheck or when it is merged" })); await assert.rejects(merge(), /was judged against trunk 7a8b9c0d1e2f, and main is now 3c4d5e6f7a8b\. The check .* was asked/); assert.equal(state.emitted.length, 2); // A verdict that names no trunk commit — a build seat from before the rule — is not taken as the trunk's head. state.statuses = [{ context: "mesh/merge-gate", state: "success", description: "pass: x" }, { context: "mesh/repo-check", state: "success", description: "pass: y" }]; await assert.rejects(merge(), /names no trunk commit it was judged against, and main is now 3c4d5e6f7a8b/); assert.equal(state.emitted.length, 3); assert.equal(state.merged, false); // An ask that cannot be announced puts the statuses back to out of date, so the next ask asks again. state.statuses = judgedAt(TRUNK); const failing = mergeTool(client, { gates, stateDir, emit: async () => { throw new Error("the bus is away"); } }); await assert.rejects(failing.run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED }), /its check could not be asked: the bus is away\. Nothing was merged/); assert.match(state.statuses.find((s) => s.context === "mesh/merge-gate")!.description!, /^out of date: /); await assert.rejects(merge(), /was asked/); assert.equal(state.emitted.length, 4, "asked again after the announcement failed"); // The check judged the merge into the trunk's head and passed: asked again at the reviewed head, it merges. state.statuses = judgedAt(MOVED); const done = (await merge()) as Record; assert.equal(done.merged, true); assert.equal(state.emitted.length, 4, "a merge asks no check"); }); // A merge lands only the head that was reviewed (novox/hq issue 381): mesh-controller#197 was reviewed at one head, // force-pushed, checked green at the new one, and merged there, because the tool merged whatever the head was. test("gitea_merge_pull_request merges only the head it is named, and refuses one that is not the head", async () => { const merge = (head?: string) => tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, ...(head === undefined ? {} : { head }) }); // No head named: there is no review to hold the merge to. await assert.rejects(merge(), /name the head you reviewed/); await assert.rejects(merge(" "), /name the head you reviewed/); await assert.rejects(merge("main"), /does not name one commit/); // Seven characters can be matched by a commit made to match them: twelve are asked for. await assert.rejects(merge(REVIEWED.slice(0, 7)), /does not name one commit — give its sha, or at least its first 12/); await assert.rejects(merge(REVIEWED.slice(0, 11)), /does not name one commit/); // The head moved after the review, and its checks passed where it is now: the case of mesh-controller#197. state.head = "3ced96fc6f8ad6cc7b93698912cb08a71635158a"; await assert.rejects(merge(REVIEWED), /its head is 3ced96fc6f8a, not the abc1234def5678901234567890abcdef12345678 you named/); await assert.rejects(merge(REVIEWED.slice(0, 12)), /not the abc1234def56 you named\. If you reviewed an earlier head/); assert.deepEqual(writes(), [], "a refused merge sends nothing"); // Named as reviewed, by its first 12 characters, in either case: the forge is handed the whole head, and the merge // commit's second parent says that head landed. state.head = REVIEWED; const done = (await merge(REVIEWED.slice(0, 12).toUpperCase())) as Record; const sent = state.sent.find((s) => s.path.endsWith("/pulls/7/merge"))!; assert.equal(sent.body.head_commit_id, REVIEWED); assert.equal(done.head, REVIEWED); assert.equal(done.landed, REVIEWED); assert.equal(done.read_back, undefined); }); // The forge is handed the head, and what landed is read from the merge commit, not from the forge's record of the // pull request (which here still says the head it was handed). Another head landing is an error that says the merge // happened, never a field beside merged: true. test("gitea_merge_pull_request is loud when the forge merged a head other than the one it was handed", async () => { state.landed = "3ced96fc6f8ad6cc7b93698912cb08a71635158a"; await assert.rejects(tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED }), /WAS merged \(feedface0000.*\), but the forge merged 3ced96fc6f8a, not the abc1234def56 it was handed/); assert.equal(state.merged, true, "the merge happened: the error is what says so"); }); // The merge has happened once the forge says so: a read-back that fails after it must not read as nothing merged. test("gitea_merge_pull_request says the merge happened when reading back what landed fails", async () => { state.commitsFail = 500; const done = (await tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED })) as Record; assert.equal(state.merged, true); assert.equal(done.merged, true); assert.equal(done.landed, undefined); assert.match(String(done.read_back), /the merge happened, but reading back what landed failed/); // A parent the forge names without its sha is a failed read-back, never an empty head said to have landed. state.merged = false; state.commitsFail = 0; state.parents = ["8a53532d89b20f0fe4fd0fbc814f2e86b61cbc6b", ""]; const blank = (await tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED })) as Record; assert.equal(blank.merged, true); assert.match(String(blank.read_back), /without its sha/); }); test("gitea_merge_pull_request reads back a fast-forward, and says it cannot read back a squash or a rebase", async () => { state.mergeCommit = REVIEWED; // a fast-forward lands the head itself // Its parents name another commit second, so reading a fast-forward as a merge's second parent would fail here. state.parents = ["8a53532d89b20f0fe4fd0fbc814f2e86b61cbc6b"]; const ff = (await tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED, method: "fast-forward-only" })) as Record; assert.equal(ff.landed, REVIEWED); assert.equal(ff.read_back, undefined); // A fast-forward whose commit the forge names otherwise is said, not raised: that it names the head is assumed. state.merged = false; state.mergeCommit = "feedface00000000000000000000000000000000"; const odd = (await tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED, method: "fast-forward-only" })) as Record; assert.equal(odd.merged, true); assert.equal(odd.landed, undefined); assert.match(String(odd.read_back), /as this fast-forward's commit, not the head/); for (const method of ["squash", "rebase", "rebase-merge"]) { state.merged = false; state.mergeCommit = "feedface00000000000000000000000000000000"; const done = (await tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED, method })) as Record; assert.equal(done.landed, undefined, method); assert.match(String(done.read_back), new RegExp(`a ${method} makes new commits that name no head`)); } }); test("gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full", async () => { const del = (owner: string, name: string, confirm: unknown) => tool("gitea_delete_repo").run({ owner, name, confirm }); await assert.rejects(del("novox", "mesh-catalog", true), /confirm must be the repository's full name/); await assert.rejects(del("novox", "mesh-catalog", "novox/mesh-catalog"), /not a repository of the tools' own user/); state.lastCommit = new Date().toISOString(); await assert.rejects(del("mesh-admin", "scratch", "mesh-admin/scratch"), /within 30 days/); assert.deepEqual(writes(), []); state.lastCommit = "2026-01-01T00:00:00Z"; await del("mesh-admin", "scratch", "mesh-admin/scratch"); assert.deepEqual(writes(), ["DELETE /repos/mesh-admin/scratch"]); }); test("gitea_api sends the path it judged, and refuses moving a pull request's base", async () => { await tool("gitea_api").run({ path: "/api/v1/repos/novox/mesh-catalog/issues/3/comments/?page=1", method: "POST", body: { body: "hi" } }); assert.deepEqual(writes(), ["POST /repos/novox/mesh-catalog/issues/3/comments"]); await assert.rejects(tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/pulls/7", method: "PATCH", body: { base: "unguarded" } }), /changing a pull request's base/); await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/pulls/7", method: "PATCH", body: { title: "x" } }); assert.equal(writes().length, 2); }); test("gitea_merge_pull_request merges only into a branch whose protection requires the checks", async () => { state.rules = []; await assert.rejects(tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED }), /no protection rule requiring the checks guards/); state.rules = [{ rule_name: "main", enable_push: false, enable_status_check: false, status_check_contexts: [] }]; await assert.rejects(tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED }), /no protection rule requiring the checks guards/); assert.deepEqual(writes(), []); }); test("a rule named for a branch is not made weaker than the glob rule that guards it now", async () => { state.rules.push({ rule_name: "release/*", enable_push: false, enable_status_check: true, status_check_contexts: ["mesh/merge-gate"], block_admin_merge_override: true }); const set = (args: Record) => tool("gitea_branch_protection_set").run({ owner: "novox", repo: "mesh-catalog", branch: "release/1", ...args }); await assert.rejects(set({ status_checks: "" }), /drops the required status mesh\/merge-gate/); await assert.rejects(set({ status_checks: "mesh/merge-gate" }), /merge past a status/); assert.deepEqual(writes(), []); await set({ status_checks: "mesh/merge-gate", block_admin_override: true }); assert.deepEqual(writes(), ["POST /repos/novox/mesh-catalog/branch_protections"]); }); test("a repository owner or name is never another path", async () => { await assert.rejects(tool("gitea_update_pull_request").run({ owner: "novox", repo: "mesh-catalog/branch_protections/main?", number: 1, title: "x" }), /not a repository name/); await assert.rejects(tool("gitea_get_issue").run({ owner: "..", repo: "admin", number: 1 }), /not a repository owner/); assert.deepEqual(writes(), []); }); test("the judges, without a forge", () => { assert.equal(routedPath("/api/v1/repos/a/b/issues?x=1"), "/repos/a/b/issues"); assert.throws(() => routedPath("/repos/a/b/../c"), /dot segment/); assert.ok("allowed" in judgeApiCall("GET", "/repos/a/b/branch_protections", undefined)); assert.ok("refused" in judgeApiCall("POST", "/repos/a/b/branch_protections", {})); assert.ok("contents" in judgeApiCall("POST", "/repos/a/b/contents", { new_branch: "x" })); assert.ok(ruleCovers("release/**", "release/a/b") && ruleCovers("feat/*", "feat/a") && !ruleCovers("feat/*", "feat/a/b")); assert.equal(judgeProtectionChange(null, { statusChecks: [] }), null); assert.equal(judgeProtectionChange(protectedMain(), { statusChecks: ["mesh/repo-check", "mesh/merge-gate"] }), null); }); test("gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused", async () => { for (const path of ["/user?sudo=jschoubben", "/user?SUDO=jschoubben", "/repos/novox/mesh-catalog/issues?state=open&Sudo=jschoubben", "/repos/novox/mesh-catalog/issues?token=abc", "/repos/novox/mesh-catalog/issues?access_token=abc"]) { await assert.rejects(tool("gitea_api").run({ path }), /query key/, path); } await assert.rejects(tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/issues/3/comments?sudo=jschoubben", method: "POST", body: { body: "as the operator" } }), /query key sudo/); assert.deepEqual(state.asked, []); await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/issues?state=open&limit=5" }); assert.deepEqual(state.asked, ["GET /repos/novox/mesh-catalog/issues?state=open&limit=5"]); }); test("gitea_api never approves a pull request nor requests changes: those are the operator's", async () => { for (const [path, event] of [["/repos/novox/mesh-catalog/pulls/7/reviews", "APPROVE"], ["/repos/novox/mesh-catalog/pulls/7/reviews", "approve"], ["/repos/novox/mesh-catalog/pulls/7/reviews", "REQUEST_CHANGES"], ["/repos/novox/mesh-catalog/pulls/7/reviews/5", "APPROVE"]]) { await assert.rejects(tool("gitea_api").run({ path, method: "POST", body: { event, body: "lgtm" } }), /operator's at the forge/, `${path} ${event}`); } assert.deepEqual(writes(), []); await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/pulls/7/reviews", method: "POST", body: { event: "COMMENT", body: "a note" } }); assert.deepEqual(writes(), ["POST /repos/novox/mesh-catalog/pulls/7/reviews"]); }); test("no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge", async () => { const repo = { owner: "novox", repo: "mesh-catalog" }; const cases: Array<[string, Record]> = [ ["gitea_delete_branch", { ...repo, branch: ".." }], ["gitea_delete_branch", { ...repo, branch: "feat/../.." }], ["gitea_delete_branch", { ...repo, branch: "x?sudo=jschoubben" }], ["gitea_delete_branch", { ...repo, branch: "%2e%2e" }], ["gitea_get_file", { ...repo, path: "../../../../version" }], ["gitea_get_file", { ...repo, path: "README.md?sudo=jschoubben" }], ["gitea_commit_status", { ...repo, sha: "abc1234?sudo=jschoubben", context: "mesh/delivery", state: "success" }], ["gitea_commit_status", { ...repo, sha: "../../../admin/users", context: "mesh/delivery", state: "success" }], ["gitea_commit_statuses", { ...repo, sha: ".." }], ["gitea_commit_statuses", { ...repo, sha: "main?sudo=jschoubben" }], ["gitea_branch_protection_set", { ...repo, branch: "..", status_checks: "mesh/merge-gate" }], ["gitea_branch_protection_set", { ...repo, branch: "main?sudo=jschoubben", status_checks: "mesh/merge-gate" }], ["gitea_contains", { ...repo, commit: "..", in: "abc1234" }], ]; for (const [name, args] of cases) { state.asked = []; await assert.rejects(tool(name).run(args), (err: unknown) => err instanceof Error, `${name} ${JSON.stringify(args)}`); // Nothing that is not a read of the repository's own rules reached the forge. assert.deepEqual(state.asked.filter((a) => !a.startsWith("GET /repos/novox/mesh-catalog/branch_protections")), [], `${name} ${JSON.stringify(args)} reached the forge`); } // What is ordinary still goes: a branch with a slash, a file in a directory. state.asked = []; await tool("gitea_delete_branch").run({ ...repo, branch: "feat/x" }); await tool("gitea_get_file").run({ ...repo, path: "modules/gitea/module.json" }).catch(() => undefined); assert.deepEqual(state.asked, ["DELETE /repos/novox/mesh-catalog/branches/feat/x", "GET /repos/novox/mesh-catalog/contents/modules/gitea/module.json"]); });