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 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-30 21:41:24 +01:00
parent cdd1897b14
commit 7d6d74fd7f
2 changed files with 60 additions and 7 deletions

View File

@@ -298,9 +298,11 @@ how to override it. Items marked ✓ are shipped.
shell's copy — resolved with `gpgconf --list-dirs agent-ssh-socket` at 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 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 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 way `NIXOS_OZONE_WL` does. Verified by building the home generation and
substitution; home config builds). Pending an on-machine check that a GUI inspecting the rendered `hm-session-vars.sh` — it carries
git/ssh client picks up the agent. `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 - **Sanitize & organize the repo:** a housekeeping pass for consistency
and clarity. and clarity.
- ✓ pruned now-redundant config: the installer no longer writes - ✓ 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 unplug gap — the match is by adapter *type*, not kernel name
(AC/AC0/ADP1/ACAD vary), and `restart` (not `try-restart`) re-applies (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 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 with charge limit on / absent off), and **VM-verified the trigger**
`sudo nixos-rebuild` + a physical unplug to fire the udev event — the (`checks.battery-charge-limit`: the `test_power` module fakes a Mains
dev box has both an `AC` Mains adapter and a `BAT0` adapter, toggling `ac_online` emits a real `power_supply` uevent, and the
`charge_control_end_threshold`, so it can exercise it). 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 - ✓ **installer:** writes `power.laptop = true` (battery probe) and
`power.thermal.enable` (Intel) into the generated `system.nix`. `power.thermal.enable` (Intel) into the generated `system.nix`.
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on - ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on

View File

@@ -159,6 +159,53 @@
machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo") 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 ──────────────────────────────────────────── # ─── Reference host ────────────────────────────────────────────