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

@@ -28,72 +28,22 @@ in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in
## NEXT
### 76. Hibernation default `[big]`
**Product intent (Bernardo 2026-07-10):** hibernation on by default;
zram on alongside for live memory pressure. Disk swap ≥ RAM for the
hibernate image; zram high-priority for day-to-day swap (not sufficient
alone for resume).
### 76. Hibernation default — all agent work shipped `[blocked:hw]`
Hibernation + zram on by default (product intent, Bernardo 2026-07-10).
**All V0/V1/V2 slices done** (2026-07-10); only the V3 laptop check remains
(HARDWARE-QUEUE → "AMD dev box"). Design record in **ROADMAP § Hibernation +
zram by default**. Shipped:
- zram default (`modules/nixos/oom.nix`, zstd/50%/prio 100) — `checks.zram-swap`.
- Installer/template already default new installs to a RAM-sized encrypted
`@swap` swapfile + `resume_offset` wiring; `swapSize=0` = no-swap opt-out.
- `docs/MIGRATION.md` enable-hibernation runbook (existing machines).
- `rofi.nix` Power → Hibernate: kept unconditional, **notifies on failure**.
- `checks.hibernate` VM test (V2 PASS) — hibernate→resume→crash round-trip;
zram (prio 100) doesn't swallow the image, disk swap carries it.
**✓ zram slice shipped (2026-07-10):** `zramSwap` on by default in
`modules/nixos/oom.nix` (zstd, 50% RAM, priority 100 so day-to-day paging
stays off the disk swap reserved for the hibernate image); `checks.zram-swap`
VM test asserts device/algorithm/priority (V2 pass). **Remainder below is the
hibernation half** — disk swapfile default + resume + installer.
**Reference layout (this machine — already hibernation-capable, no zram
yet):** LUKS (`crypto_LUKS` → mapper `crypted`) holds one BTRFS with
`@swap` mounted at `/swap`; swapfile `/swap/swapfile` (here 16G);
`swapDevices = [{ device = "/swap/swapfile"; }]`;
`boot.resumeDevice = "/dev/disk/by-uuid/<LUKS-root-uuid>"`;
`boot.kernelParams = [ "resume_offset=…" ]` from
`btrfs inspect-internal map-swapfile -r`. The swapfile lives **inside
the encrypted volume** (subvolume, not a separate cleartext partition).
Installer path already sketches this (`@swap` + resume_offset patch in
`nomarchy-install` / `patch-template.py`) when `swapSize` ≥ RAM-ish.
**✓ Design settled (Bernardo 2026-07-10):**
- **Swap sizing** — keep **exactly RAM** (rounded up to whole GiB), as the
installer already does. Hibernate image ≤ RAM; zram (priority 100) takes
day-to-day paging so the file stays reserved for the image. No change.
- **`swapSize=0`** — stays a no-swap / no-resume opt-out (already handled by
installer + patch-template: no `@swap`, no `swapDevices`/resume wiring).
- **Migration (existing machines)** — **docs runbook in `docs/MIGRATION.md`**,
not a tool: create `@swap` subvol + swapfile, `map-swapfile -r` offset,
add `swapDevices`/`boot.resumeDevice`/`resume_offset` to `system.nix`.
- **No-swap Hibernate UX** — **keep the Power-menu Hibernate row**; when
hibernate fails (no swap), show a desktop **notification** explaining no
swap is configured (rather than hiding the row or a silent no-op).
**Already shipped for NEW installs** (verify, don't rebuild): installer
defaults `NOMARCHY_SWAP_GB=RAM` → hibernation-ready `@swap` swapfile encrypted
with root; `patch-template.py` writes `swapDevices` + `boot.resumeDevice` +
`resume_offset`; Power menu has a Hibernate row; hibernate+LUKS+hyprlock
interplay handled in `modules/nixos/default.nix` / `modules/home/idle.nix`.
**Remaining slices:**
1. ~~**Docs** — `docs/MIGRATION.md` hibernation-enable runbook (V0).~~
shipped 2026-07-10 (§ "Enabling hibernation on an existing machine";
commands verified live against the dev machine's LUKS+@swap layout).
2. ~~**Menu** — Hibernate row: notify-on-failure when no swap configured.~~
✓ shipped 2026-07-10 (`rofi.nix` power arm: `systemctl hibernate ||
notify-send …`; success blocks until resume so the notify only fires on
real failure). V1 (HM generation builds; `writeShellScriptBin` bash -n) +
control-flow proof (fail→notify, success→no notify). Row kept unconditional.
The session-level "no-swap pick surfaces a mako notification" is folded
into the V3 laptop check below.
3. **Verify (required)**
- **V0:** eval / option / disko contracts.
- **V2 (mandatory, agent):** exercise hibernation in the **VM harness**
(`runNixOSTest` or install-test style) — encrypted root + BTRFS
`@swap` swapfile + resume_offset; prove hibernate→resume path at
least as far as QEMU allows (fail the item if only “config
evaluates”).
- **V3:** real laptop (this LUKS+@swap layout): Hibernate → power off
→ resume session; queue HARDWARE-QUEUE.
**Out of scope:** TLP; formatter. Prefer boring NixOS knobs; extend the
existing installer swapfile story rather than inventing a second layout.
**V3 remaining:** real hibernate→resume on the LUKS+@swap swapfile+offset
layout (QEMU can't feed a boot-time offset / drive LUKS-initrd) + the no-swap
notify in a real session. Steps queued in HARDWARE-QUEUE. Close #76 on PASS.
## LATER