One-node bed: SDK-by-version, rename, and the store/broker upgrade proofs #27

Merged
jschoubben merged 26 commits from feat/a-bed-that-hands-over-nothing into main 2026-09-16 21:25:34 +00:00
Showing only changes of commit 572aae2eb4 - Show all commits
+117 -33
View File
@@ -38,7 +38,7 @@
*/
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { existsSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
@@ -294,27 +294,45 @@ function tokenFrom(said: string): string {
// ---- steps, recorded rather than thrown --------------------------------------------------------
interface Step { ok: boolean; why: string; said: string }
interface Step {
code: string; title: string; ok: boolean; why: string; said: string; seconds: number;
}
const steps = new Map<string, Step>();
const order: string[] = [];
/** Run a step, remember what it said, and never throw. A step whose predecessor failed is skipped. */
async function step(name: string, after_: string | null, fn: () => Promise<string>): Promise<void> {
order.push(name);
if (after_ && !steps.get(after_)?.ok) {
steps.set(name, { ok: false, why: `not attempted — "${after_}" did not succeed`, said: "" });
console.log(`SKIPPED ${name}`);
/**
* Run one step of the plan, remember what it said, and never throw.
*
* **Each step carries a code, and the code is the point.** A step used to be identified by its own
* sentence, so "which one failed" meant reading prose, and rewording a step silently made it a
* different step with no history. A code is stable, sorts, and can be pointed at: "V1 failed" says
* something that a whole sentence does not.
*
* A step whose dependency did not succeed is not attempted — its answer would be meaningless and
* its failure would be attributed to the wrong cause. The run still continues to the end, so the
* report says what was established and what was never asked, which are different things.
*/
async function step(
code: string, title: string, needs: string | null, fn: () => Promise<string>,
): Promise<void> {
order.push(title);
if (needs && !steps.get(needs)?.ok) {
steps.set(title, { code, title, ok: false, seconds: 0, said: "",
why: `not attempted — ${steps.get(needs)?.code ?? "?"} (${needs}) did not succeed` });
console.log(`[${code}] SKIP ${title}`);
return;
}
console.log(`\n======== ${name} ========`);
console.log(`\n[${code}] ---- ${title} ----`);
const began = Date.now();
const took = () => Math.round((Date.now() - began) / 1000);
try {
const said = await fn();
steps.set(name, { ok: true, why: "", said });
console.log(`OK ${name}`);
steps.set(title, { code, title, ok: true, why: "", said, seconds: took() });
console.log(`[${code}] PASS ${title} (${took()}s)`);
} catch (err) {
const why = (err as Error).message;
steps.set(name, { ok: false, why, said: "" });
console.log(`FAILED ${name}\n${why.split("\n").slice(0, 25).join("\n")}`);
steps.set(title, { code, title, ok: false, why, said: "", seconds: took() });
console.log(`[${code}] FAIL ${title} (${took()}s)\n${why.split("\n").slice(0, 25).join("\n")}`);
}
}
@@ -328,8 +346,75 @@ function report(name: string): string {
return `${s.why}\n\nWhere this bed got to:\n${lines.join("\n")}`;
}
/**
* The plan, as data.
*
* Stated before any of it is attempted, so a reader knows what the run is trying to establish
* rather than inferring it from whatever happens to be printed. Codes are stable; titles may be
* reworded without the step losing its identity.
*
* Five phases, and the order is the argument: a mesh is RAISED, then it must be able to PRODUCE,
* then something is USED on it, then it is asked to describe itself and its networking is
* VERIFIED, and finally it has to ENDURE — a change following on its own, and coming back after
* the machine stops.
*/
const PLAN: { code: string; title: string }[] = [
{ code: "R1", title: GENESIS },
{ code: "R2", title: SUBSTRATE },
{ code: "R3", title: BUILT_CP },
{ code: "R4", title: PIVOTED },
{ code: "R5", title: HAS_REGISTRY },
{ code: "R6", title: ENROLLED },
{ code: "R7", title: HAS_BUILDER },
{ code: "P1", title: BASE_BUILT },
{ code: "P2", title: STORE_RUNS },
{ code: "P3", title: CATALOGUE_RUNS },
{ code: "P4", title: CONTROL_REBUILT },
{ code: "U1", title: MODULE_BUILT },
{ code: "U2", title: NEEDS },
{ code: "U3", title: ANCHOR_RUNS },
{ code: "V1", title: DESCRIBES },
{ code: "V2", title: NETWORK },
{ code: "E1", title: FOLLOWS },
{ code: "E2", title: SURVIVES },
];
/** What this run intends to establish, said before any of it is attempted. */
function statePlan(): void {
console.log(`\n================ THE PLAN ================`);
for (const s of PLAN) console.log(` ${s.code.padEnd(3)} ${s.title}`);
console.log(` ${PLAN.length} steps. A step whose dependency fails is not attempted.\n`);
}
/** The run's verdict: a table, and a file something other than a person can read. */
function stateOutcome(): void {
console.log(`\n================ WHAT THIS MESH DID FOR ITSELF ================`);
let established = 0;
for (const title of order) {
const s = steps.get(title);
if (!s) continue;
if (s.ok) established++;
const mark = s.ok ? "PASS" : s.why.startsWith("not attempted") ? "SKIP" : "FAIL";
console.log(` ${s.code.padEnd(3)} ${mark} ${title}${s.seconds ? ` (${s.seconds}s)` : ""}`);
}
console.log(` ${established}/${PLAN.length} established.`);
const where = process.env["MESH_LAB_REPORT"] ?? "one-node-mesh-report.json";
try {
writeFileSync(where, JSON.stringify({ scenario: SCENARIO, established, of: PLAN.length,
steps: PLAN.map(({ code, title }) => {
const s = steps.get(title);
return { code, title, status: !s ? "never-ran"
: s.ok ? "pass" : s.why.startsWith("not attempted") ? "skip" : "fail",
seconds: s?.seconds ?? 0, why: s?.ok ? "" : s?.why ?? "" };
}) }, null, 2));
console.log(` report written to ${where}`);
} catch { /* a report that cannot be written must not fail a run that passed */ }
}
before(async () => {
if (skip) return;
statePlan();
const bed = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
onProgress: (m) => console.log(`raise: ${m}`),
@@ -344,7 +429,7 @@ before(async () => {
//
// The shared description, the same one `genesis-single` calls. The bundle is the TEMPLATE with
// nothing held: no image is pre-resolved, because none is here to resolve to.
await step("a bare machine becomes a mesh of one, raised by the installer", null, async () => {
await step("R1", GENESIS, null, async () => {
try {
raised = await genesis({
instanceId,
@@ -381,7 +466,7 @@ before(async () => {
// saying it published an image and the registry serving one are different facts, and it is the
// second that matters.
await step(SUBSTRATE, GENESIS, async () => {
await step("R2", SUBSTRATE, GENESIS, async () => {
await waitForContainer(CONTROL, "mesh-store", 120);
await waitForContainer(CONTROL, "mesh-broker", 120);
return (await on(CONTROL, `docker ps --format '{{.Names}}\t{{.Status}}'`)).out;
@@ -390,7 +475,7 @@ before(async () => {
// **The whole reason the installer carries a builder.** A carried control-plane image would
// satisfy "a control plane is running" equally well, which is what makes this worth asserting:
// the image has to be one this mesh's own registry serves.
await step(BUILT_CP, GENESIS, async () => {
await step("R3", BUILT_CP, GENESIS, async () => {
const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-control 2>&1`)).out.trim();
assert.match(image, /@sha256:[0-9a-f]{64}/,
@@ -400,14 +485,14 @@ before(async () => {
return image;
});
await step(PIVOTED, BUILT_CP, async () => {
await step("R4", PIVOTED, BUILT_CP, async () => {
const ps = (await on(CONTROL, `docker ps -a --format '{{.Names}}'`)).out;
assert.doesNotMatch(ps, /^temp-mesh-control$/m,
`the temporary control plane is still here, so the pivot did not finish:\n${ps}`);
return ps;
});
await step(HAS_REGISTRY, SUBSTRATE, async () => {
await step("R5", HAS_REGISTRY, SUBSTRATE, async () => {
await waitForContainer(CONTROL, "mesh-registry", 120);
const held = (await on(CONTROL,
`curl -sS --max-time 15 http://127.0.0.1:5000/v2/_catalog`)).out;
@@ -416,7 +501,7 @@ before(async () => {
return held.trim();
});
await step(ENROLLED, GENESIS, async () => {
await step("R6", ENROLLED, GENESIS, async () => {
const nodes = await mesh("node list");
assert.match(nodes, new RegExp(`^${CONTROL}\\b`, "m"),
`the mesh has not heard from the machine it is running on:\n${nodes}`);
@@ -426,7 +511,7 @@ before(async () => {
return `${nodes.trim()}\n${agent}`;
});
await step(HAS_BUILDER, HAS_REGISTRY, async () => {
await step("R7", HAS_BUILDER, HAS_REGISTRY, async () => {
await waitForContainer(CONTROL, "mesh-builder", 120);
const modules = await mesh("module list");
assert.match(modules, /^builder\b/m,
@@ -449,7 +534,7 @@ before(async () => {
// The toolchain and runtime every module with code of its own stands on. It is a module, and it
// is built like one — cloned from the forge by the builder installing put here, compiled on the
// machine, published into the mesh's own registry.
await step(BASE_BUILT, HAS_BUILDER, async () => {
await step("P1", BASE_BUILT, HAS_BUILDER, async () => {
// Registered from the manifest the builder will also read, so what the mesh holds and what it
// builds are the same description of the same module.
//
@@ -468,7 +553,7 @@ before(async () => {
// plane to keep its own records in, the way it raises a broker — plumbing, not a module the mesh
// has any record of, so it provides nothing to anything. A module that wants a database wants a
// provider in the graph, and the catalogue below is the first thing to want one.
await step(STORE_RUNS, BASE_BUILT, () => bringUp(STORE));
await step("P2", STORE_RUNS, BASE_BUILT, () => bringUp(STORE));
// And the catalogue, which was missing from this test altogether.
//
@@ -476,7 +561,7 @@ before(async () => {
// of, what a change to one reaches, or what must be rebuilt. A mesh in that state still runs,
// which is exactly how its absence went unnoticed — "the mesh is up" was being read off the
// installer finishing rather than off the mesh being able to answer anything.
await step(CATALOGUE_RUNS, STORE_RUNS, () => bringUp(CATALOGUE));
await step("P3", CATALOGUE_RUNS, STORE_RUNS, () => bringUp(CATALOGUE));
// The control plane, rebuilt from its own repository and rolled out.
//
@@ -484,7 +569,7 @@ before(async () => {
// MODULE PATH — build, notice the version moved, roll it out — is a different claim: it is the
// moment the mesh stops depending on the installer for anything, and the first time the thing
// that performs an upgrade performs one on itself.
await step(CONTROL_REBUILT, CATALOGUE_RUNS, async () => {
await step("P4", CONTROL_REBUILT, CATALOGUE_RUNS, async () => {
const built = await mesh(
`build ${forgeUrl(CONTROL_PLANE.repo)} --ref ${sourceRef} --wait 1200s`, 1_500_000);
assert.doesNotMatch(built, /failed/i, built);
@@ -496,7 +581,7 @@ before(async () => {
});
// ---- 7..8. SOMETHING TO RUN ---------------------------------------------------------------
await step(MODULE_BUILT, CONTROL_REBUILT, async () => {
await step("U1", MODULE_BUILT, CONTROL_REBUILT, async () => {
await registerModule(MODULE.module, resolve(catalogDir, MODULE.module, "module.json"));
const built = await mesh(
`build ${forgeUrl(MODULE.repo)} --path ${MODULE.path} --ref ${refFor(MODULE.repo)} --wait 1200s`,
@@ -520,11 +605,11 @@ before(async () => {
// but the module is not only the broker: it carries a run-once bootstrap that writes the broker's
// configuration, a provisioner that grants each consumer its own vhost and user, tools and an
// event consumer, all of it its own code.
await step(NEEDS, MODULE_BUILT, () => bringUp(PROVIDER));
await step("U2", NEEDS, MODULE_BUILT, () => bringUp(PROVIDER));
// The machine that built it. This is the case every earlier proof covered, and it is here as the
// control for the last step: if this fails, that one's failure says nothing about fetching.
await step(ANCHOR_RUNS, NEEDS, async () => {
await step("U3", ANCHOR_RUNS, NEEDS, async () => {
await mesh(`module issue ${MODULE.module} --node ${CONTROL}`);
await mesh(`assign ${CONTROL} ${MODULE.module}`);
await mesh(`push ${CONTROL}`, 600_000);
@@ -538,7 +623,7 @@ before(async () => {
// module running. The mesh holds a graph — nodes, what each is assigned, what capabilities each
// has, which claims are occupied, what each module is configured with, which provisions exist and
// who holds them — and none of it was ever asked a question.
await step(DESCRIBES, ANCHOR_RUNS, async () => {
await step("V1", DESCRIBES, ANCHOR_RUNS, async () => {
const said: string[] = [];
// The mesh's own verdict on itself. Nothing wrong, nothing waiting, nothing behind.
@@ -604,7 +689,7 @@ before(async () => {
// firewall is generated from what modules declare they listen on, and a firewall that opens the
// wrong set is either a service nobody can reach or a port nobody meant to publish. Neither shows
// up as a failed container.
await step(NETWORK, DESCRIBES, async () => {
await step("V2", NETWORK, DESCRIBES, async () => {
const said: string[] = [];
const ruleset = (await on(CONTROL, `nft list table inet mesh 2>&1`)).out;
@@ -639,7 +724,7 @@ before(async () => {
// The whole point of the mesh, and the capability the migration depends on: move a module's
// source and the running copy follows, with nobody driving the steps. Everything above is
// machinery; this is what the machinery is for.
await step(FOLLOWS, NETWORK, async () => {
await step("E1", FOLLOWS, NETWORK, async () => {
const before = await mesh(`builds ${MODULE.module}`);
const wasPinned = before.match(/sha256:[0-9a-f]{64}/)?.[0] ?? "";
assert.ok(wasPinned, `nothing is pinned to rebuild from:\n${before}`);
@@ -670,7 +755,7 @@ before(async () => {
// something to move real services onto — and the installer is explicit that a host started the
// way the lab starts it does not survive a reboot, which makes this the check that says whether
// that matters.
await step(SURVIVES, FOLLOWS, async () => {
await step("E2", SURVIVES, FOLLOWS, async () => {
await restartMachine(CONTROL);
const missing: string[] = [];
for (const container of MUST_RUN) {
@@ -686,8 +771,7 @@ before(async () => {
return ps;
});
console.log(`\n================ WHAT THIS MESH DID FOR ITSELF ================`);
for (const n of order) console.log(` ${steps.get(n)?.ok ? "PASS" : "FAIL"} ${n}`);
stateOutcome();
}, { timeout: 7_200_000 });
after(async () => {