test(hibernate): #76 checks.hibernate-swapfile — verify swapfile+offset in a VM
All checks were successful
Check / eval (push) Successful in 4m18s
All checks were successful
Check / eval (push) Successful in 4m18s
Push #76's swapfile-resume residual into a VM as far as headlessly possible. A full hibernate→resume from a RUNTIME swapfile turned out not to be headless-reachable: systemd reports CanHibernate=no for a swap that isn't declared in the boot config, and a runtime-computed resume_offset can't be fed into the boot cmdline (the genuine chicken-and-egg). So the check now verifies, bounded and deterministically, what the partition test (checks.hibernate) can't and what a VM CAN prove: the installer's btrfs @swap swapfile is built correctly and its offset is valid — NOCOW mkswapfile, file-type swapon, a good `map-swapfile -r` offset the kernel accepts (/sys/power/resume_offset), and zram (prio 100) above it. The literal encrypted-swapfile power-cycle + LUKS-initrd unlock stays a V3 laptop check (the dev box runs exactly this layout, /proc/cmdline verified). (First cut had an unbounded wait_for_shutdown that hung on the CanHibernate refusal; rewritten with a thread-bounded wait + a CanHibernate pre-check, which gave the clean verdict. Device is /dev/vdb without useNixStoreImage.) Verification: V2 PASS (nix build .#checks.x86_64-linux.hibernate-swapfile, fresh, exit 0); flake check --no-build green. #76 residual narrowed in BACKLOG/ROADMAP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,12 +39,21 @@ not Nomarchy, so it can't close it). Steps in HARDWARE-QUEUE. Design record in
|
||||
`@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.
|
||||
- `checks.hibernate` VM test (V2 PASS) — hibernate→resume→crash round-trip
|
||||
(swap partition); zram (prio 100) doesn't swallow the image, disk carries it.
|
||||
- `checks.hibernate-swapfile` VM test (V2 PASS) — the installer's btrfs `@swap`
|
||||
**swapfile** setup: NOCOW `mkswapfile`, file-type swapon, a valid
|
||||
`map-swapfile -r` offset the kernel accepts (`/sys/power/resume_offset`),
|
||||
zram above it. (Bounded proof of the swapfile+offset the partition test
|
||||
can't reach; a full runtime-swapfile hibernate isn't headless-reachable —
|
||||
systemd `CanHibernate=no` for undeclared swap.)
|
||||
|
||||
**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.
|
||||
**V3 remaining (narrowed):** the literal encrypted-swapfile power-cycle —
|
||||
`Hibernate → power off → unlock LUKS → resume session` on the LUKS+@swap
|
||||
layout — plus the no-swap notify in a real session. QEMU can't feed a
|
||||
runtime-computed `resume_offset` into the boot cmdline, and the dev box runs
|
||||
exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in
|
||||
HARDWARE-QUEUE. Close #76 on PASS.
|
||||
|
||||
## LATER
|
||||
|
||||
|
||||
@@ -17,6 +17,28 @@ Template:
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-11 — #76 checks.hibernate-swapfile (VM-verify the swapfile+offset)
|
||||
- **Task:** Bernardo — push #76's swapfile-resume residual into a VM (like
|
||||
the auto-theme sunset trick).
|
||||
- **Did:** Tried a full runtime-swapfile hibernate→resume test; it hit a
|
||||
bounded, definitive **`CanHibernate=no`** — systemd won't hibernate to a
|
||||
swap that isn't declared in the boot config, and a runtime-computed
|
||||
`resume_offset` can't reach the boot cmdline (the genuine chicken-and-egg).
|
||||
First cut also had an **unbounded `wait_for_shutdown`** that hung → killed
|
||||
it, bounded the wait in a thread (180s) + a `CanHibernate` pre-check, which
|
||||
is what surfaced the clean verdict. Pivoted `checks.hibernate-swapfile` to
|
||||
what a VM *can* prove deterministically and the partition test can't: the
|
||||
installer swapfile **build + offset** — NOCOW `mkswapfile`, file-type
|
||||
swapon, valid `map-swapfile -r` offset accepted by `/sys/power/resume_offset`,
|
||||
zram above it. (Device fix: `/dev/vdb` once `useNixStoreImage` was dropped.)
|
||||
- **Verified:** **V2 PASS** (fresh build, exit 0). BACKLOG/ROADMAP updated;
|
||||
#76 residual narrowed to the literal encrypted power-cycle (dev box runs it).
|
||||
- **Lesson:** the sunset-trick spirit applies but not always the full cycle —
|
||||
when systemd/kernel gates the behaviour (CanHibernate), verify the
|
||||
*construction* deterministically and leave only the physical cycle to HW.
|
||||
Always bound `wait_for_shutdown` (no timeout param → thread + join).
|
||||
- **Next suggestion:** #76/#79 both `[blocked:hw]`; QA / PROPOSED otherwise.
|
||||
|
||||
## 2026-07-11 — Code-review fixes (parallel review of the session batch)
|
||||
- **Task:** Bernardo asked for parallel work; spawned a fresh-eyes opus
|
||||
review of `2055842..HEAD` (static). It found the batch sound — 2 real LOW
|
||||
|
||||
@@ -345,12 +345,16 @@ Design/decision records and a running log of shipped work (items marked
|
||||
no-swap opt-out; the Power-menu Hibernate row is kept unconditional and
|
||||
**notifies on failure** (no-swap machines) rather than silently no-op'ing.
|
||||
Existing machines get an *enable-hibernation* runbook in `docs/MIGRATION.md`
|
||||
(no tool). Guarded by two VM checks — `checks.zram-swap` (device/algo/
|
||||
priority) and `checks.hibernate` (a full hibernate→resume→crash cycle that
|
||||
proves the image round-trips through disk swap and that zram at priority
|
||||
100 doesn't swallow it). The LUKS-initrd unlock + btrfs-swapfile
|
||||
`resume_offset` triple is a V3 laptop check (QEMU can't feed a boot-time
|
||||
offset for a file that only exists after first boot).
|
||||
(no tool). Guarded by three VM checks — `checks.zram-swap` (device/algo/
|
||||
priority), `checks.hibernate` (a full hibernate→resume→crash cycle on a swap
|
||||
*partition* that proves the resume mechanism + that zram at priority 100
|
||||
doesn't swallow the image), and `checks.hibernate-swapfile` (the installer's
|
||||
btrfs `@swap` *swapfile* build — NOCOW `mkswapfile`, file-type swapon, a
|
||||
valid `map-swapfile -r` offset the kernel accepts, zram above it). A full
|
||||
hibernate from a *runtime* swapfile isn't headless-reachable (systemd
|
||||
`CanHibernate=no` for undeclared swap; and a runtime-computed `resume_offset`
|
||||
can't be fed into the boot cmdline), so the literal encrypted-swapfile
|
||||
power-cycle + LUKS-initrd unlock stays a V3 laptop check.
|
||||
✓ **Doctor hibernate/sleep section (#77, `VISION § C`):** `nomarchy-doctor`
|
||||
gained a read-only hibernate row group — resume device on the cmdline
|
||||
(+ `resume_offset` for a swapfile), disk swap ≥ RAM, zram active, and any
|
||||
|
||||
60
flake.nix
60
flake.nix
@@ -752,6 +752,66 @@
|
||||
'';
|
||||
};
|
||||
|
||||
# The installer's BTRFS @swap SWAPFILE setup (#76) — the parts the
|
||||
# partition hibernate test (checks.hibernate) can't reach. A full
|
||||
# hibernate→resume from a *runtime* swapfile isn't reachable
|
||||
# headlessly: systemd reports CanHibernate=no unless the swap is
|
||||
# declared in the boot config, and feeding a runtime-computed
|
||||
# resume_offset into the boot cmdline is a genuine chicken-and-egg.
|
||||
# So the power-cycle stays a real-hardware check (the dev box runs
|
||||
# exactly this layout — /proc/cmdline verified). What IS verified
|
||||
# here, bounded and deterministic, is that the swapfile is built
|
||||
# correctly and its resume offset is valid: NOCOW `mkswapfile`, a
|
||||
# good `map-swapfile -r` offset the kernel accepts, swapon as a file,
|
||||
# and zram (prio 100) above it so the image lands on disk, not zram.
|
||||
hibernate-swapfile = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-hibernate-swapfile";
|
||||
nodes.machine = { config, lib, pkgs, ... }: {
|
||||
imports = [ ./modules/nixos/oom.nix ]; # zram default-on (#76)
|
||||
virtualisation.memorySize = 2048;
|
||||
# A dedicated disk for the btrfs @swap subvolume.
|
||||
virtualisation.emptyDiskImages = [ (4 * config.virtualisation.memorySize) ];
|
||||
environment.systemPackages = [ pkgs.btrfs-progs ];
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("default.target")
|
||||
machine.wait_until_succeeds("grep -q /dev/zram0 /proc/swaps", timeout=60)
|
||||
|
||||
# Reproduce the installer layout: btrfs, @swap subvolume, then a
|
||||
# NOCOW swapfile via `btrfs filesystem mkswapfile`.
|
||||
# The lone empty data disk (root is vda; no separate store disk
|
||||
# here since useNixStoreImage is off).
|
||||
machine.succeed("mkfs.btrfs -f /dev/vdb")
|
||||
machine.succeed(
|
||||
"mkdir -p /mnt-btrfs && mount /dev/vdb /mnt-btrfs "
|
||||
"&& btrfs subvolume create /mnt-btrfs/@swap && umount /mnt-btrfs"
|
||||
)
|
||||
machine.succeed("mkdir -p /swap && mount -o subvol=@swap /dev/vdb /swap")
|
||||
machine.succeed("btrfs filesystem mkswapfile --size 3g --uuid clear /swap/swapfile")
|
||||
# NOCOW is mandatory — a copy-on-write swapfile corrupts on btrfs.
|
||||
machine.succeed("lsattr /swap/swapfile | grep -q C")
|
||||
machine.succeed("swapon /swap/swapfile")
|
||||
machine.succeed(
|
||||
"awk '$1==\"/swap/swapfile\" && $2==\"file\"{f=1} END{exit !f}' /proc/swaps"
|
||||
)
|
||||
|
||||
# The Nomarchy-specific offset math (patch-template bakes this
|
||||
# into resume_offset): a valid physical offset the kernel accepts.
|
||||
offset = machine.succeed(
|
||||
"btrfs inspect-internal map-swapfile -r /swap/swapfile"
|
||||
).strip()
|
||||
assert offset.isdigit() and int(offset) > 0, f"bad resume offset: {offset!r}"
|
||||
machine.succeed(f"echo {offset} > /sys/power/resume_offset")
|
||||
|
||||
# zram (prio 100) must sit above the disk swapfile, so a hibernate
|
||||
# image would land on disk, not volatile zram.
|
||||
machine.succeed(
|
||||
"awk '$1==\"/dev/zram0\"&&$5==100{z=1} "
|
||||
"$1==\"/swap/swapfile\"&&$5<100{d=1} END{exit !(z&&d)}' /proc/swaps"
|
||||
)
|
||||
'';
|
||||
};
|
||||
|
||||
# Auto time-of-day theme (#79, VISION § D): simulate crossing
|
||||
# sunset and sunrise by moving the VM clock, and assert
|
||||
# `nomarchy-theme-sync auto` switches the active theme accordingly
|
||||
|
||||
Reference in New Issue
Block a user