Integrate current quattro before security review

This commit is contained in:
Afonso Oliveira committed 2026-09-06 22:56:04 +01:00
commit 1f8f5bacac
107 files changed
+4926 -1147

No files matched your search

@@ -1,113 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
require_command jq
migration="$ROOT/migrations/1785344985.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/bin"
cat >"$test_dir/bin/omarchy-restart-shell" <<'STUB'
#!/bin/bash
echo restart >>"$SHELL_RESTARTS"
STUB
chmod +x "$test_dir/bin/"*
export SHELL_RESTARTS="$test_dir/shell-restarts"
home="$test_dir/home"
config="$home/.config/omarchy/shell.json"
run_migration() {
: >"$SHELL_RESTARTS"
HOME="$home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null
}
# The shipped default minus the widget is what every machine installed before
# this migration has on disk.
write_config() {
rm -rf "$home"
mkdir -p "$home/.config/omarchy"
jq "${1:-.}" "$ROOT/config/omarchy/shell.json" >"$config"
}
without_widget='del(.bar.layout[][] | select((if type == "object" then .id else . end) == "omarchy.agents"))'
ids() {
jq -c --arg section "$1" '[.bar.layout[$section][]? | if type == "object" then .id else . end]' "$config"
}
# ------------------------------------------------------------------ shipped default
jq -e '[.bar.layout.right[].id] | index("omarchy.agents")' "$ROOT/config/omarchy/shell.json" >/dev/null ||
fail "shipped config puts the agents widget in the bar"
pass "shipped config puts the agents widget in the bar"
# ------------------------------------------------------------------ placement
write_config "$without_widget"
run_migration
[[ $(ids right) == '["omarchy.tray","omarchy.agents","omarchy.bluetooth","omarchy.network","omarchy.audio","omarchy.monitor","omarchy.power"]' ]] ||
fail "migration inserts the agents widget after the tray" "$(ids right)"
pass "migration inserts the agents widget after the tray"
(($(wc -l <"$SHELL_RESTARTS") == 0)) || fail "migration leaves the shell restart to omarchy update"
pass "migration leaves the shell restart to omarchy update"
before=$(sha256sum "$config")
run_migration
[[ $before == $(sha256sum "$config") ]] || fail "migration is idempotent" "$(ids right)"
pass "migration is idempotent"
# ------------------------------------------------------------------ curated bars
# A user who already placed the widget keeps it exactly where they put it, in
# whichever section, and never gets a second copy.
write_config "$without_widget | .bar.layout.center += [{ id: \"omarchy.agents\" }]"
run_migration
[[ $(ids center) == *'"omarchy.agents"'* ]] || fail "migration leaves a user-placed widget alone" "$(ids center)"
[[ $(ids right) != *'"omarchy.agents"'* ]] || fail "migration does not add a second copy" "$(ids right)"
pass "migration respects a widget the user already placed"
# Layouts written before entries grew options are bare id strings.
write_config "$without_widget | .bar.layout.right = [\"omarchy.tray\", \"omarchy.agents\", \"omarchy.power\"]"
run_migration
[[ $(ids right) == '["omarchy.tray","omarchy.agents","omarchy.power"]' ]] ||
fail "migration reads string-form entries" "$(ids right)"
pass "migration reads string-form entries"
# A tray dropped from the right section must not strand the widget or drop it.
write_config "$without_widget | del(.bar.layout.right[] | select(.id == \"omarchy.tray\"))"
run_migration
[[ $(ids right) == '["omarchy.agents",'* ]] || fail "migration places the widget without a tray" "$(ids right)"
pass "migration places the widget without a tray"
# ------------------------------------------------------------------ everything else
write_config "$without_widget"
cp "$config" "$test_dir/before.json"
run_migration
diff <(jq -S 'del(.bar.layout.right)' "$test_dir/before.json") <(jq -S 'del(.bar.layout.right)' "$config") >/dev/null ||
fail "migration touches nothing but the right section" "$(diff <(jq -S . "$test_dir/before.json") <(jq -S . "$config"))"
pass "migration touches nothing but the right section"
# A config the migration cannot parse is left alone rather than truncated.
rm -rf "$home"
mkdir -p "$home/.config/omarchy"
printf '{ not json' >"$config"
run_migration
[[ $(cat "$config") == '{ not json' ]] || fail "migration leaves an unparsable config untouched" "$(cat "$config")"
pass "migration leaves an unparsable config untouched"
+206
View File
@@ -0,0 +1,206 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
apply_lock="$ROOT/bin/omarchy-apply-lock"
root_path_guard=$(awk '
/^if \(\( EUID == 0 \)\); then$/ { inside = 1 }
inside { print }
inside && /^fi$/ { exit }
' "$apply_lock")
grep -Fx ' export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin' <<<"$root_path_guard" >/dev/null ||
fail "the root lock helper replaces its inherited command path"
if grep -E '(\.local/bin|target_user|target_home)' <<<"$root_path_guard" >/dev/null; then
fail "the root lock helper does not retain a user-controlled command directory"
fi
pass "the root lock helper uses only trusted command directories"
grep -F '[[ -x /usr/bin/fprintd-list ]]' "$apply_lock" >/dev/null ||
fail "the lock helper checks the trusted fprintd-list executable"
grep -F '/usr/bin/fprintd-list "$target_user"' "$apply_lock" >/dev/null ||
fail "the lock helper invokes fprintd-list by its trusted absolute path"
if grep -F 'omarchy-cmd-present fprintd-list' "$apply_lock" >/dev/null ||
grep -E '(^|[[:space:];&|])fprintd-list([[:space:]]|$)' "$apply_lock" >/dev/null ||
grep -E 'command[[:space:]]+-v[[:space:]]+fprintd-list' "$apply_lock" >/dev/null; then
fail "the lock helper does not resolve fprintd-list through PATH"
fi
pass "the lock helper pins fprintd-list to its packaged system path"
# Exercise the helper as real root when the suite already has it, or as root in
# an unprivileged user namespace otherwise. A hardened kernel can disable user
# namespaces, so preserve the static coverage above and skip only this probe.
root_runner=()
root_runtime_available=1
if (( EUID != 0 )); then
if command -v unshare >/dev/null && unshare --user --map-root-user true 2>/dev/null; then
root_runner=(unshare --user --map-root-user)
else
root_runtime_available=0
fi
fi
if (( ! root_runtime_available )); then
pass "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
exit 0
fi
# Retarget the two PAM files, the trusted fprintd-list binary, and the final
# shell status query in copies under this scratch directory. The production
# files and service stay untouched even when this suite itself runs as root.
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
poison_bin="$test_tmp/poison-bin"
trusted_root_bin="$test_tmp/trusted-root-bin"
trusted_fprintd="$test_tmp/trusted-fprintd-list"
password_pam="$test_tmp/omarchy-lock-password"
fingerprint_pam="$test_tmp/omarchy-lock-fingerprint"
attack_marker="$test_tmp/user-fprintd-list-ran"
trusted_uid="$test_tmp/trusted-fprintd-list.uid"
trusted_args="$test_tmp/trusted-fprintd-list.args"
attack_args="$test_tmp/user-fprintd-list.args"
patched_helper="$test_tmp/omarchy-apply-lock-patched"
absolute_only_helper="$test_tmp/omarchy-apply-lock-absolute-only"
root_path_only_helper="$test_tmp/omarchy-apply-lock-root-path-only"
unprotected_helper="$test_tmp/omarchy-apply-lock-unprotected"
target_user=omarchy-regression-user
mkdir -p "$poison_bin" "$trusted_root_bin"
# The runtime copy pins to this isolated root path. It contains every bare
# command the exercised helper needs, but deliberately no fprintd-list.
for helper in grep rm tee; do
ln -s "/usr/bin/$helper" "$trusted_root_bin/$helper"
done
export TEST_ATTACK_ARGS="$attack_args"
export TEST_ATTACK_MARKER="$attack_marker"
export TEST_TRUSTED_ARGS="$trusted_args"
export TEST_TRUSTED_UID="$trusted_uid"
cat >"$trusted_fprintd" <<'EOF'
#!/bin/bash
printf '%s\n' "$EUID" >"$TEST_TRUSTED_UID"
printf '%s\n' "$*" >"$TEST_TRUSTED_ARGS"
echo "Fingerprints are enrolled"
EOF
cat >"$poison_bin/fprintd-list" <<'EOF'
#!/bin/bash
printf '%s\n' "$EUID" >"$TEST_ATTACK_MARKER"
printf '%s\n' "$*" >"$TEST_ATTACK_ARGS"
echo "Fingerprints are enrolled"
EOF
chmod +x "$trusted_fprintd" "$poison_bin/fprintd-list"
prepare_helper() {
local destination="$1" keep_root_path="$2" use_absolute_fprintd="$3"
awk \
-v password_pam="$password_pam" \
-v fingerprint_pam="$fingerprint_pam" \
-v trusted_root_bin="$trusted_root_bin" \
-v trusted_fprintd="$trusted_fprintd" \
-v keep_root_path="$keep_root_path" \
-v use_absolute_fprintd="$use_absolute_fprintd" '
{
line = $0
gsub("/etc/pam\\.d/omarchy-lock-password", "\"" password_pam "\"", line)
gsub("/etc/pam\\.d/omarchy-lock-fingerprint", "\"" fingerprint_pam "\"", line)
if (line == "if (( EUID == 0 )); then" && keep_root_path == 0) {
print "if (( 0 )); then"
next
}
if (line == " export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin") {
print " export PATH=\"" trusted_root_bin "\""
next
}
if (line == "if [[ -x /usr/bin/fprintd-list ]] &&") {
if (use_absolute_fprintd == 1) {
print "if [[ -x \"" trusted_fprintd "\" ]] &&"
} else {
print "if command -v fprintd-list >/dev/null 2>&1 &&"
}
next
}
if (line == " /usr/bin/fprintd-list \"$target_user\" 2>/dev/null | grep -qi finger; then") {
if (use_absolute_fprintd == 1) {
print " \"" trusted_fprintd "\" \"$target_user\" 2>/dev/null | grep -qi finger; then"
} else {
print " fprintd-list \"$target_user\" 2>/dev/null | grep -qi finger; then"
}
next
}
if (line == "if omarchy-shell lock status >/dev/null 2>&1; then") {
print "if false; then"
next
}
print line
}
' "$apply_lock" >"$destination"
chmod +x "$destination"
}
prepare_helper "$patched_helper" 1 1
prepare_helper "$absolute_only_helper" 0 1
prepare_helper "$root_path_only_helper" 1 0
prepare_helper "$unprotected_helper" 0 0
for helper in "$patched_helper" "$absolute_only_helper" "$root_path_only_helper" "$unprotected_helper"; do
if grep -F '/etc/pam.d/' "$helper" >/dev/null ||
grep -F '/usr/bin/fprintd-list' "$helper" >/dev/null ||
grep -F 'omarchy-shell lock status' "$helper" >/dev/null; then
fail "the isolated root fixture redirects every live-system lock-helper target"
fi
done
reset_runtime_files() {
rm -f "$password_pam" "$fingerprint_pam" "$trusted_uid" "$trusted_args" "$attack_marker" "$attack_args"
}
run_as_root() {
local helper="$1" description="$2" output
if ! output=$(PATH="$poison_bin:/usr/bin:/bin" OMARCHY_INSTALL_USER="$target_user" \
"${root_runner[@]}" /bin/bash "$helper" 2>&1); then
fail "$description" "$output"
fi
}
reset_runtime_files
run_as_root "$patched_helper" "the fully hardened lock helper runs in an isolated root context"
[[ ! -e $attack_marker ]] || fail "the hardened root lock helper executes the user-planted fprintd-list"
grep -Fx '0' "$trusted_uid" >/dev/null || fail "the trusted fprintd-list probe runs with EUID 0"
grep -Fx "$target_user" "$trusted_args" >/dev/null || fail "the trusted fprintd-list probe receives the target user"
[[ -s $password_pam && -s $fingerprint_pam ]] ||
fail "the isolated root lock-helper run writes both scratch PAM fixtures"
pass "the hardened root lock helper uses the trusted fingerprint probe"
reset_runtime_files
run_as_root "$absolute_only_helper" "the absolute-path-only lock helper runs in an isolated root context"
[[ ! -e $attack_marker ]] || fail "an absolute fprintd-list path permits the user-planted command"
grep -Fx '0' "$trusted_uid" >/dev/null || fail "the absolute-path defense runs the trusted probe as root"
pass "the absolute fprintd-list path independently blocks the user-planted command"
reset_runtime_files
run_as_root "$root_path_only_helper" "the root-PATH-only lock helper runs in an isolated root context"
[[ ! -e $attack_marker ]] || fail "the trusted root path permits the user-planted fprintd-list"
pass "the trusted root path independently blocks the user-planted command"
# Mutation control: removing both protections must execute the planted command
# as UID 0, proving the matrix detects the original privilege-boundary failure.
reset_runtime_files
run_as_root "$unprotected_helper" "the unprotected mutation runs in an isolated root context"
grep -Fx '0' "$attack_marker" >/dev/null ||
fail "the root lock-helper fixture detects a PATH-resolved fprintd-list regression"
grep -Fx "$target_user" "$attack_args" >/dev/null ||
fail "the planted fprintd-list receives the target user"
[[ -s $fingerprint_pam ]] || fail "the planted fprintd-list controls the fingerprint PAM branch"
pass "the root lock-helper matrix rejects the vulnerable PATH lookup"
+13
View File
@@ -5,6 +5,7 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command magick
require_command ffmpeg
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
@@ -28,3 +29,15 @@ pass "transparent bar text keeps text color on dark wallpaper"
result=$(HOME="$TMPDIR" omarchy-bar-text-color top 20 '#ffffff' '#101010' --background "$TMPDIR/missing.png" --screen 100x100)
[[ $result == "#ffffff" ]] || fail "transparent bar text falls back to text color when sampling fails" "expected #ffffff, got $result"
pass "transparent bar text falls back to text color when sampling fails"
# A video background must be sampled one frame at a time. Reading the whole file
# emits a value per frame, which parses as nothing and silently falls back —
# and decodes the entire wallpaper to find that out.
light_top_video="$TMPDIR/light-top.mp4"
ffmpeg -y -f lavfi -i "testsrc=size=640x360:rate=10:duration=2" \
-vf "drawbox=x=0:y=0:w=640:h=40:color=0xf5f5f5:t=fill" \
-c:v libx264 -preset ultrafast -pix_fmt yuv420p "$light_top_video" -loglevel error
result=$(HOME="$TMPDIR" omarchy-bar-text-color top 40 '#ffffff' '#101010' --background "$light_top_video" --screen 640x360)
[[ $result == "#101010" ]] || fail "transparent bar text samples one frame of a video wallpaper" "expected #101010, got $result"
pass "transparent bar text samples one frame of a video wallpaper"
-109
View File
@@ -1,109 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1786380259.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/bin"
# sudo runs the real command, so sed acts on the redirected main.conf below and
# the elevated power calls land in the stub beside it.
cat >"$test_dir/bin/sudo" <<'STUB'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$CALLS"
exec "$@"
STUB
cat >"$test_dir/bin/omarchy-bluetooth-power" <<'STUB'
#!/bin/bash
printf 'omarchy-bluetooth-power %s\n' "$*" >>"$CALLS"
[[ $1 == "is-on" ]] || exit 0
[[ ${POWERED:-} == "yes" ]]
STUB
chmod +x "$test_dir/bin/"*
export CALLS="$test_dir/calls"
marker="$test_dir/marker"
main_conf="$test_dir/main.conf"
reset_machine() {
rm -f "$marker"
printf '[Policy]\nAutoEnable=false\n' >"$main_conf"
}
run_migration() {
: >"$CALLS"
OMARCHY_BLUETOOTH_MIGRATION_MARKER="$marker" \
OMARCHY_BLUETOOTH_MAIN_CONF="$main_conf" \
PATH="$test_dir/bin:$PATH" \
bash -euo pipefail "$migration" >/dev/null
}
# An adapter that is powered right now is one the user turned on, so it stays on.
reset_machine
POWERED=yes run_migration
grep -qx 'omarchy-bluetooth-power on' "$CALLS" ||
fail "migration keeps a powered adapter on" "$(cat "$CALLS")"
pass "migration keeps a powered adapter on"
grep -qx '#AutoEnable=true' "$main_conf" ||
fail "migration puts AutoEnable back to its default" "$(cat "$main_conf")"
pass "migration puts AutoEnable back to its default"
[[ -e $marker ]] || fail "migration records the machine as done"
pass "migration records the machine as done"
# Anything else is a machine that has been booting with Bluetooth off, and the
# block is what carries that over now AutoEnable no longer holds the adapter down.
reset_machine
POWERED=no run_migration
grep -qx 'omarchy-bluetooth-power off' "$CALLS" ||
fail "migration carries an unpowered adapter over to the block" "$(cat "$CALLS")"
pass "migration carries an unpowered adapter over to the block"
# No daemon to ask reads the same way: off is what the machine has been doing.
reset_machine
run_migration
grep -qx 'omarchy-bluetooth-power off' "$CALLS" ||
fail "migration blocks when no adapter can be read" "$(cat "$CALLS")"
pass "migration blocks when no adapter can be read"
# /dev/rfkill is only writable unelevated from an active graphical seat, so an
# update run over SSH would abort here and abort again on every retry.
grep -qx 'sudo omarchy-bluetooth-power off' "$CALLS" ||
fail "migration changes the radio through sudo" "$(cat "$CALLS")"
pass "migration changes the radio through sudo"
# A second account must not undo an administrator's later choice, since migration
# completion is recorded per user.
printf '[Policy]\nAutoEnable=false\n' >"$main_conf"
POWERED=yes run_migration
grep -qx 'AutoEnable=false' "$main_conf" ||
fail "migration leaves a later opt-out alone" "$(cat "$main_conf")"
pass "migration leaves a later opt-out alone"
[[ ! -s $CALLS ]] ||
fail "migration touches no radio state on a second run" "$(cat "$CALLS")"
pass "migration touches no radio state on a second run"
# Only the exact line Omarchy wrote is reverted, so a hand-edited opt-out stands.
reset_machine
printf '[Policy]\nAutoEnable = false\n' >"$main_conf"
POWERED=yes run_migration
grep -qx 'AutoEnable = false' "$main_conf" ||
fail "migration keeps a hand-edited AutoEnable" "$(cat "$main_conf")"
pass "migration keeps a hand-edited AutoEnable"
+4
View File
@@ -71,6 +71,10 @@ jq -e --arg path "$ROOT/bin/omarchy-chromium-copy-url-host" '
' "$native_manifest" >/dev/null || fail "copy-url native host manifest uses Omarchy host path and extension id"
pass "copy-url native host installer registers the stable extension id"
[[ -f $test_home/.config/BraveSoftware/Brave-Origin/NativeMessagingHosts/com.omarchy.copy_url.json ]] ||
fail "copy-url native host installer covers Brave Origin"
pass "copy-url native host installer covers Brave Origin"
# Chromium ships in the base packages, so fresh installs do not go through
# omarchy-install-browser, and they mark every migration as already applied.
# The user install still has to register the host itself.
+4
View File
@@ -33,6 +33,10 @@ jq -e --arg path "$ROOT/bin/omarchy-chromium-ytdlp-host" '
' "$manifest_path" >/dev/null
pass "yt-dlp native host manifest uses Omarchy host path and extension id"
[[ -f $test_home/.config/BraveSoftware/Brave-Origin/NativeMessagingHosts/com.omarchy.ytdlp.json ]] ||
fail "yt-dlp native host installer covers Brave Origin"
pass "yt-dlp native host installer covers Brave Origin"
parse_result=$(bash -c '
OMARCHY_PATH="$3"
source "$1"
+9 -36
View File
@@ -38,6 +38,15 @@ jq -e '
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default clock date format has no leading zero"
jq -e '
def ids: map(.id // .);
(.bar.layout.right | ids) as $ids |
($ids | index("omarchy.tray")) as $tray |
($ids | index("omarchy.agents")) as $agents |
$tray != null and $agents == $tray + 1
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default right layout keeps agents next to the tray"
ROOT="$ROOT" python3 <<'PY'
import json
import os
@@ -418,39 +427,3 @@ if grep -RIl 'upgrade-to-quattro\|Omarchy 4\.0 is upgraded' "$ROOT/migrations" >
fail "4.0 upgrade is not modeled as a migration"
fi
pass "4.0 upgrade is handled outside the migration runner"
clock_migration=$(grep -rl 'Remove leading zero from bar clock date' "$ROOT/migrations" | head -n 1 || true)
[[ -n $clock_migration ]] || fail "clock date format user migration exists"
cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
{
"version": 1,
"bar": {
"layout": {
"left": [],
"center": [
{ "id": "omarchy.clock", "formatAlt": "dd MMMM 'W'ww yyyy" },
{ "id": "omarchy.weather" }
],
"right": [
{ "id": "local.clock", "formatAlt": "dd MMMM 'W'ww yyyy" }
]
}
},
"plugins": []
}
JSON
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" bash "$clock_migration"
jq -e '
.bar.layout.center[0].formatAlt == "d MMMM \u0027W\u0027ww yyyy" and
.bar.layout.right[0].formatAlt == "dd MMMM \u0027W\u0027ww yyyy"
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "clock date format migration removes leading zero from clock"
before=$(sha256sum "$TMPDIR/home/.config/omarchy/shell.json" | awk '{print $1}')
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" bash "$clock_migration"
after=$(sha256sum "$TMPDIR/home/.config/omarchy/shell.json" | awk '{print $1}')
[[ $before == "$after" ]] || fail "clock date format migration is idempotent"
pass "clock date format migration is idempotent"
+262 -3
View File
@@ -19,6 +19,7 @@ mise_history="$test_tmp/mise-history"
stub_log="$test_tmp/stubs"
terminal_log="$test_tmp/terminal"
menu_log="$test_tmp/menu"
muse_login_log="$test_tmp/muse-login"
mkdir -p "$mock_bin" "$test_home"
cat >"$mock_bin/omarchy-notification-send" <<'SH'
@@ -69,6 +70,22 @@ cat >"$mock_bin/omarchy-menu" <<'SH'
printf '%s\0' "$@" >"$OMARCHY_TEST_AGENT_MENU_LOG"
SH
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
echo "Muse must install through mise" >&2
exit 1
SH
ln -s omarchy-pkg-add "$mock_bin/omarchy-pkg-aur-add"
cat >"$mock_bin/muse" <<'SH'
#!/bin/bash
if [[ ${1:-} == "login" ]]; then
printf 'muse %s\n' "$*" >>"$OMARCHY_TEST_MUSE_LOGIN_LOG"
else
printf '%s\0' muse "$@" >"$OMARCHY_TEST_AGENT_INLINE_LOG"
fi
SH
cat >"$mock_bin/omarchy-test-noop" <<'SH'
#!/bin/bash
exit 0
@@ -91,6 +108,7 @@ export OMARCHY_TEST_MISE_HISTORY="$mise_history"
export OMARCHY_TEST_STUB_LOG="$stub_log"
export OMARCHY_TEST_AGENT_TERMINAL_LOG="$terminal_log"
export OMARCHY_TEST_AGENT_MENU_LOG="$menu_log"
export OMARCHY_TEST_MUSE_LOGIN_LOG="$muse_login_log"
export OMARCHY_PATH="$ROOT"
grok_package="npm:@xai-official/grok"
@@ -98,6 +116,8 @@ omp_package="github:can1357/oh-my-pi"
crush_package="crush"
agy_package="antigravity-cli"
ori_package="github:OpenRouterLabs/ori-releases"
cursor_agent_package="cursor-agent"
muse_package="http:muse[url=https://api.meta.ai/muse-launcher.sh,bin=muse,version_list_url=https://api.meta.ai/muse-code/channels/muse-stable,version_json_path=.version]"
assert_lazy_stub() {
local package=$1
@@ -116,16 +136,41 @@ assert_lazy_stub "$grok_package" grok
assert_lazy_stub "$omp_package" omp
assert_lazy_stub "$crush_package" crush
assert_lazy_stub "$ori_package" ori
assert_lazy_stub "$cursor_agent_package" cursor-agent
assert_lazy_stub "$muse_package" muse
pass "custom agent lazy stubs preserve their mise packages"
source "$ROOT/install/user/mise.sh"
OMARCHY_TEST_MISSING_COMMAND=cursor-agent source "$ROOT/install/user/mise.sh"
grep -Fx "$agy_package agy" "$stub_log" >/dev/null || fail "user setup creates the Antigravity lazy stub"
grep -Fx "$grok_package grok" "$stub_log" >/dev/null || fail "user setup creates the Grok lazy stub"
grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null || fail "user setup creates the Cursor CLI lazy stub"
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "user setup creates the Oh My Pi lazy stub"
grep -Fx "$crush_package" "$stub_log" >/dev/null || fail "user setup creates the Crush lazy stub"
grep -Fx "$ori_package ori" "$stub_log" >/dev/null || fail "user setup creates the Ori lazy stub"
OMARCHY_TEST_MISSING_COMMAND=muse source "$ROOT/install/user/mise.sh"
grep -Fx "$muse_package muse" "$stub_log" >/dev/null || fail "user setup creates the Muse lazy stub"
pass "user setup creates the custom agent lazy stubs"
: >"$stub_log"
source "$ROOT/install/user/mise.sh"
grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null && fail "user setup replaces an existing cursor-agent command"
pass "user setup keeps an existing Cursor CLI install"
grep -Fx "$muse_package muse" "$stub_log" >/dev/null && fail "user setup replaces an existing Muse command"
: >"$stub_log"
OMARCHY_TEST_MISSING_COMMAND=muse source "$ROOT/migrations/1788724825.sh" >/dev/null
grep -Fx "$muse_package muse" "$stub_log" >/dev/null || fail "Muse migration creates its lazy stub"
: >"$stub_log"
source "$ROOT/migrations/1788724825.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "Muse migration replaces an existing command"
mkdir -p "$test_home/.local/state/omarchy"
touch "$test_home/.local/state/omarchy/preinstalls-removed"
OMARCHY_TEST_MISSING_COMMAND=muse source "$ROOT/migrations/1788724825.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "Muse migration ignores the preinstall opt-out"
rm "$test_home/.local/state/omarchy/preinstalls-removed"
pass "Muse migration preserves existing installs and the preinstall opt-out"
: >"$stub_log"
source "$ROOT/migrations/1785617047.sh" >/dev/null
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration creates a working lazy stub"
@@ -134,6 +179,17 @@ grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration c
source "$ROOT/migrations/1787342993.sh" >/dev/null
grep -Fx "$ori_package ori" "$stub_log" >/dev/null || fail "Ori migration creates a working lazy stub"
: >"$stub_log"
export OMARCHY_TEST_MISSING_COMMAND=cursor-agent
source "$ROOT/migrations/1788577553.sh" >/dev/null
unset OMARCHY_TEST_MISSING_COMMAND
grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null || fail "Cursor CLI migration creates a working lazy stub"
: >"$stub_log"
source "$ROOT/migrations/1788577553.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "Cursor CLI migration reinstalls an existing cursor-agent command"
pass "Cursor CLI migration preserves an existing Cursor CLI install"
: >"$stub_log"
source "$ROOT/migrations/1785846769.sh" >/dev/null
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "agent migration repairs the Oh My Pi lazy stub"
@@ -216,6 +272,7 @@ touch "$test_home/.local/state/omarchy/preinstalls-removed"
source "$ROOT/migrations/1785617047.sh" >/dev/null
source "$ROOT/migrations/1785846769.sh" >/dev/null
source "$ROOT/migrations/1787342993.sh" >/dev/null
OMARCHY_TEST_MISSING_COMMAND=cursor-agent source "$ROOT/migrations/1788577553.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "agent migrations respect the preinstall opt-out"
[[ ! -e $test_home/.local/bin/omp ]] || fail "agent migration removes the obsolete Oh My Pi wrapper after opt-out"
@@ -240,13 +297,30 @@ rm "$test_home/.local/state/omarchy/preinstalls-removed"
rm -f "$agent_file"
pass "agent migrations install working wrappers without overriding the preinstall opt-out"
"$ROOT/bin/omarchy-mise-install" "$muse_package" muse
touch "$test_home/.local/bin/agy" "$test_home/.local/bin/ori"
omarchy-remove-preinstalls >/dev/null
for command in agy omp ori grok crush; do
for command in agy omp ori grok crush cursor-agent muse; do
[[ ! -e $test_home/.local/bin/$command ]] || fail "Remove Preinstalls deletes the $command lazy stub"
done
pass "Remove Preinstalls deletes every optional agent lazy stub"
# Cursor's installer links the same path, so anything but the mise wrapper is
# the user's own install.
touch "$test_home/.local/bin/cursor-agent.official"
ln -s cursor-agent.official "$test_home/.local/bin/cursor-agent"
omarchy-remove-preinstalls >/dev/null
[[ -L $test_home/.local/bin/cursor-agent ]] || fail "Remove Preinstalls keeps an official Cursor CLI install"
rm -f "$test_home/.local/bin/cursor-agent" "$test_home/.local/bin/cursor-agent.official"
pass "Remove Preinstalls keeps an official Cursor CLI install"
printf '#!/bin/bash\necho user-muse\n' >"$test_home/.local/bin/muse"
chmod +x "$test_home/.local/bin/muse"
omarchy-remove-preinstalls >/dev/null
[[ $("$test_home/.local/bin/muse") == "user-muse" ]] || fail "Remove Preinstalls deletes a user-managed Muse"
rm "$test_home/.local/bin/muse"
pass "Remove Preinstalls keeps a user-managed Muse install"
[[ -z $(omarchy-default-agent) ]] || fail "default agent is unset until one is chosen"
pass "default agent is unset until one is chosen"
@@ -307,6 +381,11 @@ declare -A expected_agents=(
[gemini-cli]="agy"
[copilot]="copilot"
[github-copilot]="copilot"
[cursor]="cursor-agent"
[cursor-agent]="cursor-agent"
[muse]="muse"
[muse-code]="muse"
[musecode]="muse"
)
declare -A expected_packages=(
@@ -320,6 +399,8 @@ declare -A expected_packages=(
[grok]="$grok_package"
[agy]="$agy_package"
[copilot]="copilot"
[cursor-agent]="$cursor_agent_package"
[muse]="$muse_package"
)
for selection in "${!expected_agents[@]}"; do
@@ -329,8 +410,12 @@ for selection in "${!expected_agents[@]}"; do
[[ $(omarchy-default-agent) == $expected ]] || fail "default agent canonicalizes $selection"
mapfile -d '' -t mise_args <"$mise_log"
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" && ${mise_args[2]} == ${expected_packages[$expected]} ]] ||
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" ]] ||
fail "default agent installs $selection globally through mise"
case ${mise_args[2]} in
"${expected_packages[$expected]}") ;;
*) fail "default agent preserves $selection backend options" ;;
esac
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] ||
@@ -380,6 +465,35 @@ mapfile -d '' -t agent_open_args <"$agent_open_log"
fail "installed agent opens in a new terminal after selection"
pass "installed agents select and open without notifications"
# Cursor's installer links the wrapper's path, and the mise shims precede
# ~/.local/bin, so a mise copy would shadow the user's own install.
touch "$test_home/.local/bin/cursor-agent.official"
chmod +x "$test_home/.local/bin/cursor-agent.official"
ln -s cursor-agent.official "$test_home/.local/bin/cursor-agent"
: >"$terminal_log"
: >"$mise_log"
: >"$agent_open_log"
omarchy-default-agent cursor-agent
[[ ! -s $terminal_log ]] || fail "an official Cursor CLI install needs no install terminal"
[[ ! -s $mise_log ]] || fail "an official Cursor CLI install is left to itself by mise"
[[ $(<"$agent_file") == "cursor-agent" ]] || fail "an official Cursor CLI install becomes the default"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] ||
fail "an official Cursor CLI install opens after selection"
rm -f "$test_home/.local/bin/cursor-agent" "$test_home/.local/bin/cursor-agent.official"
printf '%s\n' copilot >"$agent_file"
pass "selecting an official Cursor CLI install skips mise"
# A file nothing can run is not an install; the wrapper is still wanted.
touch "$test_home/.local/bin/cursor-agent"
: >"$terminal_log"
omarchy-default-agent cursor-agent
mapfile -d '' -t terminal_args <"$terminal_log"
[[ ${terminal_args[*]} == "omarchy-default-agent --install cursor-agent" ]] ||
fail "a dead file at the wrapper's path still installs Cursor CLI"
rm -f "$test_home/.local/bin/cursor-agent"
pass "a dead file at the wrapper's path does not pass for an install"
: >"$agent_open_log"
if omarchy-default-agent unsupported >"$test_tmp/invalid-output" 2>&1; then
fail "default agent rejects unsupported providers"
@@ -414,6 +528,76 @@ grep -F "Could not set Codex as the default coding agent" "$test_tmp/setup-failu
[[ ! -s $agent_open_log ]] || fail "failed activation does not open an agent"
pass "default agent reports mise failures without notifications"
# Muse follows the shared mise installation and launch path.
: >"$notification_history"
: >"$agent_open_log"
: >"$terminal_log"
omarchy-default-agent muse
mapfile -d '' -t terminal_args <"$terminal_log"
[[ ${terminal_args[0]} == "omarchy-default-agent" && ${terminal_args[1]} == "--install" && ${terminal_args[2]} == "muse" ]] ||
fail "missing Muse installation opens in a terminal"
[[ ! -s $notification_history ]] || fail "missing Muse installation skips notifications"
[[ ! -s $agent_open_log ]] || fail "missing Muse installation waits to open the agent"
[[ $(omarchy-default-agent) == "copilot" ]] || fail "missing Muse installation waits to change the selection"
if OMARCHY_TEST_MISE_FAIL=true omarchy-default-agent --install muse >"$test_tmp/muse-install-failure-output" 2>&1; then
fail "missing Muse rejects a failed mise installation"
fi
[[ $(omarchy-default-agent) == "copilot" ]] || fail "failed Muse installation preserves the current default"
[[ ! -s $muse_login_log && ! -s $agent_open_log ]] || fail "failed Muse installation skips login and launch"
grep -F "Could not install Muse Code with mise" "$test_tmp/muse-install-failure-output" >/dev/null ||
fail "failed Muse installation identifies mise"
pass "failed Muse mise installation preserves the selection and skips login"
: >"$mise_history"
: >"$stub_log"
omarchy-default-agent --install muse >"$test_tmp/muse-install-output"
grep -Fx "use -g $muse_package" "$mise_history" >/dev/null || fail "visible Muse installation uses the HTTP backend"
[[ ! -s $stub_log ]] || fail "Muse selection recreates its preinstalled wrapper"
[[ ! -s $muse_login_log ]] || fail "Muse selection runs a separate login flow"
[[ $(omarchy-default-agent) == "muse" ]] || fail "visible Muse installation changes the selection"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 2 && ${agent_open_args[0]} == "omarchy-agent" && ${agent_open_args[1]} == "--inline" ]] ||
fail "newly installed Muse opens in the installation terminal"
pass "Muse installs visibly through mise and opens directly"
: >"$terminal_log"
: >"$muse_login_log"
: >"$agent_open_log"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent muse-code
[[ ! -s $terminal_log ]] || fail "installed Muse selection skips the terminal"
[[ ! -s $muse_login_log ]] || fail "installed Muse selection skips the login"
[[ $(omarchy-default-agent) == "muse" ]] || fail "default agent canonicalizes muse-code"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] ||
fail "installed Muse opens in a new terminal after selection"
pass "installed Muse selects and opens directly"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
: >"$agent_open_log"
if OMARCHY_TEST_AGENT_INSTALLED=true OMARCHY_TEST_MISE_FAIL=true omarchy-default-agent musecode >"$test_tmp/muse-failure-output" 2>&1; then
fail "default agent rejects a failed Muse activation"
fi
[[ $(omarchy-default-agent) == "pi" ]] || fail "failed Muse activation preserves the current default agent"
grep -F "Could not set Muse Code as the default coding agent" "$test_tmp/muse-failure-output" >/dev/null ||
fail "default agent reports a failed Muse activation"
[[ ! -s $agent_open_log ]] || fail "failed Muse activation does not open an agent"
pass "default agent reports Muse mise failures without changing the selection"
# A manually installed launcher belongs to the user; selecting it must not
# install a second copy or replace it with the Omarchy wrapper.
printf '#!/bin/bash\necho user-muse\n' >"$test_home/.local/bin/muse"
chmod +x "$test_home/.local/bin/muse"
: >"$mise_history"
: >"$stub_log"
: >"$terminal_log"
omarchy-default-agent muse
[[ $(omarchy-default-agent) == "muse" ]] || fail "a user-installed Muse can be selected"
[[ ! -s $mise_history && ! -s $stub_log && ! -s $terminal_log ]] || fail "a user-installed Muse skips installation and wrapper creation"
[[ $("$test_home/.local/bin/muse") == "user-muse" ]] || fail "a user-installed Muse is preserved"
rm "$test_home/.local/bin/muse"
pass "selecting a user-installed Muse preserves its launcher"
rm "$mock_bin/omarchy-agent"
hash -r
@@ -462,13 +646,21 @@ assert_launch opencode opencode --auto --prompt "Review this project"
assert_launch ori ori code --interactive --prompt "Review this project"
assert_launch claude claude --permission-mode auto -- "Review this project"
assert_launch codex codex --approve-for-me -- "Review this project"
assert_launch muse muse --approval-mode never -- "Review this project"
assert_launch crush crush run "Review this project"
assert_launch grok grok --permission-mode bypassPermissions -- "Review this project"
assert_launch cursor-agent cursor-agent --yolo --trust agent -- "Review this project"
assert_launch hermes env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=Review this project"
assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project"
assert_launch copilot copilot --allow-all --interactive "Review this project"
pass "agent launcher adapts initial prompts for every supported agent"
literal_muse_prompt=$'--disable-sandbox !Crash {$(touch must-not-run)}\ntrailing\\ '
printf '%s\n' "muse" >"$agent_file"
omarchy-agent-prompt "$literal_muse_prompt"
assert_launched muse "separates prompt text from options" muse --approval-mode never -- "$literal_muse_prompt"
pass "Muse receives option-like prompts as one literal argument"
literal_hermes_prompt=$' --help !Crash /quit {$(touch must-not-run)}\ntrailing\\ '
printf '%s\n' "hermes" >"$agent_file"
omarchy-agent-prompt "$literal_hermes_prompt"
@@ -482,8 +674,10 @@ assert_bypass opencode opencode --auto
assert_bypass ori ori code
assert_bypass claude claude --permission-mode auto
assert_bypass codex codex --approve-for-me
assert_bypass muse muse --approval-mode never
assert_bypass crush crush --yolo
assert_bypass grok grok --permission-mode bypassPermissions
assert_bypass cursor-agent cursor-agent --yolo --trust
assert_bypass hermes hermes --yolo
assert_bypass agy agy --dangerously-skip-permissions
assert_bypass copilot copilot --allow-all
@@ -542,3 +736,68 @@ fi
grep -F "missing is not installed" "$test_tmp/missing-output" >/dev/null ||
fail "agent launcher explains when the default command is missing"
pass "agent launcher reports a missing default command"
# OpenClaw comes from its pacman package, not mise: choosing it must route
# through omarchy-install-openclaw-cli and never touch a mise environment.
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == openclaw && ${OMARCHY_TEST_OPENCLAW_INSTALLED:-false} == "true" ]]
SH
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
printf '%s\n' "pkg-add $*" >>"$OMARCHY_TEST_STUB_LOG"
SH
cat >"$mock_bin/omarchy-launch-openclaw" <<'SH'
#!/bin/bash
printf '%s\0' omarchy-launch-openclaw "$@" >"$OMARCHY_TEST_AGENT_INLINE_LOG"
SH
cat >"$mock_bin/openclaw" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin/omarchy-pkg-present" "$mock_bin/omarchy-pkg-add" \
"$mock_bin/omarchy-launch-openclaw" "$mock_bin/openclaw"
: >"$launch_log"
: >"$terminal_log"
: >"$mise_history"
OMARCHY_TEST_OPENCLAW_INSTALLED=true omarchy-default-agent openclaw
read -r chosen <"$agent_file"
[[ $chosen == openclaw ]] || fail "choosing OpenClaw records it as the default agent"
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent omarchy-launch-openclaw --tui" ]] ||
fail "choosing OpenClaw launches its terminal UI"
[[ ! -s $terminal_log ]] || fail "an installed OpenClaw needs no install terminal"
! grep -q 'use -g openclaw' "$mise_history" || fail "OpenClaw never installs through mise"
pass "choosing OpenClaw uses the package and launches its terminal UI"
: >"$terminal_log"
OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent openclaw
mapfile -d '' -t terminal_args <"$terminal_log"
[[ ${terminal_args[*]} == "omarchy-default-agent --install openclaw" ]] ||
fail "a missing OpenClaw routes through the install terminal"
pass "a missing OpenClaw routes through the install terminal"
: >"$stub_log"
: >"$inline_log"
OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent --install openclaw >/dev/null
grep -Fx "pkg-add openclaw" "$stub_log" >/dev/null ||
fail "installing OpenClaw as default agent adds its package"
mapfile -d '' -t inline_args <"$inline_log"
[[ ${inline_args[*]} == "omarchy-launch-openclaw --tui" ]] ||
fail "installing OpenClaw as default agent hands over to its terminal UI"
pass "installing OpenClaw as default agent adds its package"
: >"$launch_log"
omarchy agent prompt "Review this project"
mapfile -d '' -t launch_args <"$launch_log"
# Element-wise: the prompt must travel as one argv entry, which a space-joined
# comparison could not tell apart from a prompt split into words.
[[ ${#launch_args[@]} == 5 &&
${launch_args[0]} == "--app-id=org.omarchy.agent" &&
${launch_args[1]} == "omarchy-launch-openclaw" &&
${launch_args[2]} == "--tui" &&
${launch_args[3]} == "--message" &&
${launch_args[4]} == "Review this project" ]] ||
fail "OpenClaw receives prompts through --message" "argv: ${launch_args[*]}"
pass "OpenClaw receives prompts through --message"
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
#
# The fingerprint driver migration only repairs a machine an earlier version of
# it left with fprintd and no libfprint; any installed driver is left alone.
# The real package helpers run over a stubbed pacman.
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1785090473.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin"
export CALL_LOG="$scratch/calls"
export PATH="$scratch/bin:$ROOT/bin:$PATH"
cat > "$scratch/bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
# INSTALLED lists the installed package names, one per line; an install adds
# its packages to INSTALLED_LOG so omarchy-pkg-add's follow-up query sees them.
cat > "$scratch/bin/pacman" <<'STUB'
#!/bin/bash
case "$1" in
-Q) grep -qx "$2" <<< "${INSTALLED:-}" || grep -qx "$2" "$INSTALLED_LOG" ;;
-S)
printf 'pacman %s\n' "$*" >> "$CALL_LOG"
for arg in "$@"; do
[[ $arg == -* ]] || printf '%s\n' "$arg" >> "$INSTALLED_LOG"
done
;;
*) printf 'pacman %s\n' "$*" >> "$CALL_LOG" ;;
esac
STUB
chmod +x "$scratch/bin/"*
export INSTALLED_LOG="$scratch/installed"
run_migration() {
: > "$CALL_LOG"
: > "$INSTALLED_LOG"
bash -euo pipefail "$migration" > /dev/null
}
INSTALLED='fprintd' run_migration
grep -qx 'pacman -S --noconfirm --needed libfprint-git' "$CALL_LOG" || fail "fprintd without a library gets libfprint-git"
pass "fprintd without a library gets libfprint-git"
INSTALLED=$'libfprint-git\nfprintd' run_migration
[[ ! -s $CALL_LOG ]] || fail "an installed libfprint-git is left alone" "$(<"$CALL_LOG")"
pass "an installed libfprint-git is left alone"
INSTALLED=$'libfprint\nfprintd' run_migration
[[ ! -s $CALL_LOG ]] || fail "an installed stock libfprint is left alone" "$(<"$CALL_LOG")"
pass "an installed stock libfprint is left alone"
INSTALLED='' run_migration
[[ ! -s $CALL_LOG ]] || fail "a machine without fprintd is left alone" "$(<"$CALL_LOG")"
pass "a machine without fprintd is left alone"
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
#
# The fingerprint setup installs libfprint-git in place of stock libfprint. The
# two conflict, so the swap has to happen inside one --ask 4 transaction, and a
# rerun with everything installed must not touch pacman at all. The real
# omarchy-pkg-missing runs; pacman and the privileged calls are stubbed.
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin"
export CALL_LOG="$scratch/calls"
export PATH="$scratch/bin:$ROOT/bin:$PATH"
cat > "$scratch/bin/omarchy-hw-fingerprint" <<'STUB'
#!/bin/bash
exit "${HARDWARE_STATUS:-0}"
STUB
cat > "$scratch/bin/sudo" <<'STUB'
#!/bin/bash
case "$1" in
pacman | fprintd-enroll) exec "$@" ;;
*) echo "Unexpected privileged call: $*" >> "$CALL_LOG"; exit 99 ;;
esac
STUB
# INSTALLED lists the installed package names, one per line.
cat > "$scratch/bin/pacman" <<'STUB'
#!/bin/bash
case "$1" in
-Q) grep -qx "$2" <<< "${INSTALLED:-}" ;;
-S)
printf 'pacman %s\n' "$*" >> "$CALL_LOG"
exit "${INSTALL_STATUS:-0}"
;;
*) printf 'pacman %s\n' "$*" >> "$CALL_LOG"; exit 99 ;;
esac
STUB
cat > "$scratch/bin/fprintd-enroll" <<'STUB'
#!/bin/bash
# Stop before verification/PAM; no host authentication files may be changed.
echo enroll >> "$CALL_LOG"
exit 1
STUB
cat > "$scratch/bin/fprintd-verify" <<'STUB'
#!/bin/bash
echo verify >> "$CALL_LOG"
exit 1
STUB
chmod +x "$scratch/bin/"*
run_setup() {
: > "$CALL_LOG"
if "$ROOT/bin/omarchy-setup-security-fingerprint" > "$scratch/output" 2>&1; then
fail "setup stops on the simulated enrollment or installation failure"
fi
if grep -q 'Unexpected privileged call' "$CALL_LOG"; then
fail "setup does not change PAM after failed enrollment"
fi
}
assert_installs() {
grep -qx 'pacman -S --needed --noconfirm --ask 4 libfprint-git fprintd usbutils' "$CALL_LOG" || fail "$1"
(( $(grep -c '^pacman ' "$CALL_LOG") == 1 )) || fail "$1: one pacman transaction"
}
run_setup
assert_installs "a fresh machine installs libfprint-git, fprintd and usbutils"
grep -qx enroll "$CALL_LOG" || fail "installation is followed by enrollment"
pass "a fresh machine installs libfprint-git and reaches enrollment"
INSTALLED=$'libfprint\nfprintd\nusbutils' run_setup
assert_installs "installed stock libfprint is replaced in the same transaction"
pass "installed stock libfprint is replaced without a removal step"
INSTALLED=$'libfprint-git\nfprintd\nusbutils' run_setup
if grep -q '^pacman' "$CALL_LOG"; then
fail "a rerun with everything installed does not touch pacman"
fi
grep -qx enroll "$CALL_LOG" || fail "a rerun with everything installed reaches enrollment"
pass "a rerun with everything installed goes straight to enrollment"
INSTALL_STATUS=1 run_setup
if grep -qx enroll "$CALL_LOG"; then
fail "a failed package transaction prevents enrollment"
fi
pass "a failed installation stops before enrollment"
HARDWARE_STATUS=1 run_setup
[[ ! -s $CALL_LOG ]] || fail "missing hardware stops before package operations"
pass "missing hardware performs no package operations"
@@ -0,0 +1,31 @@
pragma Singleton
import QtQuick
import Quickshell.Networking
QtObject {
property int backend: NetworkBackendType.NetworkManager
property bool wifiEnabled: true
property bool canCheckConnectivity: true
property bool connectivityCheckEnabled: true
property int connectivity: NetworkConnectivity.Full
property int checks: 0
function checkConnectivity() { checks++ }
property var devices: ({ values: [wifi] })
property QtObject wifi: QtObject {
property int type: DeviceType.Wifi
property string name: "test-wifi"
property bool connected: true
property bool scannerEnabled: false
property var networks: ({ values: [network] })
}
property QtObject network: QtObject {
property string name: "Guest Wi-Fi"
property bool connected: true
property bool known: true
property bool stateChanging: false
property real signalStrength: 0.8
property int security: WifiSecurityType.Open
signal connectionFailed(int reason)
}
}
@@ -0,0 +1 @@
singleton NetworkMock 1.0 NetworkMock.qml
@@ -0,0 +1,161 @@
import QtQuick
import Quickshell
import Quickshell.Networking
import qs.Commons
import "mocks"
import "network" as Network
ShellRoot {
id: test
property bool failed: false
function check(ok, message) {
if (!ok) {
failed = true
console.log("RESULT fail " + message)
}
}
// Not visible in the normal test run. The optional preview maps the real
// KeyboardPanel for a screenshot, without ever altering the host network.
Item {
Network.Panel {
id: panel
bar: QtObject {
property color foreground: Color.foreground
property color barForeground: Color.foreground
property color urgent: Color.urgent
property string fontFamily: Style.font.family
property string position: "top"
property int barSize: 24
property bool vertical: false
property bool foregroundAnimationEnabled: false
property var activePopout: null
function requestPopout(owner) { activePopout = owner }
function releasePopout(owner) { activePopout = null }
function registerClickTarget(target) {}
function unregisterClickTarget(target) {}
function hideTooltip(target) {}
function showTooltip(target, text) {}
}
}
}
Timer {
interval: 250
running: true
onTriggered: {
test.check(panel.kind === "wifi", "connected Wi-Fi fixture")
test.check(panel.connectivity === "full", "normal connectivity")
test.check(!panel.testButton.visible && !panel.testBarButton.active, "no false portal banner")
test.check(!panel.testPoll.running, "normal connectivity adds no polling")
test.check(NetworkMock.checks > 0, "checks at connection/startup")
var before = NetworkMock.checks
panel.checkConnectivity()
test.check(NetworkMock.checks === before + 1, "manual check delegates to NM")
NetworkMock.connectivity = NetworkConnectivity.Portal
Qt.callLater(portalChecks)
}
}
function portalChecks() {
check(panel.hasCaptivePortal && panel.restricted, "native portal activates restricted mode")
check(panel.testButton.visible, "portal button visible")
check(panel.testButton.text === "Open Captive Portal", "prominent action label")
check(panel.icon === "󰤩" && panel.testBarButton.active, "blocked bar icon and warning color")
check(panel.testMeta.text === "SIGN-IN REQUIRED", "status replaces cheerful connection phrase")
check(panel.testTitle.text === "Guest Wi-Fi", "connected SSID survives missing route details")
check(panel.testPoll.running && panel.testPoll.interval === 10000, "restricted recheck runs while closed")
var before = NetworkMock.checks
panel.testPoll.triggered()
check(NetworkMock.checks === before + 1, "background timer rechecks through NM")
panel.testKeys.textKey("r")
check(NetworkMock.checks === before + 2, "r requests fresh connectivity")
// Exercise the existing cursor model, not a separate test-only action.
panel.cursorActive = true
panel.focusSection = "header"
panel.testKeys.moveRequested(0, 1)
check(panel.focusSection === "portal", "down from header reaches portal")
panel.testKeys.moveRequested(0, 1)
check(panel.focusSection === "dns", "down from portal skips absent band")
panel.testKeys.moveRequested(0, -1)
check(panel.focusSection === "portal", "up from DNS reaches portal")
panel.bandAvailable = ["2.4", "5"]
panel.testKeys.moveRequested(0, 1)
check(panel.focusSection === "band", "down from portal reaches available band")
panel.testKeys.moveRequested(0, -1)
check(panel.focusSection === "portal", "up from band reaches portal")
panel.testKeys.activateRequested()
NetworkMock.connectivity = NetworkConnectivity.Full
Qt.callLater(recoveryChecks)
}
function recoveryChecks() {
check(!panel.hasCaptivePortal && !panel.restricted, "login recovery clears restriction")
check(!panel.testPoll.running, "recovery stops extra checks")
check(!panel.testButton.visible && !panel.testBarButton.active, "recovery hides button and warning color")
check(panel.focusSection === "header", "disappearing button leaves valid cursor")
check(panel.icon !== "󰤩", "signal icon returns")
// No browser launch when the portal is gone (runner asserts one launch).
panel.openCaptivePortal()
NetworkMock.connectivity = NetworkConnectivity.Limited
Qt.callLater(limitedChecks)
}
function limitedChecks() {
check(panel.restricted && !panel.hasCaptivePortal, "outage is not mislabelled as a portal")
check(!panel.testButton.visible && panel.testMeta.text === "LIMITED INTERNET ACCESS", "limited state has no login button")
NetworkMock.connectivity = NetworkConnectivity.Portal
NetworkMock.connectivityCheckEnabled = false
Qt.callLater(disabledChecks)
}
function disabledChecks() {
check(!panel.hasCaptivePortal && panel.connectivity === "unknown", "disabled checks ignore cached portal")
check(!panel.testPoll.running, "disabled checks stop polling")
var before = NetworkMock.checks
panel.checkConnectivity()
check(NetworkMock.checks === before, "does not enable or invoke disabled checks")
NetworkMock.connectivityCheckEnabled = true
NetworkMock.network.connected = false
NetworkMock.wifi.connected = false
Qt.callLater(disconnectedChecks)
}
function disconnectedChecks() {
check(panel.kind === "disconnected" && !panel.hasCaptivePortal, "disconnect clears stale portal")
check(!panel.testButton.visible && panel.icon === "󰤮", "disconnected icon not portal icon")
if (failed) { Qt.quit(); return }
console.log("RESULT pass")
var preview = Quickshell.env("NETWORK_TEST_PREVIEW")
if (preview === "portal" || preview === "full") {
NetworkMock.network.connected = true
NetworkMock.wifi.connected = true
NetworkMock.connectivity = preview === "portal" ? NetworkConnectivity.Portal : NetworkConnectivity.Full
panel.open()
previewCapture.start()
previewDone.start()
} else {
// Give the detached, stubbed browser command time to append its argv.
done.start()
}
}
// Optional fresh, panel-only captures. Rendering the card itself excludes
// the host desktop, and the network details above come only from fixtures.
// NETWORK_TEST_PREVIEW=portal (or full), NETWORK_TEST_SCREENSHOT=/tmp/new.png
Timer {
id: previewCapture
interval: 750
onTriggered: {
var path = Quickshell.env("NETWORK_TEST_SCREENSHOT")
if (!path) return
var card = panel.testKeys.parent.parent
card.grabToImage(function(result) {
test.check(result.saveToFile(path), "save fresh preview screenshot")
Qt.quit()
})
}
}
Timer { id: done; interval: 300; onTriggered: Qt.quit() }
Timer { id: previewDone; interval: 15000; onTriggered: Qt.quit() }
}
+180 -2
View File
@@ -23,6 +23,11 @@ cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
SH
# `mise where` must fail so the installer sees no Hermes behind the stub.
#
# With OMARCHY_TEST_MISE_X_HERMES=1, `mise x -- hermes ...` emulates the Hermes
# the Omarchy stub runs, so the readiness probe can be exercised through a
# mise-installed hermes and not only the foreign and desktop wrappers. Off by
# default, so `mise x` stays silent for every test that does not opt in.
cat >"$mock_bin/mise" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
@@ -30,6 +35,18 @@ if [[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]]; then
printf '%s\n' "$OMARCHY_TEST_MISE_ROOT"
exit 0
fi
if [[ $1 == "x" && ${OMARCHY_TEST_MISE_X_HERMES:-0} == 1 ]]; then
# Args are `x <tool> -- hermes <hermes-args...>`; skip to what follows hermes.
shift
while (( $# )) && [[ $1 != "--" ]]; do shift; done
shift 2
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
[[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "[-q QUERY, --query QUERY] [--tui]"
else
echo "hermes-agent 0.0.0-test"
fi
exit 0
fi
[[ $1 != "where" ]]
SH
@@ -98,7 +115,7 @@ mkdir -p "$test_home/.hermes/hermes-agent/venv/bin"
cat >"$test_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
[[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "--oneshot"
[[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "[-q QUERY, --query QUERY] [--tui]"
else
echo "hermes-agent 0.0.0-test"
fi
@@ -237,6 +254,26 @@ tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "an older owned Hermes enviro
tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "an older owned Hermes environment is uninstalled"
pass "reinstalling replaces an older owned Hermes environment"
# The mise-installed path is what a machine without the desktop app runs, and
# --check gates the default agent there too. The stub is present and its mise
# environment resolves, so readiness turns on the hermes mise runs -- exercised
# here in both directions, since the desktop and foreign cases cover only their
# own wrappers.
run_mise_check() {
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_WHERE_OK=1 \
OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
OMARCHY_TEST_MISE_X_HERMES=1 \
OMARCHY_TEST_HERMES_CAPABLE="$1" \
HOME="$test_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --check >/dev/null 2>&1
}
run_mise_check 1 || fail "--check accepts a mise-installed hermes that runs the seeded session"
run_mise_check 0 && fail "--check rejects a mise-installed hermes without the flags omarchy-agent passes"
pass "--check follows the mise-installed hermes it would actually run"
rm -f "$test_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 &&
@@ -365,6 +402,71 @@ marker_copies=$(grep -rl "Written by omarchy-install-hermes-cli" \
fail "only omarchy-install-hermes-cli spells out the ownership marker"
pass "the ownership marker is written down once"
# --remove tears down a Hermes CLI this installer owns, so Remove Hermes can
# clear one the desktop app never superseded. It turns on the same ownership as
# the rest of the file, so its cases mirror that split.
remove_home="$test_tmp/remove-home"
mkdir -p "$remove_home/.local/bin"
run_remove() {
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$remove_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --remove
}
rm -f "$remove_home/.local/bin/hermes"
: >"$mise_log"
run_remove || fail "--remove succeeds when there is nothing to remove"
# No stub means no proof the mise environment -- if one even exists -- is
# Omarchy's, so nothing may reach mise at all.
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
fail "--remove leaves mise alone when nothing proves ownership"
pass "--remove is idempotent when no Hermes CLI is present"
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
: >"$mise_log"
run_remove || fail "--remove succeeds tearing down an owned CLI"
tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "--remove drops the mise tool from config"
tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "--remove uninstalls the mise tool"
[[ ! -e $remove_home/.local/bin/hermes ]] || fail "--remove takes the stub it owns"
pass "--remove tears down the mise CLI and the stub this installer owns"
# When mise still resolves the tool after the teardown, the environment
# survived whatever uninstall claimed, and --remove has to say so.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
OMARCHY_TEST_MISE_WHERE_OK=1 run_remove && fail "--remove claims success while mise still resolves the tool"
pass "--remove fails when the mise environment survives the teardown"
foreign_remove_body="#!/bin/bash
exec /usr/local/bin/my-own-hermes \"\$@\""
printf '%s\n' "$foreign_remove_body" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_remove || fail "--remove succeeds with a foreign hermes present"
[[ -f $remove_home/.local/bin/hermes && $(cat "$remove_home/.local/bin/hermes") == "$foreign_remove_body" ]] ||
fail "--remove leaves a hermes it does not own untouched"
# The wrapper may front a mise environment the user built against the very same
# spec; without the marker there is no telling, so the environment stays too.
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
fail "--remove never removes a mise environment it cannot prove is Omarchy's"
pass "--remove leaves a Hermes the user installed themselves"
# Judged by what is left, not by what rm claimed: a stub that survives the
# teardown is a CLI still installed, and --remove has to say so.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
chmod 555 "$remove_home/.local/bin"
run_remove && fail "--remove claims success while the stub survives"
chmod 755 "$remove_home/.local/bin"
rm -f "$remove_home/.local/bin/hermes"
pass "--remove fails when the stub cannot be removed"
# The app's marker says its install once landed, not that it is still there. A
# wrapper whose runtime has since gone answers for nothing, so readiness runs
# the command, exactly as it does for a hermes the user installed themselves.
@@ -388,7 +490,7 @@ run_ready_check && fail "--check rejects the app's wrapper when its runtime is g
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "--oneshot"
echo "[-q QUERY, --query QUERY] [--tui]"
else
echo "hermes-agent 0.0.0-test"
fi
@@ -396,3 +498,79 @@ SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check || fail "--check accepts the app's wrapper once it runs"
pass "readiness runs the app's command rather than trusting its marker"
# A release whose help lists only the old probe's --oneshot marker cannot run
# the seeded --tui --query session omarchy-agent starts, so it is not ready.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "--oneshot"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts a release without the flags omarchy-agent passes"
pass "a release listing only --oneshot is not prompt-ready"
# A release that lists --tui-theme and --query-log but has dropped the bare
# --tui/--query omarchy-agent passes must not read as ready on the substring
# alone. The probe matches at a flag boundary for exactly this case.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "[--tui-theme THEME] [--query-log FILE]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts a release whose flags only contain --tui/--query as a substring"
pass "a flag that merely contains --tui or --query is not prompt-ready"
# Each flag answers for itself: a release that kept --tui but dropped --query,
# or the reverse, cannot run the seeded session either, so neither grep may
# ride on the other's match.
for kept in '--tui' '-q QUERY, --query QUERY'; do
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<SH
#!/bin/bash
if [[ \${1:-} == "chat" && \${2:-} == "--help" ]]; then
echo "[$kept]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts a release listing only $kept"
done
pass "either flag alone is not prompt-ready"
# An underscore continues a flag name just as a dash does: --tui_mode is not
# --tui.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "[--tui_mode MODE] [--query_log FILE]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts flags that extend --tui/--query with an underscore"
pass "an underscore continuation is not the bare flag"
# A flag mentioned in another option's help text is not that option. Hermes
# already writes "With --tui:" into --dev's description, so prose has to stay
# prose even when both names appear in it.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo " --dev With --tui: run sources via tsx"
echo " --log FILE Where --query output lands"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts flags that appear only in option descriptions"
pass "a flag mentioned in prose is not a defined option"
+121 -2
View File
@@ -15,6 +15,30 @@ cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
SH
# The CLI teardown is the installer's own, exercised in hermes-cli-test.sh; here
# it is mocked to a logger so this test stays about what Remove Hermes does with
# ~/.hermes, and to keep real mise out of a run with HOME pointed at a fixture.
cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_INSTALLER_LOG"
exit "${OMARCHY_TEST_INSTALLER_STATUS:-0}"
SH
# The remover asks through gum whether the user's data should go too. The stub
# answers "no" unless a test says otherwise, and logs every call: a real gum
# would hang a test run, and one that answered "yes" on its own would be the
# very data loss the default-no exists to prevent.
cat >"$mock_bin/gum" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_GUM_LOG"
exit "${OMARCHY_TEST_GUM_STATUS:-1}"
SH
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
echo "systemctl $*" >>"$OMARCHY_TEST_SYSTEMCTL_LOG"
SH
chmod +x "$mock_bin"/*
seed_install() {
@@ -34,9 +58,34 @@ seed_install() {
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
}
# </dev/null pins stdin off a terminal, so these runs exercise the
# non-interactive path no matter where the suite itself is running.
remove() {
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" HOME="$test_home" PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-remove-ai-hermes" >/dev/null 2>&1
: >"$test_tmp/installer-log"
: >"$test_tmp/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \
OMARCHY_TEST_INSTALLER_STATUS="${OMARCHY_TEST_INSTALLER_STATUS:-0}" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-remove-ai-hermes" </dev/null >/dev/null 2>&1
}
# script(1) puts the remover on a pty, which is the only way -t 0 answers true
# without a person at a real one; the stubbed gum then supplies the answer.
remove_tty() {
: >"$test_tmp/installer-log"
: >"$test_tmp/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
OMARCHY_TEST_GUM_STATUS="${OMARCHY_TEST_GUM_STATUS:-1}" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
script -qec "bash '$ROOT/bin/omarchy-remove-ai-hermes'" /dev/null >/dev/null 2>&1
}
# The app brings its own uv and its own node; both are runtime, not data.
@@ -49,6 +98,10 @@ remove || fail "remove succeeds"
[[ ! -d $test_home/.hermes/node ]] || fail "the node the app installed is removed"
pass "removal takes the whole runtime the app installed"
grep -Fxq 'systemctl --user stop omarchy-hermes-theme.service' "$test_tmp/systemctl-log" ||
fail "the unit the installer left waiting to hand over the theme is stopped" "$(cat "$test_tmp/systemctl-log")"
pass "removal stops the installer's theme hand-over"
[[ -d $test_home/.config/Hermes ]] ||
fail "gateway connections, tokens and settings survive removal"
pass "removal keeps the app's connections and settings"
@@ -64,9 +117,21 @@ pass "removal clears only the managed Node links it stranded"
[[ -f $test_home/.hermes/SOUL.md ]] || fail "SOUL.md survives removal"
pass "removal keeps what belongs to the user"
# Without a terminal there is nobody to ask, so gum must not even be reached:
# a gum that answered "yes" on its own would be a data loss.
[[ ! -s $test_tmp/gum-log ]] ||
fail "removal does not ask about the user's data without a terminal"
pass "removal keeps the user's data unasked when there is no terminal"
[[ ! -e $test_home/.local/bin/hermes ]] || fail "the app's own hermes command is removed"
pass "removal takes the command the app installed"
# Removal also asks the installer to tear down a mise CLI the app superseded, so
# a copy left from before the app took over does not linger once Hermes is gone.
tr '\0' '\n' <"$test_tmp/installer-log" | grep -qx -- '--remove' ||
fail "removal asks the installer to tear down its own CLI"
pass "removal tears down the mise CLI through the installer"
# A hermes command the app did not write survives even when the app did install
# a runtime of its own.
seed_install
@@ -87,6 +152,10 @@ printf 'my local edit\n' >"$test_home/.hermes/hermes-agent/PATCH"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds when the app never finished installing Hermes"
# The stranded pre-desktop CLI is exactly the interrupted-install case, so the
# teardown must be asked for here too, not only when the app's runtime landed.
tr '\0' '\n' <"$test_tmp/installer-log" | grep -qx -- '--remove' ||
fail "removal tears down the CLI even when the app never finished installing"
[[ -d $test_home/.hermes/hermes-agent ]] ||
fail "a Hermes runtime the app never installed survives removal"
[[ -f $test_home/.hermes/hermes-agent/PATCH ]] ||
@@ -110,3 +179,53 @@ remove || fail "remove succeeds with a wrapper pointing at a sibling directory"
[[ -f $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$sibling_body" ]] ||
fail "a wrapper pointing at ~/xhermes is not mistaken for one pointing into ~/.hermes"
pass "removal matches the runtime path as a plain string"
# On a terminal the user is asked, default no: declining leaves every piece of
# data where it was.
seed_install
remove_tty || fail "remove succeeds when the data question is declined"
tr '\0' '\n' <"$test_tmp/gum-log" | grep -qx 'confirm' ||
fail "removal asks about the user's data on a terminal"
[[ -f $test_home/.hermes/sessions/one.json && -d $test_home/.config/Hermes ]] ||
fail "declining the question keeps the user's data"
pass "removal asks on a terminal and declining keeps the data"
# An explicit yes is the one path that takes the data too.
seed_install
OMARCHY_TEST_GUM_STATUS=0 remove_tty || fail "remove succeeds when the data goes too"
[[ ! -e $test_home/.hermes && ! -e $test_home/.config/Hermes ]] ||
fail "a yes deletes ~/.hermes and ~/.config/Hermes"
pass "removal deletes the user's data only on an explicit yes"
# Without the bootstrap marker the runtime is not the app's to take unasked,
# but the data question is still the user's to answer: declining keeps the
# whole tree -- runtime included -- untouched.
seed_install
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
remove_tty || fail "remove succeeds when the app never installed Hermes"
tr '\0' '\n' <"$test_tmp/gum-log" | grep -qx 'confirm' ||
fail "removal still asks about the data without the bootstrap marker"
[[ -d $test_home/.hermes/hermes-agent && -d $test_home/.config/Hermes ]] ||
fail "declining keeps a Hermes the app never installed"
pass "removal asks without the marker and declining keeps everything"
# The prompt names ~/.hermes itself, so a yes takes the whole tree there too,
# unowned runtime and all -- that is what was asked and answered.
seed_install
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_GUM_STATUS=0 remove_tty ||
fail "remove succeeds when the data goes too without the marker"
[[ ! -e $test_home/.hermes && ! -e $test_home/.config/Hermes ]] ||
fail "a yes takes ~/.hermes whole when the marker never appeared"
pass "removal honors a yes on the named paths without the marker"
# A CLI teardown that fails must not stop the runtime handling, and must not be
# papered over either: the data work still happens, and the failure reaches the
# caller's exit code.
seed_install
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
OMARCHY_TEST_INSTALLER_STATUS=1 remove && fail "a failed CLI teardown surfaces in the exit code"
[[ ! -d $test_home/.hermes/hermes-agent ]] ||
fail "a failed CLI teardown does not stop the runtime removal"
pass "a failed CLI teardown is reported after the runtime is handled"
@@ -0,0 +1,59 @@
#!/bin/bash
set -euo pipefail
# The migration hands an existing Hermes Desktop install the Omarchy skin. It
# is exercised here with the package probe and the skin hook stubbed, so a
# migration that reached a Hermes Omarchy did not install, or that marked a
# failed hand-over done, shows up in what it ran.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1788619462.sh"
[[ -f $migration ]] || fail "Hermes skin migration exists"
[[ $(stat -c %a "$migration") == "644" ]] || fail "migration is a plain 0644 file"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
calls="$test_tmp/calls"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-theme-set-hermes" <<'SH'
#!/bin/bash
echo "omarchy-theme-set-hermes $*" >>"$OMARCHY_TEST_CALLS"
[[ ${OMARCHY_TEST_HOOK_FAILS:-0} == 0 ]]
SH
chmod +x "$mock_bin"/*
run_migration() {
: >"$calls"
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
OMARCHY_TEST_HOOK_FAILS="${OMARCHY_TEST_HOOK_FAILS:-0}" \
OMARCHY_TEST_CALLS="$calls" \
PATH="$mock_bin:$PATH" \
HOME="$test_tmp/home" \
OMARCHY_PATH="$ROOT" \
bash -euo pipefail "$migration" >/dev/null
}
OMARCHY_TEST_DESKTOP_INSTALLED=0 run_migration || fail "migration exits clean without Hermes Desktop"
[[ ! -s $calls ]] || fail "a machine without Hermes Desktop is left alone" "$(cat "$calls")"
pass "migration only applies where Omarchy installed Hermes Desktop"
run_migration || fail "migration exits clean with Hermes Desktop installed"
[[ $(cat "$calls") == "omarchy-theme-set-hermes --activate" ]] ||
fail "the skin is rendered, published and activated through the hook's deliberate form" "$(cat "$calls")"
pass "migration hands the skin over through the hook"
if OMARCHY_TEST_HOOK_FAILS=1 run_migration; then
fail "a hand-over that failed on Omarchy's side stays pending"
fi
pass "migration stays pending when the hand-over fails"
+401
View File
@@ -0,0 +1,401 @@
#!/bin/bash
set -euo pipefail
# omarchy-theme-set-hermes writes a file another program parses and asks that
# program to switch to it while it is still on its default. Both are exercised
# here against a throwaway HOME with the Hermes readiness probe, the hermes
# command and the theme refresh stubbed, so a skin that stopped being
# validated, a write into a Hermes that was never set up, or an activation
# that trampled a chosen skin shows up in what landed on disk and what was run.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH'
#!/bin/bash
echo "check" >>"$OMARCHY_TEST_HERMES_CALLS"
[[ $1 == "--check" && ${OMARCHY_TEST_HERMES_READY:-0} == "1" ]]
SH
# A theme switch finishes the hand-over only for the desktop app Omarchy
# installed; --activate is asked for by name and does not look.
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-1} == "1" ]]
SH
# The hook runs the hermes the probe vets, ~/.local/bin/hermes, not one on PATH;
# reset_home installs this stub there and a decoy on PATH that must never run.
cat >"$mock_bin/hermes" <<'SH'
#!/bin/bash
echo "PATH hermes ran: $*" >>"$OMARCHY_TEST_HERMES_CALLS"
exit 1
SH
cat >"$mock_bin/hermes-stub" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_HERMES_CALLS"
if [[ $1 == "config" && $2 == "get" ]]; then
[[ ${OMARCHY_TEST_HERMES_GET_FAILS:-0} == 0 ]] || exit 1
printf '%s\n' "${OMARCHY_TEST_HERMES_SKIN-default}"
fi
if [[ $1 == "config" && $2 == "set" ]]; then
[[ ${OMARCHY_TEST_HERMES_SET_FAILS:-0} == 0 ]] || exit 1
fi
SH
# A refresh re-stages the current theme, which is where the skin gets rendered.
cat >"$mock_bin/omarchy-theme-refresh" <<'SH'
#!/bin/bash
echo "refresh" >>"$OMARCHY_TEST_HERMES_CALLS"
printf 'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\n' \
>"$HOME/.local/state/omarchy/current/theme/hermes.yaml"
SH
# --wait sleeps between its polls and once more after activating; the stub
# records the delays it was asked for and returns at once.
cat >"$mock_bin/sleep" <<'SH'
#!/bin/bash
printf 'sleep %s\n' "$1" >>"$OMARCHY_TEST_HERMES_CALLS"
if [[ $1 == 60 && -n ${OMARCHY_TEST_SWAP_SOURCE:-} ]]; then
printf '%s\n' "$OMARCHY_TEST_SWAP_SOURCE" >"$HOME/.local/state/omarchy/current/theme/hermes.yaml"
fi
if [[ $1 == 60 && ${OMARCHY_TEST_DROP_SKINS:-0} == 1 ]]; then
rm -f "$HOME/.hermes/skins/omarchy.yaml" "$HOME"/.hermes/profiles/*/skins/omarchy.yaml
fi
SH
chmod +x "$mock_bin"/*
good_skin='name: omarchy
description: Omarchy system theme
colors:
background: "#1a1b26"
ui_text: "#a9b1d6"
ui_accent: "#7aa2f7"'
test_home="$test_tmp/home"
hermes_home="$test_home/.hermes"
skin="$hermes_home/skins/omarchy.yaml"
hermes_calls="$test_tmp/hermes-calls"
# Each case gets a fresh HOME so no file survives from the one before. The
# Hermes home is created the way provisioning does on every machine; only
# --set-up adds the config that says Hermes itself has run, on its default
# skin unless --on names another.
reset_home() {
local source="$good_skin"
rm -rf "$test_home"
mkdir -p "$test_home/.local/state/omarchy/current/theme" "$test_home/.local/bin" "$hermes_home/skills"
cp "$mock_bin/hermes-stub" "$test_home/.local/bin/hermes"
: >"$hermes_calls"
while (( $# > 0 )); do
case "$1" in
--set-up) printf 'display:\n skin: default\n' >"$hermes_home/config.yaml" ;;
--on) printf 'display:\n skin: %s\n' "$2" >"$hermes_home/config.yaml"; shift ;;
*) source="$1" ;;
esac
shift
done
printf '%s\n' "$source" >"$test_home/.local/state/omarchy/current/theme/hermes.yaml"
}
run_hook() {
OMARCHY_TEST_HERMES_READY="${OMARCHY_TEST_HERMES_READY:-0}" \
OMARCHY_TEST_HERMES_SKIN="${OMARCHY_TEST_HERMES_SKIN-default}" \
OMARCHY_TEST_HERMES_GET_FAILS="${OMARCHY_TEST_HERMES_GET_FAILS:-0}" \
OMARCHY_TEST_HERMES_SET_FAILS="${OMARCHY_TEST_HERMES_SET_FAILS:-0}" \
OMARCHY_TEST_HERMES_CALLS="$hermes_calls" \
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
OMARCHY_TEST_SWAP_SOURCE="${OMARCHY_TEST_SWAP_SOURCE:-}" \
OMARCHY_TEST_DROP_SKINS="${OMARCHY_TEST_DROP_SKINS:-0}" \
PATH="$mock_bin:$PATH" \
HOME="$test_home" \
HERMES_HOME='' \
"$ROOT/bin/omarchy-theme-set-hermes" "$@"
}
# -- publishing ---------------------------------------------------------------
reset_home
run_hook
[[ ! -e $hermes_home/skins ]] || fail "a Hermes home that only holds the Omarchy skill gets no skin"
[[ ! -s $hermes_calls ]] || fail "nothing is run for a Hermes that never ran" "$(cat "$hermes_calls")"
pass "a theme switch leaves a machine that never ran Hermes alone"
reset_home --set-up
mkdir -p "$hermes_home/profiles/work"
run_hook 2>"$test_tmp/stderr"
diff -q "$test_home/.local/state/omarchy/current/theme/hermes.yaml" "$skin" >/dev/null ||
fail "the generated skin is published to ~/.hermes/skins/omarchy.yaml"
diff -q "$skin" "$hermes_home/profiles/work/skins/omarchy.yaml" >/dev/null ||
fail "an existing Hermes profile gets the skin too"
[[ $(ls "$hermes_home/skins") == "omarchy.yaml" ]] || fail "no temporary file is left beside the skin"
[[ $(cat "$hermes_calls") == "check" ]] || fail "a Hermes that is not ready is asked nothing more" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch says nothing about Hermes" "$(cat "$test_tmp/stderr")"
pass "the skin is published to the Hermes home and every profile"
reset_home --set-up 'name: omarchy
description: Omarchy system theme
colors:
background: "{{ background }}"'
mkdir -p "$hermes_home/skins"
printf 'name: omarchy\ncolors:\n background: "#000000"\n' >"$skin"
run_hook 2>"$test_tmp/stderr"
grep -q '#000000' "$skin" || fail "an unresolved placeholder keeps the previous skin in place"
grep -q 'not a plain color palette' "$test_tmp/stderr" || fail "an unresolved placeholder is reported"
pass "a skin with unresolved colors is not published"
# mv would otherwise move the temp file inside a directory at the skin's path,
# leaving Hermes a directory to read and the temp file behind.
reset_home --set-up
mkdir -p "$skin"
if run_hook 2>/dev/null; then
fail "a directory at the skin's path is an error, not a place to put the skin"
fi
[[ -z $(ls -A "$skin") && $(ls "$hermes_home/skins") == "omarchy.yaml" ]] ||
fail "nothing is left inside or beside a directory at the skin's path" "$(ls -R "$hermes_home/skins")"
pass "a directory at the skin's path is refused cleanly"
for bad in \
$'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\nbanner_logo: "[link=file:///etc/passwd]x[/link]"' \
$'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26\\"\\n ui_text: \\"#ffffff"' \
$'name: nord\ndescription: Nord\ncolors:\n background: "#2e3440"' \
$'description: Omarchy system theme\ncolors:\n background: "#1a1b26"' \
$'name: omarchy\ndescription: Nord: arctic palette\ncolors:\n background: "#2e3440"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\rbanner_logo: "[link=file:///etc/passwd]x[/link]"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\xe2\x80\xa8banner_logo: "evil"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\xc2\x85banner_logo: "evil"' \
$'name: omarchy\n background: "#1a1b26"\ncolors:' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\ncolors:' \
$'colors:\n background: "#1a1b26"\nname: omarchy' \
$'name: omarchy\ncolors:'; do
reset_home --set-up "$bad"
run_hook 2>/dev/null
[[ ! -e $skin ]] || fail "a skin that is not exactly a named palette of hex colors is not published" "$bad"
done
pass "a skin is held to the shape Hermes loads, on the lines Hermes' YAML reader sees"
# A NUL cannot travel through a shell string, so it is written straight to the
# source; grep reads past one where YAML stops.
reset_home --set-up
printf 'name: omarchy\ncolors:\n background: "#1a1b26"\0\n' >"$test_home/.local/state/omarchy/current/theme/hermes.yaml"
run_hook 2>/dev/null
[[ ! -e $skin ]] || fail "a NUL byte in the skin is rejected"
pass "a skin carrying a NUL byte is not published"
reset_home --set-up $'# rendered by Omarchy\nname: omarchy\n\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\n'
run_hook 2>/dev/null
[[ -f $skin ]] || fail "comments and blank lines are allowed around the palette"
pass "a well-formed skin with comments and blank lines is published"
# -- a theme switch finishes a missed hand-over ---------------------------------
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 run_hook 2>"$test_tmp/stderr"
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy' ]] ||
fail "a ready Hermes still on its default is switched by a theme switch" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch activates quietly" "$(cat "$test_tmp/stderr")"
pass "a theme switch activates the skin on a Hermes still on its default"
reset_home --on omarchy
OMARCHY_TEST_HERMES_READY=1 run_hook
[[ -f $skin ]] || fail "the skin is published when it is already active"
[[ ! -s $hermes_calls ]] || fail "a Hermes already on the skin is not started" "$(cat "$hermes_calls")"
pass "a theme switch does not start a Hermes already on the skin"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DESKTOP_INSTALLED=0 run_hook
[[ -f $skin ]] || fail "a Hermes installed some other way still gets the skin published"
[[ ! -s $hermes_calls ]] || fail "a theme switch does not touch a Hermes Omarchy did not install as the app" "$(cat "$hermes_calls")"
pass "a theme switch activates only for the desktop app Omarchy installed"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DESKTOP_INSTALLED=0 run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "--activate switches whichever Hermes it is asked about" "$(cat "$hermes_calls")"
pass "--activate does not ask which Hermes it is"
reset_home --on ares
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "a chosen skin still gets the Omarchy skin published beside it"
[[ ! -s $hermes_calls ]] || fail "a theme switch does not start a Hermes whose config names a chosen skin" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a chosen skin is left without comment on a theme switch" "$(cat "$test_tmp/stderr")"
pass "a theme switch leaves a skin the user chose in Hermes"
# Hermes reads the config of the profile named in active_profile, so that is
# the config that says whether there is anything left to do.
reset_home --on omarchy
mkdir -p "$hermes_home/profiles/work"
printf 'display:\n skin: default\n' >"$hermes_home/profiles/work/config.yaml"
echo work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "an active profile still on its default is switched even when the root config names the skin" "$(cat "$hermes_calls")"
pass "a theme switch follows the active Hermes profile"
reset_home --set-up
mkdir -p "$hermes_home/profiles/work"
printf 'display:\n skin: ares\n' >"$hermes_home/profiles/work/config.yaml"
echo work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
[[ ! -s $hermes_calls ]] || fail "a skin chosen in the active profile is not replaced" "$(cat "$hermes_calls")"
pass "a theme switch leaves a skin chosen in the active Hermes profile"
# Hermes selects a profile once its directory exists; without a config of its
# own it is on the default skin whatever the root config says.
reset_home --on omarchy
mkdir -p "$hermes_home/profiles/work"
echo Work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "an active profile without a config of its own is on the default and gets switched" "$(cat "$hermes_calls")"
pass "a theme switch follows an active profile that has no config yet"
# Only a plainly named skin ends a switch early; anything Hermes might read as
# its default is left for Hermes to answer.
for line in 'skin: "default"' 'skin: default # chosen long ago' 'skin: null' 'skin: false'; do
reset_home --set-up
printf 'display:\n %s\n' "$line" >"$hermes_home/config.yaml"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "a config line Hermes reads as the default still gets the skin activated" "$line: $(cat "$hermes_calls")"
done
pass "a theme switch asks Hermes about any skin line that is not a plain name"
reset_home --set-up
mkdir -p "$hermes_home/profiles/broken"
: >"$hermes_home/profiles/broken/skins"
OMARCHY_TEST_HERMES_READY=1 run_hook 2>"$test_tmp/stderr" || fail "a profile that cannot take the skin does not fail the hook"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch stays quiet about a profile it could not reach" "$(cat "$test_tmp/stderr")"
[[ -f $skin ]] || fail "the Hermes home still gets the skin beside a broken profile"
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "activation still happens beside a broken profile" "$(cat "$hermes_calls")"
pass "a profile that cannot take the skin costs nobody else"
# -- activation ---------------------------------------------------------------
reset_home
run_hook --activate 2>"$test_tmp/stderr"
[[ ! -e $hermes_home/skins && ! -e $hermes_home/config.yaml ]] ||
fail "--activate writes nothing into a Hermes that has never run"
grep -q 'not set up yet' "$test_tmp/stderr" || fail "--activate says why nothing happened"
pass "--activate waits for Hermes to have been set up"
reset_home --set-up
run_hook --activate 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "--activate publishes the skin when Hermes is not ready"
[[ $(cat "$hermes_calls") == "check" ]] || fail "a Hermes that is not ready is not run" "$(cat "$hermes_calls")"
grep -q 'hermes config set display.skin omarchy' "$test_tmp/stderr" || fail "an unready Hermes gets the command that finishes the job"
pass "--activate publishes but does not run a Hermes that is not ready"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 run_hook --activate 2>"$test_tmp/stderr"
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy' ]] ||
fail "a ready Hermes is asked for its skin and then to switch" "$(cat "$hermes_calls")"
grep -q 'on the Omarchy skin' "$test_tmp/stderr" || fail "--activate reports success"
pass "--activate goes through hermes config set when Hermes runs"
reset_home --on omarchy
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=omarchy run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "--activate goes through Hermes even when the config already names the skin" "$(cat "$hermes_calls")"
pass "--activate always asks Hermes to switch"
for chosen in omarchy ""; do
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN="$chosen" run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "the default and the omarchy skin are both replaced" "skin='$chosen': $(cat "$hermes_calls")"
done
pass "--activate replaces Hermes' default skin"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook --activate 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "a chosen skin still gets the Omarchy skin published beside it"
! grep -q 'config set' "$hermes_calls" || fail "a skin the user chose is not replaced" "$(cat "$hermes_calls")"
grep -q "'ares' skin" "$test_tmp/stderr" || fail "leaving a chosen skin is reported"
pass "--activate leaves a skin the user chose in Hermes"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_GET_FAILS=1 run_hook --activate 2>"$test_tmp/stderr" || fail "a Hermes that does not answer is not an error"
! grep -q 'config set' "$hermes_calls" || fail "no answer from Hermes is not taken for the default" "$(cat "$hermes_calls")"
grep -q 'did not say' "$test_tmp/stderr" || fail "an unanswered question is reported"
pass "--activate does not switch a Hermes that did not say which skin it is on"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SET_FAILS=1 run_hook --wait 2>"$test_tmp/stderr" || fail "a Hermes that refuses the write is not an error"
grep -q 'refused' "$test_tmp/stderr" || fail "a refused write is reported"
! grep -q 'sleep 60' "$hermes_calls" || fail "nothing is announced for a write that did not happen" "$(cat "$hermes_calls")"
pass "--activate reports a Hermes that refused the skin and moves on"
# -- a skin the current theme has not rendered yet -------------------------------
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
run_hook
[[ ! -e $hermes_home/skins && ! -s $hermes_calls ]] ||
fail "a theme switch without a rendered skin publishes nothing" "$(cat "$hermes_calls")"
pass "a theme switch has nothing to do without a rendered skin"
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
if run_hook --activate 2>"$test_tmp/stderr"; then
fail "--activate fails when no theme has been selected"
fi
grep -q 'Select an Omarchy theme' "$test_tmp/stderr" || fail "a missing theme is reported"
pass "--activate fails without a current theme to render the skin from"
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
echo tokyo-night >"$test_home/.local/state/omarchy/current/theme.name"
OMARCHY_TEST_HERMES_READY=1 run_hook --activate 2>/dev/null
[[ $(head -1 "$hermes_calls") == "refresh" ]] || fail "a theme applied before the template existed is re-staged" "$(cat "$hermes_calls")"
[[ -f $skin ]] || fail "the freshly rendered skin is published"
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" || fail "the freshly rendered skin is activated" "$(cat "$hermes_calls")"
pass "--activate renders the skin for a theme that predates it"
# -- waiting for the desktop app's first launch ----------------------------------
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>/dev/null
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy\nsleep 60' ]] ||
fail "--wait activates as soon as the runtime marker is there, then republishes after the gateway is up" "$(cat "$hermes_calls")"
[[ -f $skin ]] || fail "--wait publishes the skin"
pass "--wait activates once the desktop app has built its runtime"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent" "$hermes_home/profiles/work"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_SWAP_SOURCE=$'name: omarchy\ncolors:\n background: "#1a1b26"\nbanner_logo: "evil"' run_hook --wait 2>/dev/null
! grep -q 'banner_logo' "$skin" || fail "the republish after the gateway is up does not publish a theme that changed underneath into something rejected"
pass "--wait checks the skin again before republishing it"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent" "$hermes_home/profiles/work"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DROP_SKINS=1 run_hook --wait 2>/dev/null
[[ -f $skin && -f $hermes_home/profiles/work/skins/omarchy.yaml ]] ||
fail "the republish after the gateway is up writes the skin to the Hermes home and every profile again" "$(ls -R "$hermes_home")"
pass "--wait republishes the skin everywhere once the gateway is up"
reset_home
OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>"$test_tmp/stderr"
[[ $(head -1 "$hermes_calls") == "sleep 10" && ! -e $hermes_home/skins ]] ||
fail "--wait polls for the runtime instead of running Hermes" "$(head -3 "$hermes_calls")"
[[ $(grep -c 'sleep 10' "$hermes_calls") == 180 ]] || fail "--wait gives up after 30 minutes" "$(grep -c 'sleep 10' "$hermes_calls")"
grep -q 'did not finish setting up' "$test_tmp/stderr" || fail "giving up is reported"
pass "--wait polls until the desktop app has built its runtime and gives up in time"
@@ -40,6 +40,13 @@ hl = setmetatable({
release = opts.release == true,
})
end,
unbind = function(keys)
for index = #bindings, 1, -1 do
if bindings[index].keys == keys then
table.remove(bindings, index)
end
end
end,
config = function() end,
env = function() end,
monitor = function() end,
@@ -188,3 +195,17 @@ probe=$(PATH="$stub_bin:$PATH" list_bindings "$home" \
grep -Fqx "ALT+SHIFT+SUPER+RIGHT" <<<"$probe" ||
fail "the conflict check ignores modifier order"
pass "the conflict check catches collisions across keycodes and modifier order"
rebound=$(PATH="$stub_bin:$PATH" list_bindings "$home" \
'o.rebind("SUPER + SHIFT + F", "Flea", { launch = "flea" })' | \
awk -F'\t' '$1 == "SHIFT+SUPER+F"')
[[ $rebound == $'SHIFT+SUPER+F\tSUPER + SHIFT + F\tFlea' ]] ||
fail "rebinding replaces the default file manager without stacking actions" "$rebound"
pass "rebinding replaces the default file manager without stacking actions"
rebound=$(PATH="$stub_bin:$PATH" list_bindings "$home" \
'o.rebind("F9", "Dictation on release", "voxtype record toggle", { release = true })' | \
awk -F'\t' '$2 == "F9"')
[[ $rebound == $'F9 (release)\tF9\tDictation on release' ]] ||
fail "rebinding replaces all bindings for a key and preserves binding options" "$rebound"
pass "rebinding replaces all bindings for a key and preserves binding options"
+189
View File
@@ -0,0 +1,189 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
export HOME="$tmp_dir/home"
for stub in omarchy-launch-webapp omarchy-launch-floating-terminal-with-presentation omarchy-openclaw-onboard; do
cat >"$tmp_dir/bin/$stub" <<SCRIPT
#!/bin/bash
printf '$stub:%s\n' "\$*" >>"\$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/$stub"
done
# The retry loop sleeps between polls; a no-op keeps the suite fast.
printf '#!/bin/bash\n' >"$tmp_dir/bin/sleep"
chmod +x "$tmp_dir/bin/sleep"
# The launcher asks systemd whether the gateway unit is enabled; a flag file
# stands in for the user manager.
cat >"$tmp_dir/bin/systemctl" <<SCRIPT
#!/bin/bash
printf 'systemctl:%s\\n' "\$*" >>"\$TEST_LOG"
[[ \$* == *is-enabled* ]] && [[ -f $tmp_dir/gateway-enabled ]]
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
# A machine that never onboarded gets the wizard, not a dashboard probe.
"$ROOT/bin/omarchy-launch-openclaw"
# Through omarchy-openclaw-onboard, never bare `openclaw onboard`, which as of
# 2026.9.1 ends in a browser tab with a foreground gateway and never returns.
grep -q '^omarchy-launch-floating-terminal-with-presentation:omarchy-openclaw-onboard && \[\[ -f \$HOME/.openclaw/openclaw.json \]\] && omarchy-launch-openclaw$' "$TEST_LOG" ||
fail "OpenClaw launch hands a never-onboarded machine to the wizard" "$(grep floating "$TEST_LOG")"
! grep -q '^omarchy-launch-webapp:' "$TEST_LOG" ||
fail "OpenClaw launch hands a never-onboarded machine to the wizard" "webapp opened anyway"
pass "OpenClaw launch hands a never-onboarded machine to the wizard"
mkdir -p "$HOME/.openclaw"
touch "$HOME/.openclaw/openclaw.json"
# A running gateway answers the first probe; its handoff URL opens as the app.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $* == *--json* ]] &&
echo '{"ok":true,"url":"http://127.0.0.1:18789/?token=shared","browserUrl":"http://127.0.0.1:18789/#handoff"}'
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
"$ROOT/bin/omarchy-launch-openclaw"
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#handoff$' "$TEST_LOG" ||
fail "OpenClaw launch opens the running gateway's handoff URL"
pass "OpenClaw launch opens the running gateway's handoff URL"
# A machine whose gateway unit was never enabled (never installed, or an
# install that died after writing the unit) gets `gateway install --force`,
# then is polled until the dashboard answers. Never `dashboard --yes`: since
# 2026.9.1 that neither installs nor starts anything and, once the gateway is
# up, pushes a one-time pairing URL into the clipboard.
cat >"$tmp_dir/bin/openclaw" <<SCRIPT
#!/bin/bash
printf 'openclaw:%s\\n' "\$*" >>"\$TEST_LOG"
if [[ \$* == *--json* ]]; then
[[ -f $tmp_dir/gateway-up ]] || { echo '{"ok":false,"reason":"Gateway is not running."}'; exit 1; }
echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#cold-start"}'
elif [[ \$1 == gateway ]]; then
touch "$tmp_dir/gateway-up"
fi
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rm -f "$tmp_dir/gateway-up" "$tmp_dir/gateway-enabled"
mkdir -p "$HOME/.config/systemd/user"
touch "$HOME/.config/systemd/user/openclaw-gateway.service"
: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-openclaw"
grep -q '^openclaw:gateway install --force$' "$TEST_LOG" ||
fail "OpenClaw launch installs a never-enabled gateway before opening the app"
! grep -q '^openclaw:gateway start$' "$TEST_LOG" ||
fail "OpenClaw launch installs a never-enabled gateway before opening the app" "started the half-installed unit instead"
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#cold-start$' "$TEST_LOG" ||
fail "OpenClaw launch installs a never-enabled gateway before opening the app" "webapp never opened"
pass "OpenClaw launch installs a never-enabled gateway before opening the app"
# An enabled but stopped unit is started, not reinstalled.
touch "$tmp_dir/gateway-enabled"
rm -f "$tmp_dir/gateway-up"
: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-openclaw"
grep -q '^openclaw:gateway start$' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app"
! grep -q '^openclaw:gateway install' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app" "reinstalled the unit"
! grep -q -- '--yes' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app" "fell back to dashboard --yes"
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#cold-start$' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app" "webapp never opened"
pass "OpenClaw launch starts a stopped gateway before opening the app"
rm -f "$tmp_dir/gateway-enabled"
# A dashboard probe that hangs is cut off, so the launch still fails cleanly
# instead of sitting forever behind an app-grid icon with no terminal.
cat >"$tmp_dir/bin/timeout" <<SCRIPT
#!/bin/bash
printf 'timeout:%s\\n' "\$1" >>"\$TEST_LOG"
exit 124
SCRIPT
chmod +x "$tmp_dir/bin/timeout"
: >"$TEST_LOG"
rc=0
"$ROOT/bin/omarchy-launch-openclaw" >/dev/null 2>&1 || rc=$?
[[ $rc != 0 ]] || fail "OpenClaw launch bounds a hanging dashboard probe"
grep -q '^timeout:10$' "$TEST_LOG" || fail "OpenClaw launch bounds a hanging dashboard probe" "probe ran without a timeout"
pass "OpenClaw launch bounds a hanging dashboard probe"
rm -f "$tmp_dir/bin/timeout"
# A gateway that never answers fails the launch instead of opening a dead page.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $* == *--json* ]] && exit 1
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
webapp_calls_before=$(grep -c '^omarchy-launch-webapp:' "$TEST_LOG" || true)
rc=0
"$ROOT/bin/omarchy-launch-openclaw" >/dev/null 2>&1 || rc=$?
webapp_calls_after=$(grep -c '^omarchy-launch-webapp:' "$TEST_LOG" || true)
[[ $rc != 0 ]] || fail "OpenClaw launch fails cleanly when the gateway never comes up"
[[ $webapp_calls_before == "$webapp_calls_after" ]] ||
fail "OpenClaw launch fails cleanly when the gateway never comes up" "webapp opened anyway"
pass "OpenClaw launch fails cleanly when the gateway never comes up"
# --tui runs onboarding in the terminal it is already in, then attaches to the
# gateway with `openclaw tui` -- never the embedded chat, which the running
# gateway's state-directory lock would refuse.
rm -f "$HOME/.openclaw/openclaw.json"
cat >"$tmp_dir/bin/omarchy-openclaw-onboard" <<SCRIPT
#!/bin/bash
printf 'omarchy-openclaw-onboard:%s\n' "\$*" >>"\$TEST_LOG"
mkdir -p "\$HOME/.openclaw" && touch "\$HOME/.openclaw/openclaw.json"
SCRIPT
chmod +x "$tmp_dir/bin/omarchy-openclaw-onboard"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $* == *--json* ]] && echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#tui"}'
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
floating_calls_before=$(grep -c '^omarchy-launch-floating-terminal-with-presentation:' "$TEST_LOG" || true)
"$ROOT/bin/omarchy-launch-openclaw" --tui
grep -q '^omarchy-openclaw-onboard:$' "$TEST_LOG" ||
fail "--tui onboards in the current terminal" "$(grep 'onboard' "$TEST_LOG")"
floating_calls_after=$(grep -c '^omarchy-launch-floating-terminal-with-presentation:' "$TEST_LOG" || true)
[[ $floating_calls_before == "$floating_calls_after" ]] ||
fail "--tui onboards in the current terminal" "spawned a floating terminal"
grep -q '^openclaw:tui$' "$TEST_LOG" || fail "--tui attaches to the gateway"
pass "--tui onboards in place and attaches to the gateway"
# The stub also logs argv one entry per line, so a prompt split into words
# would show up as extra lines rather than pass a space-joined comparison.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == tui ]] && printf 'argv:[%s]\n' "$@" >>"$TEST_LOG"
[[ $* == *--json* ]] && echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#tui"}'
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-openclaw" --tui --message "Review this project"
[[ $(grep -c '^argv:' "$TEST_LOG") == 3 ]] && grep -q '^argv:\[Review this project\]$' "$TEST_LOG" ||
fail "--tui seeds the session through --message" "$(grep '^argv:' "$TEST_LOG" | tr '\n' ' ')"
! grep -q '^omarchy-launch-webapp:' "$TEST_LOG" ||
fail "--tui seeds the session through --message" "webapp opened instead"
pass "--tui seeds the session through --message"
@@ -1,111 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1786482992.sh"
packaged_defaults="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" ||
fail "the packaged Limine defaults still unpack the initramfs synchronously"
pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin"
: >"$calls"
cat >"$stub_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
(( ${LIMINE_MKINITCPIO_INSTALLED:-1} == 1 ))
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo' >>"$TEST_LOG"
printf '\t%s' "$@" >>"$TEST_LOG"
printf '\n' >>"$TEST_LOG"
"$@"
SH
cat >"$stub_bin/limine-mkinitcpio" <<'SH'
#!/bin/bash
echo 'limine-mkinitcpio' >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
defaults_conf="$test_tmp/omarchy-defaults.conf"
running_cmdline="$test_tmp/cmdline"
rebuild_marker="$test_tmp/rebuild-complete"
cp "$packaged_defaults" "$defaults_conf"
# A boot image baked before quattro's defaults landed: the pre-quattro command
# line plus the machine's own root parameters, and none of the new ones.
stale_cmdline='quiet splash cryptdevice=PARTUUID=fake:root root=/dev/mapper/root rw'
# Everything the packaged defaults ask for, as a rebuilt image would carry it.
configured=$(sed -n 's/^KERNEL_CMDLINE\[default\]+="\(.*\)"[[:space:]]*$/\1/p' "$defaults_conf" | tr '\n' ' ')
current_cmdline="cryptdevice=PARTUUID=fake:root root=/dev/mapper/root rw $configured"
run_migration() {
PATH="$stub_bin:$PATH" \
TEST_LOG="$calls" \
OMARCHY_LIMINE_DEFAULTS_CONF="$defaults_conf" \
OMARCHY_RUNNING_CMDLINE="$running_cmdline" \
OMARCHY_LIMINE_REBUILD_MARKER="$rebuild_marker" \
bash -euo pipefail "$migration" >/dev/null
}
echo "$stale_cmdline" >"$running_cmdline"
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "a boot image older than the Limine defaults is rebuilt"
[[ -f $rebuild_marker ]] || fail "the rebuild records the machine-wide repair"
pass "migration rebuilds a boot image that predates the Limine defaults"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a recorded rebuild is not repeated" "$(cat "$calls")"
pass "migration is machine-idempotent before reboot"
rm -f "$rebuild_marker"
: >"$calls"
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "an interrupted rebuild is retried"
[[ -f $rebuild_marker ]] || fail "a retried rebuild records completion"
pass "migration retries an interrupted rebuild"
echo "$current_cmdline" >"$running_cmdline"
rm -f "$rebuild_marker"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a boot image matching the defaults is left alone" "$(cat "$calls")"
[[ ! -e $rebuild_marker ]] || fail "an untouched machine is not marked as repaired"
pass "migration skips a boot image that already carries the defaults"
echo "$stale_cmdline" >"$running_cmdline"
: >"$calls"
LIMINE_MKINITCPIO_INSTALLED=0 run_migration
[[ ! -s $calls ]] || fail "installs without limine-mkinitcpio are skipped" "$(cat "$calls")"
mv "$defaults_conf" "$defaults_conf.away"
run_migration
mv "$defaults_conf.away" "$defaults_conf"
[[ ! -s $calls ]] || fail "installs without the Limine defaults are skipped" "$(cat "$calls")"
pass "migration skips installs it does not apply to"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
packaged_defaults="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" ||
fail "the packaged Limine defaults still unpack the initramfs synchronously"
pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt"
+1 -1
View File
@@ -69,7 +69,7 @@ PATH="$stub_bin:$PATH" XDG_CACHE_HOME="$cache_home" \
fail "image menu recovers thumbnails from stranded locks"
(( $(awk 'END { print NR }' "$cache_dir/$cache_key.rows") == 3 )) ||
fail "image menu rebuilds every row after cache invalidation"
[[ $(head -n 1 "$cache_dir/$cache_key.signature") == "v3" ]] ||
[[ $(head -n 1 "$cache_dir/$cache_key.signature") == "v4" ]] ||
fail "image menu invalidates stale row caches"
[[ ! -e $stale_tmp ]] ||
fail "image menu clears partial thumbnails left by killed generators"
+7 -3
View File
@@ -225,8 +225,12 @@ const expectedAgents = {
codex: { icon: '\ue905', iconFont: 'omarchy', label: 'Codex' },
grok: { icon: '\ue904', iconFont: 'omarchy', label: 'Grok' },
hermes: { icon: '\ue90a', iconFont: 'omarchy', label: 'Hermes' },
openclaw: { icon: '\ue90c', iconFont: 'omarchy', label: 'OpenClaw' },
copilot: { icon: '', label: 'Copilot' },
crush: { icon: '󰋑', label: 'Crush' },
muse: { icon: '󰛤', label: 'Muse Code' },
'cursor-agent': { icon: '\ue90d', iconFont: 'omarchy', label: 'Cursor CLI' },
}
assert(
Object.entries(expectedAgents).every(([agent, expected]) => {
@@ -239,13 +243,13 @@ assert(
&& !entry.when
&& entry.checked.includes(`== \"${agent}\"`)
}),
'menu exposes every mise-installable coding agent with its own glyph under Defaults > Agent'
'menu exposes every supported coding agent with its own glyph under Defaults > Agent'
)
assertDeepEqual(
defaultItems
.filter(item => item.parent === 'setup.default.agent')
.map(item => item.label),
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenCode', 'Ori', 'Pi'],
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Cursor CLI', 'Grok', 'Hermes', 'Muse Code', 'omp', 'OpenClaw', 'OpenCode', 'Ori', 'Pi'],
'menu sorts coding agents alphabetically'
)
const expectedDefaults = {
@@ -637,5 +641,5 @@ assert(
JS
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
[[ $font_charset == *"e900-e90a"* ]] || fail "Omarchy icon font includes every custom menu glyph"
[[ $font_charset == *"e900-e90d"* ]] || fail "Omarchy icon font includes every custom menu glyph"
pass "Omarchy icon font includes the official agent marks"
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const network = requireFromRoot('shell/plugins/panels/network/Model.js')
const states = { Unknown: 0, None: 1, Portal: 2, Limited: 3, Full: 4 }
for (const kind of ['wifi', 'ethernet']) {
for (const [native, expected] of [
['Unknown', 'unknown'], ['None', 'none'], ['Portal', 'portal'],
['Limited', 'limited'], ['Full', 'full']
]) {
assertEqual(network.connectivityState(kind, states[native], states, true), expected,
`${kind} maps native ${native} connectivity without confusing an outage with a portal`)
}
assertEqual(network.connectivityState(kind, 99, states, true), 'unknown', `${kind} handles unknown connectivity`)
for (const native of Object.values(states)) {
assertEqual(network.connectivityState(kind, native, states, false), 'unknown', `${kind} ignores stale results with probing disabled (${native})`)
}
}
for (const native of Object.values(states)) {
assertEqual(network.connectivityState('disconnected', native, states, true), 'none', `disconnect clears stale connectivity (${native})`)
}
for (const state of ['portal', 'limited']) {
assertEqual(network.connectionIcon('wifi', 80, state), '󰤩', `${state} uses a blocked Wi-Fi icon`)
assertEqual(network.connectionIcon('ethernet', 80, state), '󰈂', `${state} uses a blocked Ethernet icon`)
assertEqual(network.connectionIcon('disconnected', 80, state), '󰤮', `${state} does not override the disconnected icon`)
}
for (const state of ['full', 'unknown', 'none', undefined]) {
for (const signal of [-1, 0, 20, 40, 60, 80, 100]) {
assertEqual(network.connectionIcon('wifi', signal, state), network.wifiIconFor(signal), `${state} preserves Wi-Fi strength ${signal}`)
}
assertEqual(network.connectionIcon('ethernet', -1, state), '󰈀', `${state} preserves the Ethernet icon`)
}
const url = new URL(network.captivePortalUrl)
assertEqual(url.protocol, 'http:', 'browser entry point uses plain HTTP so a portal can intercept it')
assertEqual(url.hostname, 'ping.archlinux.org', 'browser entry point is fixed rather than portal-supplied')
assertEqual(url.username + url.password, '', 'browser entry point contains no credentials')
JS
require_compositor "network captive-portal runtime test"
require_command quickshell
stage=$(mktemp -d)
trap 'rm -rf -- "$stage"' EXIT
fixture="$SHELL_TEST_DIR/fixtures/network-captive-portal"
mkdir -p "$stage/network" "$stage/bin" "$stage/home"
ln -s "$ROOT/shell/Ui" "$stage/Ui"
ln -s "$ROOT/shell/Commons" "$stage/Commons"
cp -r "$fixture/mocks" "$stage/mocks"
cp "$fixture/shell.qml" "$stage/shell.qml"
cp "$ROOT/shell/plugins/panels/network/Model.js" "$stage/network/Model.js"
node - "$ROOT" "$stage" <<'JS'
const fs = require('fs')
const [root, stage] = process.argv.slice(2)
let source = fs.readFileSync(`${root}/shell/plugins/panels/network/Panel.qml`, 'utf8')
// Keep installed enum values and actual UI bindings. Only replace the singleton
// and expose private IDs in the disposable copy, never in production code.
source = source.replace('import Quickshell.Networking', 'import Quickshell.Networking\nimport "../mocks"')
source = source.replace(/\bNetworking\./g, 'NetworkMock.')
source = source.replace(' id: root', ` id: root
property alias testButton: portalAction
property alias testKeys: keyCatcher
property alias testMeta: heroMeta
property alias testTitle: heroSsid
property alias testPoll: connectivityPoll
property alias testBarButton: button`)
fs.writeFileSync(`${stage}/network/Panel.qml`, source)
JS
printf '#!/bin/bash\nexit 0\n' > "$stage/bin/noop"
chmod +x "$stage/bin/noop"
for command in omarchy-dns omarchy-network-band; do
ln -s noop "$stage/bin/$command"
done
# Preview uses only synthetic details, never the host's SSID or addresses.
# Normal assertions keep the details empty to exercise missing-route handling.
printf '#!/bin/bash\nif [[ -n ${NETWORK_TEST_PREVIEW:-} ]]; then\n printf "type\\twifi\\niface\\ttest-wifi\\nssid\\tGuest Wi-Fi\\nip\\t192.0.2.10\\ngateway\\t192.0.2.1\\n"\nfi\n' > "$stage/bin/omarchy-network-status"
chmod +x "$stage/bin/omarchy-network-status"
printf '#!/bin/bash\nprintf "%%s\\n" "$@" >> "$NETWORK_TEST_BROWSER_LOG"\n' > "$stage/bin/omarchy-launch-browser"
chmod +x "$stage/bin/omarchy-launch-browser"
# All networking and external actions are mocked; the real connection and
# browser are never touched, and the fixture writes only to its scratch HOME.
output=$(HOME="$stage/home" OMARCHY_PATH="$ROOT" PATH="$stage/bin:$PATH" \
NETWORK_TEST_BROWSER_LOG="$stage/browser.log" \
timeout 30 quickshell -p "$stage" --no-color 2>&1) || fail "network portal fixture exits cleanly" "$output"
[[ $output == *"RESULT pass"* ]] || fail "network portal runtime assertions pass" "$output"
if rg -q 'RESULT fail|ReferenceError|TypeError|Error:|Unable to assign|Binding loop' <<< "$output"; then
fail "network portal fixture has no QML errors" "$output"
fi
[[ -f $stage/browser.log ]] || fail "portal action launches the browser"
[[ $(<"$stage/browser.log") == "http://ping.archlinux.org/nm-check.txt" ]] || fail "portal opens exactly one fixed HTTP URL"
pass "network portal, recovery, disabled checks, outage, disconnect, keyboard navigation, and browser argv work in QML"
@@ -6,7 +6,6 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
dns="$ROOT/bin/omarchy-dns"
hardware_network="$ROOT/install/hardware/network.sh"
migration="$ROOT/migrations/1782002156.sh"
! grep -F 'systemd-networkd' "$dns" >/dev/null || fail "omarchy-dns no longer restarts systemd-networkd"
grep -F 'NetworkManager/conf.d/20-omarchy-dns.conf' "$dns" >/dev/null
@@ -26,10 +25,3 @@ grep -F 'systemd-networkd.socket' "$hardware_network" >/dev/null
grep -F '20-wlan.network' "$hardware_network" >/dev/null
grep -F 'omarchy-networkd-retired' "$hardware_network" >/dev/null
pass "hardware setup retires archinstall networkd state"
grep -F 'OMARCHY_UPGRADE_TO_QUATTRO_LIVE' "$migration" >/dev/null
grep -F 'systemctl disable --now "$unit"' "$migration" >/dev/null
grep -F 'systemctl stop systemd-networkd.service' "$migration" >/dev/null
grep -F 'NetworkManager.service' "$migration" >/dev/null
grep -F '20-wlan.network' "$migration" >/dev/null
pass "migration repairs upgraded systems with networkd still active"
-131
View File
@@ -1,131 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1786605598.sh"
packaged_hooks="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin"
: >"$calls"
cat >"$stub_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
(( ${LIMINE_MKINITCPIO_INSTALLED:-1} == 1 ))
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo' >>"$TEST_LOG"
printf '\t%s' "$@" >>"$TEST_LOG"
printf '\n' >>"$TEST_LOG"
"$@"
SH
cat >"$stub_bin/limine-mkinitcpio" <<'SH'
#!/bin/bash
echo 'limine-mkinitcpio' >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
hooks_conf="$test_tmp/omarchy_hooks.conf"
nvidia_conf="$test_tmp/nvidia.conf"
rebuild_marker="$test_tmp/rebuild-complete"
cp "$packaged_hooks" "$hooks_conf"
echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' >"$nvidia_conf"
# Each argument is a PCI device as "vendor:class", in sysfs's own format.
write_pci_devices() {
rm -rf "$test_tmp/devices"
mkdir -p "$test_tmp/devices"
local index=0
local spec
for spec in "$@"; do
local slot
slot=$(printf '0000:%02x:00.0' "$index")
mkdir -p "$test_tmp/devices/$slot"
printf '%s\n' "${spec%%:*}" >"$test_tmp/devices/$slot/vendor"
printf '%s\n' "${spec##*:}" >"$test_tmp/devices/$slot/class"
index=$((index + 1))
done
}
run_migration() {
PATH="$stub_bin:$PATH" \
TEST_LOG="$calls" \
OMARCHY_MKINITCPIO_HOOKS_CONF="$hooks_conf" \
OMARCHY_MKINITCPIO_NVIDIA_CONF="$nvidia_conf" \
OMARCHY_KMS_REBUILD_MARKER="$rebuild_marker" \
OMARCHY_PCI_DEVICES_PATH="$test_tmp/devices" \
bash -euo pipefail "$migration" >/dev/null
}
# NVIDIA-only machine with the proprietary driver: the packaged conditional
# drops kms, so the stale initramfs must be rebuilt once.
write_pci_devices 0x10de:0x030000
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "an NVIDIA-only machine rebuilds its initramfs"
[[ -f $rebuild_marker ]] || fail "the rebuild records the machine-wide repair"
pass "migration rebuilds the initramfs on an NVIDIA-only machine"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a recorded rebuild is not repeated" "$(cat "$calls")"
pass "migration is machine-idempotent across users"
rm -f "$rebuild_marker"
: >"$calls"
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "an interrupted rebuild is retried"
pass "migration retries an interrupted rebuild"
# Hybrid machine: the conditional keeps kms, so the initramfs already matches.
write_pci_devices 0x1002:0x030000 0x10de:0x030200
rm -f "$rebuild_marker"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a hybrid machine is left alone" "$(cat "$calls")"
[[ ! -e $rebuild_marker ]] || fail "an untouched machine is not marked as repaired"
pass "migration skips a hybrid machine that keeps kms"
# A user-edited hooks conf predating the conditional (the packaged update sits
# in a .pacnew) still carries kms unconditionally: nothing to rebuild for.
write_pci_devices 0x10de:0x030000
echo 'HOOKS=(base udev autodetect modconf kms block filesystems fsck)' >"$hooks_conf"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a user-edited hooks conf is left alone" "$(cat "$calls")"
pass "migration skips a hooks conf without the conditional"
cp "$packaged_hooks" "$hooks_conf"
: >"$calls"
LIMINE_MKINITCPIO_INSTALLED=0 run_migration
[[ ! -s $calls ]] || fail "installs without limine-mkinitcpio are skipped" "$(cat "$calls")"
mv "$nvidia_conf" "$nvidia_conf.away"
run_migration
mv "$nvidia_conf.away" "$nvidia_conf"
[[ ! -s $calls ]] || fail "machines without the proprietary NVIDIA driver are skipped" "$(cat "$calls")"
pass "migration skips installs it does not apply to"
+288
View File
@@ -0,0 +1,288 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
export HOME="$tmp_dir/home"
export OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS=0
unit="$HOME/.config/systemd/user/openclaw-gateway.service"
export unit
# systemd and ss stand in for themselves. Once the wizard has written the unit
# it is running as pid 4242, and that same process holds the gateway port
# unless a test plants a different listener (an orphan) in $listener_file.
listener_file="$tmp_dir/listener"
export listener_file
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
if [[ $* == *MainPID* ]]; then
[[ -f $unit ]] && echo 4242 || echo 0
exit 0
fi
[[ $* == *is-active* ]] && [[ -f $unit ]]
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
cat >"$tmp_dir/bin/ss" <<'SCRIPT'
#!/bin/bash
pid=
[[ -f $listener_file ]] && pid=$(cat "$listener_file")
[[ -z $pid && -f $unit ]] && pid=4242
[[ -n $pid ]] && echo "LISTEN 0 511 127.0.0.1:18789 0.0.0.0:* users:((\"node\",pid=$pid,fd=3))"
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/ss"
export OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT=1
# A wizard that configures OpenClaw, prints its outro, and then lingers forever
# on an open handle: the 2026.9.1 --skip-ui behaviour. It "installs the
# gateway" by writing the config a moment in, so the gateway only starts
# answering partway through, never at once.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard)
( sleep 1; mkdir -p "$HOME/.openclaw" "${unit%/*}"; touch "$HOME/.openclaw/openclaw.json" "$unit" ) &
trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM
while :; do sleep 0.2; done
;;
dashboard)
[[ -f $HOME/.openclaw/openclaw.json ]] && echo '{"ok":true,"port":18789}' || { echo '{"ok":false}'; exit 1; }
;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
start=$SECONDS
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
elapsed=$((SECONDS - start))
grep -q '^openclaw:onboard --flow quickstart --install-daemon --skip-ui$' "$TEST_LOG" ||
fail "onboarding runs the classic quickstart wizard with the service install" "$(grep '^openclaw:onboard' "$TEST_LOG" || true)"
pass "onboarding runs the classic quickstart wizard with the service install"
[[ $rc == 0 ]] || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" ||
fail "a wizard that lingers after the gateway is up is stopped and counts as success" "wizard was never signalled"
(( elapsed < 30 )) || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "took ${elapsed}s"
pass "a wizard that lingers after the gateway is up is stopped and counts as success"
# The wizard only starts being stopped once the gateway actually answers: a
# stub that never writes the config is left alone and must be ended by its own
# exit, not the watcher.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == onboard ]] && { echo "Skipped for now."; exit 3; }
echo '{"ok":false}'; exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 3 ]] || fail "an abandoned wizard passes its exit code through" "rc=$rc"
[[ ! -f $HOME/.openclaw/openclaw.json ]] || fail "an abandoned wizard passes its exit code through" "config appeared"
! grep -q '^terminated$' "$TEST_LOG" || fail "an abandoned wizard passes its exit code through" "watcher signalled it anyway"
pass "an abandoned wizard passes its exit code through"
# A wizard that finishes and exits on its own is simply waited for.
: >"$TEST_LOG"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == onboard ]] && { mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"; exit 0; }
echo '{"ok":true,"port":18789}'
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "a wizard that exits cleanly is waited for" "rc=$rc"
pass "a wizard that exits cleanly is waited for"
# Input reaches the wizard: it runs in the foreground, not backgrounded (which
# would stop it on SIGTTIN the moment it read stdin).
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard)
read -r answer
printf 'answer:%s\n' "$answer" >>"$TEST_LOG"
mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"
exit 0
;;
dashboard) echo '{"ok":true,"port":18789}' ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
printf 'gpt-5\n' | "$ROOT/bin/omarchy-openclaw-onboard" >/dev/null 2>&1
grep -q '^answer:gpt-5$' "$TEST_LOG" ||
fail "the wizard reads terminal input" "$(grep '^answer' "$TEST_LOG" || echo 'no input reached it')"
pass "the wizard reads terminal input"
# An already-configured machine whose gateway answers is not onboarded again:
# the wizard's repair pass would otherwise be stopped mid-prompt the moment
# the watcher saw the pre-existing gateway.
: >"$TEST_LOG"
mkdir -p "$HOME/.openclaw" && touch "$HOME/.openclaw/openclaw.json"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == onboard ]] && { echo wizard-ran >>"$TEST_LOG"; exit 0; }
echo '{"ok":true,"port":18789}'
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "an already-running OpenClaw is left alone" "rc=$rc"
! grep -q '^wizard-ran$' "$TEST_LOG" || fail "an already-running OpenClaw is left alone" "wizard ran anyway"
pass "an already-running OpenClaw is left alone"
# Setup applied but the gateway never answers: the wizard would linger
# forever, so it is stopped after the deadline and that is reported as failure.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard)
mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"
trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM
while :; do sleep 0.2; done
;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
start=$SECONDS
rc=0
err=$("$ROOT/bin/omarchy-openclaw-onboard" </dev/null 2>&1 >/dev/null) || rc=$?
elapsed=$((SECONDS - start))
[[ $rc == 1 ]] || fail "a gateway that never comes up ends the wait with a failure" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" || fail "a gateway that never comes up ends the wait with a failure" "wizard left running"
[[ $err == *"gateway status"* ]] || fail "a gateway that never comes up ends the wait with a failure" "no diagnostic: $err"
(( elapsed < 30 )) || fail "a gateway that never comes up ends the wait with a failure" "took ${elapsed}s"
pass "a gateway that never comes up ends the wait with a failure"
# A signal at this script alone takes the wizard down with it.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 &
wrapper=$!
sleep 1
kill -TERM "$wrapper"
wait "$wrapper" 2>/dev/null || true
sleep 1
grep -q '^terminated$' "$TEST_LOG" || fail "stopping the wrapper stops the wizard" "wizard survived"
! pgrep -f "$tmp_dir/bin/openclaw onboard" >/dev/null || fail "stopping the wrapper stops the wizard" "wizard process still alive"
pass "stopping the wrapper stops the wizard"
# A config left by an earlier, incomplete setup does not start the gateway
# deadline: only this run applying setup does. The wizard here takes longer
# than the deadline at its prompts, never touches the old config, and must be
# left to finish on its own.
: >"$TEST_LOG"
mkdir -p "$HOME/.openclaw" && touch "$HOME/.openclaw/openclaw.json"
sleep 1
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; sleep 4; echo finished >>"$TEST_LOG"; exit 0 ;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "a stale config does not start the gateway deadline" "rc=$rc"
grep -q '^finished$' "$TEST_LOG" || fail "a stale config does not start the gateway deadline" "wizard was cut short"
! grep -q '^terminated$' "$TEST_LOG" || fail "a stale config does not start the gateway deadline" "wizard was signalled"
pass "a stale config does not start the gateway deadline"
# ...while the same stale config being rewritten by this run does arm it.
: >"$TEST_LOG"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) touch "$HOME/.openclaw/openclaw.json"; trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 1 ]] || fail "a config rewritten by this run arms the gateway deadline" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" || fail "a config rewritten by this run arms the gateway deadline" "wizard left running"
pass "a config rewritten by this run arms the gateway deadline"
# A gateway that answers while no config exists is not this run's: with no
# config `openclaw dashboard --json` still probes the default port, so a
# process left behind by something else (an earlier guided onboarding's
# foreground gateway) would otherwise end a wizard still at its first prompt.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; sleep 4; echo finished >>"$TEST_LOG"; exit 0 ;;
dashboard) echo '{"ok":true,"port":18789}' ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "an orphaned gateway answering with no config does not end the wizard" "rc=$rc"
grep -q '^finished$' "$TEST_LOG" || fail "an orphaned gateway answering with no config does not end the wizard" "wizard was cut short"
! grep -q '^terminated$' "$TEST_LOG" || fail "an orphaned gateway answering with no config does not end the wizard" "wizard was signalled"
pass "an orphaned gateway answering with no config does not end the wizard"
# And after setup, an orphan still bound to the port is not success even
# while the unit reports active: Type=simple counts it active from the fork,
# before it has discovered the port is taken. The port's owner has to be the
# unit's own main process. Here it never is, so the deadline ends the wait as
# a failure rather than opening the app on the orphan.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
echo 999 >"$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) mkdir -p "$HOME/.openclaw" "${unit%/*}"; touch "$HOME/.openclaw/openclaw.json" "$unit"; trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
dashboard) echo '{"ok":true,"port":18789}' ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 1 ]] || fail "an orphan holding the port is not mistaken for the active unit" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" || fail "an orphan holding the port is not mistaken for the active unit" "wizard left running"
grep -q '^systemctl:--user show -p MainPID --value openclaw-gateway.service$' "$TEST_LOG" ||
fail "an orphan holding the port is not mistaken for the active unit" "unit ownership never checked"
pass "an orphan holding the port is not mistaken for the active unit"
rm -f "$listener_file"
+13
View File
@@ -197,5 +197,18 @@ for (const [id, section] of Object.entries({
}
check(byId['omarchy.media']?.barWidget?.defaultSection === undefined, 'omarchy.media must use the center fallback')
for (const id of ['omarchy.lock', 'omarchy.idle', 'omarchy.polkit', 'omarchy.notifications', 'omarchy.media']) {
check(byId[id]?.keepLoaded === true, `${id} must stay loaded across plugin reloads`)
}
const shellSource = fs.readFileSync(path.join(root, 'shell/shell.qml'), 'utf8')
const unloadMatch = shellSource.match(/function unloadPluginServices\(\) \{[\s\S]*?\n \}/)
check(!!unloadMatch, 'unloadPluginServices is defined')
check(!!unloadMatch && /serviceKeepLoaded/.test(unloadMatch[0]), 'unloadPluginServices honors keepLoaded')
check(
/function _syncServices\(\) \{[\s\S]*Drop services for plugins that have been disabled/.test(shellSource),
'_syncServices still drops disabled or removed services'
)
assert(errors.length === 0, 'plugin manifests match shell registry contract', errors.join('\n'))
JS
+256
View File
@@ -15,6 +15,22 @@ printf 'drop:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/omarchy-pkg-drop"
# omarchy-remove-ai-perplexity asks through gum whether the user's data goes
# too. The stub answers "no" unless a test says otherwise and logs the call: a
# real gum would hang the run, and one that answered "yes" on its own would be
# the data loss the default-no exists to prevent. It logs to its own file
# because the OpenClaw section below greps TEST_LOG to prove gum was never
# reached, and the pty runs here call it on purpose.
cat >"$tmp_dir/bin/gum" <<'SCRIPT'
#!/bin/bash
printf 'gum:%s\n' "$*" >>"$TEST_GUM_LOG"
exit "${TEST_GUM_STATUS:-1}"
SCRIPT
chmod +x "$tmp_dir/bin/gum"
export TEST_GUM_LOG="$tmp_dir/gum-log"
touch "$TEST_GUM_LOG"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
@@ -75,6 +91,88 @@ for kept in .grok .claude.json .npm .local/share/opencode; do
done
pass "T3 Code removal keeps the agent state it bootstrapped"
# Perplexity's other products (the CLI, Comet) share the perplexity-* prefix;
# the desktop app owns only its rpc-server runtime and Electron caches. The
# logins, vault, and flags are the user's and only go when a terminal said so.
seed_perplexity() {
fresh_home
mkdir -p "$HOME/.config/Perplexity" "$HOME/.cache/Perplexity" \
"$HOME/.cache/perplexity-rpc-server" "$HOME/.local/share/perplexity-rpc-server" \
"$HOME/.local/state/perplexity" "$HOME/.cache/perplexity-personal-computer-poc"
touch "$HOME/.config/perplexity-flags.conf"
}
# </dev/null pins stdin off a terminal, so this run exercises the
# non-interactive path no matter where the suite itself is running.
seed_perplexity
"$ROOT/bin/omarchy-remove-ai-perplexity" </dev/null >/dev/null
for gone in .cache/Perplexity .cache/perplexity-rpc-server .local/share/perplexity-rpc-server; do
[[ ! -e $HOME/$gone ]] || fail "Perplexity removal deletes the app's runtime and caches" "$gone"
done
pass "Perplexity removal deletes the app's runtime and caches"
for kept in .config/Perplexity .local/state/perplexity .config/perplexity-flags.conf .cache/perplexity-personal-computer-poc; do
[[ -e $HOME/$kept ]] || fail "Perplexity removal keeps the user's data and other products' caches" "$kept"
done
pass "Perplexity removal keeps the user's data and other products' caches"
grep -qx 'drop:perplexity' "$TEST_LOG" || fail "Perplexity removal drops the perplexity package"
pass "Perplexity removal drops the perplexity package"
# Without a terminal there is nobody to ask, so gum must not even be reached:
# one that answered "yes" on its own would be a data loss.
! grep -q '^gum:' "$TEST_GUM_LOG" || fail "Perplexity removal keeps the user's data unasked when there is no terminal"
pass "Perplexity removal keeps the user's data unasked when there is no terminal"
# script(1) puts the remover on a pty, the only way -t 0 answers true in a
# test; the stubbed gum then supplies the answer.
seed_perplexity
script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity'" /dev/null >/dev/null 2>&1
[[ -d $HOME/.config/Perplexity && -d $HOME/.local/state/perplexity && -f $HOME/.config/perplexity-flags.conf ]] ||
fail "Perplexity removal keeps the user's data when the answer is no"
pass "Perplexity removal keeps the user's data when the answer is no"
# The stub answers "no" on its own, so only the logged arguments prove the
# real gum would not default to yes on a bare Enter.
grep -q '^gum:confirm --default=false ' "$TEST_GUM_LOG" ||
fail "Perplexity removal asks with the destructive answer defaulted off"
pass "Perplexity removal asks with the destructive answer defaulted off"
# A partial install has no user data to measure; du failing must not abort
# the removal between the package and the prompt.
seed_perplexity
rm -rf "$HOME/.config/Perplexity" "$HOME/.local/state/perplexity"
script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity'" /dev/null >/dev/null 2>&1 ||
fail "Perplexity removal survives user-data directories that are already gone"
pass "Perplexity removal survives user-data directories that are already gone"
# gum draws its prompt on stderr; with stderr redirected the question would be
# invisible, so the remover must keep the data instead of blocking on it.
seed_perplexity
gum_calls_before=$(wc -l <"$TEST_GUM_LOG")
script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity' 2>/dev/null" /dev/null >/dev/null 2>&1 ||
fail "Perplexity removal completes when stderr is not a terminal"
[[ -d $HOME/.config/Perplexity ]] && (( $(wc -l <"$TEST_GUM_LOG") == gum_calls_before )) ||
fail "Perplexity removal keeps the user's data unasked when stderr is not a terminal"
pass "Perplexity removal keeps the user's data unasked when stderr is not a terminal"
seed_perplexity
TEST_GUM_STATUS=0 script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity'" /dev/null >/dev/null 2>&1
for gone in .config/Perplexity .local/state/perplexity .config/perplexity-flags.conf; do
[[ ! -e $HOME/$gone ]] || fail "Perplexity removal deletes the user's data on an explicit yes" "$gone"
done
pass "Perplexity removal deletes the user's data on an explicit yes"
# Without HOME the rm -rf paths would degrade to /-rooted ones; -u makes that a
# refusal instead.
if env -u HOME "$ROOT/bin/omarchy-remove-ai-perplexity" </dev/null >/dev/null 2>&1; then
fail "Perplexity removal refuses to run without HOME"
fi
pass "Perplexity removal refuses to run without HOME"
# ~/.grok belongs to the Grok CLI that omarchy-default-agent installs.
fresh_home
mkdir -p "$HOME/.config/Grok Bot" "$HOME/.grokbot" "$HOME/.grok"
@@ -93,3 +191,161 @@ ollama_row=$(grep '^ "remove.ai.ollama":' "$ROOT/default/omarchy/omarchy-menu.j
[[ $ollama_row == *'"when":"omarchy-pkg-present ollama"'* ]] ||
fail "Ollama removal is offered only where the package is installed" "$ollama_row"
pass "Ollama removal is offered only where the package is installed"
# OpenClaw's gateway unit and web app launcher are the app's own; the agent
# state in ~/.openclaw is the user's. systemctl and openclaw are stubbed so the
# sandbox never reaches the real user manager or a real gateway.
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
# Default openclaw stub: the packaged CLI predates `gateway uninstall`, so the
# remover has to fall back to its manual systemd path.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
fresh_openclaw_home() {
fresh_home
mkdir -p "$HOME/.config/systemd/user/default.target.wants" "$HOME/.openclaw" \
"$HOME/.local/share/applications" "$HOME/.local/share/icons/hicolor/256x256/apps"
touch "$HOME/.config/systemd/user/openclaw-gateway.service" \
"$HOME/.config/systemd/user/openclaw-gateway.service.bak" \
"$HOME/.config/systemd/user/openclaw-node.service" \
"$HOME/.openclaw/openclaw.json" \
"$HOME/.local/share/applications/OpenClaw.desktop" \
"$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
ln -s ../openclaw-gateway.service \
"$HOME/.config/systemd/user/default.target.wants/openclaw-gateway.service"
ln -s ../openclaw-node.service \
"$HOME/.config/systemd/user/default.target.wants/openclaw-node.service"
}
# gum would ask about ~/.openclaw; the tests never run on a terminal, so it
# must not even be reached. Logging it proves that.
cat >"$tmp_dir/bin/gum" <<'SCRIPT'
#!/bin/bash
printf 'gum:%s\n' "$*" >>"$TEST_LOG"
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/gum"
fresh_openclaw_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
for gone in .config/systemd/user/openclaw-gateway.service \
.config/systemd/user/openclaw-gateway.service.bak \
.config/systemd/user/default.target.wants/openclaw-gateway.service \
.config/systemd/user/openclaw-node.service \
.config/systemd/user/default.target.wants/openclaw-node.service \
.local/share/applications/OpenClaw.desktop \
.local/share/icons/hicolor/256x256/apps/openclaw.png; do
[[ ! -e $HOME/$gone && ! -L $HOME/$gone ]] || fail "OpenClaw removal deletes the service and launcher it installed" "$gone"
done
pass "OpenClaw removal deletes the service and launcher it installed"
grep -q '^systemctl:--user disable --now openclaw-gateway.service$' "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service"
grep -q '^systemctl:--user disable --now openclaw-node.service$' "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service" "node host service left running"
for unit in openclaw-gateway.service openclaw-node.service; do
grep -q "^systemctl:--user reset-failed $unit\$" "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service" "$unit left in systemd's failed list"
done
pass "OpenClaw removal stops the gateway service"
# Without a terminal there is nobody to ask, so the state stays and gum is
# never invoked (a gum that answered "yes" on its own would be a data loss).
[[ -f $HOME/.openclaw/openclaw.json ]] || fail "OpenClaw removal keeps the user's agent state"
! grep -q '^gum:' "$TEST_LOG" ||
fail "OpenClaw removal keeps the user's agent state" "asked about ~/.openclaw without a terminal"
pass "OpenClaw removal keeps the user's agent state"
# A CLI that knows `gateway uninstall` owns the teardown; the manual systemd
# fallback must not run.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
rm -f "$HOME/.config/systemd/user/openclaw-$1.service" \
"$HOME/.config/systemd/user/default.target.wants/openclaw-$1.service"
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
: >"$TEST_LOG"
fresh_openclaw_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
grep -q '^openclaw:gateway uninstall$' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown"
grep -q '^openclaw:node uninstall$' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown" "node host not handed to upstream"
! grep -q '^systemctl:--user disable' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown" "manual disable ran too"
pass "OpenClaw removal prefers upstream's own gateway teardown"
# Without the unit file there is nothing of ours registered, so systemd stays untouched.
systemctl_calls_before=$(grep -c '^systemctl:' "$TEST_LOG" || true)
fresh_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
systemctl_calls_after=$(grep -c '^systemctl:' "$TEST_LOG" || true)
[[ $systemctl_calls_before == "$systemctl_calls_after" ]] ||
fail "OpenClaw removal leaves systemd alone when onboarding never ran"
pass "OpenClaw removal leaves systemd alone when onboarding never ran"
# A gateway that will not stop aborts the removal before the package drop:
# pacman would otherwise strand the live process on deleted code.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
[[ $2 == disable ]] && exit 1
[[ $2 == is-active ]] && exit 0
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
drop_calls_before=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
fresh_openclaw_home
rc=0
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
drop_calls_after=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when the gateway cannot be stopped"
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "unit file deleted"
[[ $drop_calls_before == "$drop_calls_after" ]] ||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "package dropped anyway"
pass "OpenClaw removal aborts when the gateway cannot be stopped"
# An unreachable user manager is not a stopped gateway: every probe failing
# with no answer must still abort, not read as "confirmed inactive".
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
echo "Failed to connect to user scope bus" >&2
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
: >"$TEST_LOG"
fresh_openclaw_home
rc=0
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when systemd cannot be reached"
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
fail "OpenClaw removal aborts when systemd cannot be reached" "unit file deleted"
! grep -q '^drop:openclaw$' "$TEST_LOG" ||
fail "OpenClaw removal aborts when systemd cannot be reached" "package dropped anyway"
pass "OpenClaw removal aborts when systemd cannot be reached"
+72
View File
@@ -74,6 +74,44 @@ Item {
}
QML
# A keepLoaded service must keep its instance (and in-memory state) across a
# plugin rescan. The marker below can only survive if the object does.
keep_service_id="acme.keep-service"
keep_service_dir="$test_home/.config/omarchy/plugins/$keep_service_id"
mkdir -p "$keep_service_dir"
cat >"$keep_service_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$keep_service_id",
"name": "Keep Service",
"version": "1.0.0",
"kinds": ["service"],
"keepLoaded": true,
"entryPoints": {"service": "Service.qml"}
}
JSON
cat >"$keep_service_dir/Service.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
property string marker: ""
IpcHandler {
target: "acme-keep"
function set(value: string): string {
marker = value
return "ok"
}
function get(): string {
return marker
}
}
}
QML
cat >"$stub_bin/omarchy-update-available" <<'SH'
#!/bin/bash
echo "Omarchy update available (test)"
@@ -188,6 +226,15 @@ pass "shell IPC summon and hide contract works"
jq -e '.hasPlayer | type == "boolean"' <<<"$(shell_ipc media status)" >/dev/null || fail_with_log "media IPC returns status JSON"
jq -e '.enabled | type == "boolean"' <<<"$(shell_ipc idle status)" >/dev/null || fail_with_log "idle IPC returns status JSON"
jq -e '.locked | type == "boolean"' <<<"$(shell_ipc lock status)" >/dev/null || fail_with_log "lock IPC returns status JSON"
[[ $(shell_ipc shell setPluginEnabled "$keep_service_id" true) == "ok" ]] ||
fail_with_log "keepLoaded fixture service could not be enabled"
keep_marker_set=""
for _ in {1..80}; do
keep_marker_set=$(shell_ipc acme-keep set "survived" 2>/dev/null || true)
[[ $keep_marker_set == "ok" ]] && break
sleep 0.1
done
[[ $keep_marker_set == "ok" ]] || fail_with_log "keepLoaded fixture service IPC responds"
[[ $(shell_ipc image-selector ping) == "ok" ]] || fail_with_log "image selector IPC responds"
[[ $(shell_ipc osd ping) == "ok" ]] || fail_with_log "OSD IPC responds"
[[ $(shell_ipc osd show '{"message":"Runtime smoke","duration":0}') == "ok" ]] || fail_with_log "OSD IPC opens"
@@ -215,6 +262,31 @@ shell_ipc_quiet image-selector cancel "$selector_done_file" >/dev/null
rm -f "$selector_selection_file" "$selector_done_file"
pass "image selector IPC survives plugin rescan"
lock_status_after=$(shell_ipc lock status)
jq -e '.locked | type == "boolean"' <<<"$lock_status_after" >/dev/null || fail_with_log "lock IPC survives plugin rescan"
lock_event_after=$(jq -r '.lastEvent // empty' <<<"$lock_status_after")
[[ $lock_event_after != lock-stranded* ]] ||
fail_with_log "plugin rescan does not strand the session lock ($lock_event_after)"
# A recreated instance would answer with a fresh, empty marker.
[[ $(shell_ipc acme-keep get) == "survived" ]] ||
fail_with_log "plugin rescan keeps the keepLoaded service instance mounted"
pass "keepLoaded service instance survives plugin rescan"
# Dropping the service entry point from the manifest must drop the kept
# instance instead of leaving a zombie behind.
jq 'del(.keepLoaded) | .kinds = ["overlay"] | .entryPoints = {"overlay": "Service.qml"}' \
"$keep_service_dir/manifest.json" >"$keep_service_dir/manifest.json.tmp"
mv "$keep_service_dir/manifest.json.tmp" "$keep_service_dir/manifest.json"
keep_gone=""
for _ in {1..80}; do
keep_gone=$(shell_ipc acme-keep get 2>/dev/null || true)
[[ $keep_gone != "survived" ]] && break
sleep 0.1
done
[[ $keep_gone != "survived" ]] ||
fail_with_log "kept service is dropped when its plugin stops declaring a service"
pass "kept service is dropped when its plugin stops declaring a service"
shell_ipc_quiet omarchy.system-update refresh >/dev/null 2>&1 || true
sleep 0.8
@@ -1,162 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/systemctl" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$SYSTEMCTL_CALLS"
case "$*" in
'--user show-environment')
[[ ${MANAGER_AVAILABLE:-true} == "true" ]]
;;
'--user daemon-reload')
if [[ ${FAIL_SYSTEMCTL_ACTION:-} == "daemon-reload" ]]; then
echo "reload failed" >&2
exit 1
fi
;;
'--user show --property=ActiveState --value graphical-session.target')
if [[ ${FAIL_SYSTEMCTL_ACTION:-} == "show-graphical" ]]; then
echo "state lookup failed" >&2
exit 1
fi
printf '%s\n' "${GRAPHICAL_STATE:-inactive}"
;;
'--user show --property=ActiveState --value omarchy-sleep-lock.service')
printf '%s\n' "${SLEEP_LOCK_STATE:-inactive}"
;;
'--user restart omarchy-sleep-lock.service')
if [[ ${FAIL_SYSTEMCTL_ACTION:-} == "restart" ]]; then
echo "restart failed" >&2
exit 1
fi
;;
'--user stop omarchy-sleep-lock.service')
if [[ ${FAIL_SYSTEMCTL_ACTION:-} == "stop" ]]; then
echo "stop failed" >&2
exit 1
fi
;;
'--user reset-failed omarchy-sleep-lock.service') ;;
*) exit 1 ;;
esac
STUB
chmod +x "$stub_bin/systemctl"
migration="$ROOT/migrations/1785608166.sh"
run_migration() {
local home="$1" calls="$2"
shift 2
mkdir -p "$home/run"
HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_RUNTIME_DIR="$home/run" \
SYSTEMCTL_CALLS="$calls" PATH="$stub_bin:$PATH" \
"$@" bash -euo pipefail "$migration"
}
active_home="$test_tmp/active-home"
active_calls="$test_tmp/active-calls"
mkdir -p "$active_home/.config/systemd/user"
printf '%s\n' '[Service]' 'ExecStart=/usr/bin/omarchy-system-sleep-monitor' \
>"$active_home/.config/systemd/user/omarchy-sleep-lock.service"
run_migration "$active_home" "$active_calls" \
env GRAPHICAL_STATE=active SLEEP_LOCK_STATE=active >/dev/null
dropin="$active_home/.config/systemd/user/omarchy-sleep-lock.service.d/90-omarchy-session-environment.conf"
grep -Fx 'After=dbus.socket wayland-session-waitenv.service' "$dropin" >/dev/null ||
fail "sleep lock migration orders a retained unit after the session environment import"
grep -Fx 'PartOf=graphical-session.target' "$dropin" >/dev/null ||
fail "sleep lock migration ties a retained unit to the graphical session lifecycle"
grep -Fx 'ConditionEnvironment=OMARCHY_PATH' "$dropin" >/dev/null ||
fail "sleep lock migration gates a retained unit on the Omarchy path"
grep -Fx 'ConditionEnvironment=WAYLAND_DISPLAY' "$dropin" >/dev/null ||
fail "sleep lock migration gates a retained unit on the Wayland display"
pass "sleep lock migration repairs retained full-unit overrides with a drop-in"
grep -Fx -- '--user daemon-reload' "$active_calls" >/dev/null ||
fail "sleep lock migration reloads the live user manager"
grep -Fx -- '--user reset-failed omarchy-sleep-lock.service' "$active_calls" >/dev/null ||
fail "sleep lock migration cannot recover a start-limited monitor"
grep -Fx -- '--user restart omarchy-sleep-lock.service' "$active_calls" >/dev/null ||
fail "sleep lock migration leaves an active monitor with its inherited environment"
grep -Fx -- '--user stop omarchy-sleep-lock.service' "$active_calls" >/dev/null &&
fail "sleep lock migration stops the monitor inside an active graphical session"
pass "sleep lock migration replaces the monitor inside an active graphical session"
inactive_home="$test_tmp/inactive-home"
inactive_calls="$test_tmp/inactive-calls"
mkdir -p "$inactive_home"
run_migration "$inactive_home" "$inactive_calls" \
env GRAPHICAL_STATE=inactive >/dev/null
grep -Fx -- '--user stop omarchy-sleep-lock.service' "$inactive_calls" >/dev/null ||
fail "sleep lock migration leaves a stale monitor running after logout"
grep -Fx -- '--user restart omarchy-sleep-lock.service' "$inactive_calls" >/dev/null &&
fail "sleep lock migration starts the monitor outside a graphical session"
pass "sleep lock migration stops a monitor left behind after logout"
failed_calls="$test_tmp/failed-calls"
if run_migration "$test_tmp/failed-home" "$failed_calls" \
env GRAPHICAL_STATE=active SLEEP_LOCK_STATE=active FAIL_SYSTEMCTL_ACTION=restart \
>"$test_tmp/failed-output" 2>&1; then
fail "sleep lock migration marks a failed active-session repair complete"
fi
grep -F 'will be retried by omarchy-migrate' "$test_tmp/failed-output" >/dev/null ||
fail "sleep lock migration does not explain that a failed repair remains pending"
pass "sleep lock migration keeps an active-session repair failure retryable"
reload_failed_calls="$test_tmp/reload-failed-calls"
if run_migration "$test_tmp/reload-failed-home" "$reload_failed_calls" \
env FAIL_SYSTEMCTL_ACTION=daemon-reload >"$test_tmp/reload-failed-output" 2>&1; then
fail "sleep lock migration ignores a failed user-manager reload"
fi
grep -F 'Could not reload the user service manager' "$test_tmp/reload-failed-output" >/dev/null ||
fail "sleep lock migration does not report a failed user-manager reload"
pass "sleep lock migration keeps a failed user-manager reload retryable"
stop_failed_calls="$test_tmp/stop-failed-calls"
if run_migration "$test_tmp/stop-failed-home" "$stop_failed_calls" \
env GRAPHICAL_STATE=inactive FAIL_SYSTEMCTL_ACTION=stop \
>"$test_tmp/stop-failed-output" 2>&1; then
fail "sleep lock migration ignores a stale monitor that could not be stopped"
fi
grep -F 'Could not stop stale omarchy-sleep-lock.service' "$test_tmp/stop-failed-output" >/dev/null ||
fail "sleep lock migration does not report a stale monitor stop failure"
pass "sleep lock migration keeps a stale-monitor stop failure retryable"
state_failed_calls="$test_tmp/state-failed-calls"
if run_migration "$test_tmp/state-failed-home" "$state_failed_calls" \
env FAIL_SYSTEMCTL_ACTION=show-graphical >"$test_tmp/state-failed-output" 2>&1; then
fail "sleep lock migration ignores a failed session-state inspection"
fi
grep -F 'Could not inspect graphical-session.target' "$test_tmp/state-failed-output" >/dev/null ||
fail "sleep lock migration does not report a failed session-state inspection"
pass "sleep lock migration keeps an indeterminate session repair retryable"
deferred_home="$test_tmp/deferred-home"
deferred_calls="$test_tmp/deferred-calls"
mkdir -p "$deferred_home/.config/systemd/user"
touch "$deferred_home/.config/systemd/user/omarchy-sleep-lock.service"
run_migration "$deferred_home" "$deferred_calls" \
env MANAGER_AVAILABLE=false >/dev/null
[[ -f $deferred_home/.config/systemd/user/omarchy-sleep-lock.service.d/90-omarchy-session-environment.conf ]] ||
fail "sleep lock migration does not persist the repair without a live user manager"
deferred_call_count=$(wc -l <"$deferred_calls")
(( deferred_call_count == 1 )) ||
fail "sleep lock migration tries to mutate a user manager that is not running"
pass "sleep lock migration defers safely when no user manager is running"
+1 -15
View File
@@ -71,14 +71,6 @@ grep -Fx 'WantedBy=graphical-session.target' "$fcitx_service" >/dev/null ||
grep -Fx 'ConditionEnvironment=WAYLAND_DISPLAY' "$fcitx_service" >/dev/null ||
fail "an update over SSH has a live user manager and no display; starting fcitx5 there wedges the unit active-but-blind, and Wants= will not replace it at graphical login"
fcitx_migration="$ROOT/migrations/1785167800.sh"
grep -F 'is-active --quiet graphical-session.target' "$fcitx_migration" >/dev/null ||
fail "migration kills fcitx5 and starts the unit outside a graphical session"
grep -F 'systemctl --user enable omarchy-fcitx5.service' "$fcitx_migration" >/dev/null ||
fail "migration must enable without --now; --now starts the unit before the session-gate check"
grep -F 'Could not start omarchy-fcitx5.service' "$fcitx_migration" >/dev/null ||
fail "migration pkills a working fcitx5, so a failed handover must be reported instead of marked complete"
grep -F 'pkill -x fcitx5' "$ROOT/bin/omarchy-restart-xcompose" >/dev/null ||
fail "restart-xcompose cannot reload a fcitx5 running outside the unit, so it silently keeps serving the old table"
@@ -111,10 +103,4 @@ pass "systemd-oomd acts on sustained memory stall"
grep -Fx 'systemctl enable systemd-oomd.service' "$ROOT/install/config/enable-services.sh" >/dev/null ||
fail "new installs ship the oomd drop-ins with the daemon that reads them disabled"
oomd_migration=$(grep -rl 'systemd-oomd.service' "$ROOT/migrations" | head -n 1 || true)
[[ -n $oomd_migration ]] ||
fail "existing installs never enable systemd-oomd; enable-services.sh only runs at install time"
grep -F 'systemctl --user daemon-reload' "$oomd_migration" >/dev/null ||
fail "migration leaves the user manager unaware of app.slice candidacy until the next login"
pass "existing installs enable systemd-oomd and report app.slice without a relogin"
pass "new installs enable systemd-oomd"
@@ -1,59 +0,0 @@
#!/bin/bash
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
migration=$(grep -rl 'Make Shift+Enter distinguishable for terminals and Codex' "$ROOT/migrations" | head -n 1 || true)
[[ -n $migration ]] || fail "terminal Shift+Enter migration exists"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
test_home="$TMPDIR/home"
mkdir -p \
"$test_home/.config/alacritty" \
"$test_home/.config/foot" \
"$test_home/.config/ghostty" \
"$test_home/.config/kitty"
cp "$ROOT/config/alacritty/alacritty.toml" "$test_home/.config/alacritty/alacritty.toml"
sed -i 's/\\u001B\[13;2u/\\u001B\\r/' "$test_home/.config/alacritty/alacritty.toml"
cp "$ROOT/config/kitty/kitty.conf" "$test_home/.config/kitty/kitty.conf"
sed -i '/shift+enter/d' "$test_home/.config/kitty/kitty.conf"
cp "$ROOT/config/ghostty/config" "$test_home/.config/ghostty/config"
sed -i '/shift+enter/d' "$test_home/.config/ghostty/config"
cp "$ROOT/config/foot/foot.ini" "$test_home/.config/foot/foot.ini"
sed -i '/text-bindings/,$d' "$test_home/.config/foot/foot.ini"
HOME="$test_home" bash "$migration" >/dev/null
grep -qxF '{ key = "Return", mods = "Shift", chars = "\u001B[13;2u" },' "$test_home/.config/alacritty/alacritty.toml" ||
fail "migration updates Alacritty Shift+Return to CSI-u"
pass "migration updates Alacritty Shift+Return to CSI-u"
grep -qxF 'map shift+enter send_text all \e[13;2u' "$test_home/.config/kitty/kitty.conf" ||
fail "migration adds Kitty Shift+Enter CSI-u binding"
grep -qxF 'map alt+shift+enter send_text all \e[13;4u' "$test_home/.config/kitty/kitty.conf" ||
fail "migration adds Kitty Alt+Shift+Enter CSI-u binding"
pass "migration adds Kitty CSI-u bindings"
grep -qxF 'keybind = shift+enter=csi:13;2u' "$test_home/.config/ghostty/config" ||
fail "migration adds Ghostty Shift+Enter CSI-u binding"
grep -qxF 'keybind = alt+shift+enter=csi:13;4u' "$test_home/.config/ghostty/config" ||
fail "migration adds Ghostty Alt+Shift+Enter CSI-u binding"
pass "migration adds Ghostty CSI-u bindings"
grep -qxF '\x1b[13;2u=Shift+Return' "$test_home/.config/foot/foot.ini" ||
fail "migration adds Foot Shift+Return CSI-u binding"
grep -qxF '\x1b[13;4u=Mod1+Shift+Return' "$test_home/.config/foot/foot.ini" ||
fail "migration adds Foot Alt+Shift+Return CSI-u binding"
pass "migration adds Foot CSI-u bindings"
before=$(find "$test_home/.config" -type f -print0 | sort -z | xargs -0 sha256sum)
HOME="$test_home" bash "$migration" >/dev/null
after=$(find "$test_home/.config" -type f -print0 | sort -z | xargs -0 sha256sum)
[[ $before == "$after" ]] || fail "terminal Shift+Enter migration is idempotent"
pass "terminal Shift+Enter migration is idempotent"
+1 -1
View File
@@ -207,7 +207,7 @@ pass "a theme name cannot climb out of the theme directories"
# generates. Every generated theme file is either denied to an installed theme or
# recorded here as carrying colour, so a new template fails until it is placed.
denied=(alacritty.toml foot.ini ghostty.conf kitty.conf gum_env.lua hyprland.lua neovim.lua vscode.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
for tpl in "$ROOT"/default/themed/*.tpl; do
generated=$(basename "$tpl" .tpl)
+42 -4
View File
@@ -6,6 +6,10 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
upgrade_to_quattro="$ROOT/bin/omarchy-upgrade-to-quattro"
function_body() {
awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro"
}
snapshot_line=$(grep -n '^create_pre_upgrade_snapshot$' "$upgrade_to_quattro" | cut -d: -f1)
pacman_line=$(grep -n '^configure_pacman_channel$' "$upgrade_to_quattro" | cut -d: -f1)
[[ -n $snapshot_line && -n $pacman_line ]] || fail "upgrade snapshot and first mutation calls exist"
@@ -73,6 +77,44 @@ grep -F 'OMARCHY_INSTALL_USER="$target_user"' "$upgrade_to_quattro" >/dev/null
grep -F '"$apply_lock"' "$upgrade_to_quattro" >/dev/null
pass "Omarchy 4 upgrade configures lock screen authentication for the target user"
root_path_count=$(awk '/^root_path=/{ count++ } END { print count + 0 }' "$upgrade_to_quattro")
(( root_path_count == 1 )) || fail "Omarchy 4 upgrade defines exactly one root command path"
grep -Fx 'root_path=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade limits root command lookup to trusted system directories"
if grep -E '^root_path=.*(target_home|\.local/bin)' "$upgrade_to_quattro" >/dev/null; then
fail "Omarchy 4 upgrade does not put the target user's bin directory on the root command path"
fi
grep -Fx 'package_path="$root_path:$target_home/.local/bin"' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade retains the target user's bin directory for user commands"
lock_authentication_body=$(function_body configure_lock_authentication)
lock_path_assignment_count=$(awk '{ count += gsub(/(^|[[:space:]])PATH=/, "") } END { print count + 0 }' <<<"$lock_authentication_body")
(( lock_path_assignment_count == 1 )) ||
fail "Omarchy 4 upgrade gives the privileged lock helper exactly one command path"
grep -Fx ' PATH="$root_path" \' <<<"$lock_authentication_body" >/dev/null ||
fail "Omarchy 4 upgrade gives the privileged lock helper the trusted root path"
if grep -E '(package_path|target_home|\.local/bin)' <<<"$lock_authentication_body" >/dev/null; then
fail "Omarchy 4 upgrade does not give the privileged lock helper the target user's path"
fi
firewall_body=$(function_body apply_firewall_defaults)
firewall_path_assignment_count=$(awk '{ count += gsub(/(^|[[:space:]])PATH=/, "") } END { print count + 0 }' <<<"$firewall_body")
(( firewall_path_assignment_count == 1 )) ||
fail "Omarchy 4 upgrade gives the privileged firewall helper exactly one command path"
grep -Fx ' as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$root_path" \' <<<"$firewall_body" >/dev/null ||
fail "Omarchy 4 upgrade gives the privileged firewall helper the trusted root path"
if grep -E '(package_path|target_home|\.local/bin)' <<<"$firewall_body" >/dev/null; then
fail "Omarchy 4 upgrade does not give the privileged firewall helper the target user's path"
fi
user_omarchy_body=$(function_body run_as_user_omarchy)
grep -F 'PATH="$package_path"' <<<"$user_omarchy_body" >/dev/null ||
fail "Omarchy 4 upgrade retains the package and user path for target-user commands"
if grep -F 'PATH="$root_path"' <<<"$user_omarchy_body" >/dev/null; then
fail "Omarchy 4 upgrade does not narrow target-user commands to the root-only path"
fi
pass "Omarchy 4 upgrade separates privileged and target-user command paths"
grep -F 'install/helpers/browser-policy.sh' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade uses the shared browser-policy helper"
grep -F 'as_root test -f "$browser_policy_helper"' "$upgrade_to_quattro" >/dev/null ||
@@ -100,10 +142,6 @@ pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager"
# Booting with both managers enabled leaves them fighting over the Wi-Fi
# adapter, so enabling NetworkManager and disabling iwd cannot be separated by
# any step that might abort in between.
function_body() {
awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro"
}
migrations_body=$(function_body run_post_upgrade_migrations)
grep -F 'fail "Omarchy migrations did not complete.' <<<"$migrations_body" >/dev/null ||
fail "Omarchy 4 upgrade fails when a migration cannot complete"
+366
View File
@@ -0,0 +1,366 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8')
const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8')
const videoQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundVideo.qml'), 'utf8')
const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8')
const lockQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const themeSwitcher = fs.readFileSync(path.join(root, 'bin/omarchy-theme-switcher'), 'utf8')
const quattroUpgrade = fs.readFileSync(path.join(root, 'bin/omarchy-upgrade-to-quattro'), 'utf8')
const multimediaMigration = fs.readFileSync(path.join(root, 'migrations/1786609204.sh'), 'utf8')
const barTextColor = fs.readFileSync(path.join(root, 'bin/omarchy-bar-text-color'), 'utf8')
const menuImages = fs.readFileSync(path.join(root, 'bin/omarchy-menu-images'), 'utf8')
const lockView = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const lockService = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8')
const batteryService = fs.readFileSync(path.join(root, 'shell/plugins/services/battery/Service.qml'), 'utf8')
const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8')
const directImageList = fs.readFileSync(path.join(root, 'shell/plugins/image-picker/list.sh'), 'utf8')
assert(
/function isVideoPath\(path\)[\s\S]*\.test\(String\(path \|\| ""\)\)/.test(utilQml) &&
!utilQml.includes('split(/[?#]/)'),
'shared media helper identifies video paths without truncating valid local names'
)
assert(
videoQml.includes('loops: MediaPlayer.Infinite') &&
videoQml.includes('autoPlay: root.playbackEnabled') &&
videoQml.includes('fillMode: VideoOutput.PreserveAspectCrop') &&
/imageUrl: path && !Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
/videoUrl: path && Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) : ""/.test(mediaQml),
'background media plays aspect-cropped videos on a loop, and hands each loader only its own kind of file'
)
assert(
videoQml.includes('MediaPlayer.LoadedMedia') &&
videoQml.includes('primePauseTimer') &&
videoQml.includes('mediaGeneration') &&
videoQml.includes('videoSink') &&
videoQml.includes('onVideoFrameChanged') &&
videoQml.includes('MediaPlayer.BufferedMedia') &&
videoQml.includes('mediaStatus !== MediaPlayer.BufferedMedia') &&
videoQml.includes('interval: 1000') &&
videoQml.includes('interval: 50') &&
videoQml.includes('frameReceived') &&
videoQml.includes('output.clearOutput()') &&
!videoQml.includes('KeepLastFrame') &&
/onPlaybackEnabledChanged:[\s\S]*?if \(playbackEnabled\) player\.play\(\)[\s\S]*?else player\.pause\(\)/.test(videoQml) &&
videoQml.includes('primingGeneration') &&
videoQml.includes('player.play()') &&
videoQml.includes('player.pause()'),
'paused video sources are primed to display their first frame'
)
assert(
!/^\s*import QtMultimedia/m.test(mediaQml) &&
mediaQml.includes('source: "BackgroundVideo.qml"'),
'the still-image path never imports QtMultimedia, so image-only sessions do not map it'
)
assert(
!/^\s*Video\s*\{/m.test(videoQml) &&
/property bool audioEnabled: false/.test(videoQml) &&
/property bool audioEnabled: false/.test(mediaQml) &&
/active: root\.audioEnabled && player\.hasAudio/.test(videoQml) &&
/audioOutput: audioLoader\.item/.test(videoQml) &&
/muted: root\.priming \|\| !root\.playbackEnabled/.test(videoQml) &&
/property: "audioEnabled"\s*\n\s*value: root\.audioEnabled/.test(mediaQml) &&
/firstScreen: Quickshell\.screens\.length > 0\s*\n\s*&& String\(Quickshell\.screens\[0\]\.name/.test(backgroundQml) &&
backgroundQml.includes('audioEnabled: panel.firstScreen') &&
!lockQml.includes('audioEnabled'),
'a sound track plays from the first monitor only, a silent file builds no audio output, and the lock stays quiet'
)
assert(
/property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) &&
!videoQml.includes('Component.onDestruction'),
'a player on its way out keeps its source, so nothing is left loading for its destructor to cancel'
)
assert(
!mediaQml.includes('mipmap'),
'the shared image path leaves mipmapping off, as the desktop background had it'
)
assert(
/instant \|\| !displayedBackground \|\| isVideo\(path\) \|\| isVideo\(displayedBackground\)[\s\S]*displayedBackground = finalPath/.test(backgroundQml),
'video switches bypass the image-only reveal stack and use the durable background path'
)
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering')
assert(
lockQml.includes('source: wallpaper.video ? null : wallpaper') &&
lockQml.includes('visible: !wallpaper.video') &&
lockQml.includes('visible: wallpaper.video'),
'lock screen bypasses its image effect for video output'
)
assert(
/sessionObscured:\s*lockActive \|\| screensaverActive/.test(backgroundQml) &&
backgroundQml.includes('playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere') &&
backgroundQml.includes('omarchy.lock') &&
backgroundQml.includes('omarchy.idle') &&
backgroundQml.includes('omarchy.battery'),
'desktop playback stops while covered or on battery power-saver'
)
assert(
backgroundQml.includes('Hyprland.monitorFor(modelData)') &&
/fullscreenHere: visibleWorkspace \? visibleWorkspace\.hasFullscreen : false/.test(backgroundQml) &&
!backgroundQml.includes('ToplevelManager.activeToplevel'),
'a fullscreen window pauses only the output it covers, wherever focus is'
)
assert(
/if \(displayedBackground === finalPath\) displayedReloads \+= 1/.test(backgroundQml) &&
backgroundQml.includes('reloads: root.displayedReloads') &&
/active: root\.path !== "" && root\.video && !root\.reloading/.test(mediaQml) &&
/onReloadsChanged: \{[\s\S]*?reloading = true/.test(mediaQml),
'a theme switch that keeps the video path still reopens the replaced file'
)
assert(
barTextColor.includes('magick "$background_path[0]"'),
'bar colour sampling reads one frame instead of decoding a whole video'
)
assert(
menuImages.includes('pending_video_file') && /video_jobs=\$\(\( \$\(nproc\) \/ 4 \)\)/.test(menuImages),
'video thumbnails fan out narrower than single-threaded vips jobs'
)
assert(
themeSwitcher.includes('fast_signature="v2"'),
'the theme preview cache rebuilds after preview discovery learned about video'
)
assert(
/lazy_thumbnails == true && \$cache_only != true \]\] && ! is_video_path/.test(menuImages),
'a video never stands in as its own lazy thumbnail, so the fan out cap always applies'
)
assert(
/thumbnail_command=\(timeout -k \d+ \d+ ffmpegthumbnailer/.test(menuImages),
'a stalled video cannot hold the picker shut, because its generator is time bounded'
)
assert(
directImageList.includes('generate_video_thumbnail') &&
/timeout -k \d+ \d+ ffmpegthumbnailer/.test(directImageList) &&
/if \[\[ ! -f \$thumbnail \]\] && ! is_video_path/.test(directImageList),
'a direct picker scan generates bounded video thumbnails without content-hashing the media'
)
assert(
themeSet.includes('choose_staged_theme_background') &&
themeSet.includes('background_transition_uses_snapshots') &&
themeSet.includes('BACKGROUND_TRANSITION_SNAPSHOTS=false') &&
/if \[\[ \$BACKGROUND_TRANSITION_SNAPSHOTS == "true" \]\]/.test(themeSet) &&
/if \[\[ -z \$CHOSEN_THEME_BACKGROUND \|\| ! -f \$CHOSEN_THEME_BACKGROUND \]\]/.test(themeSet),
'theme changes disable both transition snapshots whenever either side is a video'
)
assert(
/function onScreensChanged\(\) \{[\s\S]*?root\.displaysBlank = false/.test(lockService),
'a display coming back gives up the blank state instead of freezing a visible wallpaper'
)
assert(
lockService.includes('function screenBlank(screenName)') &&
lockService.includes('function applyMonitorDpms(text)') &&
lockService.includes('command: ["hyprctl", "monitors", "-j"]') &&
lockService.includes('running: root.locked && root.videoBackground') &&
/displaysBlank: root\.screenBlank\(lockSurface\.screen/.test(lockService) &&
/function runWake\(\) \{[\s\S]*?root\.monitorDpmsKnown = false/.test(lockService) &&
/function runBlank\(\) \{[\s\S]*?root\.monitorDpmsKnown = false/.test(lockService),
'a locked video wallpaper follows what each panel actually did, not only what the lock asked for'
)
assert(
lockView.includes('playbackEnabled: root.loadBackground && !root.displaysBlank') &&
lockView.includes('&& !root.powerSaverActive') &&
/displaysBlank: root\.screenBlank\(/.test(lockService) &&
/powerSaverActive: root\.powerSaverActive/.test(lockService) &&
/function runBlank\(\) \{\s*\n\s*root\.displaysBlank = true/.test(lockService) &&
/function runWake\(\) \{\s*\n\s*root\.displaysBlank = false/.test(lockService),
'the lock screen stops playback once displays go dark or power-saver is active'
)
assert(
batteryService.includes('property string activePowerProfile') &&
batteryService.includes('UPower.onBattery && activePowerProfile === "power-saver"') &&
batteryService.includes('["powerprofilesctl", "get"]') &&
batteryService.includes('interval: 2000'),
'the battery service tracks the active power-saver profile'
)
assert(
themeSwitcher.includes("-iname '*.mp4'") &&
themeSwitcher.includes('mp4 m4v mov webm mkv avi') &&
themeSwitcher.includes('preview.mp4'),
'theme switcher previews video-only themes, named preview files included'
)
assert(quattroUpgrade.includes("-iname '*.mp4'"), 'Quattro upgrade can seed a video-only theme background')
assert(
multimediaMigration.includes('omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg'),
'existing Quattro installations receive video playback dependencies'
)
JS
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin" "$test_tmp/backgrounds" "$test_tmp/generator-cache" "$test_tmp/direct-cache"
printf 'not a real video\n' >"$test_tmp/backgrounds/sample.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
while (( $# > 0 )); do
case "$1" in
-i) input=$2; shift 2 ;;
-o) output=$2; shift 2 ;;
*) shift ;;
esac
done
printf 'thumbnail for %s\n' "$input" >"$output"
SH
chmod +x "$test_tmp/bin/ffmpegthumbnailer"
cat >"$test_tmp/bin/md5sum" <<'SH'
#!/bin/bash
if (( $# > 0 )); then
printf 'unexpected file hash: %s\n' "$*" >>"$MD5_FILE_CALLS"
fi
exec /usr/bin/md5sum "$@"
SH
chmod +x "$test_tmp/bin/md5sum"
md5_file_calls="$test_tmp/md5-file-calls"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/generator-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$test_tmp/backgrounds"
generator_thumbnail=$(find "$test_tmp/generator-cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.jpg' -print -quit)
[[ -s $generator_thumbnail ]] || fail "menu image generator creates a video thumbnail"
generator_row=$(XDG_CACHE_HOME="$test_tmp/generator-cache" "$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
IFS=$'\t' read -r generator_row_path generator_row_thumbnail <<<"$generator_row"
[[ $generator_row_path == "$test_tmp/backgrounds/sample.mp4" && $generator_row_thumbnail == "$generator_thumbnail" ]] || \
fail "direct picker consumes the menu image generator thumbnail" "$generator_row"
row=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/direct-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
IFS=$'\t' read -r row_path row_thumbnail <<<"$row"
[[ $row_path == "$test_tmp/backgrounds/sample.mp4" && $row_thumbnail == *.jpg && -s $row_thumbnail ]] || \
fail "image picker lists videos with their cached thumbnail" "$row"
[[ ! -s $md5_file_calls ]] || fail "direct video scans avoid hashing the complete media file" "$(<"$md5_file_calls")"
failed_backgrounds="$test_tmp/failed-backgrounds"
failed_cache="$test_tmp/failed-cache"
mkdir -p "$failed_backgrounds" "$failed_cache"
printf 'broken video\n' >"$failed_backgrounds/broken.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
exit 1
SH
cached_row=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/direct-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
[[ $cached_row == "$row" ]] || fail "direct picker reuses its cached video thumbnail" "$cached_row"
failed_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$failed_cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$failed_backgrounds")
[[ -z $failed_rows ]] || fail "direct picker omits a video whose thumbnail fails" "$failed_rows"
failed_marker=$(find "$failed_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -n $failed_marker ]] || fail "direct picker remembers a video the converter rejected"
thumbnailer_calls="$test_tmp/thumbnailer-calls"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$THUMBNAILER_CALLS"
exit 1
SH
failed_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$failed_backgrounds")
[[ -z $failed_rows && ! -e $thumbnailer_calls ]] || fail "direct picker skips a rejected video on the next scan" "$(cat "$thumbnailer_calls" 2>/dev/null)"
generator_failed_cache="$test_tmp/generator-failed-cache"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ -s $thumbnailer_calls ]] || fail "menu image generator tries a video it has not seen"
generator_marker=$(find "$generator_failed_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -n $generator_marker ]] || fail "menu image generator remembers a video the converter rejected"
rm -f "$thumbnailer_calls"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ ! -e $thumbnailer_calls ]] || fail "menu image generator skips a rejected video on the next open" "$(<"$thumbnailer_calls")"
# A repaired file gets a fresh key, so the old marker no longer applies, and
# the rows are not cached over its absence, so an in-place repair that leaves
# the directory's mtime alone is still noticed.
touch -d '2 minutes' "$failed_backgrounds/broken.mp4"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ -s $thumbnailer_calls ]] || fail "menu image generator retries a video that changed since it was rejected"
timeout_backgrounds="$test_tmp/timeout-backgrounds"
timeout_cache="$test_tmp/timeout-cache"
mkdir -p "$timeout_backgrounds"
printf 'slow video\n' >"$timeout_backgrounds/slow.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
exit 124
SH
timeout_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$timeout_cache" \
"$ROOT/shell/plugins/image-picker/list.sh" "$timeout_backgrounds")
[[ -z $timeout_rows ]] || fail "direct picker omits a video whose thumbnail timed out" "$timeout_rows"
timeout_marker=$(find "$timeout_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -z $timeout_marker ]] || fail "a timed out video is left to retry rather than remembered as failed"
grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia runtime is a base package"
grep -qx 'qt6-multimedia-ffmpeg' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia FFmpeg backend is a base package"
pass "menu image generator creates thumbnails consumed by the picker"
pass "direct picker generates and reuses still thumbnails"
pass "direct picker omits videos whose thumbnails cannot be generated"
pass "a rejected video is remembered so it costs nothing on the next open"
pass "a timed out video is left to retry"
pass "a locked video wallpaper follows the panels' real DPMS state"
pass "Qt Multimedia playback dependencies are declared"
source <(awk '
/^(is_video_path|snapshot_background_path|background_transition_uses_snapshots|choose_theme_background|choose_staged_theme_background|set_theme_background)\(\) \{/ { copying=1 }
copying { print }
copying && /^}$/ { copying=0 }
' "$ROOT/bin/omarchy-theme-set")
transition_home="$test_tmp/transition-home"
CURRENT_THEME_PATH="$transition_home/.local/state/omarchy/current/theme"
NEXT_THEME_PATH="$transition_home/.local/state/omarchy/current/next-theme"
CURRENT_BACKGROUND_LINK="$transition_home/.local/state/omarchy/current/background"
BACKGROUND_TRANSITION_CACHE="$transition_home/.cache/omarchy/background-transitions"
THEME_NAME="video-test"
HOME="$transition_home"
mkdir -p "$CURRENT_THEME_PATH/backgrounds" "$NEXT_THEME_PATH/backgrounds" "$HOME/.config/omarchy/backgrounds/$THEME_NAME"
printf 'old image\n' >"$CURRENT_THEME_PATH/backgrounds/old.png"
printf 'old image staged\n' >"$NEXT_THEME_PATH/backgrounds/old.png"
printf 'new video\n' >"$NEXT_THEME_PATH/backgrounds/new.mp4"
ln -s "$CURRENT_THEME_PATH/backgrounds/old.png" "$CURRENT_BACKGROUND_LINK"
choose_staged_theme_background || fail "staged video background is selected before the theme swap"
expected_staged_background="$CURRENT_THEME_PATH/backgrounds/new.mp4"
[[ $CHOSEN_THEME_BACKGROUND == $expected_staged_background ]] || \
fail "staged background resolves to its durable post-swap path" "$CHOSEN_THEME_BACKGROUND"
if background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
fail "image to video theme transitions skip snapshots"
fi
background_transition_uses_snapshots "$CURRENT_THEME_PATH/backgrounds/new.png" || \
fail "image to image theme transitions retain snapshots"
rm "$CURRENT_BACKGROUND_LINK"
printf 'old video\n' >"$CURRENT_THEME_PATH/backgrounds/old.mp4"
ln -s "$CURRENT_THEME_PATH/backgrounds/old.mp4" "$CURRENT_BACKGROUND_LINK"
if background_transition_uses_snapshots "$CURRENT_THEME_PATH/backgrounds/new.png"; then
fail "video to image theme transitions skip snapshots"
fi
video_snapshot=$(snapshot_background_path "$CURRENT_THEME_PATH/backgrounds/old.mp4" "video")
[[ -z $video_snapshot && ! -e $BACKGROUND_TRANSITION_CACHE ]] || fail "video files are never snapshotted"
CHOSEN_THEME_BACKGROUND="$transition_home/disappeared.mp4"
BACKGROUND_TRANSITION_SNAPSHOTS=false
OLD_BACKGROUND_SNAPSHOT=""
colors_payload=""
shell_payload=""
shell_ipc() { :; }
set_theme_background
[[ -f $CHOSEN_THEME_BACKGROUND && $(readlink "$CURRENT_BACKGROUND_LINK") == "$CHOSEN_THEME_BACKGROUND" ]] || \
fail "theme changes recover when a preselected background disappears" "$CHOSEN_THEME_BACKGROUND"
pass "theme transitions skip snapshots whenever either side is a video"
pass "theme changes recover from a missing preselected background"
-104
View File
@@ -1,104 +0,0 @@
#!/bin/bash
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
migration=$(grep -rl 'Move zram tuning to a vendor drop-in' "$ROOT/migrations" | head -n 1 || true)
[[ -n $migration ]] || fail "zram drop-in migration exists"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# The migration shells out to pacman (ownership check) and sudo (removal).
# Stub both so the test never touches the real system, and let each case pick
# what `pacman -Qo` reports through PACMAN_OWNS.
stub_bin="$TMPDIR/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/pacman" <<'STUB'
#!/bin/bash
[[ ${PACMAN_OWNS:-0} == 1 ]]
STUB
cat >"$stub_bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
chmod +x "$stub_bin/pacman" "$stub_bin/sudo"
# The migration removes the /etc copy only once the drop-in that replaces it is
# installed. Point that at a fixture so the result does not depend on whether
# the machine running the tests happens to carry the real one.
dropin="$TMPDIR/90-omarchy.conf"
: >"$dropin"
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
# whole chain on a non-zero exit, so match that invocation exactly.
run_migration() {
local conf="$1"
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean for $(basename "$conf")"
}
# archinstall's own output: a [zram0] section with nothing but the algorithm.
conf="$TMPDIR/archinstall.conf"
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration removes archinstall's generated config"
pass "migration removes archinstall's generated config"
# Same shape, different algorithm, plus comments and blank lines.
conf="$TMPDIR/commented.conf"
printf '# written by archinstall\n\n[zram0]\ncompression-algorithm = lz4\n\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration ignores comments and a non-zstd algorithm"
pass "migration ignores comments and a non-zstd algorithm"
# A config that sets nothing decides nothing, and must not take the migration
# chain down with it.
conf="$TMPDIR/comments-only.conf"
printf '# nothing to see here\n\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration removes a config that sets nothing"
pass "migration removes a config that sets nothing"
conf="$TMPDIR/empty.conf"
: >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration removes an empty config"
pass "migration removes an empty config"
# A local override must survive.
conf="$TMPDIR/local.conf"
printf '[zram0]\ncompression-algorithm = zstd\nzram-size = ram / 4\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] || fail "migration keeps a locally edited config"
pass "migration keeps a locally edited config"
# Package-owned copies go away with their package; the migration must not touch
# them.
conf="$TMPDIR/owned.conf"
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
PATH="$stub_bin:$PATH" PACMAN_OWNS=1 OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean for a package-owned config"
[[ -f $conf ]] || fail "migration keeps a package-owned config"
pass "migration keeps a package-owned config"
# Nothing to do, and running twice must stay clean.
conf="$TMPDIR/absent.conf"
run_migration "$conf"
run_migration "$conf"
pass "migration no-ops when the config is already gone"
# Without the drop-in installed, the /etc copy is the only thing configuring
# zram at all. Removing it would leave the machine with no zram device, so the
# migration has to leave it alone and stay clean doing it.
conf="$TMPDIR/no-dropin.conf"
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$TMPDIR/absent-dropin.conf" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean when the drop-in is missing"
[[ -f $conf ]] || fail "migration keeps the config when the drop-in is missing"
pass "migration keeps the config until the drop-in is installed"