Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Steamix is tested with the NixOS test driver: each test boots a QEMU virtual machine built from a NixOS configuration that imports the module, then drives it from a Python test script. The tests live in tests/, one file per test.

TestWhat it covers
greetdThe default login path end to end. Boot lands in Gaming Mode; the gamescope command line and Steam’s environment match the options; the session logs to the journal; the performance-overlay presets are generated; “Switch to Desktop” lands in the configured desktop session as a logind class=user, type=wayland session with its identity in the systemd user manager; “Return to Gaming Mode” goes back; a killed gamescope falls back to Gaming Mode.
sddmThe SteamOS-shaped login path. SDDM autologin lands in Gaming Mode; both SteamOS Manager daemons run and publish SessionManagement1; the session’s stand-in unit is active and the configured desktop is the manager’s default; “Switch to Desktop” and “Return to Gaming Mode” go through the manager (temporary autologin drop-in, graphical-session.target stopped, Steam shut down cleanly); a killed gamescope falls back to Gaming Mode and stays there; a temporary session left behind does not survive a reboot.
steamos-managerSteamOS Manager finds the data it ships. A VM presenting a ROG Ally’s DMI identity is recognised as one from the device configs; a VM with QEMU’s own identity is unknown without an error, like a desktop PC; none of the Deck-only interfaces (factory reset, fan control, BIOS and dock updates, storage) appear on a PC; and nothing fails to read under /usr/share/steamos-manager.
decky-loaderThe loader serves on port 1337; declared plugins are linked from the store and load (frontend-only ones as passive); backend plugins run as the unprivileged user; the CEF flag lands in the Steam user’s home with every directory on the way owned by that user; a store-installed plugin coexists with declared ones; a declared plugin removed through the UI is back after a reboot.
libraryBoilR syncs a fake Heroic install’s game into Steam’s library inside the session, before Steam starts; the game launches through Heroic; a shortcut added by hand is kept; the next login does not duplicate anything; the SteamGridDB key is read from its file into a config only the user can read.
emulationThe emulation kiosk boots into RetroArch, full screen in cage, as the configured user, with the configured cores and settings (--appendconfig), the user can read controllers, and RetroArch comes back after quitting. Takes a screenshot of the menu.
decky-lsfg-vkNo VM: the Decky LSFG-VK plugin’s backend driven directly, as Decky would, with a scratch home. “Install lsfg-vk” puts every file in place, the plugin then sees lsfg-vk as installed, settings validate through the NixOS build of its CLI, and the CLI it installed starts. Guards the package’s NixOS repairs against plugin updates.
rocknix-ablNo VM: the handheld boot image writer run against a stand-in system. The image unpacks to Armada’s geometry, the kernel is gzip(Image) with the device trees appended in order, the command line points at the system’s init, a new system moves the old image to KERNEL.BAK, and an over-long command line or a missing device tree is refused.
linux-armada-configNo VM: the handheld kernel’s config, cross-compiled from x86_64, compared with Armada’s: nothing Armada builds in may end up a module or off. Generating it applies Armada’s whole patch series, so a patch that stops applying fails here too.
boot-retroid-pocket-6No device: the Retroid Pocket 6 bring-up image booted in QEMU’s generic ARM machine, emulated. The kernel, initrd and command line come out of the image’s own /KERNEL; it passes when the system reaches its login prompt with no failed units. Covers the kernel, the initrd finding the root partition and NixOS’s userland on this kernel’s config, not the Qualcomm hardware. On x86_64 with the kernel cross-compiled, and on aarch64 (CI’s ARM runners) natively.
optionsNo VM: options simple enough that evaluating a configuration with them proves everything, such as steamix.heroic installing Heroic only when both it and steamix.enable are on, steamix.losslessScaling installing the layer and writing the config each lsfg-vk version reads, and steamix.proton adding GE-Proton and its FSR 4 variant, whose wrapped launcher the check also runs against a stand-in to see the flag arrive. Runs in seconds; a failing case names itself in the evaluation error.

Running them

The tests are the Steamix flake’s own checks. From steamix/, against its nixos-unstable input:

nix build -L .#checks.x86_64-linux.greetd

Against the current NixOS 26.05 release instead, which is what CI does for the stable channel:

nix build -L --no-write-lock-file \
  --override-input nixpkgs github:nixos/nixpkgs/nixos-26.05 \
  .#checks.x86_64-linux.greetd

All of them need /dev/kvm; without it the driver falls back to emulation and is many times slower. A test VM boots in about 15 seconds and each test script runs in 20 to 45, so the cost of a run is almost entirely building the VM’s closure the first time.

To poke at a test machine by hand, build .driverInteractive and run the driver; start_all() boots the VMs and every machine.* call from the test script works at the prompt:

nix build .#checks.x86_64-linux.greetd.driverInteractive
./result/bin/nixos-test-driver

The CI workflow runs every test against both channels on every push to main and every pull request: unstable at the revision flake.lock pins, and stable at the current nixos-26.05 channel head, so a release-branch change that breaks Steamix shows up without waiting for a lock bump. The weekly lock update runs all tests against the new lock and only commits it when they pass.

What is real and what is not

Three things are replaced by stand-ins, defined in tests/lib.nix:

  • Steam is unfree, downloads its client on first start and needs an account. The stub accepts Valve’s arguments, records its environment, runs steamos-session-select the way the power menu does, and exits on steam -shutdown.
  • gamescope needs a Vulkan device with a DRM render node for every backend, headless included. The only Vulkan driver in a sandboxed VM is lavapipe, which has none, so the real binary starts, selects llvmpipe and dies with “Failed to create backend”. The stub records the command line the session script built and runs Steam as its child.
  • The desktop is a script registered as a Wayland session, which records the identity and logind session it was given.

Everything between them is the real thing: greetd, the PAM stack, logind, the launcher, the session script and the switcher. That is the part of Gaming Mode this module owns. What the tests cannot see is gamescope’s own behaviour: mode selection, HDR, VRR and the overlay rendering need real hardware.

What they have caught

Every bug below was found by these tests, and none of them showed up on the host that uses the module:

  • greetd’s PAM stack changed shape on nixpkgs-unstable. From 26.11 it is a single include login, and the identity rule was ordered after an inline env rule that no longer exists, so the greetd path failed to evaluate. The host uses the SDDM path and runs 26.05, so nothing evaluated it.
  • The CEF flag was never created on a fresh install. The system tmpfiles instance created the missing ~/.local as root, then refused to write through it, so Decky never appeared, and Steam’s own first-run install into ~/.local/share/Steam had nowhere to write. The host’s Steam had created those directories long before Decky was enabled.
  • SteamOS Manager’s user daemon crash-looped on a fresh install. The module seeds its state file with the desktop session, but the seed lacked default_login_mode, which the 26.4.1 daemon requires, so the daemon exited at startup and took SessionManagement1, and with it “Switch to Desktop”, along. The host’s daemon had written its own state file before the seed existed, and the seed never overwrites one.
  • SteamOS Manager never recognised a handheld. It reads its platform file and device configs from /usr/share/steamos-manager, and the package installed them into its store path, so every device lookup failed and the TDP, GPU and performance-profile controls the device configs describe never appeared. The package now points the daemon at its own data.
  • A crash in Gaming Mode on the SDDM path became a restart loop. The next session replaced the stand-in unit the dead one had left. Stopping the old unit left graphical-session.target unneeded, systemd stopped it, and that stop took the new stand-in, and so the new Steam, down within a fraction of a second. SDDM relogged and the race repeated about twice a second, 117 times in the minute CI watched, until a reboot. It is timing dependent and only one CI run in several hit it, which is why both login tests now also check that the recovered session stays up.