Step 1: an invariant that holds of any raised scenario

The address collision was found by eye. This is the mechanical form of it: no
two machines hold one address on one segment.

Pure over already-collected facts, so the logic is tested without a hypervisor
— including the cases that would make it useless if got wrong: the same address
on DIFFERENT segments is normal and must not be reported, and one machine
holding an address twice is not two machines.

Asserted against whatever the integration suite has standing, read from the
hypervisor rather than from the declaration. The declaration is what was
accepted, and it was accepted.
This commit is contained in:
2026-08-25 00:21:09 +02:00
parent a6b7d67e19
commit 715f367147
3 changed files with 147 additions and 0 deletions
+20
View File
@@ -13,6 +13,7 @@ import { labIsUsable, destroyAll } from "./harness.ts";
import { diagramFromLive } from "../../src/diagram/from-live.ts";
import { diagramFromDeclaration } from "../../src/diagram/from-declaration.ts";
import { toDrawio } from "../../src/diagram/drawio.ts";
import { duplicateAddresses, describeConflicts } from "../../src/lifecycle/invariants.ts";
const capability = await labIsUsable();
const skip = capability.usable ? false : `lab not usable: ${capability.why}`;
@@ -121,6 +122,25 @@ test("ADR 0032 — the workstation has no route into the scenario", { skip, time
assert.equal(stdout.trim(), "inside", "exec is the only way in, and it works");
});
test("no two machines hold one address on one segment", { skip }, async () => {
// True of ANY raised scenario, so it is asserted against whatever is standing rather than
// against something this test declares. Two gateways with the same public address became
// two containers both holding it, and the address resolved to whichever answered ARP last.
//
// Read from the hypervisor, never from the declaration — the declaration is what was
// accepted, and it was accepted.
const drawn = await diagramFromLive(instanceId);
const held = drawn.machines.flatMap((machine) =>
machine.attachments.flatMap((attachment) =>
attachment.addresses.map((address) => ({ machine: machine.name, segment: attachment.segment, address })),
),
);
assert.ok(held.length > 0, "no addresses were read back at all");
const conflicts = duplicateAddresses(held);
assert.deepEqual(conflicts, [], `address conflict: ${describeConflicts(conflicts)}`);
});
// The diagram tests read the instance the file raised, so they run before the one that
// tears it down. Ordering is load-bearing here: appended after the destroy test they read
// an instance that no longer existed, and reported it as the diagram failing.