// gitea_branch_protection_set_full (the operator, 2026-10-28: "just make a generic tool so you can setup any // configuration"): any field of a branch protection rule, only the fields given, as the forge's administrator; it // answers the rule before and after, and whether the controller's trunk rule (judgedRule, mesh-control // build_source.go) now holds. Against a fake forge that keeps the rules and answers them to the administrator alone. import { test, after, beforeEach } from "node:test"; import assert from "node:assert/strict"; import { createServer } from "node:http"; import { GiteaAdmin, GiteaClient } from "../dist/client.js"; import { ConfiguredToken } from "../dist/token.js"; import { getGiteaTools } from "../dist/tools/index.js"; import { trunkRule } from "../dist/protectfull.js"; const adminBasic = "Basic " + Buffer.from("mesh-admin:secret").toString("base64"); let rules: any[] = []; let repoState: any = {}; const branches = ["master", "main"]; let sent: { method: string; path: string; body: any }[] = []; beforeEach(() => { // de-spiegel's main as found: no push, but the operator alone merges and an administrator merges past it. rules = [{ rule_name: "main", enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["jschoubben"], enable_status_check: false, status_check_contexts: [], block_admin_merge_override: false, required_approvals: 0, enable_push_whitelist: false, push_whitelist_deploy_keys: false, enable_force_push: false, enable_force_push_allowlist: false, force_push_allowlist_deploy_keys: false, merge_whitelist_teams: [] }]; source = {}; repoState = { full_name: "novox/de-spiegel", default_branch: "master", description: "", has_wiki: true }; sent = []; }); // What gitea 1.27.3's EditBranchProtection does with a body (routers/api/v1/repo/branch.go): the push whitelist flags are // read only beside enable_push true and cleared by enable_push false, the force-push allowlist flags likewise with // enable_force_push; teams are passed over on a repository no organisation owns (de-spiegel here, for the test). function giteaEdit(r: any, body: any) { const nested = ["enable_push", "enable_push_whitelist", "push_whitelist_deploy_keys", "enable_force_push", "enable_force_push_allowlist", "force_push_allowlist_deploy_keys", "merge_whitelist_teams", "push_whitelist_teams"]; for (const [k, v] of Object.entries(body)) if (!nested.includes(k) && k !== "rule_name") r[k] = v; if (body.enable_push === false) Object.assign(r, { enable_push: false, enable_push_whitelist: false, push_whitelist_deploy_keys: false }); if (body.enable_push === true) { r.enable_push = true; if (body.enable_push_whitelist !== undefined) r.enable_push_whitelist = body.enable_push_whitelist; if (body.push_whitelist_deploy_keys !== undefined) r.push_whitelist_deploy_keys = body.push_whitelist_deploy_keys; } if (body.enable_force_push === false) Object.assign(r, { enable_force_push: false, enable_force_push_allowlist: false, force_push_allowlist_deploy_keys: false }); if (body.enable_force_push === true) { r.enable_force_push = true; if (body.enable_force_push_allowlist !== undefined) r.enable_force_push_allowlist = body.enable_force_push_allowlist; if (body.force_push_allowlist_deploy_keys !== undefined) r.force_push_allowlist_deploy_keys = body.force_push_allowlist_deploy_keys; } } const server = createServer((req, res) => { const path = (req.url ?? "").replace(/^\/api\/v1/, ""); const send = (status: number, body: unknown) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(body)); }; if (req.headers.authorization !== adminBasic) return send(403, { message: "not an administrator of the repository" }); let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", () => { const body = raw ? JSON.parse(raw) : undefined; if (req.method !== "GET") sent.push({ method: req.method!, path, body }); if (path === "/repos/novox/de-spiegel") { if (req.method === "GET") return send(200, repoState); if (req.method === "PATCH") { // gitea's updateBasicProperties: a default branch that does not exist is passed over, the call still 200. // enable_prune is a mirror's, kept beside the repository and never in its answer; mirror_interval is answered in Go's // duration form. const { default_branch, enable_prune, mirror_interval, ...rest } = body; Object.assign(repoState, rest); if (mirror_interval !== undefined) repoState.mirror_interval = mirror_interval === "8h" ? "8h0m0s" : mirror_interval; if (default_branch !== undefined && branches.includes(default_branch)) repoState.default_branch = default_branch; return send(200, repoState); } } const m = path.match(/^\/repos\/novox\/de-spiegel\/branch_protections(?:\/(.+))?$/); if (!m) return send(404, { message: "not found" }); if (req.method === "GET" && !m[1]) return send(200, rules); if (req.method === "PATCH" && m[1]) { const r = rules.find((x) => x.rule_name === decodeURIComponent(m[1])); if (!r) return send(404, { message: "no rule" }); giteaEdit(r, body); return send(200, r); } if (req.method === "POST" && !m[1]) { const r = { rule_name: body.rule_name }; giteaEdit(r, body); rules.push(r); return send(201, r); } send(405, { message: "no" }); }); }); 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 url = `http://127.0.0.1:${port}`; // The gate's source as the tools read it (mesh-delivery-gate.source): one key per trunk, each naming the repositories // installed from it. A test sets it; empty is a source that names no trunk of de-spiegel. let source: Record }> = {}; const readSource = async () => source; const tools = getGiteaTools(new GiteaClient(url, new ConfiguredToken("tools-token")), () => new GiteaAdmin(url, "mesh-admin", "secret"), readSource); const setRepo = (args: Record) => tools.find((t) => t.name === "gitea_repo_settings_set_full")!.run({ owner: "novox", repo: "de-spiegel", why: "L2", ...args }); const setFull = (args: Record) => tools.find((t) => t.name === "gitea_branch_protection_set_full")!.run({ owner: "novox", repo: "de-spiegel", branch: "main", why: "L2", ...args }); test("it sets the merge whitelist and the administrator's override, only the fields given, and says the trunk rule now holds", async () => { const out: any = await setFull({ merge_whitelist_usernames: ["mesh-tools", "jschoubben"], block_admin_merge_override: true }); assert.deepEqual(sent, [{ method: "PATCH", path: "/repos/novox/de-spiegel/branch_protections/main", body: { merge_whitelist_usernames: ["mesh-tools", "jschoubben"], block_admin_merge_override: true } }]); assert.deepEqual(out.before.merge_whitelist, ["jschoubben"]); assert.equal(out.before.admin_may_override, true); assert.deepEqual(out.after.merge_whitelist, ["mesh-tools", "jschoubben"]); assert.equal(out.after.admin_may_override, false); assert.equal(out.why, "L2"); assert.equal(out.trunk_rule.holds, true, out.trunk_rule.why); }); test("it loosens as told too, and says the trunk rule no longer holds and why", async () => { const out: any = await setFull({ enable_push: true, status_check_contexts: ["mesh/check"], enable_status_check: true, required_approvals: 1 }); assert.equal(out.after.push, true); assert.deepEqual(out.after.required_statuses, ["mesh/check"]); assert.equal(out.after.required_approvals, 1); assert.equal(out.trunk_rule.holds, false); assert.match(out.trunk_rule.why, /push to main directly/); }); test("a branch with no rule gets one with the fields given", async () => { const out: any = await setFull({ branch: "release/*", enable_push: false, protected_file_patterns: "*.lock" }); assert.equal(out.created, true); assert.deepEqual(sent[0], { method: "POST", path: "/repos/novox/de-spiegel/branch_protections", body: { rule_name: "release/*", enable_push: false, protected_file_patterns: "*.lock" } }); }); test("an unknown field, a field of the wrong kind, no field, no reason or a login that is none: refused, nothing sent", async () => { await assert.rejects(setFull({ enable_pushes: true }), /no field of a branch protection rule/); await assert.rejects(setFull({ enable_push: "yes" }), /true or false/); await assert.rejects(setFull({ required_approvals: -1 }), /whole number/); await assert.rejects(setFull({ merge_whitelist_usernames: ["../x"] }), /no forge login/); await assert.rejects(setFull({}), /at least one field/); await assert.rejects(setFull({ why: "", enable_push: false }), /say why/); assert.deepEqual(sent, []); }); // judgedRule's cases (mesh-control cmd/mesh-controller/build_source.go), each in its own words. test("the trunk rule is judged as the controller judges it", () => { const ok = { rule_name: "main", enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"], block_admin_merge_override: true }; const cases: [any[], string, boolean, RegExp][] = [ [[], "main", false, /no protection rule covers main/], [[{ ...ok, enable_push: true }], "main", false, /push to main directly/], [[{ ...ok, enable_merge_whitelist: false }], "main", false, /no merge whitelist/], [[{ ...ok, merge_whitelist_usernames: ["jschoubben"] }], "main", false, /and not mesh-tools/], [[{ ...ok, merge_whitelist_usernames: ["mesh-tools", "agent-g14"] }], "main", false, /agent's account/], [[{ ...ok, block_admin_merge_override: false }], "main", false, /administrator/], [[ok], "main", true, /takes no push/], [[{ ...ok, rule_name: "ma*" }], "main", true, /ma\*/], [[{ ...ok, rule_name: "ma*", enable_push: true }, { ...ok }], "main", true, /rule main/], ]; for (const [rs, branch, holds, why] of cases) { const got = trunkRule(rs, branch); assert.equal(got.holds, holds, `${JSON.stringify(rs)}: ${got.why}`); assert.match(got.why, why); } }); // Review round 1 of feat/protect-as-trunk: gitea passes over a dependent field without its parent set true, and answers 200. test("a field the forge reads only beside its parent is refused without it; with it, it is applied", async () => { await assert.rejects(setFull({ enable_push_whitelist: true }), /only when the same call sets enable_push: true/); await assert.rejects(setFull({ force_push_allowlist_deploy_keys: true, enable_force_push: false }), /enable_force_push: true/); assert.deepEqual(sent, []); const out: any = await setFull({ enable_push: true, enable_push_whitelist: true, push_whitelist_usernames: ["jschoubben"] }); assert.equal(out.after.fields.enable_push_whitelist, true); await setFull({ enable_push: false, enable_push_whitelist: false }); // a false beside a false parent is what the forge does }); test("a field the forge answered and did not apply fails the call, naming it; what it changed beside the call is said", async () => { await assert.rejects(setFull({ merge_whitelist_teams: ["owners"] }), /did not apply merge_whitelist_teams \(sent \["owners"\], the forge holds \[\]\)/); rules[0].enable_push = true; rules[0].enable_push_whitelist = true; const out: any = await setFull({ enable_push: false }); assert.deepEqual(out.also_changed, ["enable_push_whitelist"]); }); test("gitea_repo_settings_set_full sets the default branch, and fails loudly on a branch the forge passed over", async () => { const out: any = await setRepo({ default_branch: "main" }); assert.deepEqual(sent.at(-1), { method: "PATCH", path: "/repos/novox/de-spiegel", body: { default_branch: "main" } }); assert.equal(out.before.default_branch, "master"); assert.equal(out.after.default_branch, "main"); await assert.rejects(setRepo({ default_branch: "trunk" }), /did not apply default_branch/); await assert.rejects(setRepo({ name: "x" }), /no field of a repository's settings/); await assert.rejects(setRepo({ why: "", has_wiki: false }), /say why/); }); test("every field gitea 1.27.3 edits on a rule is taken: 32, the bypass allowlist among them", async () => { const { PROTECTION_FIELDS } = await import("../dist/protectfull.js"); assert.equal(Object.keys(PROTECTION_FIELDS).length, 32); for (const f of ["enable_bypass_allowlist", "bypass_allowlist_usernames", "bypass_allowlist_teams"]) assert.ok(f in PROTECTION_FIELDS, f); }); // Review round 2: the settings tool's read-back meets the forge's own answer. test("A: enable_prune, which the forge never answers, is said as not read back, never failed", async () => { const pruned: any = await setRepo({ enable_prune: true }); assert.deepEqual(pruned.not_read_back, ["enable_prune"]); }); test("B: mirror_interval is compared as a duration: 8h and the forge's 8h0m0s are one", async () => { const mirror: any = await setRepo({ mirror_interval: "8h" }); assert.equal(mirror.after.mirror_interval, "8h0m0s"); await assert.rejects(setRepo({ mirror_interval: "often" }), /duration/); }); test("C: allow_merge_update, default_update_style and has_code are taken; projects_mode needs has_projects; what is left out is named", async () => { const more: any = await setRepo({ allow_merge_update: true, default_update_style: "rebase", has_code: true }); assert.deepEqual(more.set, { allow_merge_update: true, default_update_style: "rebase", has_code: true }); await assert.rejects(setRepo({ projects_mode: "repo" }), /only when the same call sets has_projects: true/); await setRepo({ has_projects: true, projects_mode: "repo" }); const desc = tools.find((t) => t.name === "gitea_repo_settings_set_full")!.description; for (const w of ["name", "mirror_username", "mirror_password", "mirror_token", "tracker", "wiki"]) assert.match(desc, new RegExp(w)); }); // The operator, 2026-10-28 16:4x: a call waits for their Approve only if it removes branch protection or allows a // force-push to main. No tool of this module can ask yet (no TypeScript asker; ADR 0286's mark is unbuilt in the // runner), so until one can, such a call is refused, with nothing sent, and says it is the operator's; everything else // runs at once. test("a force-push onto a rule covering main is refused, nothing sent; on another branch, and tightening, it runs", async () => { for (const args of [{ enable_force_push: true }, { enable_force_push: true, enable_force_push_allowlist: true }, { force_push_allowlist_usernames: ["jschoubben"] }, { force_push_allowlist_teams: ["owners"] }, { branch: "ma*", enable_force_push: true }, { branch: "**", force_push_allowlist_usernames: ["x"] }]) { await assert.rejects(setFull(args), /force-push to a trunk is the operator's/, JSON.stringify(args)); } assert.deepEqual(sent, []); await setFull({ branch: "release/*", enable_force_push: true }); const tight: any = await setFull({ enable_force_push: false, force_push_allowlist_usernames: [] }); assert.equal(tight.trunk_rule.holds, false, "trunk_rule is in the answer"); }); // The operator, 2026-10-28 16:5x: the gates and trunk_rule apply to every trunk, the branches the mesh installs from, as // the gate's source names them; "main" only when it names none, and said. test("a trunk the gate's source names, beta, is gated and judged as main is; main is then no trunk of the repository", async () => { source = { main: { heads: { "novox/okioki": "a1" } }, beta: { heads: { "novox/de-spiegel": "b2" } } }; await assert.rejects(setFull({ branch: "beta", enable_force_push: true }), /force-push to a trunk is the operator's/); await assert.rejects(setFull({ branch: "b*", force_push_allowlist_usernames: ["x"] }), /covers beta/); assert.deepEqual(sent, []); const main: any = await setFull({ enable_force_push: true }); assert.deepEqual(main.trunks, { names: ["beta"], from: "mesh-delivery-gate.source" }); assert.equal(main.trunk_rule.is_trunk, false); const beta: any = await setFull({ branch: "beta", enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"], block_admin_merge_override: true }); assert.equal(beta.trunk_rule.is_trunk, true); assert.equal(beta.trunk_rule.holds, true, beta.trunk_rule.why); }); test("a source that names no trunk of the repository falls back to main, and says so", async () => { const out: any = await setFull({ block_admin_merge_override: true }); assert.equal(out.trunks.names[0], "main"); assert.match(out.trunks.from, /names no trunk of novox\/de-spiegel: main assumed/); assert.equal(out.trunk_rule.is_trunk, true); });