Step 2: raise a second scenario, and share the invariants

The suite next door raises one scenario and asks deep questions of it. This one
asks shallow questions of every scenario — the half that was missing, since both
faults found by hand lived in scenarios nothing ever built.

Adds bootstrap-single, the cheapest, and the loop that lets the list grow. Also
adds the second universal invariant: every address a scenario declared is one
the machine actually holds. A machine that came up bare looks identical to one
that came up correctly until something asks it.

Verified to bite rather than assumed: against a live instance, the real
declaration passes and a declaration claiming an address nothing holds fails
with 'anchor declared 192.0.2.99 on hosting but holds 192.0.2.10'.

Integration now runs with --test-concurrency=1. Two files raise real instances,
node --test runs files in parallel by default, and two concurrent runs of this
suite already produced a whole-suite failure once — every test red, from
resource contention rather than from any fault in the code.

Gate: 45.7s -> 60.2s.
This commit is contained in:
2026-08-25 00:29:59 +02:00
parent 715f367147
commit b015068921
4 changed files with 126 additions and 20 deletions
+5 -19
View File
@@ -9,11 +9,10 @@ import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec, list, restore, snapshot } from "../../src/lifecycle/operate.ts";
import { incus, incusOk } from "../../src/incus/client.ts";
import { labIsUsable, destroyAll } from "./harness.ts";
import { labIsUsable, destroyAll, assertUniversalInvariants } 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}`;
@@ -122,23 +121,10 @@ 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)}`);
test("what came up holds what was declared, with no address held twice", { skip, timeout: 120_000 }, async () => {
// The same invariants every scenario is held to, asserted here too — this instance is
// already standing, so it costs nothing to ask.
await assertUniversalInvariants(loadScenario("scenarios/behind-nat.yml"), instanceId);
});
// The diagram tests read the instance the file raised, so they run before the one that