Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -30,6 +30,9 @@ const state = {
|
||||
// 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 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
|
||||
@@ -41,8 +44,13 @@ const state = {
|
||||
// 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 }[],
|
||||
};
|
||||
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).
|
||||
@@ -59,12 +67,16 @@ beforeEach(() => {
|
||||
state.trunk = TRUNK;
|
||||
state.emitted = [];
|
||||
state.rules = [protectedMain()];
|
||||
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 => {
|
||||
@@ -87,8 +99,18 @@ const server = createServer(async (req, res) => {
|
||||
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 (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);
|
||||
if (method === "GET" && path === `${repo}/branches/main`) return json(res, 200, { name: "main", commit: { id: state.trunk } });
|
||||
const branch = method === "GET" && path.startsWith(`${repo}/branches/`) ? decodeURIComponent(path.slice(`${repo}/branches/`.length)) : null;
|
||||
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: false } : {}) });
|
||||
}
|
||||
if (method === "GET" && path === `${repo}/pulls/7/files`) {
|
||||
return json(res, 200, url.searchParams.get("page") === "1" ? [{ filename: "modules/gitea/x.ts", status: "modified" }] : []);
|
||||
}
|
||||
@@ -113,8 +135,17 @@ const server = createServer(async (req, res) => {
|
||||
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 });
|
||||
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 === "POST" && path === `${repo}/pulls/7/merge`) {
|
||||
state.merged = true;
|
||||
return json(res, 200, null);
|
||||
@@ -199,6 +230,7 @@ test("a protection rule covers a branch as the forge decides it: classes, altern
|
||||
});
|
||||
|
||||
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" });
|
||||
@@ -206,6 +238,7 @@ test("gitea_api still writes what an author writes", async () => {
|
||||
});
|
||||
|
||||
test("gitea_branch_protection_set only strengthens", 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 });
|
||||
await assert.rejects(set({ status_checks: "" }), /drops the required status mesh\/merge-gate, mesh\/repo-check/);
|
||||
@@ -396,6 +429,7 @@ test("gitea_merge_pull_request reads back a fast-forward, and says it cannot rea
|
||||
});
|
||||
|
||||
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/);
|
||||
@@ -427,6 +461,7 @@ test("gitea_merge_pull_request merges only into a branch whose protection requir
|
||||
});
|
||||
|
||||
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>) =>
|
||||
@@ -511,3 +546,64 @@ test("no tool's caller text becomes another path or another user: `..` and `?sud
|
||||
assert.deepEqual(state.asked, ["DELETE /repos/novox/mesh-catalog/branches/feat/x",
|
||||
"GET /repos/novox/mesh-catalog/contents/modules/gitea/module.json"]);
|
||||
});
|
||||
|
||||
|
||||
// **A member judged beside its siblings merges only with its group passed and reviewed** (novox/hq ADR 0382 §6, issue 867):
|
||||
// the verdict names the sibling; mesh/delivery-group must be a success at the member's head and the sibling's head that
|
||||
// verdict read, and the sibling's head must carry an approving review. A verdict that read none merges as before.
|
||||
test("gitea_merge_pull_request merges a verdict that read siblings only with its group passed and each sibling head reviewed", async () => {
|
||||
const { keepSiblings, siblingsFile } = await import("../dist/siblings.js");
|
||||
const merge = () => tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED });
|
||||
const beside = `head ${REVIEWED.slice(0, 12)} merged into trunk ${TRUNK.slice(0, 12)} beside mesh-host#114@${SIBLING_HEAD.slice(0, 8)}`;
|
||||
const named = (siblings: unknown[]) => keepSiblings(siblingsFile(stateDir),
|
||||
{ "novox/mesh-catalog#7": { head: REVIEWED, siblings: siblings as any } });
|
||||
state.statuses = [{ context: "mesh/merge-gate", state: "success", description: `${beside} — pass: x` },
|
||||
{ context: "mesh/repo-check", state: "success", description: `${beside} — pass: y` }];
|
||||
named([{ repository: "novox/mesh-host", number: 114, head: SIBLING_HEAD, base: "main", base_commit: "c".repeat(40) }]);
|
||||
|
||||
// Nothing passed: every missing thing is named, nothing merged.
|
||||
await assert.rejects(merge(), (err: Error) => {
|
||||
assert.match(err.message, /is not merged: its verdict was judged beside 1 sibling\(s\), and mesh\/delivery-group is missing at mesh-catalog#7@abc1234def56; mesh\/delivery-group is missing at mesh-host#114@bbbbbbbbbbbb; mesh-host#114@bbbbbbbbbbbb carries no approving review at that head/);
|
||||
return true;
|
||||
});
|
||||
assert.equal(state.merged, false);
|
||||
|
||||
// The group passed at the member's head only: the sibling's head still lacks it.
|
||||
state.groupStatus = "success";
|
||||
await assert.rejects(merge(), /mesh\/delivery-group is missing at mesh-host#114@bbbbbbbbbbbb; mesh-host#114@bbbbbbbbbbbb carries no approving review/);
|
||||
// Passed at both heads, but the only approval is at another head, or dismissed: not an approval at the head read.
|
||||
state.siblingGroup = "success";
|
||||
state.reviews = [{ state: "APPROVED", commit_id: "d".repeat(40) }, { state: "APPROVED", commit_id: SIBLING_HEAD, dismissed: true },
|
||||
{ state: "COMMENT", commit_id: SIBLING_HEAD }];
|
||||
await assert.rejects(merge(), /judged beside 1 sibling\(s\), and mesh-host#114@bbbbbbbbbbbb carries no approving review at that head\. A member judged beside/);
|
||||
assert.equal(state.merged, false);
|
||||
|
||||
// Passed and approved at the very heads read: it merges.
|
||||
state.reviews.push({ state: "APPROVED", commit_id: SIBLING_HEAD });
|
||||
assert.equal(((await merge()) as Record<string, unknown>).merged, true);
|
||||
|
||||
// A status that says it read siblings, with no record of them, is no verdict to merge on.
|
||||
state.merged = false;
|
||||
keepSiblings(siblingsFile(stateDir), {});
|
||||
await assert.rejects(merge(), /its verdict read siblings, and which ones is not held here/);
|
||||
assert.equal(state.merged, false);
|
||||
|
||||
// A verdict that read none (an older controller, or a group of one) merges exactly as before.
|
||||
state.statuses = judgedAt(TRUNK);
|
||||
state.groupStatus = "";
|
||||
assert.equal(((await merge()) as Record<string, unknown>).merged, true);
|
||||
});
|
||||
|
||||
test("a verdict put out of date by a sibling's move is refused in its own words, and asked again once", async () => {
|
||||
const merge = () => tool("gitea_merge_pull_request").run({ owner: "novox", repo: "mesh-catalog", number: 7, head: REVIEWED });
|
||||
const words = "out of date: judged beside mesh-host#114 at bbbbbbbbbbbb, it is now at dddddddddddd; judged again on a recheck or when it is merged";
|
||||
state.statuses = judgedAt(TRUNK).map((s) => ({ ...s, state: "pending", description: words }));
|
||||
await assert.rejects(merge(), (err: Error) => {
|
||||
assert.match(err.message, /its verdict was put out of date by a sibling's move \(out of date: judged beside mesh-host#114 at bbbbbbbbbbbb, it is now at dddddddddddd/);
|
||||
assert.match(err.message, /was asked/);
|
||||
assert.match(err.message, /never merges by itself/);
|
||||
return true;
|
||||
});
|
||||
assert.equal(state.merged, false);
|
||||
assert.equal(state.emitted.length, 1, "the check is asked, once");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user