feat(nix): opt-in NixOS VM integration test for the module

First VM-level coverage for nixosModules.default: boots a VM with
services.hermes-agent enabled, then asserts tmpfiles state-dir modes
(2770 hermes:hermes across the .hermes tree), the sealed CLI working as
the hermes user, the activation .managed marker, and a credential-less
gateway that starts and STAYS active (asserted via the 'No messaging
platforms enabled.' startup log rather than a bare wait_for_unit, which
could pass mid-restart-loop).

Exposed as packages.x86_64-linux.nixos-vm-test, deliberately NOT a flake
check: booting a VM over the ~700MB closure is too costly for every
'nix flake check'. Run explicitly: nix build .#nixos-vm-test -L
(requires kvm/nixos-test system features).
This commit is contained in:
alt-glitch 2026-07-22 21:36:34 +05:30
parent cbc1054e23
commit da20df2647
2 changed files with 79 additions and 0 deletions

View file

@ -42,6 +42,7 @@
./nix/overlays.nix
./nix/nixosModules.nix
./nix/checks.nix
./nix/vmTest.nix
./nix/devShell.nix
];
};

78
nix/vmTest.nix Normal file
View file

@ -0,0 +1,78 @@
# nix/vmTest.nix — Opt-in NixOS VM integration test
#
# Covers the NixOS module's activation, tmpfiles rules, packaged CLI wrapper,
# and credential-less systemd gateway startup. It is a package rather than a
# flake check because booting a VM and its large package closure is expensive.
# Run explicitly with: nix build .#nixos-vm-test -L
{ inputs, ... }: {
perSystem = { pkgs, lib, ... }: {
packages = lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
nixos-vm-test = pkgs.testers.runNixOSTest {
name = "hermes-agent-nixos-module";
nodes.machine = { pkgs, ... }: {
imports = [ inputs.self.nixosModules.default ];
services.hermes-agent = {
enable = true;
addToSystemPackages = true;
settings.model = {
provider = "openrouter";
name = "openai/gpt-4o-mini";
};
environmentFiles = [
"${pkgs.writeText "hermes-vm-test-env" ''
OPENROUTER_API_KEY=dummy-vm-test-key
''}"
];
};
virtualisation = {
memorySize = 3072;
diskSize = 8192;
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed(
"test \"$(stat -c '%a %U %G' /var/lib/hermes)\" = '2770 hermes hermes'"
)
machine.succeed(
"for path in .hermes .hermes/cron .hermes/sessions .hermes/logs "
".hermes/memories .hermes/plugins; do "
"test \"$(stat -c '%a %U %G' /var/lib/hermes/$path)\" "
"= '2770 hermes hermes'; done"
)
machine.succeed(
"runuser -u hermes -- env HOME=/var/lib/hermes "
"HERMES_HOME=/var/lib/hermes/.hermes hermes version 2>&1 "
"| grep -E 'Hermes Agent v[0-9]+'"
)
machine.succeed("hermes --help 2>&1 | grep -q gateway")
machine.succeed("test -f /var/lib/hermes/.hermes/.managed")
machine.succeed("systemctl cat hermes-agent.service >/dev/null")
# With no messaging platform credentials configured, the gateway
# intentionally stays active to run cron jobs. Assert both the live
# unit and its credential-less startup log instead of relying on a
# bare wait_for_unit(), which could pass during a restart loop.
machine.wait_until_succeeds(
"systemctl is-active --quiet hermes-agent.service"
)
machine.wait_until_succeeds(
"journalctl -u hermes-agent.service --no-pager "
"| grep -F 'No messaging platforms enabled.'"
)
machine.succeed(
"test \"$(systemctl show -P ActiveState hermes-agent.service)\" = active"
)
'';
};
};
};
}