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
+73
View File
@@ -8,8 +8,12 @@
* an honest "not run" instead of a green suite that checked nothing.
*/
import assert from "node:assert/strict";
import { isReachable, pools, supportedDrivers } from "../../src/incus/client.ts";
import { destroy, list } from "../../src/lifecycle/operate.ts";
import { diagramFromLive } from "../../src/diagram/from-live.ts";
import { duplicateAddresses, describeConflicts, type Held } from "../../src/lifecycle/invariants.ts";
import type { Scenario } from "../../src/declaration/types.ts";
export interface Capability {
usable: boolean;
@@ -42,3 +46,72 @@ export async function destroyAll(prefix: string): Promise<void> {
}
}
}
/**
* Every address the hypervisor says is held, by which machine, on which segment.
*
* Read through the live diagram because that is already the one place that joins addresses
* to devices by MAC and devices to segments by tag. A second reader would be a second thing
* to get wrong in the same way — and the way it was wrong once, a virtual machine's
* addresses silently going missing, is exactly what these assertions would then miss.
*/
export async function heldAddresses(instanceId: string): Promise<Held[]> {
const drawn = await diagramFromLive(instanceId);
return drawn.machines.flatMap((machine) =>
machine.attachments.flatMap((attachment) =>
attachment.addresses.map((address) => ({
machine: machine.name,
segment: attachment.segment,
address,
})),
),
);
}
function bare(address: string): string {
const slash = address.lastIndexOf("/");
return slash === -1 ? address : address.slice(0, slash);
}
/**
* Invariants that hold of ANY raised scenario, whatever it declares.
*
* Asserted against what actually came up, never against the declaration — the declaration
* is what was accepted, and in the fault that prompted these, it was accepted.
*/
export async function assertUniversalInvariants(
scenario: Scenario,
instanceId: string,
): Promise<void> {
const held = await heldAddresses(instanceId);
assert.ok(held.length > 0, `${scenario.scenario}: no addresses were read back at all`);
const conflicts = duplicateAddresses(held);
assert.deepEqual(
conflicts,
[],
`${scenario.scenario}: address conflict — ${describeConflicts(conflicts)}`,
);
// Every address the 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.
const holders = new Map<string, Set<string>>();
for (const entry of held) {
const key = `${entry.machine} ${entry.segment}`;
holders.set(key, (holders.get(key) ?? new Set<string>()).add(bare(entry.address)));
}
for (const [name, spec] of Object.entries(scenario.machines)) {
if (spec.at === "detached") continue;
for (const attachment of spec.at) {
const actual = holders.get(`${name} ${attachment.segment}`) ?? new Set<string>();
for (const address of attachment.address) {
assert.ok(
actual.has(address),
`${scenario.scenario}: '${name}' declared ${address} on '${attachment.segment}' ` +
`but holds ${[...actual].join(", ") || "nothing"}`,
);
}
}
}
}