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

195 lines
14 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { createServer } from "node:http";
// A pull request's merge check (novox/hq to-be 45 §9): each new head announced once, and the verdict set
// where the pull request shows it — an error never as a success.
test("each open pull request's new head is announced once, and nothing closed or merged", async () => {
const { headsToAnnounce } = await import("../pulls.ts");
const pulls = [
{ number: 1, title: "a", state: "open", merged: false, head_sha: "aaa", html_url: "" },
{ number: 2, title: "b", state: "open", merged: false, head_sha: "bbb", html_url: "" },
{ number: 3, title: "c", state: "closed", merged: true, head_sha: "ccc", html_url: "" },
{ number: 4, title: "d", state: "open", merged: false, html_url: "" },
];
const announced = { "novox/mesh-catalog#1": "aaa", "novox/mesh-catalog#2": "old" };
assert.deepEqual(headsToAnnounce("novox/mesh-catalog", pulls, announced).map((p) => p.number), [2],
"only a head not announced, of a pull request that is open");
});
test("a verdict is the head commit's status; an error is the forge's error, never a success", async () => {
const { statusFor, commentFor, CHECK_CONTEXT } = await import("../pulls.ts");
const base = { owner: "novox", repo: "mesh-catalog", number: 7, commit: "0123456789abcdef", id: "build-1", on: "laptop" };
assert.equal(statusFor({ ...base, verdict: "pass", summary: "every machine composes" }).state, "success");
// A warning is a note, never a blocking state: the forge combines `warning` as a failure (issue 293).
const wide = statusFor({ ...base, verdict: "warning", summary: "a merge rebuilds 14 module(s)" });
assert.equal(wide.state, "success");
assert.match(wide.description, /^pass, with a note: a merge rebuilds 14 module/);
assert.equal(statusFor({ ...base, verdict: "fail", summary: "x" }).state, "failure");
assert.equal(statusFor({ ...base, verdict: "error", summary: "the check could not run" }).state, "error");
assert.equal(statusFor({ ...base, verdict: "", summary: "" }).state, "error", "no verdict is no pass");
const long = statusFor({ ...base, verdict: "fail", summary: "y".repeat(500) });
assert.ok(long.description.length <= 140 && long.context === CHECK_CONTEXT);
assert.equal(commentFor({ ...base, verdict: "pass", summary: "fine" }), null, "a pass leaves no comment");
const said = commentFor({ ...base, verdict: "fail", summary: "lemurs refused", report: "fails:\n - ```x```" }) ?? "";
assert.match(said, /mesh\/merge-gate`: \*\*FAIL\*\*/);
assert.match(said, /01234567/);
assert.match(said, /builds --log build-1/);
assert.ok(!said.slice(said.indexOf("```") + 3, said.lastIndexOf("```")).includes("```"), "the report cannot close its own block");
});
test("each layer is its own status: the gate with the modules it judged, the repository's own check beside it", async () => {
const { statusesFor, commentFor, CHECK_CONTEXT, REPO_CHECK_CONTEXT } = await import("../pulls.ts");
const base = { owner: "novox", repo: "mesh-catalog", number: 7, commit: "0123456789abcdef", id: "build-1" };
const both = statusesFor({ ...base, verdict: "pass", summary: "every machine composes",
gate: { verdict: "pass", summary: "every machine composes", modules: ["gitea", "keycloak"], dependents: ["node-tools"] },
"repo-check": { verdict: "fail", summary: "its merge-check.sh failed: FAIL x" } });
assert.deepEqual(both.map((s) => [s.context, s.state]), [[CHECK_CONTEXT, "success"], [REPO_CHECK_CONTEXT, "failure"]]);
assert.match(both[0].description, /gitea, keycloak \+1 dependent/);
assert.match(commentFor({ ...base, verdict: "pass", summary: "", gate: { verdict: "pass", summary: "" },
"repo-check": { verdict: "fail", summary: "its merge-check.sh failed" } }) ?? "", /mesh\/repo-check`: \*\*FAIL/);
// Nothing of the graph touched, no script: a pass and a warning, and no comment on every push.
const quiet = { ...base, verdict: "pass", summary: "the change touches no module of the mesh's graph",
gate: { verdict: "pass", summary: "the change touches no module of the mesh's graph" },
"repo-check": { verdict: "warning", summary: "the repository declares no merge-check.sh" } };
assert.deepEqual(statusesFor(quiet, { defined: false, required: false }).map((s) => [s.state, s.description]),
[["success", "pass: the change touches no module of the mesh's graph"], ["success", "no repository check defined"]]);
assert.equal(commentFor(quiet, { defined: false, required: false }), null);
// A repository outside the mesh, touching nothing: the gate alone, a pass.
assert.equal(statusesFor({ ...base, verdict: "pass", summary: "x", gate: { verdict: "pass", summary: "x" } }).length, 1);
// A controller from before the layers: its verdict is the gate's.
assert.deepEqual(statusesFor({ ...base, verdict: "warning", summary: "wide" }).map((s) => [s.context, s.state]),
[[CHECK_CONTEXT, "success"]]);
});
// **A required status is success or it blocks** (novox/hq issue 293): branch protection requires
// mesh/merge-gate (and mesh/repo-check on the core repositories) with no administrator override, and the
// forge combines `warning` as a failure. No verdict ever sets `warning` on either; a note is a success that
// says it; only what a person must decide is a failure, with why.
test("no verdict sets warning on a required check; only a person's decision fails", async () => {
const { statusesFor, commentFor, UNDEFINED_REQUIRED, REPO_CHECK_CONTEXT } = await import("../pulls.ts");
const base = { owner: "novox", repo: "photos", number: 3, commit: "0123456789abcdef", id: "build-2" };
const noScript = { verdict: "warning", summary: "the repository declares no merge-check.sh" };
const quiet = { ...base, verdict: "pass", summary: "x", gate: { verdict: "pass", summary: "x" }, "repo-check": noScript };
for (const verdict of ["pass", "warning", "fail", "error", ""]) {
for (const facts of [{}, { defined: true }, { defined: false }, { defined: false, required: true }, { defined: false, required: false }]) {
const c = { ...base, verdict, summary: "s", gate: { verdict, summary: "s" }, "repo-check": { verdict, summary: "s" } };
for (const s of statusesFor(c, facts)) assert.notEqual(s.state, "warning", `${verdict} ${JSON.stringify(facts)} → ${s.context}`);
}
}
// A repository without a merge-check.sh where repo-check is not required: success, said.
assert.deepEqual(statusesFor(quiet, { defined: false, required: false })[1],
{ state: "success", context: REPO_CHECK_CONTEXT, description: "no repository check defined" });
// Where it is required — or the protection could not be read — a person decides: a failure, with why.
for (const facts of [{ defined: false, required: true }, { defined: false }]) {
const s = statusesFor(quiet, facts)[1];
assert.equal(s.state, "failure");
assert.ok(s.description.length <= 140 && UNDEFINED_REQUIRED.startsWith(s.description.replace(/…$/, "")));
assert.match(s.description, /^fail: mesh\/repo-check is required/);
assert.match(commentFor(quiet, facts) ?? "", /mesh\/repo-check`: \*\*FAIL\*\* — mesh\/repo-check is required/);
}
// A script that defines its check and said a warning itself: a note, a success.
const noted = statusesFor({ ...quiet, "repo-check": { verdict: "warning", summary: "2 tests skipped" } }, { defined: true })[1];
assert.deepEqual([noted.state, noted.description], ["success", "pass, with a note: 2 tests skipped"]);
// The gate's notes — a wide rebuild, a problem already so on the base — are successes that say so.
const already = statusesFor({ ...base, verdict: "warning", summary: "s",
gate: { verdict: "warning", summary: "the module check's problems were all so on main already" } })[0];
assert.deepEqual([already.state, already.description],
["success", "pass, with a note: the module check's problems were all so on main already"]);
assert.match(commentFor({ ...base, verdict: "warning", summary: "wide", gate: { verdict: "warning", summary: "wide" } }) ?? "",
/PASS, WITH A NOTE/);
});
test("a commit status is set on the commit, under the merge check's context", async () => {
const { GiteaClient } = await import("../client.ts");
let seen: { path: string; body: any } | null = null;
const server = createServer((req, res) => {
let raw = "";
req.on("data", (c) => (raw += c));
req.on("end", () => {
seen = { path: `${req.method} ${req.url}`, body: JSON.parse(raw) };
res.statusCode = 201;
res.end("{}");
});
});
await new Promise<void>((r) => server.listen(0, r));
const port = (server.address() as any).port;
const client = new GiteaClient(`http://127.0.0.1:${port}`, "t");
try {
await client.setCommitStatus("novox", "mesh-catalog", "abc1234", { state: "failure", context: "mesh/merge-gate", description: "x" });
} finally {
server.close();
}
assert.equal(seen!.path, "POST /api/v1/repos/novox/mesh-catalog/statuses/abc1234");
assert.equal(seen!.body.state, "failure");
assert.equal(seen!.body.context, "mesh/merge-gate");
});
test("a change plan is the gate's result: said on the status and, when it builds something, as a comment", async () => {
const { statusFor, commentFor } = await import("../pulls.ts");
const plan = {
repository: "novox/mesh-catalog", base: "main", head: "0123456789abcdef", moved: ["gitea"],
tiers: [["gitea"]], machines: [{ machine: "anchor", receives: ["gitea"] }], steps: [],
summary: "builds gitea → anchor; no bus step",
};
const c = { owner: "novox", repo: "mesh-catalog", number: 7, commit: "0123456789abcdef", id: "b", verdict: "pass",
summary: "every machine composes", gate: { verdict: "pass", summary: "every machine composes", modules: ["gitea"] }, plan };
assert.equal(statusFor(c).description, "pass: builds gitea → anchor; no bus step; every machine composes");
const said = commentFor(c) ?? "";
assert.match(said, /Delivery plan\*\* — builds gitea → anchor/);
assert.match(said, /- anchor: receives gitea/);
// A plan that builds nothing, passing: the statuses say it, no comment.
const nothing = { ...c, plan: { ...plan, moved: [], tiers: [], machines: [], summary: "builds nothing" } };
assert.equal(commentFor(nothing), null);
});
// **A pending check says where it is** (novox/hq issue 446): announced, it waits in the build queue, since the time the
// operator reads — not "mapping this head onto its module graph" for the ten minutes of a controller check.
test("a head announced is pending in the build queue, since the local time it was announced", async () => {
const { pendingWords } = await import("../pulls.ts");
assert.equal(pendingWords(new Date(2026, 9, 11, 3, 8, 50)), "waiting in the build queue since 03:08");
assert.equal(pendingWords(new Date(2026, 9, 11, 14, 0, 0)), "waiting in the build queue since 14:00");
});
// **A group's composed verdict is each member's merge gate** (novox/hq ADR 0403 §1, issue 978): set at the head the
// composed check read, naming the trunk commit it was merged onto, the group, and what the check said.
test("a composed verdict sets every member's gate at its own head, naming its trunk and the group", async () => {
const { groupGateStatus, CHECK_CONTEXT } = await import("../pulls.ts");
const c = { owner: "", repo: "", commit: "", id: "build-1", group: "feat/x", verdict: "pass", summary: "both compose",
members: [{ owner: "novox", repo: "mesh-controller", number: 1, commit: "c".repeat(40), trunk: "a".repeat(40) },
{ owner: "novox", repo: "mesh-catalog", number: 2, commit: "d".repeat(40) }] };
const [first, second] = c.members.map((m) => groupGateStatus(c, m));
assert.equal(first.context, CHECK_CONTEXT);
assert.equal(first.state, "success");
assert.match(first.description, /^head cccccccccccc merged into trunk aaaaaaaaaaaa — pass: group feat\/x composed: both compose/);
assert.equal(second.state, "success");
assert.match(second.description, /^pass: group feat\/x composed: both compose/, "no trunk named, none said");
// A composed check that failed is every member's failure, never a success.
assert.equal(groupGateStatus({ ...c, verdict: "fail", summary: "a and b do not compose" }, c.members[0]).state, "failure");
assert.equal(groupGateStatus({ ...c, verdict: "error", summary: "could not run" }, c.members[0]).state, "error");
});
// A member's own verdict whose gate layer is the group's composed check's sets its repository check only, and never the
// gate as an error: the gate stays pending until the composed verdict lands, in either order (ADR 0403 §1, issue 978).
test("a member's own verdict with a composed gate layer leaves the gate to the composed verdict, in either order", async () => {
const { statusesFor, composedGate, groupGateStatus, CHECK_CONTEXT, REPO_CHECK_CONTEXT } = await import("../pulls.ts");
const c = { owner: "novox", repo: "mesh-catalog", number: 2, commit: "d".repeat(40), id: "build-1", verdict: "pass", summary: "x",
gate: { verdict: "composed", summary: "judged by its group" }, "repo-check": { verdict: "pass", summary: "suite passed" } };
assert.ok(composedGate(c));
// Before the composed verdict: the repository check, and the gate pending, saying it waits.
const before = statusesFor(c, {}, undefined);
assert.deepEqual(before.map((s) => [s.context, s.state]), [[CHECK_CONTEXT, "pending"], [REPO_CHECK_CONTEXT, "success"]]);
assert.match(before[0].description, /waits for its group's composed check/);
assert.ok(!before.some((s) => s.state === "error"));
// After it: the standing composed gate is left alone.
const standing = groupGateStatus({ ...c, group: "feat/x", members: [] }, { owner: "novox", repo: "mesh-catalog", commit: c.commit }).description;
const after = statusesFor(c, {}, standing);
assert.deepEqual(after.map((s) => [s.context, s.state]), [[REPO_CHECK_CONTEXT, "success"]]);
// A verdict with an ordinary gate is as before.
assert.equal(composedGate({ ...c, gate: { verdict: "pass", summary: "x" } }), false);
});