gitea: remove the code bound to no event since ADR 0571 clause 13: the verdict setter, the controller's checked and plan types, the build seat's started (issue 1126)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
+3
-48
@@ -1,8 +1,7 @@
|
||||
// **One writer of a head's mesh/merge-gate, and the verdict lands last** (novox/hq issue 446).
|
||||
//
|
||||
// A pending check says where it is: "waiting in the build queue since …" when the head is announced, "building on
|
||||
// <machine> since …" when a build machine takes the check — the build seat's `started` (novox/hq ADR 0157) — and the
|
||||
// verdict when the controller says `checked`. The forge keeps only the newest status of a context, so a pending word
|
||||
// A pending check says where it is: "waiting in the build queue since …" when the head is announced, and the verdict when
|
||||
// it is said. The forge keeps only the newest status of a context, so a pending word
|
||||
// written after the verdict hides it: a pass reads pending and the merge waits for ever; a failure hides under
|
||||
// "building". Two writers cannot order their writes, and a writer that reads the forge before writing races the other
|
||||
// between its read and its write. So this module is the one writer of mesh/merge-gate, and every write of one head's
|
||||
@@ -20,16 +19,6 @@
|
||||
import { CHECK_CONTEXT, pendingWords } from "./pulls.js";
|
||||
import type { CommitStatus, HeadCheck } from "./client.js";
|
||||
|
||||
/** The build seat's `started`, as mesh-controller's link.BuildStart publishes it. */
|
||||
export interface BuildStarted {
|
||||
id?: string;
|
||||
repository?: string;
|
||||
path?: string;
|
||||
ref?: string;
|
||||
on?: string;
|
||||
at?: string;
|
||||
}
|
||||
|
||||
/** What the gate writer needs of the forge: a status set, and a head's gate as the forge holds it. */
|
||||
export interface GateForge {
|
||||
setCommitStatus(owner: string, repo: string, sha: string, status: CommitStatus): Promise<void>;
|
||||
@@ -77,15 +66,9 @@ export function openedAt(history: GateStatus[]): number | undefined {
|
||||
/** How far a build machine's clock may stand behind this one's when a start is weighed against the pending words. */
|
||||
const CLOCK_SLACK_MS = 60_000;
|
||||
|
||||
/** How the words of a check a machine is running begin. */
|
||||
/** How the words of a check a machine was running began: a status the forge still holds from before is no opening of a check. */
|
||||
const BUILDING = "building on ";
|
||||
|
||||
/** The words of a check a machine is running, in the time the operator reads. */
|
||||
export function buildingWords(on: string, at: Date): string {
|
||||
const two = (n: number) => String(n).padStart(2, "0");
|
||||
return `${BUILDING}${on} since ${two(at.getHours())}:${two(at.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** Whether a repository as the mesh clones it (`http://forge/novox/hq.git`) is owner/repo. */
|
||||
export function clonedAs(url: string | undefined, owner: string, repo: string): boolean {
|
||||
const u = (url ?? "").trim().toLowerCase().replace(/\.git$/, "");
|
||||
@@ -196,34 +179,6 @@ export class GateWriter {
|
||||
}).then(() => written);
|
||||
}
|
||||
|
||||
/**
|
||||
* A build machine took an ask of this head: said while no verdict was heard, and only for the check now pending —
|
||||
* a start from before the pending words were set (a run before a recheck, the stream's history) is not this check's.
|
||||
* A head this process did not announce (it restarted since) is weighed against the forge's own gate: said only while
|
||||
* that is pending, and not for a start older than it.
|
||||
*/
|
||||
started(owner: string, repo: string, sha: string, b: BuildStarted): Promise<boolean> {
|
||||
const at = b.at ? new Date(b.at) : new Date(NaN);
|
||||
if (!b.on || Number.isNaN(at.getTime())) return Promise.resolve(false);
|
||||
const h = this.head(owner, repo, sha);
|
||||
let said = false;
|
||||
return this.said(h, `${owner}/${repo}@${sha.slice(0, 8)}: could not say where its check is`, async () => {
|
||||
if (h.verdict) return;
|
||||
let since = h.pendingAt;
|
||||
if (since === undefined) {
|
||||
const gate = await this.forge.gateOf(owner, repo, sha);
|
||||
// No gate on the forge: none was written (its base does not require it, issue 1301), and nothing is said.
|
||||
if (!gate || gate.state !== "pending") return;
|
||||
since = gate?.updated_at ? Date.parse(gate.updated_at) : undefined;
|
||||
}
|
||||
if (since !== undefined && at.getTime() < since - CLOCK_SLACK_MS) return;
|
||||
if (h.verdict) return; // heard while the forge was read
|
||||
await this.forge.setCommitStatus(owner, repo, sha, { state: "pending", context: CHECK_CONTEXT,
|
||||
description: buildingWords(b.on as string, at) });
|
||||
said = true;
|
||||
}).then(() => said);
|
||||
}
|
||||
|
||||
/**
|
||||
* The verdict: marked at once, so no pending word is written after it, then written in its turn — last. A write
|
||||
* that fails fails the caller, so the event is offered again.
|
||||
|
||||
+6
-119
@@ -9,13 +9,9 @@
|
||||
//
|
||||
// module.gitea.pull.closed — a pull request closed unmerged: the delivery it was is stopped (novox/hq ADR 0239)
|
||||
//
|
||||
// Once consumed mesh-controller.checked — a pull request's merge check, judged — and set it as the head
|
||||
// commit's statuses (setVerdict, kept below for a revert, and bound to no event since ADR 0571 clause 13): `mesh/merge-gate`, the modules of the mesh's graph the change touches, and
|
||||
// `mesh/repo-check`, the repository's own merge-check.sh (novox/hq ADR 0237 as amended); with a comment
|
||||
// saying why when the gate is not a pass or the repository's own check failed. Of the two, only those the base
|
||||
// still requires are written (issue 1301): main requires `mesh/check` — the checker's — everywhere, hq included since
|
||||
// ADR 0527 §2, and since R4g no controller judges a merge (its merge gate went with build-agent, ADR 0524 §5), so nothing
|
||||
// publishes a merge check's verdict, this module consumes no event of the controller, and these two are written for no base.
|
||||
// It consumes no event of the controller: no controller judges a merge (ADR 0524 §5, ADR 0571 clause 13), and the two older
|
||||
// statuses, `mesh/merge-gate` and `mesh/repo-check`, are written for no base that main requires (issue 1301); main
|
||||
// requires `mesh/check`, the checker's, everywhere, hq included since ADR 0527 §2.
|
||||
//
|
||||
// Reads mesh-delivery-checker.owed — what the checker owes the forge, mesh/check on each head it judged — by a watch, and
|
||||
// writes it (check.ts; ADR 0521, R4e finding R2-1). No tool writes mesh/check (finding K1), and no event: the watch hands
|
||||
@@ -53,14 +49,13 @@ import { announce, keepWatermark, readWatermark } from "./announce.js";
|
||||
import { mergeTool } from "./merge.js";
|
||||
import { GiteaAdmin, GiteaClient, movedSince } from "./client.js";
|
||||
import { keepMainRules, mainRulesStep } from "./mainrules.js";
|
||||
import { CHECK_CONTEXT, CHECK_STATUS, REPO_CHECK_CONTEXT, commentFor, composedGate, groupGateStatus, headKey, maintained,
|
||||
requiredOf, statusesFor, writesGate, type Announced, type Checked, repoCheckFacts } from "./pulls.js";
|
||||
import { CHECK_CONTEXT, CHECK_STATUS, REPO_CHECK_CONTEXT, headKey, requiredOf, writesGate, type Announced } from "./pulls.js";
|
||||
import { actOn, announceNewHeads, startPass } from "./watch.js";
|
||||
import { GateWriter } from "./gate.js";
|
||||
import { HOOK_PATH, HOOK_PORT, Pace, claimHead, ensureSystemHook, eventsTool, keptSecret, receiver, type ForgeEvent } from "./webhook.js";
|
||||
import { keepSiblings, noteComposed, noteVerdict, readSiblings, siblingsFile, siblingsOf, weighSiblings } from "./siblings.js";
|
||||
import { keepSiblings, readSiblings, siblingsFile, weighSiblings } from "./siblings.js";
|
||||
import { bindInBackground, bindUntilHeld } from "./bind.js";
|
||||
import { announcementOf, keepTook, lateVerdictComment, outOfDateWords, sameCommit, tookFile, weighTrunks } from "./trunk.js";
|
||||
import { announcementOf, weighTrunks } from "./trunk.js";
|
||||
import { ReadAll, readAllTools } from "./readall.js";
|
||||
import { DeliveredWrites, INDEXER_MODULES, writeTools, type ModulesChange } from "./writes.js";
|
||||
import { TOOLS_ACCOUNT, TOOLS_ORG, meshAccounts } from "./tools-account.js";
|
||||
@@ -356,114 +351,6 @@ async function siblingMoves(client: GiteaClient, repository: string, number?: nu
|
||||
}
|
||||
}
|
||||
|
||||
// **The verdict, set where the pull request shows it.** Every verdict is the head commit's status; one
|
||||
// that is not a pass also leaves the check's own account as a comment, so the reason is read where the
|
||||
// change is reviewed. An error — the check could not run — is the forge's `error`, never a success.
|
||||
async function setVerdict(client: GiteaClient, event: { body: unknown }): Promise<void> {
|
||||
const c = (event.body ?? {}) as Checked;
|
||||
if (!c.group && c.owner && c.repo && c.took) {
|
||||
// How long it took, for the merge tool to say how long the next one may (novox/hq ADR 0323 §5).
|
||||
keepTook(tookFile(process.env.MESH_GITEA_STATE_DIR), `${c.owner}/${c.repo}`,
|
||||
{ took: c.took, at: new Date().toISOString(), head: c.commit });
|
||||
}
|
||||
if (c.group) {
|
||||
// A delivery group's composed check (novox/hq ADR 0239, ADR 0403 §1): its verdict is each member's merge gate, at the
|
||||
// head it read; the group's own status, mesh/delivery-group, is its owner's to say.
|
||||
await setGroupVerdict(client, c);
|
||||
return;
|
||||
}
|
||||
if (!c.owner || !c.repo || !c.commit || !c.verdict) {
|
||||
console.error("[gitea] a merge check's verdict named no repository, commit or verdict; ignored");
|
||||
return;
|
||||
}
|
||||
// The siblings this verdict read are kept for the member (ADR 0382 §5); a verdict that read none removes the record.
|
||||
const siblings = siblingsOf(c.siblings);
|
||||
if (c.number && noteVerdict(siblingNamed, `${c.owner}/${c.repo}#${c.number}`.toLowerCase(), c.commit, siblings)) {
|
||||
keepSiblings(siblingsRecord, siblingNamed);
|
||||
}
|
||||
// A sibling that moved while the check ran is weighed once this verdict has landed, in the head's own turn after it.
|
||||
if (siblings.length) {
|
||||
for (const repository of new Set(siblings.map((s) => s.repository))) {
|
||||
void Promise.resolve().then(() => siblingMoves(client, repository));
|
||||
}
|
||||
}
|
||||
// Each status links to the pull request, where the delivery's view is kept (novox/hq ADR 0239).
|
||||
let target: string | undefined;
|
||||
let base: string | undefined;
|
||||
if (c.number) {
|
||||
const pull = await client.getPullRequest(c.owner, c.repo, c.number).catch(() => undefined);
|
||||
target = pull?.html_url || undefined;
|
||||
base = pull?.base || undefined;
|
||||
}
|
||||
// **A verdict judged against a trunk commit that is no longer the trunk's head is out of date when it lands**
|
||||
// (novox/hq ADR 0323 §4): a check asked before the trunk moved. It is said so, pending with the reason, in place of
|
||||
// its pass or failure; a trunk that cannot be read lets the verdict stand, and the merge tool compares the commits.
|
||||
const trunk = base ?? c.plan?.base;
|
||||
// Only the statuses the base still requires are written (issue 1301): on hq, mesh/repo-check alone.
|
||||
const required = trunk ? requiredOf(await client.appliedProtection(c.owner, c.repo, trunk).catch(() => undefined)) : undefined;
|
||||
const kept = maintained(required);
|
||||
if (c.trunk && trunk) {
|
||||
const head = await client.branchHead(c.owner, c.repo, trunk).catch(() => undefined);
|
||||
if (head && !sameCommit(c.trunk, head)) {
|
||||
const description = outOfDateWords(c.trunk, head);
|
||||
for (const context of [...(composedGate(c) ? [] : [CHECK_CONTEXT]), ...(c["repo-check"] ? [REPO_CHECK_CONTEXT] : [])]
|
||||
.filter((x) => kept.includes(x))) {
|
||||
await client.setCommitStatus(c.owner, c.repo, c.commit, { state: "pending", context, description,
|
||||
...(target ? { target_url: target } : {}) });
|
||||
}
|
||||
// Its account is kept and shown all the same (ADR 0323 §7): what it judged, and that it is out of date.
|
||||
const said = commentFor(c, await repoCheckFacts(client, c, trunk), required);
|
||||
if (c.number && !composedGate(c)) await client.addComment(c.owner, c.repo, c.number, lateVerdictComment(c, said, description));
|
||||
console.log(`[gitea] ${c.owner}/${c.repo}#${c.number ?? "?"} at ${c.commit.slice(0, 8)}: merge check ${c.verdict}, ` +
|
||||
`${description}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const facts = await repoCheckFacts(client, c, base ?? c.plan?.base);
|
||||
const gateNow = composedGate(c) ? (await client.headChecks(c.owner, c.repo, c.commit).catch(() => ({} as Record<string, { description?: string }>)))[CHECK_CONTEXT]?.description : undefined;
|
||||
for (const status of statusesFor(c, facts, gateNow, required)) {
|
||||
await client.setCommitStatus(c.owner, c.repo, c.commit, target ? { ...status, target_url: target } : status);
|
||||
}
|
||||
const comment = composedGate(c) ? null : commentFor(c, facts, required);
|
||||
if (comment && c.number) await client.addComment(c.owner, c.repo, c.number, comment);
|
||||
console.log(`[gitea] ${c.owner}/${c.repo}#${c.number ?? "?"} at ${c.commit.slice(0, 8)}: merge check ${c.verdict}`);
|
||||
}
|
||||
|
||||
/** **The composed verdict sets each member's gate** (novox/hq ADR 0403 §1, issue 978), in each head's own turn of the gate
|
||||
* writer (gate.ts), judged against the trunk as a member's own verdict is: one that names a trunk commit that is no longer
|
||||
* the trunk's head is out of date when it lands, and said so, pending. */
|
||||
async function setGroupVerdict(client: GiteaClient, c: Checked): Promise<void> {
|
||||
// The base branch each member merges into, read with its pull request below: the composed verdict names the siblings of
|
||||
// each member from it (issue 1049).
|
||||
const bases = new Map<string, string>();
|
||||
for (const m of c.members ?? []) {
|
||||
if (!m.owner || !m.repo || !m.commit || !c.verdict) {
|
||||
console.error(`[gitea] a composed verdict of ${c.group} names a member with no repository, commit or verdict; ignored`);
|
||||
continue;
|
||||
}
|
||||
await gates.verdict(m.owner, m.repo, m.commit, async () => {
|
||||
const pull = m.number ? await client.getPullRequest(m.owner, m.repo, m.number).catch(() => undefined) : undefined;
|
||||
const target = pull?.html_url || undefined;
|
||||
if (m.number && pull?.base) bases.set(`${m.owner}/${m.repo}#${m.number}`, pull.base);
|
||||
let status = groupGateStatus(c, m);
|
||||
if (m.trunk && pull?.base) {
|
||||
const head = await client.branchHead(m.owner, m.repo, pull.base).catch(() => undefined);
|
||||
if (head && !sameCommit(m.trunk, head)) status = { state: "pending", context: CHECK_CONTEXT, description: outOfDateWords(m.trunk, head) };
|
||||
}
|
||||
await client.setCommitStatus(m.owner, m.repo, m.commit, target ? { ...status, target_url: target } : status);
|
||||
console.log(`[gitea] ${m.owner}/${m.repo}#${m.number ?? "?"} at ${m.commit.slice(0, 8)}: the group ${c.group}'s composed check ${c.verdict}, ` +
|
||||
`its merge gate ${status.state}`);
|
||||
}, c.id);
|
||||
}
|
||||
// The siblings of each member are the other members at the heads and base commits this verdict composed (issue 1049): a
|
||||
// sibling's own merge of such a head is no move of the member's base. Kept for what the merge tool and the siblings'
|
||||
// weighing read; nothing is run.
|
||||
if (c.verdict && c.members?.length) {
|
||||
const members = c.members.map((m) => ({ ...m, base: m.number ? bases.get(`${m.owner}/${m.repo}#${m.number}`) : undefined }));
|
||||
if (noteComposed(siblingNamed, members)) keepSiblings(siblingsRecord, siblingNamed);
|
||||
}
|
||||
}
|
||||
|
||||
// **This module is the one writer of a head's mesh/merge-gate** (novox/hq issue 446): the announcement's pending words,
|
||||
// where the check is when a build machine takes it, and the verdict — each head's writes in one chain, the verdict last
|
||||
// (gate.ts). A second writer elsewhere raced the verdict and could leave a pass reading pending.
|
||||
|
||||
+7
-277
@@ -1,17 +1,15 @@
|
||||
// A pull request's merge check (novox/hq to-be 45 §9): what the forge's announcer says when a pull
|
||||
// request's head moves, and what it sets as the pull request's status when the mesh says the verdict.
|
||||
// request's head moves, and the status contexts the check is kept under.
|
||||
//
|
||||
// **Before merge, never after.** Every check the mesh had ran after a merge, on a machine: a manifest the
|
||||
// node-engine refuses (issue 236), an identity a real machine's name made too long (263). So each new head
|
||||
// of an open pull request is announced as `pull.updated`; the controller asks the build seat to check it
|
||||
// against every machine of the mesh's facts; and the verdict comes back as the controller's `checked`,
|
||||
// which this sets as the head commit's status — and, when it is not a pass, as a comment saying why.
|
||||
// **Before merge, never after.** Each new head of an open pull request is announced as `pull.updated`, and the mesh
|
||||
// checks it against every machine of its facts before it merges. The verdict is the checker's `mesh/check`, which this
|
||||
// module writes from the checker's owed state (check.ts); the controller's `checked`, which once set the two older
|
||||
// statuses, is no event any more (ADR 0571 clause 13).
|
||||
//
|
||||
// Pure functions here, so they are tested without a forge; index.ts does the asking and the setting.
|
||||
// Pure functions here, so they are tested without a forge.
|
||||
|
||||
import type { CommitStatus, GiteaClient, GiteaPull, GiteaRepo } from "./client.js";
|
||||
import type { GiteaPull, GiteaRepo } from "./client.js";
|
||||
import { movedSince } from "./client.js";
|
||||
import { UNKNOWN_MARK, besideWords, siblingsOf } from "./siblings.js";
|
||||
|
||||
/** The status context a merge check's gate is kept under: one per commit, the newest replacing the last. */
|
||||
export const CHECK_CONTEXT = "mesh/merge-gate";
|
||||
@@ -59,88 +57,6 @@ export function pendingWords(announced: Date): string {
|
||||
return `waiting in the build queue since ${two(announced.getHours())}:${two(announced.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** One layer of a check, judged. */
|
||||
export interface Layer {
|
||||
verdict: string;
|
||||
summary: string;
|
||||
/** The modules a merge of the change would move, as the controller's planner reckons it… */
|
||||
modules?: string[];
|
||||
/** …and those it would build after them because they stand on them. */
|
||||
dependents?: string[];
|
||||
}
|
||||
|
||||
/** What the controller says as `checked` (mesh-controller internal/link, Checked). */
|
||||
export interface Checked {
|
||||
owner: string;
|
||||
repo: string;
|
||||
number?: number;
|
||||
commit: string;
|
||||
verdict: string;
|
||||
summary: string;
|
||||
report?: string;
|
||||
id: string;
|
||||
on?: string;
|
||||
/** The gate — the modules of the mesh's graph the change touches — and the repository's own check. A
|
||||
* controller from before the layers says neither, and its verdict is the gate's. */
|
||||
gate?: Layer;
|
||||
"repo-check"?: Layer;
|
||||
/** The change plan of the commit checked (novox/hq ADR 0238): what a merge of it would build and send. */
|
||||
plan?: ChangePlan;
|
||||
/** Set on a delivery group's composed check (novox/hq ADR 0239): its verdict is each member's merge gate (ADR 0403 §1,
|
||||
* issue 978), set in setGroupVerdict, never read as this head's own. */
|
||||
group?: string;
|
||||
/** The members a composed check read, each at its head and the trunk commit it was merged onto (ADR 0323 §6). */
|
||||
members?: { owner: string; repo: string; number?: number; commit: string; trunk?: string }[];
|
||||
/** The trunk commit the head was merged into and judged as, twelve characters (novox/hq ADR 0323 §3); absent from a
|
||||
* controller whose build seat judged the head alone. Beside are the other repositories' commits the check read,
|
||||
* by directory; took how long it ran. */
|
||||
trunk?: string;
|
||||
beside?: Record<string, string>;
|
||||
took?: string;
|
||||
/** The other members of its delivery group the check read beside the head, each at its head merged onto its base
|
||||
* commit (novox/hq ADR 0382 §4); absent when it read none, and from a controller from before the rule. */
|
||||
siblings?: { repository: string; number: number; head: string; base: string; base_commit: string }[];
|
||||
/** Why the siblings could not be learned (mesh-delivery did not answer), when that is so. */
|
||||
siblings_unknown?: string;
|
||||
/** Where the judge came from, when it came from the group (ADR 0382 §3): said in the account. */
|
||||
judge_from?: string;
|
||||
}
|
||||
|
||||
/** A change plan, as the controller says it (mesh-controller internal/link, ChangePlan). */
|
||||
export interface ChangePlan {
|
||||
repository: string;
|
||||
base: string;
|
||||
head: string;
|
||||
moved?: string[];
|
||||
dependents?: string[];
|
||||
new?: string[];
|
||||
unread?: string[];
|
||||
tiers?: string[][];
|
||||
machines?: { machine: string; receives?: string[]; waits?: string[] }[];
|
||||
steps?: string[];
|
||||
summary: string;
|
||||
}
|
||||
|
||||
/** Whether a plan builds anything: a change that touches the mesh's graph. */
|
||||
function builds(plan?: ChangePlan): boolean {
|
||||
return !!plan && ((plan.moved?.length ?? 0) > 0 || (plan.new?.length ?? 0) > 0);
|
||||
}
|
||||
|
||||
/** A delivery plan (the controller's ChangePlan) as a person reads it on the pull request. */
|
||||
export function planText(plan: ChangePlan): string {
|
||||
const lines = [`**Delivery plan** — ${plan.summary}`];
|
||||
(plan.tiers ?? []).forEach((tier, i) => lines.push(`- tier ${i}: ${tier.join(", ")}`));
|
||||
for (const m of plan.machines ?? []) {
|
||||
const parts: string[] = [];
|
||||
if (m.receives?.length) parts.push(`receives ${m.receives.join(", ")}`);
|
||||
if (m.waits?.length) parts.push(`waits for a person: ${m.waits.join(", ")}`);
|
||||
lines.push(`- ${m.machine}: ${parts.join("; ")}`);
|
||||
}
|
||||
for (const step of plan.steps ?? []) lines.push(`- ${step}`);
|
||||
if (plan.unread?.length) lines.push(`- read by no module's build: ${plan.unread.join(", ")}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** The heads already announced, keyed by `headKey`, so a restart announces nothing twice. */
|
||||
export type Announced = Record<string, string>;
|
||||
|
||||
@@ -176,197 +92,11 @@ export function reposToLook(repos: GiteaRepo[], floor: string, pulls: PullSeen[]
|
||||
return repos.filter((r) => moved.has(r.full_name));
|
||||
}
|
||||
|
||||
// **A required status is success or it blocks** (novox/hq issue 293). The forge combines `warning` as a
|
||||
// failure, and a branch whose protection requires mesh/merge-gate or mesh/repo-check — with no
|
||||
// administrator override — will not merge past one. So the controller's `warning`, a note and never a
|
||||
// question, is set as `success` with the note in its description; only what a person must decide is a
|
||||
// `failure`, with why. The mapping, verdict by verdict:
|
||||
//
|
||||
// pass → success
|
||||
// warning (a note: rebuild width, a problem
|
||||
// already so on the base, a script's own) → success, "pass, with a note: …"
|
||||
// repo-check, no merge-check.sh, not required → success, "no repository check defined"
|
||||
// repo-check, no merge-check.sh, required (or
|
||||
// the protection unreadable) → failure: a person adds one, or lifts the requirement
|
||||
// fail → failure
|
||||
// error, or no verdict → error — never a success
|
||||
|
||||
/** The forge's state for a verdict: an error is the forge's `error`, never a success; a warning is a note. */
|
||||
function stateOf(verdict: string): CommitStatus["state"] {
|
||||
return verdict === "pass" || verdict === "warning" ? "success" : verdict === "fail" ? "failure" : "error";
|
||||
}
|
||||
|
||||
/** How a verdict is said in a status's description: a warning as the pass with a note it is. */
|
||||
function saidAs(verdict: string): string {
|
||||
return verdict === "warning" ? "pass, with a note" : verdict || "error";
|
||||
}
|
||||
|
||||
function described(verdict: string, summary: string, modules?: string[], dependents?: string[]): string {
|
||||
// The forge keeps a short description; the rest is the comment's.
|
||||
let d = `${saidAs(verdict)}: ${summary}`;
|
||||
if (modules?.length) d += ` [${modules.join(", ")}${dependents?.length ? ` +${dependents.length} dependent(s)` : ""}]`;
|
||||
return clipped(d);
|
||||
}
|
||||
|
||||
export function clipped(d: string): string {
|
||||
d = d.replace(/\s+/g, " ").trim();
|
||||
return d.length > 140 ? d.slice(0, 139) + "…" : d;
|
||||
}
|
||||
|
||||
/** What the forge says of a repository's own check beyond the controller's verdict, asked by index.ts only
|
||||
* when the verdict is a warning: whether the head holds a merge-check.sh, and whether the base branch's
|
||||
* protection requires mesh/repo-check. Unknown is undefined. */
|
||||
export interface RepoCheckFacts {
|
||||
defined?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
// **What only the forge knows of a repository check said as a warning** (novox/hq issue 293): whether the
|
||||
// head holds a merge-check.sh at all, and — when it does not — whether the base branch's protection
|
||||
// requires mesh/repo-check. Asked only for a warning; unknown is left undefined, which statusesFor reads
|
||||
// as possibly required: a failure on a status nothing requires blocks nothing, a success on one that is
|
||||
// required would let an untested repository merge. The base's protection is the one the forge applies, read
|
||||
// from the branch: the rules' own list is a repository administrator's, which the tools' account is not
|
||||
// (novox/hq ADR 0365).
|
||||
export async function repoCheckFacts(
|
||||
forge: Pick<GiteaClient, "holdsFile" | "appliedProtection">, c: Checked, base: string | undefined): Promise<RepoCheckFacts> {
|
||||
if (c["repo-check"]?.verdict !== "warning") return {};
|
||||
const defined = await forge.holdsFile(c.owner, c.repo, c.commit, "merge-check.sh").catch(() => undefined);
|
||||
if (defined !== false) return { defined };
|
||||
const rule = await forge.appliedProtection(c.owner, c.repo, base || "main").catch(() => undefined);
|
||||
if (rule === undefined) return { defined };
|
||||
const required = !!rule?.enable_status_check && (rule.status_check_contexts ?? []).includes(REPO_CHECK_CONTEXT);
|
||||
return { defined, required };
|
||||
}
|
||||
|
||||
/** The description of a required repository check that is not defined. */
|
||||
export const UNDEFINED_REQUIRED = "fail: mesh/repo-check is required here and this head defines no merge-check.sh — " +
|
||||
"a person adds one, or lifts the requirement from the branch's protection";
|
||||
|
||||
/** The repository check's status: its verdict, unless the repository defines no check at all — then
|
||||
* success where the check is not required, and a failure for a person where it is (or may be). */
|
||||
function repoStatus(repo: Layer, facts: RepoCheckFacts): CommitStatus {
|
||||
if (repo.verdict === "warning" && facts.defined === false) {
|
||||
return facts.required === false
|
||||
? { state: "success", context: REPO_CHECK_CONTEXT, description: "no repository check defined" }
|
||||
: { state: "failure", context: REPO_CHECK_CONTEXT, description: clipped(UNDEFINED_REQUIRED) };
|
||||
}
|
||||
return { state: stateOf(repo.verdict), context: REPO_CHECK_CONTEXT, description: described(repo.verdict, repo.summary) };
|
||||
}
|
||||
|
||||
/** Whether the repository check is a failure a person must read: failed, could not run, or required and
|
||||
* not defined. */
|
||||
function repoWrong(repo: Layer | undefined, facts: RepoCheckFacts): boolean {
|
||||
return !!repo && repoStatus(repo, facts).state !== "success";
|
||||
}
|
||||
|
||||
/** What a group member's gate says while the group's composed check is the one that judges it: the member's own check can say
|
||||
* nothing more of it, so asking that check again brings no verdict that names a trunk (novox/hq issue 1106). */
|
||||
export const WAITS_FOR_COMPOSED = "waits for its group's composed check";
|
||||
|
||||
/** The forge's status for the gate. */
|
||||
export function statusFor(c: Checked): CommitStatus {
|
||||
const gate = c.gate ?? { verdict: c.verdict, summary: c.summary };
|
||||
// With a plan, the status says what the change does and how it was judged: "pass: builds gitea → anchor;
|
||||
// no bus step; every machine composes…".
|
||||
const description = builds(c.plan)
|
||||
? described(gate.verdict, `${c.plan!.summary}; ${gate.summary}`)
|
||||
: described(gate.verdict, gate.summary, gate.modules, gate.dependents);
|
||||
return judgedFirst(c, { state: stateOf(gate.verdict), context: CHECK_CONTEXT, description });
|
||||
}
|
||||
|
||||
/**
|
||||
* **A member's gate from its group's composed verdict** (novox/hq ADR 0403 §1, issue 978): the composed check, which
|
||||
* composed every machine with all the members' heads, sets each member's `mesh/merge-gate` at the head it read. The
|
||||
* description names what it judged first, as a member's own gate does (ADR 0323 §3), and the group.
|
||||
*/
|
||||
export function groupGateStatus(c: Checked, m: NonNullable<Checked["members"]>[number]): CommitStatus {
|
||||
const said = described(c.verdict, `group ${c.group} composed: ${c.summary}`);
|
||||
const first = m.trunk ? `head ${m.commit.slice(0, 12)} merged into trunk ${m.trunk.slice(0, 12)} — ` : "";
|
||||
return { state: stateOf(c.verdict), context: CHECK_CONTEXT, description: clipped(first + said) };
|
||||
}
|
||||
|
||||
/** Whether a verdict's gate layer is the group's composed check's (ADR 0403 §1): no gate was run for the head alone. */
|
||||
export function composedGate(c: Checked): boolean {
|
||||
return c.gate?.verdict === "composed";
|
||||
}
|
||||
|
||||
/** Every status a verdict sets: the gate's, and the repository's own check's when it was said; of them, only those the
|
||||
* base requires (`required`, the base's rule: maintained). */
|
||||
export function statusesFor(c: Checked, facts: RepoCheckFacts = {}, gateNow?: string, required?: string[]): CommitStatus[] {
|
||||
const kept = maintained(required);
|
||||
return allStatusesFor(c, facts, gateNow).filter((s) => kept.includes(s.context));
|
||||
}
|
||||
|
||||
function allStatusesFor(c: Checked, facts: RepoCheckFacts = {}, gateNow?: string): CommitStatus[] {
|
||||
if (composedGate(c)) {
|
||||
// A member whose gate the group's composed check judges (ADR 0403 §1): only its repository's check is its own to
|
||||
// set. The gate stays pending, saying it waits for the group, unless a composed verdict already stands on it.
|
||||
const out: CommitStatus[] = [];
|
||||
if (!(gateNow ?? "").includes(" composed: ")) {
|
||||
out.push({ state: "pending", context: CHECK_CONTEXT, description: WAITS_FOR_COMPOSED });
|
||||
}
|
||||
const repo = c["repo-check"];
|
||||
if (repo) out.push(judgedFirst(c, repoStatus(repo, facts)));
|
||||
return out;
|
||||
}
|
||||
const out = [statusFor(c)];
|
||||
const repo = c["repo-check"];
|
||||
if (repo) out.push(judgedFirst(c, repoStatus(repo, facts)));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** **A status names what its verdict judged, first** (novox/hq ADR 0323 §3): "head 1a2b3c4d5e6f merged into trunk
|
||||
* 7a8b9c0d1e2f — pass: …", so the forge's 140 characters never clip it away; the trunk's move is weighed by it. A
|
||||
* verdict that names no trunk commit is said as before. */
|
||||
function judgedFirst(c: Checked, status: CommitStatus): CommitStatus {
|
||||
// The siblings it read, named after the commits (ADR 0382 §4), so the clip never takes them either.
|
||||
const siblings = siblingsOf(c.siblings);
|
||||
// A verdict that could not learn them says so on the status too (clause 1: it is still checked, and merges).
|
||||
const mark = siblings.length ? besideWords(siblings) : c.siblings_unknown ? UNKNOWN_MARK : "";
|
||||
if (!c.trunk) return mark ? { ...status, description: clipped(`${mark} — ${status.description}`) } : status;
|
||||
return { ...status, description: clipped(`head ${c.commit.slice(0, 12)} merged into trunk ${c.trunk.slice(0, 12)}${mark ? " " + mark : ""} — ${status.description}`) };
|
||||
}
|
||||
|
||||
/** The comment a verdict leaves on its pull request, with the check's own account: the change plan of a
|
||||
* change that builds something (novox/hq ADR 0238), and why, when the gate is not a pass or the
|
||||
* repository's own check failed or could not run. A repository with no merge-check.sh, touching nothing,
|
||||
* is said by its statuses alone, not by a comment on every push. */
|
||||
export function commentFor(c: Checked, facts: RepoCheckFacts = {}, required?: string[]): string | null {
|
||||
const gate = c.gate ?? { verdict: c.verdict, summary: c.summary };
|
||||
// A base that no longer requires the gate (issue 1301) is told nothing of it: its verdict is the repository's own check.
|
||||
const withGate = writesGate(required);
|
||||
const repo = c["repo-check"];
|
||||
const wrong = repoWrong(repo, facts);
|
||||
if ((!withGate || gate.verdict === "pass") && !wrong && !builds(c.plan)) return null;
|
||||
const lines = [`**Merge check** at \`${c.commit.slice(0, 8)}\``, ""];
|
||||
if (c.trunk) {
|
||||
// What was judged (novox/hq ADR 0323 §3): the head merged into the trunk commit, and what was read beside it.
|
||||
const beside = Object.entries(c.beside ?? {}).map(([d, sha]) => `${d} \`${sha}\``).join(", ");
|
||||
lines.splice(1, 0, `Judged: head \`${c.commit.slice(0, 12)}\` merged into trunk \`${c.trunk.slice(0, 12)}\`` +
|
||||
(beside ? `; read beside it: ${beside}` : ""));
|
||||
}
|
||||
const siblings = siblingsOf(c.siblings);
|
||||
if (siblings.length) {
|
||||
lines.splice(c.trunk ? 2 : 1, 0, `Siblings read beside it: ` + siblings.map((s) =>
|
||||
`${s.repository}#${s.number} \`${s.head.slice(0, 12)}\` merged onto \`${s.base_commit.slice(0, 12)}\` (${s.base})`).join(", ") +
|
||||
(c.judge_from ? `; judged by ${c.judge_from}` : ""));
|
||||
} else if (c.siblings_unknown) {
|
||||
lines.splice(c.trunk ? 2 : 1, 0, `Siblings could not be learned: ${c.siblings_unknown}`);
|
||||
}
|
||||
if (withGate) {
|
||||
lines.push(`- \`${CHECK_CONTEXT}\`: **${saidAs(gate.verdict).toUpperCase()}** — ${gate.summary}` +
|
||||
(gate.modules?.length ? ` (modules: ${gate.modules.join(", ")}` +
|
||||
(gate.dependents?.length ? `; built after them: ${gate.dependents.join(", ")}` : "") + ")" : ""));
|
||||
}
|
||||
if (repo) {
|
||||
const st = repoStatus(repo, facts);
|
||||
lines.push(`- \`${REPO_CHECK_CONTEXT}\`: ` + (repo.verdict === "warning" && facts.defined === false
|
||||
? `**${st.state === "success" ? "PASS" : "FAIL"}** — ${st.description.replace(/^fail: /, "")}`
|
||||
: `**${saidAs(repo.verdict).toUpperCase()}** — ${repo.summary}`));
|
||||
}
|
||||
if (builds(c.plan)) lines.push("", planText(c.plan!));
|
||||
const ran = c.on ? `\n\nRun by the build seat on ${c.on} as \`${c.id}\` (\`builds --log ${c.id}\`).` : "";
|
||||
const report = c.report ? `\n\n\`\`\`\n${c.report.replace(/```/g, "'''")}\n\`\`\`` : "";
|
||||
return lines.join("\n") + ran + report;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,11 @@ import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
// **A pending check says where it is, and the verdict lands last** (novox/hq issue 446). This module is the one writer
|
||||
// of a head's mesh/merge-gate: the announcement's pending words, "building on <machine> since …" when the build seat
|
||||
// says a machine took the check, and the verdict. The forge keeps the newest status of a context, so a pending word
|
||||
// of a head's mesh/merge-gate: the announcement's pending words and the verdict. The forge keeps the newest status of a context, so a pending word
|
||||
// written after the verdict hides it — a pass reads pending, a failure hides under "building".
|
||||
|
||||
const SHA = "a6b08de8507c1703528277e5cefc80548dbc9bb3";
|
||||
|
||||
// The build seat's `started` as a machine said it for novox/hq#309's check on 2026-10-11 (its log, read with the
|
||||
// controller's `builds --log build-1791680941048622697`, opens at 03:09:01 on shanks); only the forge's address
|
||||
// replaced. The shape is mesh-controller's link.BuildStart.
|
||||
const startedOnShanks = { id: "build-1791680941048622697", repository: "http://127.0.0.1:20000/novox/hq.git",
|
||||
ref: SHA, on: "shanks", at: "2026-10-11T01:09:01.204319Z" };
|
||||
|
||||
/** A forge that keeps the newest status per context, as gitea does, and whose writes take as long as a test says. */
|
||||
function aForge() {
|
||||
const kept: Record<string, { state: string; description: string; updated_at: string }> = {};
|
||||
@@ -71,99 +64,8 @@ function aForge() {
|
||||
/** The clock of the test: the head announced at 01:08:50Z. */
|
||||
const announcedAt = () => new Date("2026-10-11T01:08:50Z");
|
||||
|
||||
test("a build machine taking the check is said on the head: building on which machine, since the local time", async () => {
|
||||
const { GateWriter, buildingWords } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
await gates.pending("novox", "hq", SHA);
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), true);
|
||||
assert.equal(forge.kept["mesh/merge-gate"].state, "pending");
|
||||
assert.equal(forge.kept["mesh/merge-gate"].description, buildingWords("shanks", new Date(startedOnShanks.at)));
|
||||
assert.equal(buildingWords("shanks", new Date(2026, 9, 11, 3, 9, 1)), "building on shanks since 03:09");
|
||||
});
|
||||
|
||||
test("the verdict heard while a 'building' write is in flight lands last and stays", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
await gates.pending("novox", "hq", SHA);
|
||||
forge.holdWrites(); // the 'building' request is in flight when the verdict comes
|
||||
const building = gates.started("novox", "hq", SHA, startedOnShanks);
|
||||
await new Promise((r) => setImmediate(r));
|
||||
const verdict = gates.verdict("novox", "hq", SHA, () =>
|
||||
forge.setCommitStatus("novox", "hq", SHA, { state: "success", context: "mesh/merge-gate", description: "pass" }));
|
||||
forge.release();
|
||||
await Promise.all([building, verdict]);
|
||||
assert.equal(`${forge.kept["mesh/merge-gate"].state} ${forge.kept["mesh/merge-gate"].description}`, "success pass",
|
||||
`the verdict is not the newest status: ${forge.written.join(" | ")}`);
|
||||
});
|
||||
|
||||
// The race the review of mesh-catalog#202 reproduced: a writer reads the forge's gate (pending), the verdict is written,
|
||||
// then the writer writes "building" — and the pass reads pending until the next push. A head this process did not
|
||||
// announce is the one case where the forge is read before writing.
|
||||
test("the verdict written while a start's read of the forge is in flight stays the newest status", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
forge.kept["mesh/merge-gate"] = { state: "pending", description: "waiting", updated_at: "2026-10-11T01:08:50Z" };
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
forge.holdReads();
|
||||
const building = gates.started("novox", "hq", SHA, startedOnShanks);
|
||||
await new Promise((r) => setImmediate(r));
|
||||
const verdict = gates.verdict("novox", "hq", SHA, () =>
|
||||
forge.setCommitStatus("novox", "hq", SHA, { state: "success", context: "mesh/merge-gate", description: "pass" }));
|
||||
await new Promise((r) => setImmediate(r));
|
||||
forge.releaseReads();
|
||||
await Promise.all([building, verdict]);
|
||||
assert.equal(`${forge.kept["mesh/merge-gate"].state} ${forge.kept["mesh/merge-gate"].description}`, "success pass",
|
||||
`the verdict is not the newest status: ${forge.written.join(" | ")}`);
|
||||
});
|
||||
|
||||
test("the verdict heard while a 'building' write still waits its turn: the 'building' is never written", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
forge.holdWrites(); // the pending words are still being written
|
||||
const pending = gates.pending("novox", "hq", SHA);
|
||||
const building = gates.started("novox", "hq", SHA, startedOnShanks);
|
||||
const verdict = gates.verdict("novox", "hq", SHA, () =>
|
||||
forge.setCommitStatus("novox", "hq", SHA, { state: "failure", context: "mesh/merge-gate", description: "fail: 0 of 4 compose" }));
|
||||
forge.release();
|
||||
await Promise.all([pending, verdict]);
|
||||
assert.equal(await building, false);
|
||||
assert.equal(forge.kept["mesh/merge-gate"].state, "failure");
|
||||
assert.ok(!forge.written.some((w) => w.includes("building")), forge.written.join(" | "));
|
||||
});
|
||||
|
||||
test("a start heard after the verdict writes nothing", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
await gates.pending("novox", "hq", SHA);
|
||||
await gates.verdict("novox", "hq", SHA, () =>
|
||||
forge.setCommitStatus("novox", "hq", SHA, { state: "success", context: "mesh/merge-gate", description: "pass" }));
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false);
|
||||
assert.equal(forge.kept["mesh/merge-gate"].state, "success");
|
||||
});
|
||||
|
||||
test("a start from before the check now pending (a run before a recheck, the stream's history) writes nothing", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
let now = announcedAt();
|
||||
const gates = new GateWriter(forge, () => now);
|
||||
await gates.pending("novox", "hq", SHA);
|
||||
await gates.verdict("novox", "hq", SHA, () =>
|
||||
forge.setCommitStatus("novox", "hq", SHA, { state: "failure", context: "mesh/merge-gate", description: "error" }));
|
||||
now = new Date("2026-10-11T01:30:00Z"); // checked again: the recheck's pending, as the tool writes it (issue 456)
|
||||
await gates.rechecked("novox", "hq", SHA, { state: "pending", context: "mesh/merge-gate", description: "checking again: flaky" });
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false, "the first run's start");
|
||||
assert.equal(forge.kept["mesh/merge-gate"].description, "checking again: flaky");
|
||||
assert.equal(await gates.started("novox", "hq", SHA, { ...startedOnShanks, at: "2026-10-11T01:31:00Z" }), true,
|
||||
"the second run's start");
|
||||
});
|
||||
|
||||
// **A recheck's pending goes through the one writer** (novox/hq issue 456). mesh-delivery's recheck puts the gate back
|
||||
// to pending through the gitea_commit_status tool; before, that write bypassed the head's chain: the first run's
|
||||
// verdict mark stayed set, so the new run never said "building", and an old run's verdict could land over the pending.
|
||||
// to pending through the gitea_commit_status tool.
|
||||
|
||||
/** The ask of a check, as the controller's link.NewBuildID names it: when it was asked, in unix nanoseconds. */
|
||||
const askOf = (at: string) => `build-${BigInt(Date.parse(at)) * 1_000_000n}`;
|
||||
@@ -209,20 +111,6 @@ test("an old run's verdict in flight when the recheck comes lands before the rec
|
||||
assert.equal(gateOf(forge), "pending checking again: main moved on", forge.written.join(" | "));
|
||||
});
|
||||
|
||||
test("a rechecked head says where its new run is: building on which machine", async () => {
|
||||
const { GateWriter, buildingWords } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
let now = announcedAt();
|
||||
const gates = new GateWriter(forge, () => now);
|
||||
await gates.pending("novox", "hq", SHA);
|
||||
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
|
||||
now = recheckAt;
|
||||
await gates.rechecked("novox", "hq", SHA, checkingAgain);
|
||||
const at = "2026-10-11T01:30:03Z";
|
||||
assert.equal(await gates.started("novox", "hq", SHA, { ...startedOnShanks, id: secondRun, at }), true);
|
||||
assert.equal(forge.kept["mesh/merge-gate"].description, buildingWords("shanks", new Date(at)));
|
||||
});
|
||||
|
||||
test("the recheck's own verdict is written, even asked a little before the pending by another machine's clock", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
@@ -306,16 +194,6 @@ test("when a check was asked is read from its id; the opening of the check now p
|
||||
assert.equal(openedAt([]), undefined);
|
||||
});
|
||||
|
||||
test("a head this process did not announce (it restarted) is said only while the forge holds its gate pending", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
forge.kept["mesh/merge-gate"] = { state: "success", description: "pass", updated_at: "2026-10-11T01:20:00Z" };
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false);
|
||||
forge.kept["mesh/merge-gate"] = { state: "pending", description: "waiting", updated_at: "2026-10-11T01:08:50Z" };
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), true);
|
||||
});
|
||||
|
||||
test("a repository as the mesh clones it is told from another", async () => {
|
||||
const { clonedAs } = await import("../gate.ts");
|
||||
assert.equal(clonedAs("http://127.0.0.1:20000/novox/hq.git", "novox", "hq"), true);
|
||||
@@ -325,10 +203,3 @@ test("a repository as the mesh clones it is told from another", async () => {
|
||||
|
||||
// Issue 1301: a build machine's start on a head that holds no mesh/merge-gate (its base requires none, so none was written)
|
||||
// says nothing: no "building on" status appears on a main that does not require the gate.
|
||||
test("a start on a head with no gate on the forge writes nothing", async () => {
|
||||
const { GateWriter } = await import("../gate.ts");
|
||||
const forge = aForge();
|
||||
const gates = new GateWriter(forge, announcedAt);
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false);
|
||||
assert.equal(forge.kept["mesh/merge-gate"], undefined);
|
||||
});
|
||||
|
||||
@@ -29,92 +29,6 @@ test("a pull request retargeted keeps its head and is announced again, into its
|
||||
assert.notEqual(headKey("novox/de-spiegel", 6, "main"), headKey("novox/de-spiegel", 6, "old-trunk"));
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -140,24 +54,6 @@ test("a commit status is set on the commit, under the merge check's context", as
|
||||
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 () => {
|
||||
@@ -166,48 +62,10 @@ test("a head announced is pending in the build queue, since the local time it wa
|
||||
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);
|
||||
});
|
||||
|
||||
// Issue 1301 (ADR 0503 §3, §5): gitea writes the controller's statuses only where the base still requires them. hq's main
|
||||
// requires mesh/repo-check alone: its verdict sets mesh/repo-check and no mesh/merge-gate, and its comment names no gate.
|
||||
// requires mesh/repo-check alone, so no mesh/merge-gate is written on its heads.
|
||||
test("a base's required statuses decide which of the controller's statuses are written", async () => {
|
||||
const { maintained, writesGate, statusesFor, commentFor, CHECK_STATUS } = await import("../pulls.ts");
|
||||
const { maintained, writesGate, CHECK_STATUS } = await import("../pulls.ts");
|
||||
assert.equal(CHECK_STATUS, "mesh/check");
|
||||
assert.deepEqual(maintained(undefined), ["mesh/merge-gate", "mesh/repo-check"], "a rule not read: as before");
|
||||
assert.deepEqual(maintained(["mesh/repo-check"]), ["mesh/repo-check"]);
|
||||
@@ -217,15 +75,6 @@ test("a base's required statuses decide which of the controller's statuses are w
|
||||
assert.equal(writesGate(["mesh/repo-check"]), false);
|
||||
assert.equal(writesGate(["mesh/check"]), false);
|
||||
assert.equal(writesGate(["mesh/merge-gate", "mesh/repo-check"]), true);
|
||||
const c = { owner: "novox", repo: "hq", number: 5, commit: "a".repeat(40), id: "x", verdict: "warning", summary: "no script",
|
||||
trunk: "b".repeat(12), "repo-check": { verdict: "pass", summary: "ok" } } as any;
|
||||
const hq = statusesFor(c, {}, undefined, ["mesh/repo-check"]);
|
||||
assert.deepEqual(hq.map((s: any) => s.context), ["mesh/repo-check"]);
|
||||
assert.match(hq[0].description, /merged into trunk bbbbbbbbbbbb/);
|
||||
assert.deepEqual(statusesFor(c).map((s: any) => s.context), ["mesh/merge-gate", "mesh/repo-check"], "unread: as before");
|
||||
assert.deepEqual(statusesFor(c, {}, undefined, ["mesh/check"]), []);
|
||||
const comment = commentFor(c, {}, ["mesh/repo-check"]);
|
||||
assert.ok(comment === null || !comment.includes("mesh/merge-gate"), comment ?? "");
|
||||
});
|
||||
|
||||
test("a rule the forge gives with no contexts (null) requires nothing, and an unread rule is unknown", async () => {
|
||||
|
||||
@@ -13,7 +13,6 @@ import { join } from "node:path";
|
||||
import { approvedHeadsOf, besideWords, groupRefusal, keepSiblings, namesSiblings, noteVerdict, readSiblings, siblingOutOfDate, siblingsFile,
|
||||
siblingsOf, siblingStatuses, weighSiblings, type SiblingRecord } from "../dist/siblings.js";
|
||||
import { weighTrunks } from "../dist/trunk.js";
|
||||
import { commentFor, statusesFor } from "../dist/pulls.js";
|
||||
|
||||
const h = (c: string) => c.repeat(40);
|
||||
const MEMBER_HEAD = h("a"), HOST_HEAD = h("b"), HOST_BASE = h("c"), HOST_NEW = h("d"), HOST_MOVED = h("e"), MERGE = h("f");
|
||||
@@ -22,43 +21,6 @@ const verdict = { owner: "novox", repo: "mesh-controller", number: 346, commit:
|
||||
trunk: h("1").slice(0, 12), siblings: [sibling], judge_from: "sibling novox/mesh-controller#346 merge 123456789abc",
|
||||
gate: { verdict: "pass", summary: "every machine composes ".repeat(10) }, "repo-check": { verdict: "pass", summary: "ok" } };
|
||||
|
||||
test("both statuses name the siblings the verdict read, keeping the verdict word and the start of its reason within 140 characters", () => {
|
||||
// A realistic controller summary for a failure, long enough that the forge clips it.
|
||||
const reason = "2 of 4 machines do not compose: ace: mesh-host.service needs seat node-reads, which no module on ace claims after this change";
|
||||
const c = { ...verdict, gate: { verdict: "fail", summary: reason }, "repo-check": { verdict: "fail", summary: "its merge-check.sh failed: internal/link/check.go:145:2: undefined" } };
|
||||
const [gate, repo] = statusesFor(c);
|
||||
for (const s of [gate, repo]) {
|
||||
assert.ok(s.description.startsWith(`head ${MEMBER_HEAD.slice(0, 12)} merged into trunk 111111111111 beside mesh-host#114@bbbbbbbb — fail: `), s.description);
|
||||
assert.ok(s.description.length <= 140, `${s.description.length} characters`);
|
||||
assert.ok(namesSiblings(s.description));
|
||||
}
|
||||
assert.match(gate.description, /fail: 2 of 4 machines do not compose: ace: mesh-host/, gate.description);
|
||||
assert.match(repo.description, /fail: its merge-check\.sh failed: internal/, repo.description);
|
||||
assert.equal(besideWords([...siblingsOf([sibling]), ...siblingsOf([{ ...sibling, number: 115 }])]), "beside mesh-host#114@bbbbbbbb +1 more");
|
||||
const comment = commentFor({ ...verdict, gate: { verdict: "fail", summary: "no" } })!;
|
||||
assert.match(comment, /Siblings read beside it: novox\/mesh-host#114 `bbbbbbbbbbbb` merged onto `cccccccccccc` \(main\); judged by sibling novox\/mesh-controller#346 merge/);
|
||||
});
|
||||
|
||||
test("a verdict that could not learn its siblings says so on the status and in the comment, and names none", () => {
|
||||
const { siblings, ...unknown } = { ...verdict, siblings_unknown: "mesh-delivery did not answer" };
|
||||
const reason = "2 of 4 machines do not compose: ace: mesh-host.service needs seat node-reads, which no module claims";
|
||||
for (const s of statusesFor({ ...unknown, gate: { verdict: "fail", summary: reason } })) {
|
||||
assert.match(s.description, /^head aaaaaaaaaaaa merged into trunk 111111111111 siblings unknown — (fail|pass): /, s.description);
|
||||
assert.ok(s.description.length <= 140);
|
||||
assert.equal(namesSiblings(s.description), false, "it read no siblings, so the merge tool asks for none");
|
||||
}
|
||||
assert.match(commentFor({ ...unknown, gate: { verdict: "fail", summary: "no" } })!, /Siblings could not be learned: mesh-delivery did not answer/);
|
||||
});
|
||||
|
||||
test("a verdict without siblings is said exactly as before (an older controller)", () => {
|
||||
const { siblings, judge_from, ...old } = verdict;
|
||||
const [gate] = statusesFor(old);
|
||||
assert.equal(namesSiblings(gate.description), false);
|
||||
assert.match(gate.description, /^head aaaaaaaaaaaa merged into trunk 111111111111 — pass: /);
|
||||
assert.deepEqual(siblingsOf(undefined), []);
|
||||
assert.deepEqual(siblingsOf([{ repository: "novox/x" }]), [], "a sibling named without its head is none");
|
||||
});
|
||||
|
||||
test("the siblings a verdict names are kept per member, replaced by its next verdict, and removed by one that read none", () => {
|
||||
const file = siblingsFile(mkdtempSync(join(tmpdir(), "gitea-siblings-")));
|
||||
const record: SiblingRecord = {};
|
||||
|
||||
@@ -17,7 +17,6 @@ import { join } from "node:path";
|
||||
import { MintedToken } from "../dist/token.js";
|
||||
import { GiteaClient } from "../dist/client.js";
|
||||
import { TOOLS_ACCOUNT, TOOLS_TEAM_UNITS, BUILDER_ACCOUNT, AGENT_ACCOUNT, meshAccounts } from "../dist/tools-account.js";
|
||||
import { repoCheckFacts } from "../dist/pulls.js";
|
||||
|
||||
const ADMIN = "mesh-admin";
|
||||
const PASSWORD = "the-vault-minted-this";
|
||||
@@ -364,16 +363,6 @@ test("the manifest hands the tools their account, and the Go forge bundle holds
|
||||
|
||||
// Review of issue 469: what reads the rules' list is moved to the branch, and nothing leaves the tools without a token.
|
||||
|
||||
test("a repository check said as a warning on a head without merge-check.sh reads the base's protection from the branch", async () => {
|
||||
const client = fresh(await environment());
|
||||
const forgeOf = { holdsFile: async () => false, appliedProtection: client.appliedProtection.bind(client) };
|
||||
const c = { owner: "novox", repo: "hq", commit: "abc123def4567890abc123def4567890abc12345", "repo-check": { verdict: "warning" } } as any;
|
||||
// main requires mesh/repo-check: required, so a head that defines no check fails it for a person to see.
|
||||
assert.deepEqual(await repoCheckFacts(forgeOf, c, "main"), { defined: false, required: true });
|
||||
// A base nothing guards: not required, so the head's repository check passes rather than fails.
|
||||
assert.deepEqual(await repoCheckFacts(forgeOf, c, "feat/unguarded"), { defined: false, required: false });
|
||||
});
|
||||
|
||||
test("a mint that fails leaves the administrator's old token in place: the tools are never left without one", async () => {
|
||||
const env = await environment();
|
||||
const old = { id: 901, name: "mesh-tools", sha1: "sha-admin-kept", user: ADMIN, scopes: ["read:user"] };
|
||||
|
||||
@@ -8,37 +8,11 @@ import assert from "node:assert/strict";
|
||||
|
||||
import { checkOnItsWay, lateVerdictComment, outOfDateStatuses, outOfDateWords, trunkNamed, verdictAtTrunk, weighTrunks } from "../dist/trunk.js";
|
||||
import { GateWriter } from "../dist/gate.js";
|
||||
import { statusesFor, commentFor } from "../dist/pulls.js";
|
||||
|
||||
const HEAD = "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
const TRUNK = "a2c2c35bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const MOVED = "d28ca95ccccccccccccccccccccccccccccccccc";
|
||||
|
||||
test("both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length", () => {
|
||||
const c = { owner: "novox", repo: "mesh-host", number: 66, commit: HEAD, verdict: "pass", summary: "x", id: "build-1",
|
||||
trunk: TRUNK.slice(0, 12), beside: { "mesh-catalog": "0123456789ab", "mesh-lab": "ba9876543210" },
|
||||
gate: { verdict: "pass", summary: "4 of 4 compose ".repeat(20) },
|
||||
"repo-check": { verdict: "fail", summary: "its merge-check.sh failed: internal/store/store.go:145:2: Declared redeclared" } };
|
||||
const [gate, repo] = statusesFor(c);
|
||||
for (const s of [gate, repo]) {
|
||||
assert.ok(s.description.startsWith("head dfb1415aaaaa merged into trunk a2c2c35bbbbb — "), s.description);
|
||||
assert.ok(s.description.length <= 140, `${s.description.length} characters`);
|
||||
assert.equal(trunkNamed(s.description), "a2c2c35bbbbb");
|
||||
}
|
||||
assert.equal(gate.state, "success");
|
||||
assert.equal(repo.state, "failure");
|
||||
const comment = commentFor(c)!;
|
||||
assert.match(comment, /Judged: head `dfb1415aaaaa` merged into trunk `a2c2c35bbbbb`; read beside it: mesh-catalog `0123456789ab`, mesh-lab `ba9876543210`/);
|
||||
|
||||
// A verdict of a build seat from before the rule names no trunk, and is said as before.
|
||||
const [old] = statusesFor({ ...c, trunk: undefined, gate: { verdict: "pass", summary: "every machine composes" } });
|
||||
assert.equal(old.description, "pass: every machine composes");
|
||||
assert.equal(trunkNamed(old.description), undefined);
|
||||
// ADR 0548 §8: a carried verdict's line names the head on the new trunk first, so the merge tool merges it on that trunk.
|
||||
assert.equal(trunkNamed(`pass ${HEAD.slice(0, 12)} on ${MOVED.slice(0, 12)} | carried from ${TRUNK.slice(0, 12)}, mesh-catalog 0123456789ab | unit 0 run, 4 kept`),
|
||||
MOVED.slice(0, 12));
|
||||
});
|
||||
|
||||
test("a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way", () => {
|
||||
const verdict = (trunk: string, state = "success") => ({
|
||||
"mesh/merge-gate": { state, description: `head ${HEAD.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: x` },
|
||||
|
||||
Reference in New Issue
Block a user