test(hibernate): #76 slice 3 — V2 hibernate→resume VM check (PASS)
All checks were successful
Check / eval (push) Successful in 4m11s

Add checks.hibernate: a runNixOSTest modeled on nixpkgs' hibernate.nix,
wired to the actual risk #76 introduces — coexistence with the zram swap
that oom.nix now enables by default. It imports oom.nix, puts the disk
swap at a lower priority than zram with an explicit boot.resumeDevice,
and uses systemd-initrd. The test drives a full
hibernate -> power-off -> resume -> crash cycle: a volatile ramfs marker
survives resume but NOT a crash-boot (proving genuine resume), and it
asserts zram sits at priority 100 while the disk swap is lower, so the
hibernate image lands on disk rather than volatile zram.

What QEMU genuinely can't do — feed a boot-time resume_offset for a btrfs
swapfile (offset only exists after first-boot allocation) or drive a
LUKS-initrd unlock — stays a V3 laptop check (HARDWARE-QUEUE, dev box
already runs that exact layout), plus the no-swap notify in a real
session. With this, all V0/V1/V2 work on #76 is done; #76 collapses to a
[blocked:hw] stub and the design record moves to ROADMAP.

Verification: V2 PASS — nix build .#checks.x86_64-linux.hibernate built
the driver fresh and exited 0. nix flake check --no-build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-07-10 19:45:47 +01:00
parent 2d0cb48d5c
commit ba8963a385
5 changed files with 140 additions and 65 deletions

View File

@@ -684,6 +684,74 @@
'';
};
# Hibernate → resume (BACKLOG #76): the load-bearing risk the
# hibernation default introduces is coexistence with the zram
# swap that oom.nix now enables by default. Prove, in the VM
# harness, that (a) a machine hibernates and resumes for real —
# a volatile ramfs marker survives resume but NOT a fresh boot —
# and (b) zram (priority 100) does not swallow the hibernate
# image: the disk swap (lower priority, explicit resumeDevice)
# carries it. Modeled on nixpkgs' hibernate.nix (partition swap +
# systemd-initrd resume). What QEMU genuinely can't do is feed a
# boot-time `resume_offset` for a btrfs *swapfile* (the offset
# only exists after the file is allocated on first boot) or drive
# a LUKS-initrd unlock end-to-end — so the encrypted @swap
# swapfile + offset path stays a V3 laptop check (HARDWARE-QUEUE),
# which already runs that exact layout. This test imports oom.nix
# so it tracks the real zram default, not a hand-copy.
hibernate = pkgs.testers.runNixOSTest {
name = "nomarchy-hibernate";
nodes.machine = { config, lib, pkgs, ... }: {
imports = [ ./modules/nixos/oom.nix ]; # zram default-on (#76)
virtualisation.memorySize = 2048;
# Store + EFI must persist across the hibernate/resume/crash
# boots; a disk swap ≥ 2×RAM holds the image.
virtualisation.useNixStoreImage = true;
virtualisation.useEFIBoot = true;
virtualisation.emptyDiskImages = [ (2 * config.virtualisation.memorySize) ];
boot.initrd.systemd.enable = true; # systemd-initrd, like Nomarchy
# Disk swap at a LOWER priority than zram (100); explicit
# resumeDevice — exactly how the installer wires hibernation —
# so the image goes to disk regardless of zram's priority.
swapDevices = lib.mkOverride 0 [
{ device = "/dev/vdc"; options = [ "x-systemd.makefs" ]; priority = 1; }
];
boot.resumeDevice = "/dev/vdc";
# Let the driver's shell reconnect after resume.
systemd.services.backdoor.conflicts = [ "sleep.target" ];
powerManagement.resumeCommands = "systemctl --no-block restart backdoor.service";
};
testScript = ''
machine.wait_for_unit("default.target")
# zram (oom.nix) up at priority 100; disk swap present and lower
# so the hibernate image lands on disk, not volatile zram.
machine.wait_until_succeeds("grep -q /dev/zram0 /proc/swaps", timeout=60)
machine.succeed(
"awk '$1==\"/dev/zram0\"&&$5==100{z=1} "
"$1==\"/dev/vdc\"&&$5<100{d=1} END{exit !(z&&d)}' /proc/swaps"
)
# Marker in volatile RAM: survives a resume, never a fresh boot.
machine.succeed(
"mkdir /run/test",
"mount -t ramfs -o size=1m ramfs /run/test",
"echo not persisted to disk > /run/test/suspended",
)
# Hibernate, wait for real power-off, then resume.
machine.execute("systemctl hibernate >&2 &", check_return=False)
machine.wait_for_shutdown()
machine.start()
machine.succeed("grep 'not persisted to disk' /run/test/suspended")
# Prove it was a genuine resume: a crash-boot must lose the marker.
machine.crash()
machine.wait_for_unit("default.target")
machine.fail("grep 'not persisted to disk' /run/test/suspended")
'';
};
# Snapshot GUI canary: btrfs-assistant crashes in libbtrfsutil's
# UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed
# upstream after 6.17.1) but runs fine as root — which is how the