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.
|
`@swap` swapfile + `resume_offset` wiring; `swapSize=0` = no-swap opt-out.
|
||||||
- `docs/MIGRATION.md` enable-hibernation runbook (existing machines).
|
- `docs/MIGRATION.md` enable-hibernation runbook (existing machines).
|
||||||
- `rofi.nix` Power → Hibernate: kept unconditional, **notifies on failure**.
|
- `rofi.nix` Power → Hibernate: kept unconditional, **notifies on failure**.
|
||||||
- `checks.hibernate` VM test (V2 PASS) — hibernate→resume→crash round-trip;
|
- `checks.hibernate` VM test (V2 PASS) — hibernate→resume→crash round-trip
|
||||||
zram (prio 100) doesn't swallow the image, disk swap carries it.
|
(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
|
**V3 remaining (narrowed):** the literal encrypted-swapfile power-cycle —
|
||||||
layout (QEMU can't feed a boot-time offset / drive LUKS-initrd) + the no-swap
|
`Hibernate → power off → unlock LUKS → resume session` on the LUKS+@swap
|
||||||
notify in a real session. Steps queued in HARDWARE-QUEUE. Close #76 on PASS.
|
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
|
## 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)
|
## 2026-07-11 — Code-review fixes (parallel review of the session batch)
|
||||||
- **Task:** Bernardo asked for parallel work; spawned a fresh-eyes opus
|
- **Task:** Bernardo asked for parallel work; spawned a fresh-eyes opus
|
||||||
review of `2055842..HEAD` (static). It found the batch sound — 2 real LOW
|
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
|
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.
|
**notifies on failure** (no-swap machines) rather than silently no-op'ing.
|
||||||
Existing machines get an *enable-hibernation* runbook in `docs/MIGRATION.md`
|
Existing machines get an *enable-hibernation* runbook in `docs/MIGRATION.md`
|
||||||
(no tool). Guarded by two VM checks — `checks.zram-swap` (device/algo/
|
(no tool). Guarded by three VM checks — `checks.zram-swap` (device/algo/
|
||||||
priority) and `checks.hibernate` (a full hibernate→resume→crash cycle that
|
priority), `checks.hibernate` (a full hibernate→resume→crash cycle on a swap
|
||||||
proves the image round-trips through disk swap and that zram at priority
|
*partition* that proves the resume mechanism + that zram at priority 100
|
||||||
100 doesn't swallow it). The LUKS-initrd unlock + btrfs-swapfile
|
doesn't swallow the image), and `checks.hibernate-swapfile` (the installer's
|
||||||
`resume_offset` triple is a V3 laptop check (QEMU can't feed a boot-time
|
btrfs `@swap` *swapfile* build — NOCOW `mkswapfile`, file-type swapon, a
|
||||||
offset for a file that only exists after first boot).
|
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`
|
✓ **Doctor hibernate/sleep section (#77, `VISION § C`):** `nomarchy-doctor`
|
||||||
gained a read-only hibernate row group — resume device on the cmdline
|
gained a read-only hibernate row group — resume device on the cmdline
|
||||||
(+ `resume_offset` for a swapfile), disk swap ≥ RAM, zram active, and any
|
(+ `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
|
# Auto time-of-day theme (#79, VISION § D): simulate crossing
|
||||||
# sunset and sunrise by moving the VM clock, and assert
|
# sunset and sunrise by moving the VM clock, and assert
|
||||||
# `nomarchy-theme-sync auto` switches the active theme accordingly
|
# `nomarchy-theme-sync auto` switches the active theme accordingly
|
||||||
|
|||||||
Reference in New Issue
Block a user