Add in-guest acceptance test suite

Runs inside a live Omarchy session (typically a VM installed by
omarchy-iso-test) and verifies the desktop actually works: session
health, application launches by window class, and shell surfaces via
IPC — including typing into the launcher and launching the top hit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-21 18:36:13 -07:00
co-authored by Claude Fable 5
parent c70ae3a405
commit 5aa9e4173d
5 changed files with 301 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
#
# In-guest acceptance suite. Runs inside a live Omarchy session (typically a
# VM installed by omarchy-iso-test, reached over SSH) and verifies that the
# desktop actually works: session health, application launches, and shell
# surfaces (launcher, emoji picker, menu).
#
# Artifacts (screenshots, logs) land in $OMARCHY_ACCEPTANCE_DIR.
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
TEST_DIR="$ROOT/test/acceptance.d"
export OMARCHY_ACCEPTANCE_DIR="${OMARCHY_ACCEPTANCE_DIR:-/tmp/omarchy-acceptance}"
mkdir -p "$OMARCHY_ACCEPTANCE_DIR"
# Discover the graphical session so the suite works over SSH, where none of
# the session environment is inherited.
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=$XDG_RUNTIME_DIR/bus}"
export OMARCHY_PATH="${OMARCHY_PATH:-$ROOT}"
export PATH="$OMARCHY_PATH/bin:$PATH"
# First boot may still be settling; wait for Hyprland to come up.
boot_deadline=$((SECONDS + ${OMARCHY_ACCEPTANCE_BOOT_TIMEOUT:-300}))
wait_for_session() {
local signature
while true; do
signature=$(ls -t "$XDG_RUNTIME_DIR/hypr" 2>/dev/null | head -1)
if [[ -n $signature ]]; then
export HYPRLAND_INSTANCE_SIGNATURE="$signature"
hyprctl -j monitors >/dev/null 2>&1 && return 0
fi
if ((SECONDS >= boot_deadline)); then
echo "not ok - Hyprland session never came up" >&2
exit 1
fi
sleep 2
done
}
wait_for_session
printf 'ok - Hyprland session is up (signature %s)\n' "$HYPRLAND_INSTANCE_SIGNATURE"
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
display=$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name "wayland-*" ! -name "*.lock" -printf '%f\n' 2>/dev/null | head -1)
export WAYLAND_DISPLAY="${display:-wayland-1}"
fi
shopt -s nullglob
tests=()
for test in "$TEST_DIR"/*-test.sh; do
[[ $(basename "$test") == "base-test.sh" ]] && continue
tests+=("$test")
done
shopt -u nullglob
if (( ${#tests[@]} == 0 )); then
echo "No acceptance tests found in $TEST_DIR" >&2
exit 1
fi
for test in "${tests[@]}"; do
printf '==> %s\n' "${test#$ROOT/}"
bash "$test"
done
printf 'ok - acceptance suite passed (artifacts in %s)\n' "$OMARCHY_ACCEPTANCE_DIR"
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
launch_and_verify() {
local name="$1" command="$2" class="$3"
# A pre-existing window makes the test ambiguous (and closing it would be
# hostile on a dev machine) — acceptance runs expect a fresh session.
if window_present "$class" >/dev/null 2>&1; then
fail "$name starts with no pre-existing window" "a window matching $class is already open"
fi
launch_app "$command"
wait_until "$name opens a window" 90 window_present "$class"
sleep 1
screenshot "app-$name"
local deadline=$((SECONDS + 30))
while window_present "$class" >/dev/null 2>&1; do
close_windows "$class"
if ((SECONDS >= deadline)); then
fail "$name window closes"
fi
sleep 2
done
pass "$name window closes"
}
# name|command|window class regex
apps='terminal|foot|^foot$
browser|chromium --new-window|(?i)chromium
files|nautilus --new-window|org.gnome.Nautilus
calculator|gnome-calculator|org.gnome.Calculator
notes|obsidian|(?i)obsidian
office|libreoffice|(?i)(soffice|libreoffice)
pdf-viewer|evince|(?i)evince'
while IFS='|' read -r name command class; do
launch_and_verify "$name" "$command" "$class"
done <<<"$apps"
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
echo "source test/acceptance.d/base-test.sh from an acceptance test; do not run it directly" >&2
exit 1
fi
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
ARTIFACTS="${OMARCHY_ACCEPTANCE_DIR:-/tmp/omarchy-acceptance}"
mkdir -p "$ARTIFACTS"
pass() {
printf 'ok - %s\n' "$1"
}
fail() {
local description="$1"
local detail="${2:-}"
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
screenshot "failure-$(date +%s)"
printf 'not ok - %s\n' "$description" >&2
exit 1
}
screenshot() {
grim "$ARTIFACTS/$1.png" 2>/dev/null || true
}
# Poll a command until it succeeds; screenshot and fail on timeout.
wait_until() {
local description="$1" timeout="$2"
shift 2
local deadline=$((SECONDS + timeout))
until "$@" >/dev/null 2>&1; do
if ((SECONDS >= deadline)); then
fail "$description" "timed out after ${timeout}s waiting for: $*"
fi
sleep 1
done
pass "$description"
}
window_present() {
hyprctl -j clients | jq -e --arg class "$1" '[.[] | select(.class | test($class))] | length > 0'
}
window_absent() {
! window_present "$1"
}
layer_present() {
hyprctl -j layers | jq -e --arg ns "$1" '[.. | objects | select(.namespace? == $ns)] | length > 0'
}
layer_absent() {
! layer_present "$1"
}
# Close every window matching a class regex, by address so multi-window apps
# are fully closed. Tries the quattro Lua dispatcher first, then classic.
close_windows() {
local class="$1"
local addr
while read -r addr; do
hyprctl dispatch "hl.dsp.window.close({ window = \"address:$addr\" })" >/dev/null 2>&1 ||
hyprctl dispatch closewindow "address:$addr" >/dev/null 2>&1 || true
done < <(hyprctl -j clients | jq -r --arg class "$class" '.[] | select(.class | test($class)) | .address')
}
launch_app() {
setsid -f bash -c "$1" >/dev/null 2>&1
}
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The compositor reports at least one monitor
monitors=$(hyprctl -j monitors | jq 'length')
(( monitors >= 1 )) || fail "compositor reports a monitor"
pass "compositor reports a monitor"
# The Omarchy shell is running and responsive
wait_until "omarchy-shell responds to ping" 60 omarchy-shell shell ping
# Core shell plugins are loaded
plugins=$(omarchy-shell shell listPlugins)
for plugin in omarchy.bar omarchy.launcher omarchy.emojis omarchy.menu omarchy.notifications; do
[[ $plugins == *"$plugin"* ]] || fail "shell plugin is loaded: $plugin" "loaded plugins: $plugins"
pass "shell plugin is loaded: $plugin"
done
# The bar and background are actually on screen
wait_until "bar layer is on screen" 30 layer_present "omarchy-bar"
wait_until "background layer is on screen" 30 layer_present "omarchy-background"
# Audio stack is up
wait_until "pipewire is running" 30 wpctl status
# Root filesystem is btrfs as installed
[[ $(findmnt -no FSTYPE /) == "btrfs" ]] || fail "root filesystem is btrfs"
pass "root filesystem is btrfs"
# Omarchy reports its version
omarchy-version >/dev/null || fail "omarchy-version works"
pass "omarchy-version works"
# No failed units, system or user. OMARCHY_ACCEPTANCE_IGNORE_UNITS can hold a
# regex of units to overlook (useful on dev machines; a fresh VM should be clean).
failed_units() {
systemctl "$@" --failed --no-legend --plain | awk '{print $1}' |
grep -Ev "${OMARCHY_ACCEPTANCE_IGNORE_UNITS:-^$}" || true
}
failed_system=$(failed_units --system)
if [[ -n $failed_system ]]; then
fail "no failed system units" "failed units: $failed_system"
fi
pass "no failed system units"
failed_user=$(failed_units --user)
if [[ -n $failed_user ]]; then
fail "no failed user units" "failed units: $failed_user"
fi
pass "no failed user units"
screenshot "desktop"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
open_and_close() {
local name="$1" plugin="$2" namespace="$3" payload="${4:-}"
if [[ -n $payload ]]; then
omarchy-shell shell summon "$plugin" "$payload" >/dev/null
else
omarchy-shell shell summon "$plugin" >/dev/null
fi
wait_until "$name opens" 15 layer_present "$namespace"
sleep 1
screenshot "$name-open"
omarchy-shell shell hide "$plugin" >/dev/null
wait_until "$name closes" 15 layer_absent "$namespace"
}
# Overlays open and close on IPC command
open_and_close "emoji-picker" omarchy.emojis omarchy-emojis
open_and_close "menu" omarchy.menu omarchy-menu '{"menu":"root"}'
open_and_close "clipboard" omarchy.clipboard omarchy-clipboard
# The launcher does the full loop: open, search by typing, launch the top hit
if window_present "org.gnome.Calculator" >/dev/null 2>&1; then
fail "launcher test starts with no calculator window" "a calculator window is already open"
fi
omarchy-shell shell summon omarchy.launcher >/dev/null
wait_until "launcher opens" 15 layer_present "omarchy-launcher"
sleep 1
screenshot "launcher-open"
wtype "calculator"
sleep 1
screenshot "launcher-search"
wtype -k Return
wait_until "launcher launches the top search hit" 60 window_present "org.gnome.Calculator"
wait_until "launcher closes after launching" 15 layer_absent "omarchy-launcher"
close_windows "org.gnome.Calculator"
wait_until "calculator window closes" 30 window_absent "org.gnome.Calculator"