mesh/merge-gate head 48f0662fdea2 merged into trunk 5ce2d07c7f47 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 48f0662fdea2 merged into trunk 5ce2d07c7f47 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The tools' token reached everything the administrator may do, so their boundary rested on their own guards. The gitea module now makes a plain account, mesh-tools, with the admin login, gives it write on code, issues and pull requests of every novox repository through one team, and mints the tools' token for it; the admin's old token goes only after the new one is kept. The Go forge bundle sends that token, never the admin login. The forge shows protection rules only to a repository administrator, so every check reads a branch's protection from the branch. mesh-admin stays a ready author, so marks made before the move keep counting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
266 lines
15 KiB
TypeScript
266 lines
15 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createServer } from "node:http";
|
|
|
|
// **A pull request opened after its push is still checked** (novox/hq issue 347). The announcer read only the
|
|
// repositories whose update time moved since its last pass, less a minute's margin; opening, reopening or
|
|
// retargeting a pull request does not move a repository's update time, so a pull request opened more than a
|
|
// minute or so after its push was never announced, never checked, and nothing said so.
|
|
|
|
const repo = (full: string, updated: string) => {
|
|
const [owner, name] = full.split("/");
|
|
return { full_name: full, owner, name, private: false, html_url: "", updated_at: updated };
|
|
};
|
|
|
|
test("a pull request opened after the margin is looked at, by its own update time", async () => {
|
|
const { reposToLook } = await import("../pulls.ts");
|
|
const pushed = "2026-10-09T10:00:00Z"; // the branch pushed: the repository's update time
|
|
// The pull request opened two minutes later; the last pass was at 10:02:30, so the floor is 10:01:30.
|
|
const floor = "2026-10-09T10:01:30Z";
|
|
const repos = [repo("novox/mesh-sdk", pushed), repo("novox/hq", "2026-10-09T09:00:00Z")];
|
|
const opened = [{ owner: "novox", name: "mesh-sdk", updated_at: "2026-10-09T10:02:00Z" }];
|
|
assert.deepEqual(reposToLook(repos, floor, opened, false).map((r) => r.full_name), ["novox/mesh-sdk"],
|
|
"the repository of a pull request updated since the floor, though the repository itself did not move");
|
|
assert.deepEqual(reposToLook(repos, floor, [], false).map((r) => r.full_name), [],
|
|
"without the pull request's own time nothing is looked at — the fault");
|
|
assert.deepEqual(reposToLook(repos, floor, [], true).map((r) => r.full_name).sort(), ["novox/hq", "novox/mesh-sdk"],
|
|
"a sweep looks at every repository");
|
|
assert.deepEqual(reposToLook(repos, "", [], false).length, 2, "the first pass looks at every repository");
|
|
});
|
|
|
|
test("every open pull request is read with what its base requires and what its head has (gitea_open_pulls)", async () => {
|
|
const { GiteaClient } = await import("../client.ts");
|
|
const asked: string[] = [];
|
|
const server = createServer((req, res) => {
|
|
asked.push(req.url ?? "");
|
|
const url = req.url ?? "";
|
|
const send = (v: unknown) => res.end(JSON.stringify(v));
|
|
if (url.startsWith("/api/v1/repos/issues/search")) {
|
|
return send(url.includes("page=1") ? [
|
|
{ number: 243, repository: { owner: "novox", name: "hq", full_name: "novox/hq" }, updated_at: "2026-10-09T11:00:00Z" },
|
|
{ number: 14, repository: { owner: "novox", name: "mesh-sdk", full_name: "novox/mesh-sdk" } },
|
|
] : []);
|
|
}
|
|
if (url.startsWith("/api/v1/repos/novox/hq/pulls/243")) {
|
|
return send({ number: 243, state: "open", base: { ref: "main" }, head: { ref: "x", sha: "aaa1111" }, html_url: "h" });
|
|
}
|
|
if (url.startsWith("/api/v1/repos/novox/mesh-sdk/pulls/14")) {
|
|
return send({ number: 14, state: "open", base: { ref: "main" }, head: { ref: "y", sha: "bbb2222" }, html_url: "s" });
|
|
}
|
|
// The base's protection, read from the branch as the tools' account may (novox/hq ADR 0365); the rules' list is refused it.
|
|
if (url.includes("/branch_protections")) {
|
|
res.statusCode = 403;
|
|
return res.end(JSON.stringify({ message: "the rules are a repository administrator's" }));
|
|
}
|
|
if (url.startsWith("/api/v1/repos/novox/hq/branches/main")) {
|
|
return send({ name: "main", protected: true, effective_branch_protection_name: "main", enable_status_check: true,
|
|
status_check_contexts: ["mesh/repo-check"] });
|
|
}
|
|
if (url.startsWith("/api/v1/repos/novox/mesh-sdk/branches/main")) {
|
|
return send({ name: "main", protected: true, effective_branch_protection_name: "main", enable_status_check: true,
|
|
status_check_contexts: ["mesh/merge-gate", "mesh/repo-check"] });
|
|
}
|
|
if (url.includes("/commits/aaa1111/status")) return send({ statuses: [] });
|
|
if (url.includes("/commits/bbb2222/status")) return send({ statuses: [{ context: "mesh/merge-gate", status: "pending",
|
|
description: "the mesh is mapping this head onto its module graph", updated_at: "2026-10-11T00:11:39Z" }] });
|
|
res.statusCode = 404;
|
|
res.end("{}");
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, r));
|
|
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
try {
|
|
const open = await client.openPulls();
|
|
assert.deepEqual(open.map((p) => [p.owner, p.repo, p.number, p.base, p.head_sha, p.required, p.statuses]), [
|
|
["novox", "hq", 243, "main", "aaa1111", ["mesh/repo-check"], []],
|
|
["novox", "mesh-sdk", 14, "main", "bbb2222", ["mesh/merge-gate", "mesh/repo-check"], ["mesh/merge-gate"]],
|
|
]);
|
|
// Each status with its state and time (novox/hq issue 438): a check pending is told from one that answered.
|
|
assert.deepEqual(open.map((p) => p.checks), [{}, { "mesh/merge-gate": { state: "pending",
|
|
description: "the mesh is mapping this head onto its module graph", updated_at: "2026-10-11T00:11:39Z" } }]);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("a pull request or issue number that is not one is refused before the forge is asked", async () => {
|
|
const { GiteaClient } = await import("../client.ts");
|
|
let asked = 0;
|
|
const server = createServer((_req, res) => {
|
|
asked++;
|
|
res.end("{}");
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, r));
|
|
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
try {
|
|
await assert.rejects(client.getPullRequest("novox", "hq", Number(undefined)), /whole number/);
|
|
await assert.rejects(client.addComment("novox", "hq", 0, "x"), /whole number/);
|
|
await assert.rejects(client.listPullFiles("novox", "hq", 1.5), /whole number/);
|
|
assert.equal(asked, 0, "the forge was asked about pulls/NaN");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
// **A merge names the branch it came from, also once that branch is deleted** (novox/hq issue 578). On 2026-10-12
|
|
// the merge tool merged mesh-controller #271 and mesh-catalog #251, one delivery group, and deleted each branch. The
|
|
// forge then read each pull request's head.ref as `refs/pull/<n>/head`, and `pull.merged` named that as the head: the
|
|
// controller asked mesh-delivery for a group of that name, found none, and cut #251's walk at once, before #271's,
|
|
// which the group's order put first.
|
|
test("a merged pull request whose branch was deleted is announced with its branch, not its pull ref", async () => {
|
|
const { GiteaClient, headBranchOf } = await import("../client.ts");
|
|
const group = "feat/341-a-merge-check-judges-the-merge";
|
|
// As the forge answered for #251 after its merge.
|
|
const merged = {
|
|
number: 251, title: "A verdict goes out of date when its trunk moves", state: "closed", merged: true,
|
|
merged_at: "2026-10-12T16:11:29Z", merge_commit_sha: "23ea0f431faf91aa83e7b0297e0a9e442180f0c7",
|
|
head: { label: group, ref: "refs/pull/251/head", sha: "41b6e10077efdef694b519879fcbaeaf69d472b1" },
|
|
base: { label: "main", ref: "main" }, html_url: "http://fake/novox/mesh-catalog/pulls/251",
|
|
};
|
|
const server = createServer((req, res) => {
|
|
const url = req.url ?? "";
|
|
const send = (v: unknown) => res.end(JSON.stringify(v));
|
|
if (url.startsWith("/api/v1/repos/novox/mesh-catalog/pulls/251")) return send(merged);
|
|
if (url.startsWith("/api/v1/repos/novox/mesh-catalog/pulls?")) return send([merged]);
|
|
res.statusCode = 404;
|
|
res.end("{}");
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, r));
|
|
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
try {
|
|
// What the announcer reads each pass, and what `pull.merged` carries as its head.
|
|
const listed = await client.listPullRequests("novox", "mesh-catalog", { state: "closed", sort: "recentupdate", limit: "20" });
|
|
assert.deepEqual(listed.map((p) => p.head), [group], "the pass names the group's branch, never refs/pull/251/head");
|
|
assert.equal((await client.getPullRequest("novox", "mesh-catalog", 251)).head, group);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
// While the branch exists the forge says it as ref, and that is what is named; a label alone is the branch too.
|
|
assert.equal(headBranchOf({ label: "feat/x", ref: "feat/x" }), "feat/x");
|
|
assert.equal(headBranchOf({ label: "feat/x" }), "feat/x");
|
|
assert.equal(headBranchOf({ ref: "feat/x" }), "feat/x");
|
|
// A branch whose own name only looks like a pull ref's start is still a branch.
|
|
assert.equal(headBranchOf({ label: "y", ref: "refs/pulled" }), "refs/pulled");
|
|
// No label to read: the pull ref is all the forge said, and it is kept rather than made up.
|
|
assert.equal(headBranchOf({ ref: "refs/pull/7/head" }), "refs/pull/7/head");
|
|
assert.equal(headBranchOf(undefined), undefined);
|
|
});
|
|
|
|
// **A pull request marked ready says the head it is marked at** (novox/hq issue 722): the label mesh/ready and the
|
|
// newest comment "ready at <full head sha>" by a listed account, written after the label was added and after the last
|
|
// push. Read from the pull request's whole timeline, page by page; without the label nothing is read.
|
|
const head = "c".repeat(40), older = "d".repeat(40);
|
|
const ev = (id: number, type: string, body: string, user = "mesh-admin", label?: { id: number; name: string }) =>
|
|
({ id, type, body, user: { login: user }, created_at: `2026-10-14T10:${String(id % 60).padStart(2, "0")}:00Z`,
|
|
...(label ? { label } : {}) });
|
|
const readyLabel = { id: 31, name: "mesh/ready" };
|
|
|
|
function forgeWith(timeline: unknown[]) {
|
|
const asked: string[] = [];
|
|
const server = createServer((req, res) => {
|
|
const url = req.url ?? "";
|
|
asked.push(url);
|
|
const send = (v: unknown) => res.end(JSON.stringify(v));
|
|
if (url.startsWith("/api/v1/repos/issues/search")) {
|
|
return send(url.includes("page=1") ? [
|
|
{ number: 7, repository: { owner: "novox", name: "hq", full_name: "novox/hq" },
|
|
labels: [{ id: 31, name: "mesh/ready" }, { id: 2, name: "bug" }] },
|
|
{ number: 8, repository: { owner: "novox", name: "hq", full_name: "novox/hq" }, labels: [] },
|
|
] : []);
|
|
}
|
|
if (url.startsWith("/api/v1/repos/novox/hq/pulls/7")) {
|
|
return send({ number: 7, state: "open", base: { ref: "main" }, head: { ref: "x", sha: head }, html_url: "h" });
|
|
}
|
|
if (url.startsWith("/api/v1/repos/novox/hq/pulls/8")) {
|
|
return send({ number: 8, state: "open", base: { ref: "main" }, head: { ref: "y", sha: older }, html_url: "h" });
|
|
}
|
|
const t = /\/issues\/7\/timeline\?page=(\d+)&limit=50/.exec(url);
|
|
if (t) {
|
|
const page = Number(t[1]);
|
|
return send(timeline.slice((page - 1) * 50, page * 50));
|
|
}
|
|
if (url.includes("/branch_protections")) return send([]);
|
|
if (url.includes("/status")) return send({ statuses: [] });
|
|
res.statusCode = 404;
|
|
res.end("{}");
|
|
});
|
|
return { server, asked };
|
|
}
|
|
|
|
async function readOpen(timeline: unknown[], authors = ["mesh-admin", "jschoubben"]) {
|
|
const { GiteaClient } = await import("../client.ts");
|
|
const { server, asked } = forgeWith(timeline);
|
|
await new Promise<void>((r) => server.listen(0, r));
|
|
try {
|
|
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
|
|
return { open: await client.openPulls(authors), asked };
|
|
} finally {
|
|
server.close();
|
|
}
|
|
}
|
|
|
|
test("a pull request's ready mark is read: the label and the head the newest listed ready comment names", async () => {
|
|
const { open, asked } = await readOpen([
|
|
ev(1, "pull_push", JSON.stringify({ commit_ids: [head] })),
|
|
ev(2, "label", "1", "mesh-admin", readyLabel),
|
|
ev(3, "comment", `ready at ${older}`),
|
|
ev(4, "comment", `Reviewed: approve.\nready at ${head}\n`),
|
|
ev(5, "comment", "thanks", "jschoubben"),
|
|
]);
|
|
assert.deepEqual(open[0].labels, ["mesh/ready", "bug"]);
|
|
assert.deepEqual(open[0].ready, { head, by: "mesh-admin", at: "2026-10-14T10:04:00Z", label_id: 31 });
|
|
assert.equal(open[1].ready, undefined, "no label, no mark");
|
|
assert.ok(!asked.some((u) => u.includes("/issues/8/")), "no timeline read without the label");
|
|
});
|
|
|
|
test("a ready comment by an account not listed is ignored, and said", async () => {
|
|
const { readyMarkOf } = await import("../client.ts");
|
|
const said: string[] = [];
|
|
const mark = readyMarkOf(31, [
|
|
ev(1, "pull_push", "{}") as any, ev(2, "label", "1", "mesh-admin", readyLabel) as any,
|
|
{ ...ev(3, "comment", `ready at ${head}`, "outsider"), user: "outsider" },
|
|
].map((e: any) => ({ ...e, user: typeof e.user === "string" ? e.user : e.user.login })), ["mesh-admin"], (l) => said.push(l));
|
|
assert.equal(mark.head, "", "an outsider's mark counts");
|
|
assert.equal(said.length, 1);
|
|
assert.match(said[0], /by outsider is ignored/);
|
|
});
|
|
|
|
test("a ready comment older than the latest push, or than the label, never counts", async () => {
|
|
const pushedAfter = await readOpen([
|
|
ev(1, "label", "1", "mesh-admin", readyLabel),
|
|
ev(2, "comment", `ready at ${head}`),
|
|
ev(3, "pull_push", JSON.stringify({ commit_ids: [head] })),
|
|
]);
|
|
assert.equal(pushedAfter.open[0].ready?.head, "", "a mark written before the latest push counts, the label still on");
|
|
const beforeLabel = await readOpen([
|
|
ev(1, "pull_push", "{}"),
|
|
ev(2, "comment", `ready at ${head}`),
|
|
ev(3, "label", "1", "mesh-admin", readyLabel),
|
|
]);
|
|
assert.equal(beforeLabel.open[0].ready?.head, "", "a mark written before the label was added counts");
|
|
const takenOff = await readOpen([
|
|
ev(1, "label", "1", "mesh-admin", readyLabel),
|
|
ev(2, "comment", `ready at ${head}`),
|
|
ev(3, "label", "", "mesh-admin", readyLabel),
|
|
]);
|
|
assert.equal(takenOff.open[0].ready?.head, "", "a mark whose label was taken off since counts");
|
|
});
|
|
|
|
test("a long thread hides nothing: every page of the timeline is read", async () => {
|
|
const events: unknown[] = [ev(1, "pull_push", "{}"), ev(2, "label", "1", "mesh-admin", readyLabel),
|
|
ev(3, "comment", `ready at ${older}`)];
|
|
for (let id = 4; id < 130; id++) events.push(ev(id, "comment", "a long discussion", "jschoubben"));
|
|
events.push(ev(130, "comment", `ready at ${head}`, "jschoubben"));
|
|
const { open, asked } = await readOpen(events);
|
|
assert.equal(open[0].ready?.head, head, "the newest mark, on the third page, is not read");
|
|
assert.equal(asked.filter((u) => u.includes("/issues/7/timeline")).length, 3);
|
|
});
|
|
|
|
test("the accounts that may mark are the setting's, and only the mesh's own when it cannot be read", async () => {
|
|
const { readyAuthors } = await import("../client.ts");
|
|
assert.deepEqual(readyAuthors("f", "mesh-admin", () => "# x\nauthors=mesh-admin jschoubben\n"), ["mesh-admin", "jschoubben"]);
|
|
// The tools' account and the admin's, whose marks were made before ADR 0365 (issue 469).
|
|
assert.deepEqual(readyAuthors("f", "mesh-admin", () => { throw new Error("gone"); }, "mesh-tools"), ["mesh-tools", "mesh-admin"]);
|
|
assert.deepEqual(readyAuthors("f", "mesh-admin", () => "authors=\n", "mesh-tools"), ["mesh-tools", "mesh-admin"]);
|
|
assert.deepEqual(readyAuthors(undefined, "mesh-admin", undefined, "mesh-tools"), ["mesh-tools", "mesh-admin"]);
|
|
});
|