Integrate current quattro before security review
This commit is contained in:
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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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"
|
||||
Executable
+93
@@ -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() }
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Executable
+189
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
Executable
+95
@@ -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"
|
||||
@@ -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"
|
||||
Executable
+288
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
+366
@@ -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"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user