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

View File

@@ -290,6 +290,27 @@ that run; leftover open items still need a pass.*
screenshot loaded. Check that the UI draws on the current theme palette
(tools colored properly) and hitting save places the screenshot in
`~/Pictures/Screenshots/` while hitting copy places it in the clipboard.
- [ ] **#76 hibernate→resume on the real LUKS+@swap layout** — the V2 VM test
proves the resume mechanism + zram coexistence, but QEMU can't feed a
boot-time `resume_offset` for a btrfs *swapfile* nor drive the LUKS
initrd unlock, so this triple needs hardware (dev box already runs it:
LUKS mapper `cryptroot`, btrfs `@`, `/swap/swapfile`).
1. Confirm the layout: `swapon --show` lists `/swap/swapfile` (Type file)
**and** `/dev/zram0` at priority 100; `cat /proc/cmdline` shows
`resume_offset=…`; `nixos-option boot.resumeDevice` is the btrfs UUID.
2. Open apps, note an unsaved in-RAM state (e.g. a terminal `echo` scroll,
an editor buffer). Run `nomarchy-menu → Power → Hibernate` (or
`systemctl hibernate`). Machine writes the image and **powers off**.
3. Power on: you unlock LUKS **once** in the themed prompt and land back
in the **same session** — windows/state restored, not a fresh login.
4. **Pass** = session restored after a full power-off + single unlock.
Fail (fresh boot / stuck initrd) → NOW bug in BACKLOG (capture
`journalctl -b -1 | grep -i -E 'hibernat|resume|swap'`).
- [ ] **#76 no-swap Hibernate notify** — on a machine installed with `swap = 0`
(or temporarily `sudo swapoff -a`): `nomarchy-menu → Power → Hibernate`
must surface a desktop notification ("Couldn't hibernate — likely no
swap is configured. See docs/MIGRATION.md → Enabling hibernation."),
**not** a silent no-op. (`swapon` after, if you swapoff'd for the test.)
## AMD dev box only
- [ ] **AMD runtime bits** — VA-API (`vainfo` → radeonsi), amd-pstate EPP

View File

@@ -17,6 +17,24 @@ Template:
---
## 2026-07-10 — #76 slice 3: hibernate VM test (V2 PASS) → #76 agent-complete
- **Task:** BACKLOG #76 slice 3 — the mandatory V2 hibernate test + V3 queue.
- **Did:** Added `checks.hibernate` (flake.nix), modeled on nixpkgs'
hibernate.nix but wired to Nomarchy's #76 risk: imports oom.nix (zram
default-on), disk swap at lower priority + explicit `boot.resumeDevice`,
systemd-initrd. Test drives hibernate→power-off→resume→crash: ramfs marker
survives resume, a crash-boot loses it (real resume, not a survivor), and
asserts zram@100 + disk-swap<100 so the image lands on disk not volatile
zram. Queued the V3 laptop steps (real LUKS+@swap+offset resume + no-swap
notify) in HARDWARE-QUEUE. Collapsed #76 to a `[blocked:hw]` stub; design
record → ROADMAP § "Hibernation + zram by default".
- **Verified:** **V2 PASS**`nix build .#checks.x86_64-linux.hibernate`
built the driver fresh and exited 0 (all assertions held). `nix flake
check --no-build` green.
- **Pending:** only V3 (dev box / laptop) — #76's last check; close on PASS.
- **Next suggestion:** #76 is now blocked:hw; NEXT head falls to LATER/
PROPOSED triage or a QA sweep — pick per LOOP.md orientation next session.
## 2026-07-10 — #76 slice 2: Hibernate notify-on-failure
- **Task:** BACKLOG #76 remaining slice 2 — per the settled "keep row, notify
on failure" call.