Files
mesh-lab/test/validate.test.ts
T
jschoubben 675facdb0d The beds name images the way a machine would find them
Twenty-eight integration tests each carried their own copy of the same two helpers,
which pointed a manifest and the substrate bundle at whatever the lab's registry had
assigned. They now share two in the harness, and the difference is the point: ours is
rewritten to the ID the machine holds it under, and everything else is left exactly as
written so the machine pulls it.

**The substrate bundle is where the fiction was most load-bearing.** mesh-host's
`examples/substrate-first-node.lock` pins all three of its images at
`192.0.2.250:5000/…`, which is the address the lab's registry served from — it was
written for a target, and the target was the lab. Two of those are ordinary third-party
images and become the digests mesh-catalog's own postgres and lavinmq modules pin, so
the substrate's store and broker are literally the images the mesh runs. mesh-control
exists in no registry at all and becomes the ID the machine was handed. **The bundle
itself should be fixed in mesh-host and this substitution deleted with it.**

Beds that wrote a manifest by hand named an image by repository and let the rewrite
supply a digest. There is nothing to supply one now, so `onTheMachine` refuses an
unpinned reference and hands back the digest the catalogue pins — a bed runs the image
the mesh ships, and a bed that drifts from the catalogue is testing a different
postgres.

Three beds took a third-party image out of the raised list, which no longer contains
one: certificates (pebble), objectstore (minio and its client) and provisioner
(postgres) now name theirs and pull it. builds and mesh publish into the MESH's own
artifact store — the `registry` module's image, on the node, on 5000 — rather than into
scenery the lab raised. That is a different claim, and only one of them exists in
production.

New unit tests cover what a full raise would otherwise be the only way to check: the
routes an egress machine gets (that its gateway is still the path to the rest of the
scenario, that a range with no path is unreachable rather than leaked to the uplink,
that each family gets its own next hop), which machine is handed which of our images,
and the `images:` rule that refuses a third-party entry. The "shipped scenarios are
valid" test now loads every scenario rather than two of them.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 23:16:41 +02:00

317 lines
11 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { readdirSync } from "node:fs";
import { parseScenario } from "../src/declaration/parse.ts";
import { loadScenario } from "../src/declaration/parse.ts";
import { planRouters } from "../src/lifecycle/router.ts";
/** Every rejection below is a fault that would otherwise be silent at runtime. */
function refuses(yaml: string, pattern: RegExp): void {
assert.throws(() => parseScenario(yaml), (err: Error) => {
assert.match(err.message, pattern);
return true;
});
}
test("the shipped scenarios are valid", () => {
// **Every one of them**, not a chosen two. Thirty-odd scenarios were rewritten in one pass when
// the lab's registry was removed, and a scenario nobody loads is a scenario nobody validates —
// which is how a bed goes unraisable for weeks and is only found when somebody wants it.
const files = readdirSync("scenarios").filter((f) => f.endsWith(".yml"));
assert.ok(files.length > 20, `only ${files.length} scenarios found — is the path right?`);
for (const file of files) {
assert.doesNotThrow(() => loadScenario(`scenarios/${file}`), `scenarios/${file}`);
}
});
test("a public segment on a private range is refused — the mesh would silently never form", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [192.168.1.0/24] } }
machines: { a: { at: { segment: net, address: [192.168.1.1] } } }`,
/not documentation space/,
);
});
test("a public segment on a real routable range is refused", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [8.8.8.0/24] } }
machines: { a: { at: { segment: net, address: [8.8.8.8] } } }`,
/not documentation space/,
);
});
test("a private segment may use any range, including someone else's RFC 1918", () => {
assert.doesNotThrow(() =>
parseScenario(`scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
cafe: { kind: private, cidr: [10.50.0.0/16], gateway: { to: pub, address: [192.0.2.5], nat: [v4], forwardable: false } }
machines: { a: { at: { segment: cafe, address: [10.50.0.9] } } }`),
);
});
test("publishing through an unforwardable gateway is refused — that is the constraint", () => {
refuses(
`scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
cafe: { kind: private, cidr: [10.50.0.0/16], gateway: { to: pub, address: [192.0.2.5], nat: [v4], forwardable: false } }
machines:
a:
at: { segment: cafe, address: [10.50.0.9] }
published: [{ port: 443, on: cafe }]`,
/not forwardable/,
);
});
test("a gateway address must be on the PARENT segment, not the one behind it", () => {
refuses(
`scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.168.1.1], nat: [v4] } }
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`,
/is not within 'pub'/,
);
});
test("a machine address outside its segment is refused", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [203.0.113.9] } } }`,
/is not within segment 'net'/,
);
});
test("an unknown segment reference is refused", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: nope, address: [192.0.2.1] } } }`,
/unknown segment 'nope'/,
);
});
test("a gateway loop is refused rather than raised forever", () => {
refuses(
`scenario: x
segments:
a: { kind: private, cidr: [10.0.0.0/24], gateway: { to: b, address: [10.0.1.1], nat: [] } }
b: { kind: private, cidr: [10.0.1.0/24], gateway: { to: a, address: [10.0.0.1], nat: [] } }
machines: { m: { at: { segment: a, address: [10.0.0.9] } } }`,
/loops through/,
);
});
test("a detached machine cannot publish", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: detached, published: [{ port: 443, on: net }] } }`,
/detached but declares published/,
);
});
test("publishing on a segment the machine is not attached to is refused", () => {
refuses(
`scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines:
a:
at: { segment: pub, address: [192.0.2.10] }
published: [{ port: 443, on: home }]`,
/not attached to it/,
);
});
test("two addresses of one family on one attachment is refused", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1, 192.0.2.2] } } }`,
/two v4 addresses/,
);
});
test("place naming a machine that does not exist is refused", () => {
refuses(
`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
place: { ghost: [host] }`,
/'ghost' is not a machine/,
);
});
test("every problem is reported, not just the first", () => {
try {
parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.168.0.0/24] } }
machines: { a: { at: { segment: nope, address: [1.2.3.4] } } }
place: { ghost: [host] }`);
assert.fail("should have thrown");
} catch (err) {
const problems = (err as { problems: string[] }).problems;
assert.ok(problems.length >= 3, `expected several problems, got ${problems.length}`);
}
});
test("a detached machine is valid", () => {
assert.doesNotThrow(() =>
parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines:
a: { at: { segment: net, address: [192.0.2.1] } }
roamer: { at: detached }`),
);
});
test("a multi-homed machine is valid", () => {
assert.doesNotThrow(() =>
parseScenario(`scenario: x
segments:
pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines:
border:
at:
- { segment: pub, address: [192.0.2.60] }
- { segment: home, address: [192.168.1.2] }`),
);
});
test("an isolated private segment with no gateway is valid — a site with no internet", () => {
assert.doesNotThrow(() =>
parseScenario(`scenario: x
segments: { island: { kind: private, cidr: [10.9.0.0/24] } }
machines: { a: { at: { segment: island, address: [10.9.0.1] } } }`),
);
});
test("two gateways sharing an address are one gateway, not two", () => {
// Modelled on the real thing: a bridged modem, one gateway holding the public address,
// everything behind it. Two routers on one address is not a topology, it is a collision —
// and the lab raised it happily, with the address resolving to whichever container
// answered ARP last.
const scenario = parseScenario(`
scenario: shared-gateway
segments:
isp:
kind: public
cidr: [198.51.100.0/24, "2001:db8:b::/48"]
home:
kind: private
cidr: [192.168.1.0/24]
gateway: { to: isp, address: [198.51.100.7, "2001:db8:b::7"], nat: [v4], forwardable: true, mapping_ttl: 120s }
devices:
kind: private
cidr: [192.168.30.0/24]
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: true, mapping_ttl: 120s }
machines:
thermostat:
at: { segment: devices, address: [192.168.30.20] }
`);
const plans = planRouters(scenario, "test");
assert.equal(plans.length, 1, `expected one gateway, got ${plans.map((p) => p.inside.join("+")).join(" / ")}`);
assert.deepEqual([...plans[0]!.inside].sort(), ["devices", "home"]);
// The union: a v6 address declared on only one of the segments it serves is still carried.
assert.deepEqual([...plans[0]!.outsideAddresses].sort(), ["198.51.100.7", "2001:db8:b::7"]);
});
test("one box cannot behave two ways", () => {
// If two gateways share an address they are the same box, so a disagreement about what
// that box does is a contradiction — refused rather than silently resolved one way.
assert.throws(
() =>
parseScenario(`
scenario: contradictory-gateway
segments:
isp:
kind: public
cidr: [198.51.100.0/24]
home:
kind: private
cidr: [192.168.1.0/24]
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: true }
devices:
kind: private
cidr: [192.168.30.0/24]
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: false }
machines:
thermostat:
at: { segment: devices, address: [192.168.30.20] }
`),
/one gateway.*disagree.*forwardable/s,
);
});
test("a detached machine cannot declare egress", () => {
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: detached, egress: true } }`,
/detached but declares egress/);
});
/**
* `images:` is the mesh's own images and nothing else.
*
* **The rule that replaced the lab's registry.** Anything with somewhere to be fetched from is
* fetched from there, by the machine, over its uplink. Serving it from inside the scenario instead
* is what hid the bootstrap faults this lab exists to find — so it is refused rather than quietly
* done, or the fiction comes back one convenient line at a time.
*/
test("a third-party image in images: is refused, because nothing loads it", () => {
for (const image of ["postgres:17-alpine", "gitea/gitea:1.22", "ghcr.io/mailu/admin:1.9"]) {
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: ["${image}"]
place: { all: [runtime] }`,
/is not one of the mesh's own images/);
}
});
test("one of ours in images: is accepted", () => {
assert.doesNotThrow(() => parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: [mesh-control:development, mesh-route-proxy:development]
place: { all: [runtime] }`));
});
test("images: is named by tag — an image ID is not knowable until the image is built", () => {
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: ["mesh-control@sha256:${"0".repeat(64)}"]
place: { all: [runtime] }`,
/is pinned by digest/);
});
test("a machine cannot be handed an image the scenario does not have", () => {
// Ignoring it silently would be a machine missing a runtime, failing several minutes later
// inside an apply, as a container that will not start.
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines:
a:
at: { segment: net, address: [192.0.2.1] }
egress: true
images: [mesh-runtime-redis:development]
images: [mesh-control:development]
place: { all: [runtime] }`,
/is not in this scenario's images/);
});
test("a segment may not be named 'uplink' — the lab claims that name for egress", () => {
refuses(`scenario: x
segments: { uplink: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: uplink, address: [192.0.2.1] }, egress: true } }`,
/reserved for the lab's own NAT bridge/);
});