exit 2
fi
-# guild compile needs an output file, but the hook only cares about warnings.
-# Create a disposable directory and schedule its removal.
+# Skip guild compile for test files -- they transitively depend on
+# Guix modules which require guile-gcrypt, guile-git, and other
+# libraries not available in the hook's compilation environment.
+# guix system test provides the real validation.
+case "$file" in
+ */tests/*)
+ $GUIX_BIN/echo '{"systemMessage":"✅ Format Guile file."}'
+ exit 0
+ ;;
+esac
+
+# guild compile needs an output file, but the hook only cares about
+# warnings. Create a disposable directory and schedule its removal.
tmpdir=$($GUIX_BIN/mktemp --directory)
trap '$GUIX_BIN/rm --recursive --force "$tmpdir"' EXIT
"$file" 2>&1
)
-# guild exits non-zero for fatal errors but still exits 0 for warnings so we
-# check both.
+# guild exits non-zero for fatal errors but still exits 0 for
+# warnings so we check both.
if [ $? -ne 0 ] || $GUIX_BIN/echo "$out" | $GUIX_BIN/grep --quiet warning
then
exec 1>&2
currently spawn subshells or cd manually to operate in the right
directory. SetCwd would let them change the harness working
directory directly, eliminating the subshell overhead.
+
+* Pull SSH authorized keys from blob storage (e.g. Google Drive) for
+ the test suite instead of generating a throwaway keypair at build
+ time. The test would fetch the production authorized_keys file and
+ inject it into the test VM, verifying that the real keys work
+ against the exact configuration deployed to the VPS.
--- /dev/null
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; Claude Code test cases.
+
+(define-module (tests claude-code)
+ #:use-module (guix gexp)
+ #:use-module (tests vps-base)
+ #:use-module (conf home claude)
+ #:export (claude-code-test-cases))
+
+(define (claude-code-test-cases marionette)
+ (let ((claude-bin (file-append claude-code "/bin/claude"))
+ (cmd (file-append claude-code "/bin/claude --version")))
+ #~(begin
+ (test-begin "claude-code")
+
+ (test-assert "installed"
+ (marionette-eval '(file-exists? #$claude-bin)
+ #$marionette))
+
+ (test-assert "runnable"
+ (marionette-eval '(begin
+ (use-modules (ice-9 popen))
+ (let ((port (open-input-pipe #$cmd)))
+ (let ((output (get-string-all port)))
+ (close-pipe port)
+ (not (string-null? output)))))
+ #$marionette))
+
+ (test-end))))
--- /dev/null
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; Dotfile test cases.
+
+(define-module (tests dotfiles)
+ #:use-module (guix gexp)
+ #:use-module (tests vps-base)
+ #:export (dotfiles-test-cases))
+
+(define (dotfiles-test-cases marionette)
+ #~(begin
+ (test-begin "dotfiles")
+
+ (test-assert ".guile present"
+ (marionette-eval '(file-exists? "/home/dak/.guile")
+ #$marionette))
+
+ (test-assert ".Xdefaults present"
+ (marionette-eval '(file-exists? "/home/dak/.Xdefaults")
+ #$marionette))
+
+ (test-assert "gdb/gdbinit present"
+ (marionette-eval '(file-exists?
+ "/home/dak/.config/gdb/gdbinit")
+ #$marionette))
+
+ (test-assert "nano/nanorc present"
+ (marionette-eval '(file-exists?
+ "/home/dak/.config/nano/nanorc")
+ #$marionette))
+
+ (test-end)))
--- /dev/null
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; DHCP / networking test cases.
+
+(define-module (tests networking)
+ #:use-module (guix gexp)
+ #:export (networking-test-cases))
+
+(define (networking-test-cases marionette)
+ "Return a gexp that tests the DHCP client starts and the network
+comes up."
+ #~(begin
+ (test-begin "networking")
+
+ (test-assert "dhcpcd running"
+ (marionette-eval '(begin
+ (use-modules (gnu services herd))
+ (start-service 'networking))
+ #$marionette))
+
+ (test-end)))
--- /dev/null
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; NTP test cases.
+
+(define-module (tests ntp)
+ #:use-module (guix gexp)
+ #:export (ntp-test-cases))
+
+(define (ntp-test-cases marionette)
+ "Return a gexp that tests the NTP daemon starts."
+ #~(begin
+ (test-begin "ntp")
+
+ (test-assert "ntpd running"
+ (marionette-eval '(begin
+ (use-modules (gnu services herd))
+ (start-service 'ntpd))
+ #$marionette))
+
+ (test-end)))
--- /dev/null
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; Paseo test cases.
+
+(define-module (tests paseo)
+ #:use-module (guix gexp)
+ #:use-module (tests vps-base)
+ #:use-module (conf home paseo)
+ #:export (paseo-test-cases))
+
+(define (paseo-test-cases marionette)
+ #~(begin
+ (test-begin "paseo")
+
+ (test-assert "installed"
+ (marionette-eval '(file-exists? (string-append #$paseo
+ "/bin/paseo"))
+ #$marionette))
+
+ (test-end)))
--- /dev/null
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; SSH daemon test cases -- service start, port 72, key auth.
+
+(define-module (tests sshd)
+ #:use-module (guix gexp)
+ #:use-module (tests vps-base)
+ #:export (sshd-test-cases))
+
+(define (sshd-test-cases marionette)
+ "Return a gexp that tests the SSH daemon: service starts, port 72
+listens, and a shell command can be executed via key-based auth."
+ (let ((key-file #~(string-append #$%test-ssh-keypair "/id_ed25519"))
+ (ssh-proc #~(lambda (session)
+ (let ((ch (make-channel session))
+ (cmd "echo hello > /root/witness"))
+ (channel-open-session ch)
+ (channel-request-exec ch cmd)
+ (channel-send-eof ch)
+ (and (zero? (channel-get-exit-status ch))
+ (wait-for-file "/root/witness"
+ #$marionette)))))
+ (connect (call-with-connected-session ssh-proc key-file)))
+ #~(begin
+ (test-begin "sshd")
+
+ (test-assert "service running"
+ (marionette-eval '(begin
+ (use-modules (gnu services herd))
+ (start-service 'ssh-daemon))
+ #$marionette))
+
+ (test-assert "port 72 TCP"
+ (wait-for-tcp-port 72
+ #$marionette))
+
+ (test-equal "shell command via key auth"
+ 'hello
+ (#$connect))
+
+ (test-end))))
#:use-module (gnu services networking)
#:use-module (gnu packages ssh)
#:use-module (conf vps sshd)
+ #:use-module (conf home paseo)
+ #:use-module (conf home claude)
+ #:use-module (tests sshd)
+ #:use-module (tests networking)
+ #:use-module (tests ntp)
+ #:use-module (tests claude-code)
+ #:use-module (tests paseo)
+ #:use-module (tests dotfiles)
#:use-module (guix gexp)
#:export (%test-vps %vps-test-os call-with-connected-session run-vps-test))
;;;
-;;; Placeholder key -- replaced by a generated keypair in a later
-;;; commit.
+;;; Test SSH keypair -- generated at build time.
;;;
-(define %test-ssh-public-key
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPlaceholderKeyForTestingDoNotUse")
+(define %test-ssh-keypair
+ (computed-file "test-ssh-keypair"
+ #~(begin
+ (use-modules (guix build utils))
+ (mkdir #$output)
+ (invoke #$(file-append openssh "/bin/ssh-keygen")
+ "-t"
+ "ed25519"
+ "-f"
+ (string-append #$output "/id_ed25519")
+ "-N"
+ ""
+ "-C"
+ "guix-test@vps")
+ (copy-file (string-append #$output "/id_ed25519.pub")
+ (string-append #$output "/authorized_key")))))
+
+(define (test-authorized-keys-file)
+ "Return a file-like object containing the test public key for
+injection into the OpenSSH authorized_keys."
+ (computed-file "root-authorized_keys"
+ #~(begin
+ (use-modules (guix build utils))
+ (mkdir #$output)
+ (copy-file (string-append #$%test-ssh-keypair
+ "/authorized_key")
+ (string-append #$output "/authorized_keys")))))
;;;
;;; Test operating system.
;; key so the test VM can authenticate. All other fields -- port 72,
;; openssh-sans-x, extra-content, accepted-environment,
;; password-auth #f -- are identical.
- (let ((authorized-key-file (plain-file "test-authorized_keys"
- %test-ssh-public-key)))
+ (let ((authorized-key-file (test-authorized-keys-file)))
(openssh-configuration (openssh openssh-sans-x)
(port-number 72)
(password-authentication? #f)
"UseDNS no\n"))
(authorized-keys `(("root" ,authorized-key-file))))))
-;; Operating system based on %simple-os with the VPS services and a
-;; test authorized key injected into the OpenSSH configuration.
+(define %test-user
+ (user-account
+ (name "dak")
+ (group "users")
+ (supplementary-groups '("wheel"))
+ (home-directory "/home/dak")))
+
+;; Operating system based on %simple-os with the VPS services, a test
+;; authorized key, and the VPS home packages for user dak.
(define %vps-test-os
(marionette-operating-system (operating-system
(inherit %simple-os)
- (packages (cons* openssh
+ (users (cons %test-user
+ (operating-system-users
+ %simple-os)))
+ (packages (cons* openssh paseo claude-code
(operating-system-packages
%simple-os)))
(services
(define %test-vps
(system-test (name "vps")
- (description "VPS test suite.")
- (value (run-vps-test (list)))))
+ (description "Full VPS test suite.")
+ (value (run-vps-test (list sshd-test-cases
+ networking-test-cases
+ ntp-test-cases
+ claude-code-test-cases
+ paseo-test-cases
+ dotfiles-test-cases)))))
(use-service-modules networking)
-(operating-system
- (host-name "vps")
- (kernel linux)
- (timezone "Etc/UTC")
- (locale "en_US.utf8")
+(define %vps-operating-system
+ (operating-system
+ (host-name "vps")
+ (kernel linux)
+ (timezone "Etc/UTC")
+ (locale "en_US.utf8")
- ;; ── OVH-specific (permanent -- the hardware requires these) ──
-
- (bootloader (bootloader-configuration
- (bootloader grub-bootloader)
- (targets '("/dev/sda"))))
+ ;; ── OVH-specific (permanent -- the hardware requires these) ──
+
+ (bootloader (bootloader-configuration
+ (bootloader grub-bootloader)
+ (targets '("/dev/sda"))))
- (kernel-arguments (list "console=ttyS0 console=tty0"))
+ (kernel-arguments (list "console=ttyS0 console=tty0"))
- ;; ── Filesystems ──
-
- (file-systems (cons (file-system
- (device (uuid "38af4c98-d0f5-96b6-2fa6-251038af4c98"))
- (mount-point "/")
- (type "ext4")) %base-file-systems))
+ ;; ── Filesystems ──
+
+ (file-systems (cons (file-system
+ (device (uuid "38af4c98-d0f5-96b6-2fa6-251038af4c98"))
+ (mount-point "/")
+ (type "ext4")) %base-file-systems))
- ;; ── Users ──
-
- (users (cons (user-account
- (name "dak")
- (group "users")
- (supplementary-groups '("wheel"))
- (home-directory "/home/dak")) %base-user-accounts))
+ ;; ── Users ──
+
+ (users (cons (user-account
+ (name "dak")
+ (group "users")
+ (supplementary-groups '("wheel"))
+ (home-directory "/home/dak")) %base-user-accounts))
- ;; ── Services ──
-
- (services
- (append (list
- ;; DHCP -- OVH delivers the static IP via DHCP
- (service dhcpcd-service-type)
+ ;; ── Services ──
+
+ (services
+ (append (list
+ ;; DHCP -- OVH delivers the static IP via DHCP
+ (service dhcpcd-service-type)
- ;; SSH -- configured in conf/vps/sshd.scm
- %ssh-service
+ ;; SSH -- configured in conf/vps/sshd.scm
+ %ssh-service
- ;; NTP -- correct time is required for Guix substitutes
- (service ntp-service-type))
+ ;; NTP -- correct time is required for Guix substitutes
+ (service ntp-service-type))
- %base-services))
+ %base-services))
- ;; ── Sudoers: dak can reconfigure without a password ──
-
- (sudoers-file (plain-file "sudoers"
- (string-append (plain-file-content
- %sudoers-specification)
- "dak ALL = NOPASSWD: ALL\n"))))
+ ;; ── Sudoers: dak can reconfigure without a password ──
+
+ (sudoers-file (plain-file "sudoers"
+ (string-append (plain-file-content
+ %sudoers-specification)
+ "dak ALL = NOPASSWD: ALL\n")))))
+
+%vps-operating-system