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: [10.99.1.0/24] } } machines: { a: { at: { segment: net, address: [10.99.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: [10.99.1.0/24], gateway: { to: pub, address: [10.99.1.1], nat: [v4] } } machines: { a: { at: { segment: home, address: [10.99.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: [10.99.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: [10.99.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: [10.99.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: [10.99.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: [10.99.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-controller: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-controller@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-controller: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/); });