The review finishes before the push and its record is a Reviewed-by line in the description, so the mark repeated what the push meant. mesh-delivery asks the operator when a pull request's checks pass, keeps an ask through a trunk move by asking the check again at once, and hands the merge tool the approved heads, which replace the forge review of each sibling.
192 lines
11 KiB
TypeScript
192 lines
11 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);
|
|
});
|
|
|
|
// **An open pull request is read from the forge with no ready mark** (novox/hq ADR 0421 §4, issue 1022): its head, base,
|
|
// required contexts and statuses, and no timeline.
|
|
const head = "c".repeat(40), older = "d".repeat(40);
|
|
|
|
function forgeWith() {
|
|
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: 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" });
|
|
}
|
|
if (url.includes("/branch_protections")) return send([]);
|
|
if (url.includes("/status")) return send({ statuses: [] });
|
|
res.statusCode = 404;
|
|
res.end("{}");
|
|
});
|
|
return { server, asked };
|
|
}
|
|
|
|
test("an open pull request is read with its head and statuses, and no ready mark nor timeline", async () => {
|
|
const { GiteaClient } = await import("../client.ts");
|
|
const { server, asked } = forgeWith();
|
|
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");
|
|
const open = await client.openPulls();
|
|
assert.deepEqual(open.map((p) => [p.number, p.head_sha]), [[7, head], [8, older]]);
|
|
assert.equal((open[0] as any).ready, undefined);
|
|
assert.ok(!asked.some((u) => u.includes("/timeline")), "no timeline is read");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|