Files
mesh-lab/test/diagram.test.ts
T
jschoubben 94e617915c The home segment moves off 192.168.1.0/24
It is the commonest home LAN range there is, so on an ordinary workstation the
lab's private segment and the machine's own network are the same addresses. The
scenario routes an egress machine explicitly and marks the rest unreachable, so
nothing leaked — but that guard was carrying the whole weight of a collision
nobody chose, and a guard is a bad place for that.

10.99.1.0/24 is still RFC 1918, so the bed still models a home LAN behind an
access point. It is simply far from what this kind of machine already has:
192.168.1 is the LAN, 172.16-31 and 192.168.16-95 are container bridges, and
10.10/10.42/10.208 are a tunnel, the mesh overlay and the virtualisation daemon.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 00:00:19 +02:00

308 lines
14 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { loadScenario } from "../src/declaration/parse.ts";
import { diagramFromDeclaration } from "../src/diagram/from-declaration.ts";
import { toDrawio } from "../src/diagram/drawio.ts";
/**
* A generated diagram that will not open is worse than no diagram — it looks like a
* deliverable and is not one. The first version was unparseable because HTML labels were
* concatenated raw into an XML attribute, so these assert the file itself.
*/
function parseCells(xml: string): { id: string; vertex: boolean; edge: boolean; source?: string; target?: string }[] {
const cells: ReturnType<typeof parseCells> = [];
// <object> wrappers carry the id for any cell with a tooltip; the mxCell inside has none.
for (const match of xml.matchAll(/<object ([^>]*?)>/g)) {
const id = /id="([^"]*)"/.exec(match[1] ?? "")?.[1];
if (id) cells.push({ id, vertex: true, edge: false });
}
for (const match of xml.matchAll(/<mxCell ([^>]*?)(?:\/>|>)/g)) {
const attrs = match[1] ?? "";
const get = (name: string) => new RegExp(`${name}="([^"]*)"`).exec(attrs)?.[1];
const id = get("id");
if (!id) continue;
const entry: (typeof cells)[number] = {
id,
vertex: get("vertex") === "1",
edge: get("edge") === "1",
};
const source = get("source");
const target = get("target");
if (source) entry.source = source;
if (target) entry.target = target;
cells.push(entry);
}
return cells;
}
const scenario = loadScenario("scenarios/the-ordinary-shape.yml");
const xml = toDrawio(diagramFromDeclaration(scenario));
test("the file is well-formed XML — raw markup in an attribute is not", () => {
// No unescaped angle bracket may appear inside a value="..." attribute.
for (const match of xml.matchAll(/(?:value|label|tooltip)="([^"]*)"/g)) {
assert.doesNotMatch(match[1] ?? "", /[<>]/, "a label carries raw markup into an attribute");
}
assert.match(xml, /^<mxfile /);
assert.match(xml, /<\/mxfile>\s*$/);
});
test("every edge connects two cells that exist", () => {
const cells = parseCells(xml);
const ids = new Set(cells.map((c) => c.id));
for (const edge of cells.filter((c) => c.edge)) {
assert.ok(ids.has(edge.source ?? ""), `edge ${edge.id} has no source`);
assert.ok(ids.has(edge.target ?? ""), `edge ${edge.id} has no target`);
}
});
test("the diagram draws the implicit routers, not only what is written down", () => {
// A scenario never names its gateways. A picture that showed only declared machines
// would omit every node carrying NAT, forwarding and expiry.
const diagram = diagramFromDeclaration(scenario);
assert.ok(diagram.machines.some((m) => m.kind === "router"), "no router drawn");
assert.ok(diagram.machines.some((m) => m.kind === "transit"), "no transit drawn");
});
test("a router's badges say what the declaration decided", () => {
const diagram = diagramFromDeclaration(scenario);
const unforwardable = diagram.machines.find((m) => m.notes.some((n) => n.includes("NOT forwardable")));
assert.ok(unforwardable, "the unforwardable gateway is not marked as such");
assert.ok(
diagram.machines.some((m) => m.notes.some((n) => n.includes("mappings expire"))),
"a declared mapping expiry is not shown",
);
});
test("a metadata fact becomes a badge, and absence of the fact does not", () => {
// The point of the badges: the properties worth seeing are the ones with no visual
// consequence. A translated address looks exactly like an untranslated one.
for (const tip of ["translates v4", "no port forwarding", "mappings expire"]) {
assert.ok(xml.includes(tip), `no badge explains '${tip}'`);
}
// A gateway that does not translate gets no mark, rather than a struck-through one.
assert.doesNotMatch(xml, /label="N"[^>]*tooltip="[^"]*no NAT/);
});
test("every badge says in words what its letter means", () => {
// A one-letter code with no tooltip is a private language. Each badge is wrapped in an
// <object>, which is the only place draw.io reads a tooltip from.
const badges = [...xml.matchAll(/<object [^>]*label="([A-Z▶■]{1,2})"[^>]*tooltip="([^"]*)"/g)];
assert.ok(badges.length > 0, "no badges rendered at all");
for (const [, code, tip] of badges) {
assert.ok((tip ?? "").length > 10, `badge ${code} has no explanation`);
}
});
test("no two cells share an id — a machine may be named after a gateway", () => {
const ids = parseCells(xml).map((c) => c.id);
assert.equal(new Set(ids).size, ids.length, "duplicate cell id");
});
test("segments are ordered public first, then by depth behind them", () => {
const diagram = diagramFromDeclaration(scenario);
const publicDepths = diagram.segments.filter((s) => s.kind === "public").map((s) => s.depth);
assert.deepEqual([...new Set(publicDepths)], [0], "a public segment should be at depth 0");
const home = diagram.segments.find((s) => s.name === "home");
assert.equal(home?.depth, 1, "a segment behind one gateway is at depth 1");
});
test("a declared address appears on the machine that holds it", () => {
assert.match(xml, /10\.99\.1\.135/);
assert.match(xml, /198\.51\.100\.7/);
});
test("every shape names a stencil that exists", () => {
// A style naming a stencil draw.io does not have renders as an empty box — no error, no
// warning, just a missing picture. Checked against the names in draw.io's own
// stencils/networks.xml, which is where mxgraph.networks.* is defined.
const KNOWN = new Set([
"mxgraph.networks.server",
"mxgraph.networks.router",
"mxgraph.networks.cloud",
"mxgraph.networks.firewall",
"mxgraph.networks.switch",
"mxgraph.networks.pc",
"mxgraph.networks.laptop",
"mxgraph.networks.storage",
"mxgraph.networks.modem",
"mxgraph.networks.mainframe",
]);
const used = new Set([...xml.matchAll(/shape=([a-z0-9_.]+)/g)].map((m) => m[1] as string));
assert.ok(used.size > 0, "no stencil shapes used at all");
for (const shape of used) {
assert.ok(KNOWN.has(shape), `'${shape}' is not a stencil draw.io ships`);
}
});
test("a resource's shape is fixed by kind, and never varies with its metadata", () => {
// The split the whole design rests on: shape says what a thing is, badges say what is
// true about it. A gateway that stops translating must still look like a gateway.
const routers = [...xml.matchAll(/shape=mxgraph\.networks\.router/g)].length;
const diagram = diagramFromDeclaration(scenario);
assert.equal(routers, diagram.machines.filter((m) => m.kind === "router").length);
assert.equal(
[...xml.matchAll(/shape=mxgraph\.networks\.server/g)].length,
diagram.machines.filter((m) => m.kind === "machine").length,
);
});
test("no link crosses a network it does not touch", () => {
// The layout fault that made the first drawings unreadable: a gateway placed below the
// lane it FACES, with its link to the outside running the height of the picture through
// three networks it has nothing to do with — and overlapping another such link, so the
// two read as one wire. Every link must now be short and local.
const geometry = new Map<string, { y: number; h: number }>();
for (const match of xml.matchAll(
/<(?:mxCell|object)[^>]*id="([^"]*)"[\s\S]{0,400}?<mxGeometry x="[^"]*" y="([^"]*)" width="[^"]*" height="([^"]*)"/g,
)) {
geometry.set(match[1] as string, { y: Number(match[2]), h: Number(match[3]) });
}
const lanes = [...geometry.entries()]
.filter(([id]) => id.startsWith("lane-"))
.map(([id, g]) => ({ name: id.slice(5), top: g.y, bottom: g.y + g.h }));
assert.ok(lanes.length > 0, "no lanes found");
for (const edge of xml.matchAll(/<mxCell id="([^"]*)"[^>]*edge="1"[^>]*source="([^"]*)" target="([^"]*)"/g)) {
const [, , source, target] = edge;
const from = geometry.get(source as string);
const lane = lanes.find((l) => `lane-${l.name}` === target);
if (!from || !lane) continue;
const span = { top: Math.min(from.y, lane.top), bottom: Math.max(from.y + from.h, lane.bottom) };
const attached = new Set([target, source]);
for (const other of lanes) {
if (attached.has(`lane-${other.name}`)) continue;
const crosses = other.top >= span.top && other.bottom <= span.bottom;
assert.ok(!crosses, `link ${source}→${target} crosses '${other.name}'`);
}
}
});
test("a network behind another is drawn inside it, not merely below it", () => {
// "Behind" is shown by indentation. Without it the reader has only a wire to follow, and
// in a layered scenario that wire is exactly what became unreadable.
const laneX = new Map<string, number>();
for (const match of xml.matchAll(
/<mxCell id="lane-([^"]*)"[\s\S]{0,400}?<mxGeometry x="([^"]*)"/g,
)) {
laneX.set(match[1] as string, Number(match[2]));
}
const diagram = diagramFromDeclaration(scenario);
for (const segment of diagram.segments) {
if (!segment.behind) continue;
const mine = laneX.get(segment.name);
const parent = laneX.get(segment.behind);
assert.ok(mine !== undefined && parent !== undefined, `${segment.name} or its parent is missing`);
assert.ok(mine > parent, `${segment.name} is not indented inside ${segment.behind}`);
}
});
test("a gateway sits immediately above the network it serves", () => {
// What the grouping guarantees, and the whole reason a gateway reads as the door into a
// network rather than a box floating near one.
//
// It does NOT guarantee adjacency to the network the gateway FACES: a public network with
// two private networks behind it can only put one of them next to it. That case is carried
// by the lane's own "behind …" and by both children being indented to the same depth —
// and the link that would otherwise cross the sibling is suppressed, which is what the
// crossing test above asserts.
const diagram = diagramFromDeclaration(scenario);
const order = [...xml.matchAll(/<mxCell id="lane-([^"]*)"/g)].map((m) => m[1] as string);
let checked = 0;
for (const machine of diagram.machines) {
if (machine.kind !== "router") continue;
const served = machine.attachments.slice(1).map((a) => a.segment);
const top = served.reduce((b, n) => (order.indexOf(n) < order.indexOf(b) ? n : b), served[0] ?? "");
assert.ok(order.includes(top), `${machine.name} serves '${top}', which is not drawn`);
checked++;
}
assert.ok(checked > 0, "no gateways to check");
});
test("a public network is followed by everything behind it, before the next public one", () => {
// The ordering rule itself. Sorting by depth alone put a private network far from the
// public one it sits behind, which is what made the links long in the first place.
const diagram = diagramFromDeclaration(scenario);
const order = [...xml.matchAll(/<mxCell id="lane-([^"]*)"/g)].map((m) => m[1] as string);
const rootOf = (name: string): string => {
let current = name;
const seen = new Set<string>();
for (;;) {
const segment = diagram.segments.find((s) => s.name === current);
if (!segment?.behind || seen.has(current)) return current;
seen.add(current);
current = segment.behind;
}
};
// Reading down the page, the root never returns to one already left behind.
const finished = new Set<string>();
let previous = "";
for (const name of order) {
const root = rootOf(name);
if (root !== previous) {
assert.ok(!finished.has(root), `'${root}' is split apart by another group`);
if (previous) finished.add(previous);
previous = root;
}
}
});
test("both sources lay the same topology out identically", () => {
// The comparison is the feature. The hypervisor cannot know declaration order, so laying
// out by it would give the two pictures different shapes and nothing could be read off
// the difference. Proved by shuffling the segments and checking the layout does not move.
const shuffled = {
...diagramFromDeclaration(scenario),
segments: [...diagramFromDeclaration(scenario).segments].reverse(),
};
const laneOrder = (out: string) => [...out.matchAll(/<mxCell id="lane-([^"]*)"/g)].map((m) => m[1]);
assert.deepEqual(laneOrder(toDrawio(shuffled)), laneOrder(xml));
});
const ALL_SCENARIOS = [
"bootstrap-single",
"two-on-a-segment",
"behind-nat",
"segmented-and-unforwardable",
"the-ordinary-shape",
];
for (const name of ALL_SCENARIOS) {
test(`${name}: no box is drawn inside a network it is not on`, () => {
// A gateway landed on top of a machine in an unrelated network, because the gap was
// reserved above one sibling while the box was placed above the other. Two rules deciding
// the same thing separately; both now read one map.
const each = loadScenario(`scenarios/${name}.yml`);
const out = toDrawio(diagramFromDeclaration(each));
const boxes: { id: string; y: number; h: number; lanes: string[] }[] = [];
const lanes: { name: string; top: number; bottom: number }[] = [];
for (const match of out.matchAll(
/id="([^"]*)"[\s\S]{0,400}?<mxGeometry x="[^"]*" y="([^"]*)" width="[^"]*" height="([^"]*)"/g,
)) {
const [, id, y, h] = match;
if (id === "title" || id === "legend" || /-b\d+$|-n$/.test(id as string)) continue;
if ((id as string).startsWith("lane-")) {
lanes.push({ name: (id as string).slice(5), top: Number(y), bottom: Number(y) + Number(h) });
} else {
boxes.push({ id: id as string, y: Number(y), h: Number(h), lanes: [] });
}
}
diagramFromDeclaration(each).machines.forEach((machine, index) => {
const box = boxes.find((b) => b.id === `m${index}`);
if (box) box.lanes = machine.attachments.map((a) => a.segment);
});
for (const box of boxes) {
for (const lane of lanes) {
if (box.lanes.includes(lane.name)) continue;
const overlaps = box.y < lane.bottom && box.y + box.h > lane.top;
assert.ok(!overlaps, `${box.id} is drawn inside '${lane.name}', which it is not on`);
}
}
});
}