Group each public network with what is behind it

The drawings were confusing, and looking at them showed why: a single stack
ordered by depth put a private network far from the public one it sits behind,
so a gateway's link to the outside ran the full height of the picture through
three networks it had nothing to do with — and two such links overlapped, so
they read as one wire.

Now each public network is followed by everything behind it, depth first. Every
gateway is adjacent to the network it serves, every link is a short stub, and
"behind" is shown by INDENTATION rather than by a line to follow. Gaps are sized
to what they hold, so a gap with no gateway in it takes no room. Transit is not
on a boundary — it reaches every public network at once — so it is stated once
at the top instead of drawing a line to each.

Both sources now order by name rather than by the order the source yielded. The
hypervisor cannot know declaration order, and two pictures laid out differently
cannot be compared, which is the whole point of having both.

Fixed while testing: the gap size and the box placement each decided separately
which network a gateway sat above, and disagreed — reserving the gap above one
sibling while drawing the box above the other, which landed a gateway on top of
a machine in an unrelated network. Both now read one map.

Five new tests, run across every scenario: no link crosses a network it does not
touch, no box is drawn inside a network it is not on, a network behind another
is indented inside it, a public network is not split apart by another group, and
both sources lay the same topology out identically.
This commit is contained in:
2026-08-24 23:27:11 +02:00
parent 5b8d01eacf
commit 54d417fdb2
2 changed files with 269 additions and 44 deletions
+158
View File
@@ -147,3 +147,161 @@ test("a resource's shape is fixed by kind, and never varies with its metadata",
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`);
}
}
});
}