Integration tests, each named for the decision it defends

Reviewed and the criticism was right: 1,072 of 2,128 lines untested, all of
it the half that touches the hypervisor, and no gate. The verification I had
done was real — pings across NAT, TTL counts, ruleset comparisons — and none
of it survived the terminal it ran in, which is 04-ISSUES/005 in miniature.

Ten integration tests against a real hypervisor, each named for what it
defends. ADR 0031: a raised machine carries no overlay, no wireguard, no
mesh config — a scenario that pre-built peering would certify its own work.
ADR 0032: exec is the only way in. ADR 0033: routers are containers while
machines are virtual machines. And the design's claims: raise waits for
usable, snapshots are whole-scenario, NAT hides a private address,
published reaches the machine at the gateway's address.

Mocking the hypervisor is forbidden, so they skip with a reason on a
machine that cannot raise scenarios rather than passing green having
checked nothing.

The suite earned itself on its first run. It found that a snapshot of a
running machine could miss a file written seconds earlier — not stale,
absent — because the write was still in the guest's page cache. That is
exactly the question the lifecycle design listed as open: does a scenario
snapshot need the machines stopped? It does not, but it does need them
flushed. snapshot now syncs every machine before capturing, and the design
records the answer.

The fix buys write-durability, not application-consistency: anything
mid-transaction is still captured mid-transaction, and that is now stated
rather than assumed.

npm run check is the gate — typecheck, 40 unit tests, 10 integration tests.
This commit is contained in:
2026-08-24 22:26:34 +02:00
parent 5d01006eab
commit ca2bbab836
5 changed files with 224 additions and 7 deletions
+16 -3
View File
@@ -65,13 +65,26 @@ export async function exec(
/**
* Capture the whole scenario as one state. Every machine, one name.
*
* Machines are snapshotted while running, so what is captured is the disk and not memory —
* crash-consistent rather than a paused mesh. Whether a mesh restored that way is coherent
* is an open question in the design, not something this silently assumes away.
* **Every machine is flushed first, and that is not a precaution.** A snapshot of a running
* machine captures its disk, not its memory, so a write still sitting in the guest's page
* cache is simply not in the snapshot. Without the flush a file written seconds earlier can
* be absent after restore — not stale, absent.
*
* Found by the integration test on its first run, which is the question the design listed as
* open: *does a scenario snapshot need the machines stopped?* It does not, but it does need
* them flushed.
*
* This buys write-durability, not application-consistency. A database mid-transaction is
* still captured mid-transaction — the snapshot is crash-consistent, and anything needing
* more has to quiesce itself.
*/
export async function snapshot(instanceId: string, label: string): Promise<number> {
const machines = await machinesOf(instanceId);
const started = Date.now();
for (const name of machines) {
await incusOk(["exec", name, "--", "sync"], 60_000);
}
for (const name of machines) {
await incus(["snapshot", "create", name, label], 300_000);
}