Merge pull request 'A pending merge check says where it is: in the build queue since when, then building on which machine (hq issue 446)' (#202) from fix/446-a-pending-check-says-where-it-is into main
This commit was merged in pull request #202.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// **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
|
||||
// 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
|
||||
// gate goes through one chain: a verdict is marked the moment it is heard, before it waits its turn, and a "building"
|
||||
// still waiting behind or taking its turn after that mark is dropped, never written.
|
||||
|
||||
import { CHECK_CONTEXT, pendingWords } from "./pulls.js";
|
||||
import type { CommitStatus } 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>;
|
||||
/** The head's mesh/merge-gate, newest: its state and when it was set; undefined when it has none. */
|
||||
gateOf(owner: string, repo: string, sha: string): Promise<{ state: string; updated_at?: string } | 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;
|
||||
|
||||
/** 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 ${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$/, "");
|
||||
const full = `${owner}/${repo}`.toLowerCase();
|
||||
return u === full || u.endsWith("/" + full);
|
||||
}
|
||||
|
||||
interface Head {
|
||||
/** When the pending words were last set by this writer; undefined for a head this process did not announce. */
|
||||
pendingAt?: number;
|
||||
/** A verdict was heard for the head: nothing pending is written after it. */
|
||||
verdict: boolean;
|
||||
/** The chain the head's writes take their turns on. */
|
||||
chain: Promise<void>;
|
||||
}
|
||||
|
||||
export class GateWriter {
|
||||
private heads = new Map<string, Head>();
|
||||
private forge: GateForge;
|
||||
private now: () => Date;
|
||||
|
||||
constructor(forge: GateForge, now: () => Date = () => new Date()) {
|
||||
this.forge = forge;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
private head(owner: string, repo: string, sha: string): Head {
|
||||
const key = `${owner}/${repo}@${sha}`.toLowerCase();
|
||||
let h = this.heads.get(key);
|
||||
if (!h) {
|
||||
h = { verdict: false, chain: Promise.resolve() };
|
||||
this.heads.set(key, h);
|
||||
// Kept for the heads still in play; the oldest go first, past what any forge holds open at once.
|
||||
if (this.heads.size > 2000) this.heads.delete(this.heads.keys().next().value as string);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/** One turn on the head's chain; a turn that fails never stops the next. Its failure is the caller's. */
|
||||
private turn(h: Head, fn: () => Promise<void>): Promise<void> {
|
||||
const run = h.chain.then(fn);
|
||||
h.chain = run.catch(() => {});
|
||||
return run;
|
||||
}
|
||||
|
||||
/** A turn whose failure is said here: a pending word not written is not worth the event again. */
|
||||
private said(h: Head, what: string, fn: () => Promise<void>): Promise<void> {
|
||||
return this.turn(h, fn).catch((err) => {
|
||||
console.error(`[gitea] ${what} — ${err instanceof Error ? err.message : err}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** The head announced: it waits in the build queue from now. A new check of the head, so no verdict yet. */
|
||||
pending(owner: string, repo: string, sha: string): Promise<void> {
|
||||
const h = this.head(owner, repo, sha);
|
||||
const at = this.now();
|
||||
h.pendingAt = at.getTime();
|
||||
h.verdict = false;
|
||||
return this.said(h, `${owner}/${repo}@${sha.slice(0, 8)}: could not say a check is pending`, () =>
|
||||
this.forge.setCommitStatus(owner, repo, sha, { state: "pending", context: CHECK_CONTEXT, description: pendingWords(at) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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.
|
||||
*/
|
||||
verdict(owner: string, repo: string, sha: string, write: () => Promise<void>): Promise<void> {
|
||||
const h = this.head(owner, repo, sha);
|
||||
h.verdict = true;
|
||||
return this.turn(h, write);
|
||||
}
|
||||
}
|
||||
+39
-7
@@ -14,6 +14,9 @@
|
||||
// `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.
|
||||
//
|
||||
// Consumes node-build-agent.started — a build machine took an ask — and says, on the head a pending check is of,
|
||||
// which machine builds it since when (novox/hq issue 446). This module is the one writer of mesh/merge-gate.
|
||||
//
|
||||
// Every pull request the forge holds is announced, whatever its repository: the controller holds the
|
||||
// module graph and decides what is checked — a repository is never asked to opt in.
|
||||
//
|
||||
@@ -29,6 +32,7 @@ import { emit, on } from "@novox/mesh-sdk/events";
|
||||
import { GiteaClient, movedSince } from "./client.js";
|
||||
import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, headsToAnnounce, statusesFor, type Announced, type Checked,
|
||||
type RepoCheckFacts, reposToLook } from "./pulls.js";
|
||||
import { GateWriter, clonedAs, type BuildStarted } from "./gate.js";
|
||||
|
||||
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
|
||||
// nothing to watch; log and stay quiet rather than crash the runtime. With one, the first poll mints
|
||||
@@ -274,12 +278,9 @@ async function pollPulls(client: GiteaClient): Promise<void> {
|
||||
});
|
||||
// Said, so an operator knows the mesh was asked to check it.
|
||||
console.log(`[gitea] announced ${key} at ${String(pull.head_sha).slice(0, 8)} to be checked before it merges`);
|
||||
// Pending until the verdict comes, so the pull request says a check is running rather than nothing.
|
||||
await client
|
||||
.setCommitStatus(repo.owner, repo.name, String(pull.head_sha), {
|
||||
state: "pending", context: CHECK_CONTEXT, description: "the mesh is mapping this head onto its module graph",
|
||||
})
|
||||
.catch((err) => console.error(`[gitea] ${key}: could not say a check is pending — ${err instanceof Error ? err.message : err}`));
|
||||
// Pending until the verdict comes, so the pull request says a check is running rather than nothing — and
|
||||
// where it is: in the build queue from now, until a build machine takes it (novox/hq issue 446).
|
||||
await gates.pending(repo.owner, repo.name, String(pull.head_sha));
|
||||
}
|
||||
heads[key] = String(pull.head_sha);
|
||||
changed = true;
|
||||
@@ -337,6 +338,31 @@ async function repoCheckFacts(client: GiteaClient, c: Checked, base: string | un
|
||||
return { defined, required };
|
||||
}
|
||||
|
||||
// **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.
|
||||
const gates = new GateWriter({
|
||||
setCommitStatus: (owner, repo, sha, status) => (gitea as GiteaClient).setCommitStatus(owner, repo, sha, status),
|
||||
gateOf: async (owner, repo, sha) => (await (gitea as GiteaClient).headChecks(owner, repo, sha))[CHECK_CONTEXT],
|
||||
});
|
||||
|
||||
// **A build machine took an ask** (the build seat's `started`, novox/hq ADR 0157): said on the head it checks, when
|
||||
// the head is an open pull request's this module announced. Any other build — a branch, a merge's — says nothing.
|
||||
async function buildStarted(event: { body: unknown }): Promise<void> {
|
||||
const b = (event.body ?? {}) as BuildStarted;
|
||||
const ref = String(b.ref ?? "").toLowerCase();
|
||||
if (!/^[0-9a-f]{40}$/.test(ref)) return;
|
||||
for (const [key, sha] of Object.entries(heads)) {
|
||||
if (sha.toLowerCase() !== ref) continue;
|
||||
const [owner, rest] = key.split("/");
|
||||
const repo = rest?.split("#")[0] ?? "";
|
||||
if (owner && repo && clonedAs(b.repository, owner, repo)) {
|
||||
await gates.started(owner, repo, sha, b);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gitea) {
|
||||
const client = gitea;
|
||||
// A poll that fails says so once, not once a minute: the same reason repeating (the forge not up
|
||||
@@ -363,6 +389,12 @@ if (gitea) {
|
||||
tick(() => pollRepos(client), 60_000);
|
||||
tick(() => pollMerged(client), 30_000);
|
||||
tick(() => pollPulls(client), 30_000);
|
||||
await on("mesh-controller.checked", (event) => setVerdict(client, event));
|
||||
await on("mesh-controller.checked", (event) => {
|
||||
const c = (event.body ?? {}) as Checked;
|
||||
// A head's own verdict takes its turn after every write of that head's gate already asked for (gate.ts).
|
||||
if (!c.group && c.owner && c.repo && c.commit) return gates.verdict(c.owner, c.repo, c.commit, () => setVerdict(client, event));
|
||||
return setVerdict(client, event);
|
||||
});
|
||||
await on("node-build-agent.started", buildStarted);
|
||||
console.log("[gitea] watching for new repositories and merged pull requests");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
"pull.closed"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-controller.checked"
|
||||
"mesh-controller.checked",
|
||||
"node-build-agent.started"
|
||||
],
|
||||
"listens": [
|
||||
{
|
||||
|
||||
@@ -18,6 +18,18 @@ export const CHECK_CONTEXT = "mesh/merge-gate";
|
||||
/** The status context of the repository's own merge-check.sh, the check's second layer (novox/hq ADR 0237). */
|
||||
export const REPO_CHECK_CONTEXT = "mesh/repo-check";
|
||||
|
||||
/**
|
||||
* The pending words a head's merge check is announced with (novox/hq issue 446): where the check is, and since when,
|
||||
* in the time the operator reads. They stood as "the mesh is mapping this head onto its module graph" for the whole
|
||||
* check — the few seconds the controller maps the head and then the queue wait and the build — and readers took the
|
||||
* forge for lagging (issue 423). The controller maps and queues the check in seconds, so from the announcement it waits
|
||||
* in the build queue; mesh-delivery says "building on <machine> since …" when a build machine takes it.
|
||||
*/
|
||||
export function pendingWords(announced: Date): string {
|
||||
const two = (n: number) => String(n).padStart(2, "0");
|
||||
return `waiting in the build queue since ${two(announced.getHours())}:${two(announced.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** One layer of a check, judged. */
|
||||
export interface Layer {
|
||||
verdict: string;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
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
|
||||
// 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 }> = {};
|
||||
const written: string[] = [];
|
||||
let hold: Promise<void> | null = null;
|
||||
let release = () => {};
|
||||
let readHold: Promise<void> | null = null;
|
||||
let readRelease = () => {};
|
||||
return {
|
||||
kept, written,
|
||||
/** The next writes wait until released: a request in flight. */
|
||||
holdWrites() {
|
||||
hold = new Promise<void>((r) => (release = r));
|
||||
},
|
||||
release() {
|
||||
hold = null;
|
||||
release();
|
||||
},
|
||||
async setCommitStatus(_o: string, _r: string, _sha: string, s: { state: string; context: string; description: string }) {
|
||||
if (hold) await hold;
|
||||
kept[s.context] = { state: s.state, description: s.description, updated_at: new Date().toISOString() };
|
||||
written.push(`${s.state} ${s.description}`);
|
||||
},
|
||||
/** Reads answer what the forge held when asked, as late as the reads are held: the answer of a request in flight. */
|
||||
holdReads() {
|
||||
readHold = new Promise<void>((r) => (readRelease = r));
|
||||
},
|
||||
releaseReads() {
|
||||
readHold = null;
|
||||
readRelease();
|
||||
},
|
||||
async gateOf(_o: string, _r: string, _sha: string) {
|
||||
const held = kept["mesh/merge-gate"] ? { ...kept["mesh/merge-gate"] } : undefined;
|
||||
if (readHold) await readHold;
|
||||
return held;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 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: pending once more
|
||||
await gates.pending("novox", "hq", SHA);
|
||||
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false, "the first run's start");
|
||||
assert.match(forge.kept["mesh/merge-gate"].description, /^waiting in the build queue since /);
|
||||
assert.equal(await gates.started("novox", "hq", SHA, { ...startedOnShanks, at: "2026-10-11T01:31:00Z" }), true,
|
||||
"the second run's start");
|
||||
});
|
||||
|
||||
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);
|
||||
assert.equal(clonedAs("http://127.0.0.1:20000/novox/mesh-hq.git", "novox", "hq"), false);
|
||||
assert.equal(clonedAs("http://127.0.0.1:20000/novox/mesh-controller.git", "novox", "hq"), false);
|
||||
});
|
||||
@@ -146,3 +146,11 @@ test("a change plan is the gate's result: said on the status and, when it builds
|
||||
const nothing = { ...c, plan: { ...plan, moved: [], tiers: [], machines: [], summary: "builds nothing" } };
|
||||
assert.equal(commentFor(nothing), null);
|
||||
});
|
||||
|
||||
// **A pending check says where it is** (novox/hq issue 446): announced, it waits in the build queue, since the time the
|
||||
// operator reads — not "mapping this head onto its module graph" for the ten minutes of a controller check.
|
||||
test("a head announced is pending in the build queue, since the local time it was announced", async () => {
|
||||
const { pendingWords } = await import("../pulls.ts");
|
||||
assert.equal(pendingWords(new Date(2026, 9, 11, 3, 8, 50)), "waiting in the build queue since 03:08");
|
||||
assert.equal(pendingWords(new Date(2026, 9, 11, 14, 0, 0)), "waiting in the build queue since 14:00");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user