From 7d6d74fd7f553ed0537fe413a62562f819a2c7bb Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Tue, 30 Jun 2026 21:41:24 +0100 Subject: [PATCH] test(power): VM check for battery charge-limit udev re-apply Add checks.battery-charge-limit (runNixOSTest): the test_power module fakes a Mains adapter, toggling ac_online emits a real power_supply uevent, and the test asserts the udev rule restarts the oneshot (changed InvocationID). Verifies the re-apply *trigger* at runtime, beyond the earlier eval check; the sysfs write still needs real hardware. Also records the SSH_AUTH_SOCK rendered-artifact verification in the roadmap. Co-Authored-By: Claude Opus 4.8 --- docs/ROADMAP.md | 20 +++++++++++++------- flake.nix | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9656f8c..669a922 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -298,9 +298,11 @@ how to override it. Items marked ✓ are shipped. shell's copy — resolved with `gpgconf --list-dirs agent-ssh-socket` at session-init (the same lookup the shell integration uses, so the two can't drift), reaching GUI apps via the login shell that starts Hyprland the same - way `NIXOS_OZONE_WL` does. Eval-verified (the var resolves to the gpgconf - substitution; home config builds). Pending an on-machine check that a GUI - git/ssh client picks up the agent. + way `NIXOS_OZONE_WL` does. Verified by building the home generation and + inspecting the rendered `hm-session-vars.sh` — it carries + `export SSH_AUTH_SOCK="$(…/gpgconf --list-dirs agent-ssh-socket)"` with the + command substitution intact (unescaped, resolved at session-init). Pending + an on-machine check that a GUI git/ssh client picks up the agent. - **Sanitize & organize the repo:** a housekeeping pass for consistency and clarity. - ✓ pruned now-redundant config: the installer no longer writes @@ -361,10 +363,14 @@ how to override it. Items marked ✓ are shipped. unplug gap — the match is by adapter *type*, not kernel name (AC/AC0/ADP1/ACAD vary), and `restart` (not `try-restart`) re-applies even if the boot run was inactive. Eval-verified both ways (rule present - with charge limit on / absent off). **Pending an on-hardware check** (a - `sudo nixos-rebuild` + a physical unplug to fire the udev event — the - dev box has both an `AC` Mains adapter and a `BAT0` - `charge_control_end_threshold`, so it can exercise it). + with charge limit on / absent off), and **VM-verified the trigger** + (`checks.battery-charge-limit`: the `test_power` module fakes a Mains + adapter, toggling `ac_online` emits a real `power_supply` uevent, and the + udev rule restarts the oneshot — confirmed by a changed `InvocationID`). + The sysfs *write* itself needs a real `charge_control_end_threshold`, so + a final on-hardware check (a `sudo nixos-rebuild` + a physical unplug) + remains — the dev box has both an `AC` Mains adapter and a `BAT0` + `charge_control_end_threshold`, so it can exercise it. - ✓ **installer:** writes `power.laptop = true` (battery probe) and `power.thermal.enable` (Intel) into the generated `system.nix`. - ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on diff --git a/flake.nix b/flake.nix index 6e67080..f8dfeb1 100644 --- a/flake.nix +++ b/flake.nix @@ -159,6 +159,53 @@ machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo") ''; }; + + # Runtime VM check for the battery-charge-limit re-apply: the udev + # rule must restart the oneshot on an AC state change (firmware can + # clear the threshold on unplug). The `test_power` module fakes a + # Mains adapter we can toggle to emit a real power_supply uevent — + # so this exercises the trigger, not just the config wiring. (The + # sysfs *write* needs a real charge_control_end_threshold and stays + # an on-hardware check.) Imports options.nix (where the power + # options live) + power.nix, so the VM stays minimal. + battery-charge-limit = pkgs.testers.runNixOSTest { + name = "nomarchy-battery-charge-limit"; + nodes.machine = { ... }: { + imports = [ ./modules/nixos/options.nix ./modules/nixos/power.nix ]; + boot.kernelModules = [ "test_power" ]; # fake Mains + battery + nomarchy.system.power = { + enable = true; + laptop = true; + batteryChargeLimit = 80; + }; + }; + testScript = '' + machine.wait_for_unit("multi-user.target") + + # The oneshot exists and the fake Mains adapter is present. + machine.succeed("systemctl cat nomarchy-battery-charge-limit.service") + machine.succeed("udevadm settle") + machine.succeed("grep -lx Mains /sys/class/power_supply/*/type") + + # Let any add-event restart settle, then sample the invocation. + machine.succeed("sleep 2") + before = machine.succeed( + "systemctl show -p InvocationID --value " + "nomarchy-battery-charge-limit.service" + ).strip() + + # Simulate an AC unplug → 'change' uevent on the Mains device. + machine.succeed("echo off > /sys/module/test_power/parameters/ac_online") + machine.succeed("udevadm settle") + + # The udev rule must have restarted the oneshot (new InvocationID). + machine.wait_until_succeeds( + 'test "$(systemctl show -p InvocationID --value ' + 'nomarchy-battery-charge-limit.service)" != "' + before + '"', + timeout=30, + ) + ''; + }; }; # ─── Reference host ────────────────────────────────────────────