Files
mesh-catalog/modules/gitea/test/guard.test.ts
T

601 lines
43 KiB
TypeScript

// 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. Each route a review
// found is reproduced here against a fake forge, and each is refused with nothing sent; a merge is the seat git's
// merge-pull alone, at the head the caller names (the last test's rows).
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { readFileSync } from "node:fs";
import { GiteaClient } from "../dist/client.js";
import { ConfiguredToken } from "../dist/token.js";
import { getGiteaTools } from "../dist/tools/index.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[],
// Whether the token is a repository administrator's. The tools' account is not (novox/hq ADR 0365): the forge
// refuses it the rules' list, as gitea's reqAdmin does, and it reads a branch's protection from the branch.
admin: false,
lastCommit: "2026-01-01T00:00:00Z",
// The repository's default branch and pull request 7's base, as the forge keeps them (issue 1422 note 7).
defaultBranch: "main",
base: "main",
prState: "open",
// Branches the forge answers 404 for; "scratch" exists but no protection rule guards it.
absent: ["nope"],
// 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,
// The sibling beside which a verdict was judged (novox/hq ADR 0382 §6): its group status at its head, and its reviews.
groupStatus: "",
siblingGroup: "",
reviews: [] as { state: string; commit_id: string; dismissed?: boolean }[],
// The forge's own mergeable of pull request 7; unset, the forge does not say.
mergeable: undefined as boolean | undefined,
};
const REVIEWED = "abc1234def5678901234567890abcdef12345678";
const SIBLING_HEAD = "bbbbbbbbbbbb5678901234567890abcdef12345678".slice(0, 40);
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(), { rule_name: "release/*", enable_push: false }];
state.defaultBranch = "main";
state.base = "main";
state.prState = "open";
state.admin = false;
state.head = REVIEWED;
state.landed = "";
state.merged = false;
state.mergeCommit = "feedface00000000000000000000000000000000";
state.parents = null;
state.commitsFail = 0;
state.groupStatus = "";
state.siblingGroup = "";
state.reviews = [];
});
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<any> =>
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: state.defaultBranch, permissions: { push: true } });
if (method === "PATCH" && path === repo) {
if (body?.default_branch) state.defaultBranch = body.default_branch;
return json(res, 200, { full_name: "novox/mesh-catalog", default_branch: state.defaultBranch });
}
if (method === "PATCH" && path === `${repo}/pulls/7`) {
if (body?.base) state.base = body.base;
return json(res, 201, { number: 7, title: "x", state: state.prState, merged: false, head: { ref: "feat/x", sha: state.head },
base: { ref: state.base }, html_url: "http://fake/7" });
}
if (path.startsWith(`${repo}/branch_protections`) && !state.admin) {
return json(res, 403, { message: "user should be an owner or a collaborator with admin write of a repository" });
}
if (method === "GET" && path === `${repo}/branch_protections`) return json(res, 200, state.rules);
const branch = method === "GET" && path.startsWith(`${repo}/branches/`) ? decodeURIComponent(path.slice(`${repo}/branches/`.length)) : null;
if (branch !== null && state.absent.includes(branch)) return json(res, 404, { message: "branch does not exist" });
if (branch !== null) {
// As the forge says it to any reader of the code: the rule that covers the branch, as the forge matches it.
const rule = (state.rules as any[]).find((r) => ruleCovers(String(r.rule_name ?? r.branch_name), branch));
return json(res, 200, { name: branch, commit: { id: branch === "main" ? state.trunk : "c0ffee" }, protected: !!rule,
...(rule ? { effective_branch_protection_name: rule.rule_name ?? rule.branch_name, enable_status_check: !!rule.enable_status_check,
status_check_contexts: rule.status_check_contexts ?? [], required_approvals: rule.required_approvals ?? 0, user_can_push: !!rule.enable_push } : {}) });
}
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" : state.prState, merged: state.merged,
head: { ref: "feat/x", sha: state.head }, base: { ref: state.base }, html_url: "http://fake/7",
merge_commit_sha: state.merged ? state.mergeCommit : null, ...(state.mergeable === undefined ? {} : { mergeable: state.mergeable }) });
}
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`) {
const group = state.groupStatus ? [{ context: "mesh/delivery-group", status: state.groupStatus }] : [];
return json(res, 200, { sha: state.head, state: "success", statuses: [...state.statuses, ...group] });
}
if (method === "GET" && path === `/repos/novox/mesh-host/commits/${SIBLING_HEAD}/status`) {
return json(res, 200, { sha: SIBLING_HEAD, state: "success",
statuses: state.siblingGroup ? [{ context: "mesh/delivery-group", status: state.siblingGroup }] : [] });
}
if (method === "GET" && path === "/repos/novox/mesh-host/pulls/114/reviews") {
return json(res, 200, url.searchParams.get("page") === "1" ? state.reviews : []);
}
if (method === "GET" && path === "/repos/novox/mesh-host/issues/114/labels") return json(res, 200, []);
if (method === "GET" && path === `${repo}/pulls/404`) return json(res, 404, { message: "pull request does not exist" });
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<void>((r) => server.listen(0, "127.0.0.1", () => r()));
after(() => new Promise<void>((r) => server.close(() => r())));
const { port } = server.address() as { port: number };
const client = new GiteaClient(`http://127.0.0.1:${port}`, new ConfiguredToken("t"));
const tools = getGiteaTools(client);
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<string, unknown>) =>
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 () => {
state.admin = true; // reads the rules' list, a repository administrator's (novox/hq ADR 0365)
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"]);
});
// **The forge requires no status to merge** (novox/hq ADR 0534 §5): the tool writes no required status and takes no
// status argument; taking a required status away is no weakening it refuses, and a push or an administrator's merge
// past the rule still is.
test("gitea_branch_protection_set writes no required status, and refuses a push or an administrator's override", async () => {
state.admin = true; // a repository administrator's token: the tools' account is refused the rules by the forge
const set = (args: Record<string, unknown>) =>
tool("gitea_branch_protection_set").run({ owner: "novox", repo: "mesh-catalog", branch: "main", ...args });
assert.equal((tool("gitea_branch_protection_set").input as Record<string, unknown>).status_checks, undefined,
"the tool takes no status argument");
await assert.rejects(set({ push: true }), /push to the branch directly/);
await assert.rejects(set({ block_admin_override: false }), /merge past/);
assert.deepEqual(writes(), []);
await set({ status_checks: "mesh/check" }); // an argument it no longer takes is not read
assert.deepEqual(writes(), ["PATCH /repos/novox/mesh-catalog/branch_protections/main"]);
const sent = state.sent[0].body;
assert.equal(sent.enable_status_check, false);
assert.deepEqual(sent.status_check_contexts, []);
assert.equal(sent.enable_push, false);
});
test("gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full", async () => {
state.admin = true; // reads the rules' list, a repository administrator's (novox/hq ADR 0365)
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("a rule named for a branch is not made weaker than the glob rule that guards it now", async () => {
state.admin = true; // reads the rules' list, a repository administrator's (novox/hq ADR 0365)
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<string, unknown>) =>
tool("gitea_branch_protection_set").run({ owner: "novox", repo: "mesh-catalog", branch: "release/1", ...args });
await assert.rejects(set({}), /merge past/);
assert.deepEqual(writes(), []);
await set({ block_admin_override: true }); // the glob's required status is no longer asked (ADR 0534 §5)
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, {}), null);
assert.equal(judgeProtectionChange(protectedMain(), {}), null, "taking its required statuses off is no weakening (ADR 0534 §5)");
});
test("main's protection is never weakened in its push or its merge whitelist (issue 948, ADR 0398 §2)", () => {
const strong = () => ({ ...protectedMain(), enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"] });
assert.equal(judgeProtectionChange(strong(), {}, [], "main"), null);
assert.equal(judgeProtectionChange(strong(), { mergeWhitelist: ["mesh-tools"] }, [], "main"), null);
assert.match(judgeProtectionChange(strong(), { mergeWhitelist: [] }, [], "main") ?? "", /merge whitelist/);
assert.match(judgeProtectionChange(strong(), { mergeWhitelist: null }, [], "main") ?? "", /merge whitelist/);
assert.match(judgeProtectionChange(strong(), { mergeWhitelist: ["mesh-tools", "agent-ace"] }, [], "main") ?? "", /widens/);
assert.match(judgeProtectionChange(strong(), { mergeWhitelist: ["agent-ace"] }, [], "main") ?? "", /widens/);
assert.match(judgeProtectionChange(strong(), { push: true }, [], "main") ?? "", /push/);
// Even a rule that lets a person push already, or has no rule yet, is not given a push through a tool.
assert.match(judgeProtectionChange({ ...strong(), enable_push: true }, { push: true }, [], "main") ?? "", /push/);
assert.match(judgeProtectionChange(null, { push: true }, [], "main") ?? "", /push/);
assert.match(judgeProtectionChange(null, { mergeWhitelist: ["agent-ace"] }, [], "main") ?? "", /widens/);
// Another branch is judged as before.
assert.equal(judgeProtectionChange(null, { push: true }, [], "feat/x"), 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<string, unknown>]> = [
["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_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", // the file's size first (issue 1577)
"GET /repos/novox/mesh-catalog/contents/modules/gitea/module.json"]);
});
// Issue 1422 note 7: the two settings gitea_api refuses (ADR 0266) have named tools, held to the same guard as
// gitea_branch_protection_set: the tools' account, a stated reason, a branch that exists and that a rule guards.
test("gitea_default_branch_set answers the default before and after, and refuses a branch that is missing, unguarded or unexplained", async () => {
const set = (args: Record<string, unknown>) =>
tool("gitea_default_branch_set").run({ owner: "novox", repo: "mesh-catalog", why: "the trunk was recorded as unknown", ...args });
await assert.rejects(set({ branch: "nope" }), /no branch nope/);
await assert.rejects(set({ branch: "scratch" }), /no protection rule/);
await assert.rejects(set({ branch: "release/1", why: " " }), /why/);
await assert.rejects(set({ branch: ".." }), /dot segment|not a branch/);
assert.deepEqual(writes(), []);
assert.deepEqual(await set({ branch: "release/1" }), { branch: "release/1", before: "main", after: "release/1", changed: true,
why: "the trunk was recorded as unknown" });
assert.deepEqual(writes(), ["PATCH /repos/novox/mesh-catalog"]);
assert.deepEqual(state.sent[0].body, { default_branch: "release/1" });
assert.equal(state.defaultBranch, "release/1");
state.sent = [];
assert.equal(((await set({ branch: "release/1" })) as any).changed, false, "already the default: nothing is sent");
assert.deepEqual(writes(), []);
});
test("gitea_pull_base_set answers the base before and after, and refuses a base that is missing, a closed pull request or no reason", async () => {
const set = (args: Record<string, unknown>) =>
tool("gitea_pull_base_set").run({ owner: "novox", repo: "mesh-catalog", number: 7, why: "it was opened against the wrong trunk", ...args });
await assert.rejects(set({ base: "nope" }), /no branch nope/);
await assert.rejects(set({ base: "release/1", why: "" }), /why/);
await assert.rejects(set({ base: "release/1", number: 0 }), /pull request's number/);
state.prState = "closed";
await assert.rejects(set({ base: "release/1" }), /not open/);
state.prState = "open";
assert.deepEqual(writes(), []);
assert.deepEqual(await set({ base: "release/1" }), { number: 7, before: "main", after: "release/1", changed: true,
why: "it was opened against the wrong trunk" });
assert.deepEqual(writes(), ["PATCH /repos/novox/mesh-catalog/pulls/7"]);
assert.deepEqual(state.sent[0].body, { base: "release/1" });
assert.equal(state.base, "release/1");
});
test("the two new tools are named for what mesh_search is asked, and gitea_api still refuses both settings", () => {
assert.match(tool("gitea_default_branch_set").description, /default branch/i);
assert.match(tool("gitea_pull_base_set").description, /pull request.s base/i);
assert.ok("refused" in judgeApiCall("PATCH", "/repos/a/b", { default_branch: "x" }));
assert.ok("refused" in judgeApiCall("PATCH", "/repos/a/b/pulls/7", { base: "x" }));
});
// **The seat git's verbs judge their caller, and merge-pull judges the Approve's head and no check** (design 47 *The seat
// git*, ADR 0661 §3): a caller the trusted list does not name is refused before the forge is asked; merge-pull merges the
// head it is named into a base that takes no push, whatever its statuses say, and an id said again after the merge is
// answered performed.
test("the seat git's verbs judge their caller, and merge-pull merges the Approve's head and judges no check", async () => {
const { seatTools, gitCallers } = await import("../dist/seat.js");
const { runAs } = await import("@novox/mesh-sdk/callers");
const bridge = "ace.mesh-delivery-forge-bridge";
const list = JSON.stringify({ publish: ["mesh-build-go"], "merge-pull": ["mesh-delivery-forge-bridge"],
pull: ["mesh-delivery-forge-bridge", "mesh-delivery-gate"], status: ["mesh-delivery-forge-bridge"] });
assert.deepEqual(Object.keys(gitCallers(list)).sort(), ["merge-pull", "pull", "status"], "only the seat git's lines are its");
assert.deepEqual(gitCallers(undefined), {}, "unset, no one may call");
// The head's one writer of its statuses (gate.ts), over a forge whose first write is still in flight when the next is asked.
const { GateWriter } = await import("../dist/gate.js");
const landed: string[] = [];
let inFlight: Promise<void> | null = null, land = () => {};
const slow = {
headChecks: async () => ({}),
setCommitStatus: async (_o: string, _r: string, _sha: string, s: { state: string; description: string }) => {
if (inFlight) { const w = inFlight; inFlight = null; await w; }
landed.push(`${s.state} ${s.description}`);
},
};
const gates = new GateWriter(slow);
const seat = seatTools(Object.assign(Object.create(client), { setCommitStatus: slow.setCommitStatus }),
{ run: async () => ({ stdout: "", code: 0 }), container: "gitea", callers: async () => list, statuses: gates });
const call = (verb: string, args: Record<string, unknown>, caller = bridge) =>
runAs({ "mesh/caller": caller }, args, () => seat.find((t: any) => t.name === `git.${verb}`)!.run(args)) as Promise<any>;
const merge = (extra: Record<string, unknown> = {}, caller?: string) =>
call("merge-pull", { id: "act-1", repository: "novox/mesh-catalog", number: 7, head: REVIEWED, ...extra }, caller);
const rows: { name: string; set: () => void; caller?: string; extra?: Record<string, unknown>; want: RegExp | "merged" | "held"; loud?: boolean }[] = [
{ name: "a caller not on the list", set: () => {}, caller: "ace.mesh-delivery-checker", want: /refused for ace.mesh-delivery-checker/ },
{ name: "an unproven caller", set: () => {}, caller: "", want: /no caller the tool runner proved/ },
{ name: "a head that moved", set: () => { state.head = MOVED; }, want: /its head is 3c4d5e6f7a8b, not the abc1234def56/ },
{ name: "a base no rule guards", set: () => { state.rules = []; }, want: /guarded by no protection rule/ },
{ name: "a base that takes a push", set: () => { state.rules = [{ rule_name: "main", enable_push: true }]; }, want: /guarded by no protection rule/ },
{ name: "a failed check", set: () => { state.statuses = [{ context: "mesh/check", state: "failure", description: "fail" }]; }, want: "merged" },
{ name: "no status at all", set: () => { state.statuses = []; }, want: "merged" },
{ name: "a check of another trunk", set: () => { state.statuses = judgedAt(MOVED); }, want: "merged" },
{ name: "the id said again after the merge", set: () => { state.merged = true; }, want: "held" },
{ name: "a pull request merged at another head", set: () => { state.merged = true; state.head = MOVED; }, want: /merged at 3c4d5e6f7a8b, not the abc1234def56/ },
{ name: "a closed pull request", set: () => { state.prState = "closed"; }, want: /it is closed/ },
{ name: "a fast-forward", set: () => { state.parents = [REVIEWED]; }, want: "merged" },
{ name: "the merge read back fails", set: () => { state.commitsFail = 502; }, want: "merged" },
{ name: "the forge merged another head", set: () => { state.landed = MOVED; }, want: /WAS merged .*the forge merged 3c4d5e6f7a8b, not the abc1234def56/, loud: true },
];
for (const r of rows) {
state.merged = false; state.head = REVIEWED; state.statuses = judgedAt(TRUNK); state.sent = [];
state.rules = [protectedMain()]; state.prState = "open"; state.parents = null; state.commitsFail = 0; state.landed = "";
r.set();
const got = await merge(r.extra, r.caller);
if (r.want === "merged") {
assert.equal(got.performed, true, `${r.name}: ${JSON.stringify(got)}`);
assert.equal(got["merge-commit"], state.mergeCommit, r.name);
assert.equal(state.sent.find((s) => s.path.endsWith("/pulls/7/merge"))?.body.head_commit_id, REVIEWED, r.name);
} else if (r.want === "held") {
assert.deepEqual([got.performed, got.held], [true, true], r.name);
assert.deepEqual(writes(), [], `${r.name}: nothing merged again`);
} else {
assert.match(String(got.refused ?? got.error), r.want, r.name);
assert.deepEqual(writes(), r.loud ? ["POST /repos/novox/mesh-catalog/pulls/7/merge"] : [], `${r.name}: what was merged`);
}
}
// A read: an open pull request answers what the forge would merge now; a forge that fails is the kind failed.
state.merged = false; state.head = REVIEWED; state.rules = [protectedMain()];
const pull = await call("pull", { repository: "novox/mesh-catalog", number: 7 });
assert.equal(pull.pull, undefined, "answered whole or not at all");
assert.equal(pull.error?.startsWith("the forge did not say whether novox/mesh-catalog#7 merges now"), true, JSON.stringify(pull));
assert.equal(pull["error-kind"], "failed", "a read the forge could not make answers the verb's error, never a guess");
state.mergeable = true;
const open = await call("pull", { repository: "novox/mesh-catalog", number: 7 });
state.mergeable = undefined;
assert.deepEqual([open.pull?.head, open.pull?.state, open.merging?.protected, open.merging?.required],
[REVIEWED, "open", true, ["mesh/merge-gate", "mesh/repo-check"]], JSON.stringify(open));
assert.equal("body" in (open.pull ?? {}), false, "never its description");
// Two writes of one head out of order (issue 456): a recheck's pending still in flight when the verdict is set through the
// seat; the verdict takes its turn after it and is what the head carries.
inFlight = new Promise<void>((r) => (land = r));
const recheck = gates.pending("novox", "mesh-catalog", REVIEWED);
const verdict = call("status", { id: "act-s", repository: "novox/mesh-catalog", commit: REVIEWED, state: "success",
context: "mesh/merge-gate", description: "passed" });
await new Promise((r) => setImmediate(r));
land();
await recheck;
assert.equal((await verdict).performed, true);
assert.equal(landed.at(-1), "success passed", `the verdict lands last: ${JSON.stringify(landed)}`);
// A pull request the forge does not have is the kind not-found, so a caller never reads it out of the words.
const gone = await call("pull", { repository: "novox/mesh-catalog", number: 404 });
assert.deepEqual([gone.pull, gone["error-kind"]], [undefined, "not-found"], JSON.stringify(gone));
});
// Each verb of the seat git answers the contract's shape from what the forge gives, and an error naming why when the forge
// does not (design 47 §310, §312, §317): an empty list or an absent file only when the forge said so, a forge that failed
// the kind failed, a 404 the kind not-found, and an act whose id the pull request or the note already holds is held.
test("every read and act of the seat git answers its contract, and an error is an error, never an empty answer", async () => {
const { seatTools } = await import("../dist/seat.js");
const { runAs } = await import("@novox/mesh-sdk/callers");
const SHA = "a".repeat(40), OTHER = "b".repeat(40);
const gone = () => { throw new Error("Gitea API GET /repos/novox/app/pulls/9: 404 Not Found"); };
const down = () => { throw new Error("Gitea API GET /repos/novox/app/x: 502 Bad Gateway"); };
const pr = { number: 9, title: "t", state: "open", merged: false, head: "feat/x", head_sha: SHA, base: "main", user: "jochen", html_url: "http://f/9", mergeable: true };
let comments: { id: number; body: string }[] = [], noteShown = "", added = 0, noteWritten = 0;
const forge: Record<string, any> = {
getPullRequest: async (_o: string, _r: string, n: number) => (n === 9 ? pr : gone()),
pullOfCommit: async () => ({ ...pr, state: "closed", merged: true, merge_commit_sha: OTHER, merged_at: "2026-11-11T10:00:00Z" }),
appliedProtection: async () => ({ rule_name: "main", enable_status_check: true, status_check_contexts: ["mesh/check"] }),
getRepo: async () => ({ writes: true }),
openPulls: async () => [{ owner: "novox", repo: "app", number: 9, base: "main", head: "feat/x", head_sha: SHA, required: ["mesh/check"], checks: {} }],
tags: async () => ["go/v0.1.1", "go/v0.1.0", "v1"],
commitTrees: async () => ({ root: SHA, trees: { "": SHA } }),
getFile: async (_o: string, _r: string, path: string) => (path === "gone" ? (() => { throw new Error("Gitea API GET /repos/novox/app/contents/gone: 404 Not Found"); })() : path === "down" ? down() : { path, sha: OTHER, size: 3, content: "abc" }),
compare: async () => ({ total_commits: 2, commits: [{ sha: OTHER }, { sha: SHA }] }),
combinedStatus: async () => ({ sha: SHA, state: "success", statuses: [{ context: "mesh/check", status: "success", description: "ok" }] }),
listComments: async () => comments,
addComment: async (_o: string, _r: string, _n: number, body: string) => { added++; comments.push({ id: added, body }); },
editComment: async () => {},
};
const run = async (_f: string, args: string[]) => args.includes("show") || args.includes("--ref") && !args.includes("append")
? { stdout: noteShown, code: noteShown ? 0 : 1 } : (noteWritten++, { stdout: "", code: 0 });
const seat = seatTools(forge as any, { run: run as any, container: "gitea", callers: async () => JSON.stringify({}), statuses: { status: async () => {} } });
const everyone = JSON.stringify(Object.fromEntries(seat.map((t: any) => [t.name.slice(4), ["*"]])));
const call = (verb: string, args: Record<string, unknown>, over: Record<string, unknown> = {}) => {
const t = seat.find((x: any) => x.name === `git.${verb}`)!;
const wrapped = seatTools(Object.assign(Object.create(forge), over) as any, { run: run as any, container: "gitea", callers: async () => everyone, statuses: { status: async () => {} } });
return runAs({ "mesh/caller": "ace.x" }, args, () => wrapped.find((x: any) => x.name === t.name)!.run(args)) as Promise<any>;
};
const rows: [string, string, Record<string, unknown>, Record<string, unknown>, (a: any) => unknown, unknown][] = [
["pull", "by number", { repository: "novox/app", number: 9 }, {}, (a) => [a.pull.head, a.pull.state, a.merging.mergeable, a.merging.required, a.merging.writes], [SHA, "open", true, ["mesh/check"], true]],
["pull", "by merge commit", { repository: "novox/app", "merge-commit": OTHER }, {}, (a) => [a.pull.state, a.pull["merge-commit"], a.merging], ["merged", OTHER, undefined]],
["pull", "unknown", { repository: "novox/app", number: 404 }, {}, (a) => [a.pull, a["error-kind"]], [undefined, "not-found"]],
["pull", "forge down", { repository: "novox/app", number: 9 }, { getPullRequest: down }, (a) => [a.pull, a["error-kind"]], [undefined, "failed"]],
["branch", "a branch a rule guards", { repository: "novox/app", branch: "main" }, { branchHead: async () => SHA, appliedProtection: async () => ({ rule_name: "main", enable_push: false }) }, (a) => [a.head, a.protected], [SHA, true]],
["branch", "a branch no rule guards", { repository: "novox/app", branch: "scratch" }, { branchHead: async () => SHA, appliedProtection: async () => null }, (a) => [a.head, a.protected], [SHA, false]],
["branch", "a rule that takes a push", { repository: "novox/app", branch: "main" }, { branchHead: async () => SHA, appliedProtection: async () => ({ rule_name: "main", enable_push: true }) }, (a) => [a.protected], [false]],
["branch", "no such branch", { repository: "novox/app", branch: "nope" }, { branchHead: gone }, (a) => [a.head, a["error-kind"]], [undefined, "not-found"]],
["open-pulls", "one", {}, {}, (a) => [a.pulls.length, a.pulls[0].required], [1, ["mesh/check"]]],
["open-pulls", "forge down", {}, { openPulls: down }, (a) => [a.pulls, a["error-kind"]], [undefined, "failed"]],
["tags", "a page", { repository: "novox/app", prefix: "go/" }, {}, (a) => [a.tags, a.page, a.bound, a.more], [["go/v0.1.1", "go/v0.1.0"], 1, 50, undefined]],
["tags", "forge down", { repository: "novox/app" }, { tags: down }, (a) => [a.tags, a["error-kind"]], [undefined, "failed"]],
["trees", "root", { repository: "novox/app", commit: SHA }, {}, (a) => [a.commit, a.root], [SHA, SHA]],
["trees", "forge down", { repository: "novox/app", commit: SHA }, { commitTrees: down }, (a) => [a.root, a["error-kind"]], [undefined, "failed"]],
["file", "present", { repository: "novox/app", path: "a.txt" }, {}, (a) => [a.content, a.absent], ["abc", undefined]],
["file", "the forge said absent", { repository: "novox/app", path: "gone" }, {}, (a) => [a.absent, a.error], [true, undefined]],
["file", "forge down is no absent file", { repository: "novox/app", path: "down" }, {}, (a) => [a.absent, a["error-kind"]], [undefined, "failed"]],
["contains", "ahead", { repository: "novox/app", commit: SHA, in: OTHER }, {}, (a) => [a.contains, a.ahead], [false, 2]],
["contains", "forge down", { repository: "novox/app", commit: SHA, in: OTHER }, { compare: down }, (a) => [a.contains, a["error-kind"]], [undefined, "failed"]],
["statuses", "newest of each", { repository: "novox/app", commit: SHA }, {}, (a) => [a.combined, a.statuses.map((s: any) => s.context)], ["success", ["mesh/check"]]],
["statuses", "forge down", { repository: "novox/app", commit: SHA }, { combinedStatus: down }, (a) => [a.statuses, a["error-kind"]], [undefined, "failed"]],
["status", "refused state", { id: "s1", repository: "novox/app", commit: SHA, context: "c", state: "bogus" }, {}, (a) => [a.performed, a["error-kind"]], [undefined, "refused"]],
["comment", "first time", { id: "c1", repository: "novox/app", number: 9, text: "hi" }, {}, (a) => [a.performed, a.held, added], [true, undefined, 1]],
["comment", "forge down", { id: "c2", repository: "novox/app", number: 9, text: "hi" }, { listComments: down }, (a) => [a.performed, a["error-kind"]], [undefined, "failed"]],
["view", "first time", { id: "v1", repository: "novox/app", number: 9, text: "state" }, {}, (a) => [a.performed, a.held], [true, undefined]],
["note", "a line", { id: "n1", repository: "novox/app", commit: SHA, line: "planned" }, {}, (a) => [a.performed, a.held, noteWritten], [true, undefined, 1]],
];
for (const [verb, name, args, over, pick, want] of rows) {
assert.deepEqual(pick(await call(verb, args, over)), want, `${verb}: ${name}`);
}
// The same comment id said again is held, and nothing is written again; so is a note line whose id the note holds.
const before = added;
const again = await call("comment", { id: "c1", repository: "novox/app", number: 9, text: "hi" });
assert.deepEqual([again.performed, again.held, added], [true, true, before]);
const view = await call("view", { id: "v1", repository: "novox/app", number: 9, text: "state" });
assert.deepEqual([view.performed, view.held], [true, true]);
noteShown = "planned [n1]"; noteWritten = 0;
const note = await call("note", { id: "n1", repository: "novox/app", commit: SHA, line: "worded differently" });
assert.deepEqual([note.performed, note.held, noteWritten], [true, true, 0]);
});