Run The Integration Test Suite
on this page
Use this guide when you want to run Preflight’s live integration tests against a real Windows VM. The suite exercises the full end-to-end execution path — bootstrap, auth, transport, guard, oracle, cleanup, and idempotency — through a single module (registry) as the proving slice, repeated over every configured transport (WinRM and/or SSH-to-Windows).
Prerequisites
- A Windows VM on the same network as your dev machine (or otherwise reachable)
- The
preflightrepository checked out on your dev machine - Go 1.21+ on your dev machine
- Network connectivity from your dev machine to the VM on port 5985 (WinRM) and/or port 22 (SSH)
1. Get A Windows VM
VMware Fusion (macOS)
The fastest path is a free Windows evaluation VM from Microsoft:
# Download a Windows 11 Dev Environment VM
brew install wget
wget -O ~/Downloads/win11-dev.vmwarevm.zip \
'https://aka.ms/windev_VM_vmware'
# Extract and open in VMware Fusion
cd ~/Downloads
unzip win11-dev.vmwarevm.zip
open win11-dev.vmwarevmThe Windows 11 Dev Environment VMs come with Visual Studio and developer tools pre-installed. They expire after 90 days, which makes them ideal disposable test targets.
After the VM boots:
- Complete the OOBE (accept defaults, set a local user password)
- Note the IP address shown on the login screen, or find it later with
ipconfiginside the VM - Ensure both machines are on the same network (NAT or bridged)
VirtualBox (cross-platform)
Microsoft also publishes Hyper-V and VirtualBox images from the same download page. The bootstrap scripts work identically regardless of hypervisor.
2. Run The Bootstrap Scripts
Setup is split into three scripts so identity and each transport are independent: provision the account once, then enable whichever transports you want. Inside the Windows VM, open PowerShell as Administrator.
Provision the test account (always required)
# Set the password for the pf-test user (use a strong, unique password)
$env:PREFLIGHT_TEST_WINRM_PASS = 'YourStrongPassword123!'
# Run directly from the repo (or copy scripts/dev/bootstrap-user-vm.ps1 to the VM first)
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
iex ((New-Object System.Net.WebClient).DownloadString(
'https://raw.githubusercontent.com/bluecadet/preflight/main/scripts/dev/bootstrap-user-vm.ps1'
))This creates the pf-test local admin user and writes the sacrificial
sentinel, then prints the connection vars (with the password) for your
.env.test. If the VM has no internet access, copy the script over and run
.\bootstrap-user-vm.ps1 — you will be prompted for the password.
Two setup steps are not covered by the scripts; run them manually in the same elevated PowerShell session:
# A second throwaway admin account for the become integration tests, which
# exercise credential delegation to a non-connecting user.
$pass2 = ConvertTo-SecureString "password" -AsPlainText -Force
New-LocalUser "pf-become" -Password $pass2 -PasswordNeverExpires
Add-LocalGroupMember -Group "Administrators" -Member "pf-become"
# Allow remote token elevation (required for WinRM admin sessions).
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" `
-Name LocalAccountTokenFilterPolicy -Value 1 -PropertyType DWord -ForceEnable WinRM
iex ((New-Object System.Net.WebClient).DownloadString(
'https://raw.githubusercontent.com/bluecadet/preflight/main/scripts/dev/bootstrap-winrm-vm.ps1'
))Enables WinRM over HTTP with Basic auth on port 5985, adds pf-test to Remote
Management Users, and opens the firewall. Secret-free — it reuses the account
from the provision step.
Enable SSH-to-Windows
iex ((New-Object System.Net.WebClient).DownloadString(
'https://raw.githubusercontent.com/bluecadet/preflight/main/scripts/dev/bootstrap-ssh-vm.ps1'
))Installs OpenSSH Server, starts sshd, and opens the firewall for port 22.
Preflight authenticates over SSH with password auth by default (the same
pf-test password), so no key generation is required — key auth is optional
via PREFLIGHT_TEST_SSH_KEY.
3. Set The Environment Variables
The test harness reads individual KEY=VALUE pairs from the environment or
from a .env.test file. Create .env.test at the repo root (gitignored;
never commit it):
PREFLIGHT_TEST_WINRM_HOST=192.168.x.x
PREFLIGHT_TEST_WINRM_PORT=5985
PREFLIGHT_TEST_WINRM_USER=pf-test
PREFLIGHT_TEST_WINRM_PASS=YourStrongPassword123!
# Optional: SSH-to-Windows for the same VM (requires OpenSSH Server)
PREFLIGHT_TEST_SSH_HOST=192.168.x.x
PREFLIGHT_TEST_SSH_PORT=22
PREFLIGHT_TEST_SSH_USER=pf-test
PREFLIGHT_TEST_SSH_PASS=YourStrongPassword123!
# PREFLIGHT_TEST_SSH_KEY=/path/to/id_rsa # optional, password auth is defaultPREFLIGHT_TEST_WINRM_PORT and PREFLIGHT_TEST_SSH_PORT default to 5985
and 22 respectively when omitted. Each transport is independently optional —
set only the vars for the transports you want to test.
Security note: The password appears in the file in plain text because the WinRM transport sends it as Basic auth. Only use this against disposable VMs. Never point these vars at a production machine.
4. Run The Test
The test runner loads .env.test automatically — no source or direnv needed.
Variables already exported in your shell take precedence over the file.
The suite is behind the integration build tag, so it is excluded from a
plain go test ./... entirely. Run the whole suite through the Makefile
target, which adds the tag for you:
make test-integrationOr run a single test directly — the -tags integration flag is required,
or go test reports no tests at all:
go test -tags integration -v -run TestIntegration_Registry ./internal/target/Expected output when both WinRM and SSH are configured:
=== RUN TestIntegration_Registry
=== RUN TestIntegration_Registry/winrm
--- PASS: TestIntegration_Registry/winrm (XX.XXs)
=== RUN TestIntegration_Registry/ssh
--- PASS: TestIntegration_Registry/ssh (XX.XXs)
--- PASS: TestIntegration_Registry (XX.XXs)To run all integration tests (both the multi-transport registry test and the WinRM-only tests for other modules):
go test -tags integration -v -run 'TestIntegration|TestWinRMIntegration' ./internal/target/Tests named TestIntegration_* run every configured transport as a
subtest, so you can filter by transport name:
# Registry test via SSH only / WinRM only
go test -tags integration ./internal/target/ -run TestIntegration_Registry/ssh -v
go test -tags integration ./internal/target/ -run TestIntegration_Registry/winrm -vTests prefixed TestWinRMIntegration_* connect via WinRM directly and run
only when PREFLIGHT_TEST_WINRM_HOST / _USER / _PASS are set.
TestWinRMIntegration_WindowsFeature toggles a Windows optional feature
(TelnetClient). DISM operations can occasionally trigger a reboot on some
Windows editions — run this test alone or last so a surprise reboot does
not kill other in-flight tests:
go test -tags integration ./internal/target/ -run TestWinRMIntegration_WindowsFeature -v -timeout 5mSkipping behaviour
Each transport is independently opt-in. When the env vars for a transport are unset, its subtest skips cleanly:
=== RUN TestIntegration_Registry
=== RUN TestIntegration_Registry/winrm
integration_registry_test.go:XX: PREFLIGHT_TEST_WINRM_HOST / _USER / _PASS not set
--- SKIP: TestIntegration_Registry/winrm (0.00s)
=== RUN TestIntegration_Registry/ssh
integration_registry_test.go:XX: PREFLIGHT_TEST_SSH_HOST / _USER / _PASS not set
--- SKIP: TestIntegration_Registry/ssh (0.00s)
--- SKIP: TestIntegration_Registry (0.00s)When no transports are configured, the parent test also skips. CI jobs stay green without any configuration changes.
Sentinel guard
If a transport points at a machine that is missing the sacrificial sentinel, the test hard-skips with a loud message instead of mutating the target:
=== RUN TestIntegration_Registry
=== RUN TestIntegration_Registry/winrm
winrm_integration_harness_test.go:XX: sacrificial sentinel not found on target ...
--- SKIP: TestIntegration_Registry/winrm (0.00s)Test Anatomy
TestIntegration_Registry is wrapped in forEachTransport, which runs the
same body function once per configured transport:
- Gate per transport: Skips the transport subtest when its env vars are unset (HOST/USER/PASS independently per transport)
- Sentinel guard: Asserts
HKLM\SOFTWARE\PreflightTest\IsSacrificial=1via thePowerShellRunnerinterface - Cleanup:
t.Cleanupremoves the per-run registry key regardless of how far the test gets - Present: Creates a DWORD value, verifies via independent oracle
- Idempotent: Re-check and re-apply both return
StatusOK - Dry-run: Check-only with a different value predicts
StatusChanged; oracle confirms the actual value was not mutated - Drift: Mutates the value behind the module’s back via PowerShell, asserts Check detects it and Apply converges back
- Absent: Removes the value, then removes the entire key; oracle
confirms both, then asserts idempotent re-check returns
StatusOK
The independent oracle is load-bearing: asserting only through the module’s
own Check() would pass a module whose Check and Apply share a bug.
Adding A New Integration Test
To add a new module to the integration suite:
- Add a
TestIntegration_<Module>function that callsforEachTransport - Register cleanup via
t.Cleanup(use thePowerShellRunnerinterface) - Write an independent oracle that reads state without using the module’s Check method
- Use
mustExecutefor every Execute step (collapses err+status assertion) - Assert both correctness (oracle matches expectation) and idempotency
(rerun Check/Apply produce
StatusOK) - For coverage completeness, include dry-run and drift branches (see the registry test for the pattern)
When a module cannot operate over a given transport (e.g. WinRM symlink
limitation for windows_feature), gate the operation with a capability check
and t.Skip with a clear reason rather than t.Fatal.
Troubleshooting
| Symptom | Likely cause |
|---|---|
connection refused | WinRM/SSH not enabled on the VM, or wrong IP/port |
401 Unauthorized | WinRM Basic auth not enabled, or wrong username/password |
sentinel not found | Bootstrap not run on this VM, or sentinel was removed |
timeout | Firewall blocking the port, or VM unreachable |
| Test skips on CI | Expected — env vars are not set in CI |
If every test fails at the sacrificial-sentinel check — the first
PowerShell call — with errors like Starting the CLR failed with HRESULT 80004005, STATUS_DLL_INIT_FAILED (0xC0000142), or
STATUS_COMMITMENT_LIMIT (0xC000012D), the VM itself can no longer
launch powershell.exe. This is endpoint resource exhaustion, not a code
failure: the VM is out of committed memory or its non-interactive desktop
heap is exhausted, often after many runs have accumulated WinRM shells.
- Reboot the VM and re-run. This clears the exhaustion and is the usual fix.
- If it recurs across runs, raise the VM’s WinRM quotas —
MaxShellsPerUserandMaxMemoryPerShellMBunderWSMan:\localhost\Shell— and, if process launches keep failing, the non-interactive desktop-heapSharedSectionvalue underHKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\Windows.
The tests register a Close() cleanup that releases each target’s
persistent shell, so a single suite run should not leak shells; the reboot
guidance applies mainly when a VM has been driven into a bad state by older
runs or other workloads.
Re-run scripts/dev/bootstrap-winrm-vm.ps1 on the VM if you suspect the WinRM
configuration has drifted. For a completely fresh start, revert the VM to a snapshot or
redeploy the evaluation image.