From da20df2647e87728c0f5833ff9e563489f490385 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Wed, 22 Jul 2026 21:36:34 +0530 Subject: [PATCH] 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). --- flake.nix | 1 + nix/vmTest.nix | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 nix/vmTest.nix diff --git a/flake.nix b/flake.nix index 8715490be1d4..0a80421fc329 100644 --- a/flake.nix +++ b/flake.nix @@ -42,6 +42,7 @@ ./nix/overlays.nix ./nix/nixosModules.nix ./nix/checks.nix + ./nix/vmTest.nix ./nix/devShell.nix ]; }; diff --git a/nix/vmTest.nix b/nix/vmTest.nix new file mode 100644 index 000000000000..fa03c34e901b --- /dev/null +++ b/nix/vmTest.nix @@ -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" + ) + ''; + }; + }; + }; +}