Merge remote-tracking branch 'refs/remotes/portfolio/quattro' into codex/portfolio-9457-20260907

This commit is contained in:
Afonso Oliveira committed 2026-09-07 21:27:40 +01:00
commit bc235d2807
179 files changed
+10898 -1487

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"
+8
View File
@@ -55,6 +55,14 @@ const entries = [
}
]
// Keep the packaged launcher when upstream rebuilds register their own entry.
const configuredHides = new Set(fs.readFileSync(path.join(root, 'default/omarchy/launcher.hides'), 'utf8').trim().split(/\n/))
const hermesEntries = [{ name: 'Hermes', id: 'hermes' }, { name: 'Hermes', id: 'hermes-desktop' }]
for (const query of ['', 'hermes']) {
const visible = search.sortedEntries(hermesEntries, query, entry => configuredHides.has(entry.id))
assertDeepEqual(visible.map(row => row.entry.id), ['hermes-desktop'], 'only the packaged Hermes launcher is visible')
}
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
+206
View File
@@ -0,0 +1,206 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
apply_lock="$ROOT/bin/omarchy-apply-lock"
root_path_guard=$(awk '
/^if \(\( EUID == 0 \)\); then$/ { inside = 1 }
inside { print }
inside && /^fi$/ { exit }
' "$apply_lock")
grep -Fx ' export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin' <<<"$root_path_guard" >/dev/null ||
fail "the root lock helper replaces its inherited command path"
if grep -E '(\.local/bin|target_user|target_home)' <<<"$root_path_guard" >/dev/null; then
fail "the root lock helper does not retain a user-controlled command directory"
fi
pass "the root lock helper uses only trusted command directories"
grep -F '[[ -x /usr/bin/fprintd-list ]]' "$apply_lock" >/dev/null ||
fail "the lock helper checks the trusted fprintd-list executable"
grep -F '/usr/bin/fprintd-list "$target_user"' "$apply_lock" >/dev/null ||
fail "the lock helper invokes fprintd-list by its trusted absolute path"
if grep -F 'omarchy-cmd-present fprintd-list' "$apply_lock" >/dev/null ||
grep -E '(^|[[:space:];&|])fprintd-list([[:space:]]|$)' "$apply_lock" >/dev/null ||
grep -E 'command[[:space:]]+-v[[:space:]]+fprintd-list' "$apply_lock" >/dev/null; then
fail "the lock helper does not resolve fprintd-list through PATH"
fi
pass "the lock helper pins fprintd-list to its packaged system path"
# Exercise the helper as real root when the suite already has it, or as root in
# an unprivileged user namespace otherwise. A hardened kernel can disable user
# namespaces, so preserve the static coverage above and skip only this probe.
root_runner=()
root_runtime_available=1
if (( EUID != 0 )); then
if command -v unshare >/dev/null && unshare --user --map-root-user true 2>/dev/null; then
root_runner=(unshare --user --map-root-user)
else
root_runtime_available=0
fi
fi
if (( ! root_runtime_available )); then
pass "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
exit 0
fi
# Retarget the two PAM files, the trusted fprintd-list binary, and the final
# shell status query in copies under this scratch directory. The production
# files and service stay untouched even when this suite itself runs as root.
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
poison_bin="$test_tmp/poison-bin"
trusted_root_bin="$test_tmp/trusted-root-bin"
trusted_fprintd="$test_tmp/trusted-fprintd-list"
password_pam="$test_tmp/omarchy-lock-password"
fingerprint_pam="$test_tmp/omarchy-lock-fingerprint"
attack_marker="$test_tmp/user-fprintd-list-ran"
trusted_uid="$test_tmp/trusted-fprintd-list.uid"
trusted_args="$test_tmp/trusted-fprintd-list.args"
attack_args="$test_tmp/user-fprintd-list.args"
patched_helper="$test_tmp/omarchy-apply-lock-patched"
absolute_only_helper="$test_tmp/omarchy-apply-lock-absolute-only"
root_path_only_helper="$test_tmp/omarchy-apply-lock-root-path-only"
unprotected_helper="$test_tmp/omarchy-apply-lock-unprotected"
target_user=omarchy-regression-user
mkdir -p "$poison_bin" "$trusted_root_bin"
# The runtime copy pins to this isolated root path. It contains every bare
# command the exercised helper needs, but deliberately no fprintd-list.
for helper in grep rm tee; do
ln -s "/usr/bin/$helper" "$trusted_root_bin/$helper"
done
export TEST_ATTACK_ARGS="$attack_args"
export TEST_ATTACK_MARKER="$attack_marker"
export TEST_TRUSTED_ARGS="$trusted_args"
export TEST_TRUSTED_UID="$trusted_uid"
cat >"$trusted_fprintd" <<'EOF'
#!/bin/bash
printf '%s\n' "$EUID" >"$TEST_TRUSTED_UID"
printf '%s\n' "$*" >"$TEST_TRUSTED_ARGS"
echo "Fingerprints are enrolled"
EOF
cat >"$poison_bin/fprintd-list" <<'EOF'
#!/bin/bash
printf '%s\n' "$EUID" >"$TEST_ATTACK_MARKER"
printf '%s\n' "$*" >"$TEST_ATTACK_ARGS"
echo "Fingerprints are enrolled"
EOF
chmod +x "$trusted_fprintd" "$poison_bin/fprintd-list"
prepare_helper() {
local destination="$1" keep_root_path="$2" use_absolute_fprintd="$3"
awk \
-v password_pam="$password_pam" \
-v fingerprint_pam="$fingerprint_pam" \
-v trusted_root_bin="$trusted_root_bin" \
-v trusted_fprintd="$trusted_fprintd" \
-v keep_root_path="$keep_root_path" \
-v use_absolute_fprintd="$use_absolute_fprintd" '
{
line = $0
gsub("/etc/pam\\.d/omarchy-lock-password", "\"" password_pam "\"", line)
gsub("/etc/pam\\.d/omarchy-lock-fingerprint", "\"" fingerprint_pam "\"", line)
if (line == "if (( EUID == 0 )); then" && keep_root_path == 0) {
print "if (( 0 )); then"
next
}
if (line == " export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin") {
print " export PATH=\"" trusted_root_bin "\""
next
}
if (line == "if [[ -x /usr/bin/fprintd-list ]] &&") {
if (use_absolute_fprintd == 1) {
print "if [[ -x \"" trusted_fprintd "\" ]] &&"
} else {
print "if command -v fprintd-list >/dev/null 2>&1 &&"
}
next
}
if (line == " /usr/bin/fprintd-list \"$target_user\" 2>/dev/null | grep -qi finger; then") {
if (use_absolute_fprintd == 1) {
print " \"" trusted_fprintd "\" \"$target_user\" 2>/dev/null | grep -qi finger; then"
} else {
print " fprintd-list \"$target_user\" 2>/dev/null | grep -qi finger; then"
}
next
}
if (line == "if omarchy-shell lock status >/dev/null 2>&1; then") {
print "if false; then"
next
}
print line
}
' "$apply_lock" >"$destination"
chmod +x "$destination"
}
prepare_helper "$patched_helper" 1 1
prepare_helper "$absolute_only_helper" 0 1
prepare_helper "$root_path_only_helper" 1 0
prepare_helper "$unprotected_helper" 0 0
for helper in "$patched_helper" "$absolute_only_helper" "$root_path_only_helper" "$unprotected_helper"; do
if grep -F '/etc/pam.d/' "$helper" >/dev/null ||
grep -F '/usr/bin/fprintd-list' "$helper" >/dev/null ||
grep -F 'omarchy-shell lock status' "$helper" >/dev/null; then
fail "the isolated root fixture redirects every live-system lock-helper target"
fi
done
reset_runtime_files() {
rm -f "$password_pam" "$fingerprint_pam" "$trusted_uid" "$trusted_args" "$attack_marker" "$attack_args"
}
run_as_root() {
local helper="$1" description="$2" output
if ! output=$(PATH="$poison_bin:/usr/bin:/bin" OMARCHY_INSTALL_USER="$target_user" \
"${root_runner[@]}" /bin/bash "$helper" 2>&1); then
fail "$description" "$output"
fi
}
reset_runtime_files
run_as_root "$patched_helper" "the fully hardened lock helper runs in an isolated root context"
[[ ! -e $attack_marker ]] || fail "the hardened root lock helper executes the user-planted fprintd-list"
grep -Fx '0' "$trusted_uid" >/dev/null || fail "the trusted fprintd-list probe runs with EUID 0"
grep -Fx "$target_user" "$trusted_args" >/dev/null || fail "the trusted fprintd-list probe receives the target user"
[[ -s $password_pam && -s $fingerprint_pam ]] ||
fail "the isolated root lock-helper run writes both scratch PAM fixtures"
pass "the hardened root lock helper uses the trusted fingerprint probe"
reset_runtime_files
run_as_root "$absolute_only_helper" "the absolute-path-only lock helper runs in an isolated root context"
[[ ! -e $attack_marker ]] || fail "an absolute fprintd-list path permits the user-planted command"
grep -Fx '0' "$trusted_uid" >/dev/null || fail "the absolute-path defense runs the trusted probe as root"
pass "the absolute fprintd-list path independently blocks the user-planted command"
reset_runtime_files
run_as_root "$root_path_only_helper" "the root-PATH-only lock helper runs in an isolated root context"
[[ ! -e $attack_marker ]] || fail "the trusted root path permits the user-planted fprintd-list"
pass "the trusted root path independently blocks the user-planted command"
# Mutation control: removing both protections must execute the planted command
# as UID 0, proving the matrix detects the original privilege-boundary failure.
reset_runtime_files
run_as_root "$unprotected_helper" "the unprotected mutation runs in an isolated root context"
grep -Fx '0' "$attack_marker" >/dev/null ||
fail "the root lock-helper fixture detects a PATH-resolved fprintd-list regression"
grep -Fx "$target_user" "$attack_args" >/dev/null ||
fail "the planted fprintd-list receives the target user"
[[ -s $fingerprint_pam ]] || fail "the planted fprintd-list controls the fingerprint PAM branch"
pass "the root lock-helper matrix rejects the vulnerable PATH lookup"
+13
View File
@@ -5,6 +5,7 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command magick
require_command ffmpeg
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
@@ -28,3 +29,15 @@ pass "transparent bar text keeps text color on dark wallpaper"
result=$(HOME="$TMPDIR" omarchy-bar-text-color top 20 '#ffffff' '#101010' --background "$TMPDIR/missing.png" --screen 100x100)
[[ $result == "#ffffff" ]] || fail "transparent bar text falls back to text color when sampling fails" "expected #ffffff, got $result"
pass "transparent bar text falls back to text color when sampling fails"
# A video background must be sampled one frame at a time. Reading the whole file
# emits a value per frame, which parses as nothing and silently falls back —
# and decodes the entire wallpaper to find that out.
light_top_video="$TMPDIR/light-top.mp4"
ffmpeg -y -f lavfi -i "testsrc=size=640x360:rate=10:duration=2" \
-vf "drawbox=x=0:y=0:w=640:h=40:color=0xf5f5f5:t=fill" \
-c:v libx264 -preset ultrafast -pix_fmt yuv420p "$light_top_video" -loglevel error
result=$(HOME="$TMPDIR" omarchy-bar-text-color top 40 '#ffffff' '#101010' --background "$light_top_video" --screen 640x360)
[[ $result == "#101010" ]] || fail "transparent bar text samples one frame of a video wallpaper" "expected #101010, got $result"
pass "transparent bar text samples one frame of a video wallpaper"
-109
View File
@@ -1,109 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1786380259.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/bin"
# sudo runs the real command, so sed acts on the redirected main.conf below and
# the elevated power calls land in the stub beside it.
cat >"$test_dir/bin/sudo" <<'STUB'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$CALLS"
exec "$@"
STUB
cat >"$test_dir/bin/omarchy-bluetooth-power" <<'STUB'
#!/bin/bash
printf 'omarchy-bluetooth-power %s\n' "$*" >>"$CALLS"
[[ $1 == "is-on" ]] || exit 0
[[ ${POWERED:-} == "yes" ]]
STUB
chmod +x "$test_dir/bin/"*
export CALLS="$test_dir/calls"
marker="$test_dir/marker"
main_conf="$test_dir/main.conf"
reset_machine() {
rm -f "$marker"
printf '[Policy]\nAutoEnable=false\n' >"$main_conf"
}
run_migration() {
: >"$CALLS"
OMARCHY_BLUETOOTH_MIGRATION_MARKER="$marker" \
OMARCHY_BLUETOOTH_MAIN_CONF="$main_conf" \
PATH="$test_dir/bin:$PATH" \
bash -euo pipefail "$migration" >/dev/null
}
# An adapter that is powered right now is one the user turned on, so it stays on.
reset_machine
POWERED=yes run_migration
grep -qx 'omarchy-bluetooth-power on' "$CALLS" ||
fail "migration keeps a powered adapter on" "$(cat "$CALLS")"
pass "migration keeps a powered adapter on"
grep -qx '#AutoEnable=true' "$main_conf" ||
fail "migration puts AutoEnable back to its default" "$(cat "$main_conf")"
pass "migration puts AutoEnable back to its default"
[[ -e $marker ]] || fail "migration records the machine as done"
pass "migration records the machine as done"
# Anything else is a machine that has been booting with Bluetooth off, and the
# block is what carries that over now AutoEnable no longer holds the adapter down.
reset_machine
POWERED=no run_migration
grep -qx 'omarchy-bluetooth-power off' "$CALLS" ||
fail "migration carries an unpowered adapter over to the block" "$(cat "$CALLS")"
pass "migration carries an unpowered adapter over to the block"
# No daemon to ask reads the same way: off is what the machine has been doing.
reset_machine
run_migration
grep -qx 'omarchy-bluetooth-power off' "$CALLS" ||
fail "migration blocks when no adapter can be read" "$(cat "$CALLS")"
pass "migration blocks when no adapter can be read"
# /dev/rfkill is only writable unelevated from an active graphical seat, so an
# update run over SSH would abort here and abort again on every retry.
grep -qx 'sudo omarchy-bluetooth-power off' "$CALLS" ||
fail "migration changes the radio through sudo" "$(cat "$CALLS")"
pass "migration changes the radio through sudo"
# A second account must not undo an administrator's later choice, since migration
# completion is recorded per user.
printf '[Policy]\nAutoEnable=false\n' >"$main_conf"
POWERED=yes run_migration
grep -qx 'AutoEnable=false' "$main_conf" ||
fail "migration leaves a later opt-out alone" "$(cat "$main_conf")"
pass "migration leaves a later opt-out alone"
[[ ! -s $CALLS ]] ||
fail "migration touches no radio state on a second run" "$(cat "$CALLS")"
pass "migration touches no radio state on a second run"
# Only the exact line Omarchy wrote is reverted, so a hand-edited opt-out stands.
reset_machine
printf '[Policy]\nAutoEnable = false\n' >"$main_conf"
POWERED=yes run_migration
grep -qx 'AutoEnable = false' "$main_conf" ||
fail "migration keeps a hand-edited AutoEnable" "$(cat "$main_conf")"
pass "migration keeps a hand-edited AutoEnable"
+4
View File
@@ -71,6 +71,10 @@ jq -e --arg path "$ROOT/bin/omarchy-chromium-copy-url-host" '
' "$native_manifest" >/dev/null || fail "copy-url native host manifest uses Omarchy host path and extension id"
pass "copy-url native host installer registers the stable extension id"
[[ -f $test_home/.config/BraveSoftware/Brave-Origin/NativeMessagingHosts/com.omarchy.copy_url.json ]] ||
fail "copy-url native host installer covers Brave Origin"
pass "copy-url native host installer covers Brave Origin"
# Chromium ships in the base packages, so fresh installs do not go through
# omarchy-install-browser, and they mark every migration as already applied.
# The user install still has to register the host itself.
+4
View File
@@ -33,6 +33,10 @@ jq -e --arg path "$ROOT/bin/omarchy-chromium-ytdlp-host" '
' "$manifest_path" >/dev/null
pass "yt-dlp native host manifest uses Omarchy host path and extension id"
[[ -f $test_home/.config/BraveSoftware/Brave-Origin/NativeMessagingHosts/com.omarchy.ytdlp.json ]] ||
fail "yt-dlp native host installer covers Brave Origin"
pass "yt-dlp native host installer covers Brave Origin"
parse_result=$(bash -c '
OMARCHY_PATH="$3"
source "$1"
+10 -36
View File
@@ -38,6 +38,15 @@ jq -e '
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default clock date format has no leading zero"
jq -e '
def ids: map(.id // .);
(.bar.layout.right | ids) as $ids |
($ids | index("omarchy.tray")) as $tray |
($ids | index("omarchy.agents")) as $agents |
$tray != null and $agents == $tray + 1
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default right layout keeps agents next to the tray"
ROOT="$ROOT" python3 <<'PY'
import json
import os
@@ -142,6 +151,7 @@ package_defaults = [
("default/systemd/user/omarchy-fcitx5.service", "/usr/lib/systemd/user/omarchy-fcitx5.service", "systemd/user/omarchy-fcitx5.service"),
("default/systemd/user/omarchy-crash-watch.service", "/usr/lib/systemd/user/omarchy-crash-watch.service", "systemd/user/omarchy-crash-watch.service"),
("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"),
("default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf", "/usr/lib/systemd/system/plocate-updatedb.service.d/10-omarchy.conf", "systemd/system/plocate-updatedb.service.d/10-omarchy.conf"),
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"),
]
@@ -418,39 +428,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"
+275 -5
View File
@@ -19,6 +19,7 @@ mise_history="$test_tmp/mise-history"
stub_log="$test_tmp/stubs"
terminal_log="$test_tmp/terminal"
menu_log="$test_tmp/menu"
muse_login_log="$test_tmp/muse-login"
mkdir -p "$mock_bin" "$test_home"
cat >"$mock_bin/omarchy-notification-send" <<'SH'
@@ -69,6 +70,22 @@ cat >"$mock_bin/omarchy-menu" <<'SH'
printf '%s\0' "$@" >"$OMARCHY_TEST_AGENT_MENU_LOG"
SH
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
echo "Muse must install through mise" >&2
exit 1
SH
ln -s omarchy-pkg-add "$mock_bin/omarchy-pkg-aur-add"
cat >"$mock_bin/muse" <<'SH'
#!/bin/bash
if [[ ${1:-} == "login" ]]; then
printf 'muse %s\n' "$*" >>"$OMARCHY_TEST_MUSE_LOGIN_LOG"
else
printf '%s\0' muse "$@" >"$OMARCHY_TEST_AGENT_INLINE_LOG"
fi
SH
cat >"$mock_bin/omarchy-test-noop" <<'SH'
#!/bin/bash
exit 0
@@ -91,6 +108,7 @@ export OMARCHY_TEST_MISE_HISTORY="$mise_history"
export OMARCHY_TEST_STUB_LOG="$stub_log"
export OMARCHY_TEST_AGENT_TERMINAL_LOG="$terminal_log"
export OMARCHY_TEST_AGENT_MENU_LOG="$menu_log"
export OMARCHY_TEST_MUSE_LOGIN_LOG="$muse_login_log"
export OMARCHY_PATH="$ROOT"
grok_package="npm:@xai-official/grok"
@@ -98,6 +116,8 @@ omp_package="github:can1357/oh-my-pi"
crush_package="crush"
agy_package="antigravity-cli"
ori_package="github:OpenRouterLabs/ori-releases"
cursor_agent_package="cursor-agent"
muse_package="http:muse[url=https://api.meta.ai/muse-launcher.sh,bin=muse,version_list_url=https://api.meta.ai/muse-code/channels/muse-stable,version_json_path=.version]"
assert_lazy_stub() {
local package=$1
@@ -116,16 +136,41 @@ assert_lazy_stub "$grok_package" grok
assert_lazy_stub "$omp_package" omp
assert_lazy_stub "$crush_package" crush
assert_lazy_stub "$ori_package" ori
assert_lazy_stub "$cursor_agent_package" cursor-agent
assert_lazy_stub "$muse_package" muse
pass "custom agent lazy stubs preserve their mise packages"
source "$ROOT/install/user/mise.sh"
OMARCHY_TEST_MISSING_COMMAND=cursor-agent source "$ROOT/install/user/mise.sh"
grep -Fx "$agy_package agy" "$stub_log" >/dev/null || fail "user setup creates the Antigravity lazy stub"
grep -Fx "$grok_package grok" "$stub_log" >/dev/null || fail "user setup creates the Grok lazy stub"
grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null || fail "user setup creates the Cursor CLI lazy stub"
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "user setup creates the Oh My Pi lazy stub"
grep -Fx "$crush_package" "$stub_log" >/dev/null || fail "user setup creates the Crush lazy stub"
grep -Fx "$ori_package ori" "$stub_log" >/dev/null || fail "user setup creates the Ori lazy stub"
OMARCHY_TEST_MISSING_COMMAND=muse source "$ROOT/install/user/mise.sh"
grep -Fx "$muse_package muse" "$stub_log" >/dev/null || fail "user setup creates the Muse lazy stub"
pass "user setup creates the custom agent lazy stubs"
: >"$stub_log"
source "$ROOT/install/user/mise.sh"
grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null && fail "user setup replaces an existing cursor-agent command"
pass "user setup keeps an existing Cursor CLI install"
grep -Fx "$muse_package muse" "$stub_log" >/dev/null && fail "user setup replaces an existing Muse command"
: >"$stub_log"
OMARCHY_TEST_MISSING_COMMAND=muse source "$ROOT/migrations/1788724825.sh" >/dev/null
grep -Fx "$muse_package muse" "$stub_log" >/dev/null || fail "Muse migration creates its lazy stub"
: >"$stub_log"
source "$ROOT/migrations/1788724825.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "Muse migration replaces an existing command"
mkdir -p "$test_home/.local/state/omarchy"
touch "$test_home/.local/state/omarchy/preinstalls-removed"
OMARCHY_TEST_MISSING_COMMAND=muse source "$ROOT/migrations/1788724825.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "Muse migration ignores the preinstall opt-out"
rm "$test_home/.local/state/omarchy/preinstalls-removed"
pass "Muse migration preserves existing installs and the preinstall opt-out"
: >"$stub_log"
source "$ROOT/migrations/1785617047.sh" >/dev/null
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration creates a working lazy stub"
@@ -134,6 +179,17 @@ grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration c
source "$ROOT/migrations/1787342993.sh" >/dev/null
grep -Fx "$ori_package ori" "$stub_log" >/dev/null || fail "Ori migration creates a working lazy stub"
: >"$stub_log"
export OMARCHY_TEST_MISSING_COMMAND=cursor-agent
source "$ROOT/migrations/1788577553.sh" >/dev/null
unset OMARCHY_TEST_MISSING_COMMAND
grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null || fail "Cursor CLI migration creates a working lazy stub"
: >"$stub_log"
source "$ROOT/migrations/1788577553.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "Cursor CLI migration reinstalls an existing cursor-agent command"
pass "Cursor CLI migration preserves an existing Cursor CLI install"
: >"$stub_log"
source "$ROOT/migrations/1785846769.sh" >/dev/null
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "agent migration repairs the Oh My Pi lazy stub"
@@ -216,6 +272,7 @@ touch "$test_home/.local/state/omarchy/preinstalls-removed"
source "$ROOT/migrations/1785617047.sh" >/dev/null
source "$ROOT/migrations/1785846769.sh" >/dev/null
source "$ROOT/migrations/1787342993.sh" >/dev/null
OMARCHY_TEST_MISSING_COMMAND=cursor-agent source "$ROOT/migrations/1788577553.sh" >/dev/null
[[ ! -s $stub_log ]] || fail "agent migrations respect the preinstall opt-out"
[[ ! -e $test_home/.local/bin/omp ]] || fail "agent migration removes the obsolete Oh My Pi wrapper after opt-out"
@@ -240,13 +297,30 @@ rm "$test_home/.local/state/omarchy/preinstalls-removed"
rm -f "$agent_file"
pass "agent migrations install working wrappers without overriding the preinstall opt-out"
"$ROOT/bin/omarchy-mise-install" "$muse_package" muse
touch "$test_home/.local/bin/agy" "$test_home/.local/bin/ori"
omarchy-remove-preinstalls >/dev/null
for command in agy omp ori grok crush; do
for command in agy omp ori grok crush cursor-agent muse; do
[[ ! -e $test_home/.local/bin/$command ]] || fail "Remove Preinstalls deletes the $command lazy stub"
done
pass "Remove Preinstalls deletes every optional agent lazy stub"
# Cursor's installer links the same path, so anything but the mise wrapper is
# the user's own install.
touch "$test_home/.local/bin/cursor-agent.official"
ln -s cursor-agent.official "$test_home/.local/bin/cursor-agent"
omarchy-remove-preinstalls >/dev/null
[[ -L $test_home/.local/bin/cursor-agent ]] || fail "Remove Preinstalls keeps an official Cursor CLI install"
rm -f "$test_home/.local/bin/cursor-agent" "$test_home/.local/bin/cursor-agent.official"
pass "Remove Preinstalls keeps an official Cursor CLI install"
printf '#!/bin/bash\necho user-muse\n' >"$test_home/.local/bin/muse"
chmod +x "$test_home/.local/bin/muse"
omarchy-remove-preinstalls >/dev/null
[[ $("$test_home/.local/bin/muse") == "user-muse" ]] || fail "Remove Preinstalls deletes a user-managed Muse"
rm "$test_home/.local/bin/muse"
pass "Remove Preinstalls keeps a user-managed Muse install"
[[ -z $(omarchy-default-agent) ]] || fail "default agent is unset until one is chosen"
pass "default agent is unset until one is chosen"
@@ -307,6 +381,11 @@ declare -A expected_agents=(
[gemini-cli]="agy"
[copilot]="copilot"
[github-copilot]="copilot"
[cursor]="cursor-agent"
[cursor-agent]="cursor-agent"
[muse]="muse"
[muse-code]="muse"
[musecode]="muse"
)
declare -A expected_packages=(
@@ -320,6 +399,8 @@ declare -A expected_packages=(
[grok]="$grok_package"
[agy]="$agy_package"
[copilot]="copilot"
[cursor-agent]="$cursor_agent_package"
[muse]="$muse_package"
)
for selection in "${!expected_agents[@]}"; do
@@ -329,8 +410,12 @@ for selection in "${!expected_agents[@]}"; do
[[ $(omarchy-default-agent) == $expected ]] || fail "default agent canonicalizes $selection"
mapfile -d '' -t mise_args <"$mise_log"
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" && ${mise_args[2]} == ${expected_packages[$expected]} ]] ||
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" ]] ||
fail "default agent installs $selection globally through mise"
case ${mise_args[2]} in
"${expected_packages[$expected]}") ;;
*) fail "default agent preserves $selection backend options" ;;
esac
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] ||
@@ -380,6 +465,35 @@ mapfile -d '' -t agent_open_args <"$agent_open_log"
fail "installed agent opens in a new terminal after selection"
pass "installed agents select and open without notifications"
# Cursor's installer links the wrapper's path, and the mise shims precede
# ~/.local/bin, so a mise copy would shadow the user's own install.
touch "$test_home/.local/bin/cursor-agent.official"
chmod +x "$test_home/.local/bin/cursor-agent.official"
ln -s cursor-agent.official "$test_home/.local/bin/cursor-agent"
: >"$terminal_log"
: >"$mise_log"
: >"$agent_open_log"
omarchy-default-agent cursor-agent
[[ ! -s $terminal_log ]] || fail "an official Cursor CLI install needs no install terminal"
[[ ! -s $mise_log ]] || fail "an official Cursor CLI install is left to itself by mise"
[[ $(<"$agent_file") == "cursor-agent" ]] || fail "an official Cursor CLI install becomes the default"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] ||
fail "an official Cursor CLI install opens after selection"
rm -f "$test_home/.local/bin/cursor-agent" "$test_home/.local/bin/cursor-agent.official"
printf '%s\n' copilot >"$agent_file"
pass "selecting an official Cursor CLI install skips mise"
# A file nothing can run is not an install; the wrapper is still wanted.
touch "$test_home/.local/bin/cursor-agent"
: >"$terminal_log"
omarchy-default-agent cursor-agent
mapfile -d '' -t terminal_args <"$terminal_log"
[[ ${terminal_args[*]} == "omarchy-default-agent --install cursor-agent" ]] ||
fail "a dead file at the wrapper's path still installs Cursor CLI"
rm -f "$test_home/.local/bin/cursor-agent"
pass "a dead file at the wrapper's path does not pass for an install"
: >"$agent_open_log"
if omarchy-default-agent unsupported >"$test_tmp/invalid-output" 2>&1; then
fail "default agent rejects unsupported providers"
@@ -414,6 +528,76 @@ grep -F "Could not set Codex as the default coding agent" "$test_tmp/setup-failu
[[ ! -s $agent_open_log ]] || fail "failed activation does not open an agent"
pass "default agent reports mise failures without notifications"
# Muse follows the shared mise installation and launch path.
: >"$notification_history"
: >"$agent_open_log"
: >"$terminal_log"
omarchy-default-agent muse
mapfile -d '' -t terminal_args <"$terminal_log"
[[ ${terminal_args[0]} == "omarchy-default-agent" && ${terminal_args[1]} == "--install" && ${terminal_args[2]} == "muse" ]] ||
fail "missing Muse installation opens in a terminal"
[[ ! -s $notification_history ]] || fail "missing Muse installation skips notifications"
[[ ! -s $agent_open_log ]] || fail "missing Muse installation waits to open the agent"
[[ $(omarchy-default-agent) == "copilot" ]] || fail "missing Muse installation waits to change the selection"
if OMARCHY_TEST_MISE_FAIL=true omarchy-default-agent --install muse >"$test_tmp/muse-install-failure-output" 2>&1; then
fail "missing Muse rejects a failed mise installation"
fi
[[ $(omarchy-default-agent) == "copilot" ]] || fail "failed Muse installation preserves the current default"
[[ ! -s $muse_login_log && ! -s $agent_open_log ]] || fail "failed Muse installation skips login and launch"
grep -F "Could not install Muse Code with mise" "$test_tmp/muse-install-failure-output" >/dev/null ||
fail "failed Muse installation identifies mise"
pass "failed Muse mise installation preserves the selection and skips login"
: >"$mise_history"
: >"$stub_log"
omarchy-default-agent --install muse >"$test_tmp/muse-install-output"
grep -Fx "use -g $muse_package" "$mise_history" >/dev/null || fail "visible Muse installation uses the HTTP backend"
[[ ! -s $stub_log ]] || fail "Muse selection recreates its preinstalled wrapper"
[[ ! -s $muse_login_log ]] || fail "Muse selection runs a separate login flow"
[[ $(omarchy-default-agent) == "muse" ]] || fail "visible Muse installation changes the selection"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 2 && ${agent_open_args[0]} == "omarchy-agent" && ${agent_open_args[1]} == "--inline" ]] ||
fail "newly installed Muse opens in the installation terminal"
pass "Muse installs visibly through mise and opens directly"
: >"$terminal_log"
: >"$muse_login_log"
: >"$agent_open_log"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent muse-code
[[ ! -s $terminal_log ]] || fail "installed Muse selection skips the terminal"
[[ ! -s $muse_login_log ]] || fail "installed Muse selection skips the login"
[[ $(omarchy-default-agent) == "muse" ]] || fail "default agent canonicalizes muse-code"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] ||
fail "installed Muse opens in a new terminal after selection"
pass "installed Muse selects and opens directly"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
: >"$agent_open_log"
if OMARCHY_TEST_AGENT_INSTALLED=true OMARCHY_TEST_MISE_FAIL=true omarchy-default-agent musecode >"$test_tmp/muse-failure-output" 2>&1; then
fail "default agent rejects a failed Muse activation"
fi
[[ $(omarchy-default-agent) == "pi" ]] || fail "failed Muse activation preserves the current default agent"
grep -F "Could not set Muse Code as the default coding agent" "$test_tmp/muse-failure-output" >/dev/null ||
fail "default agent reports a failed Muse activation"
[[ ! -s $agent_open_log ]] || fail "failed Muse activation does not open an agent"
pass "default agent reports Muse mise failures without changing the selection"
# A manually installed launcher belongs to the user; selecting it must not
# install a second copy or replace it with the Omarchy wrapper.
printf '#!/bin/bash\necho user-muse\n' >"$test_home/.local/bin/muse"
chmod +x "$test_home/.local/bin/muse"
: >"$mise_history"
: >"$stub_log"
: >"$terminal_log"
omarchy-default-agent muse
[[ $(omarchy-default-agent) == "muse" ]] || fail "a user-installed Muse can be selected"
[[ ! -s $mise_history && ! -s $stub_log && ! -s $terminal_log ]] || fail "a user-installed Muse skips installation and wrapper creation"
[[ $("$test_home/.local/bin/muse") == "user-muse" ]] || fail "a user-installed Muse is preserved"
rm "$test_home/.local/bin/muse"
pass "selecting a user-installed Muse preserves its launcher"
rm "$mock_bin/omarchy-agent"
hash -r
@@ -431,8 +615,10 @@ assert_launched() {
fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}"
for ((index = 0; index < ${#expected[@]}; index++)); do
[[ ${actual[$index]} == ${expected[$index]} ]] ||
fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}"
case ${actual[$index]} in
"${expected[$index]}") ;;
*) fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}" ;;
esac
done
}
@@ -460,20 +646,39 @@ 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"
assert_launched hermes "binds its literal initial prompt" env -u HERMES_SESSION_SOURCE \
hermes chat --yolo --tui "--query=$literal_hermes_prompt"
pass "Hermes receives prompted launches as one literal query argument"
assert_bypass pi pi
assert_bypass omp omp --auto-approve
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
pass "agent launcher skips permission prompts for every supported agent"
@@ -531,3 +736,68 @@ fi
grep -F "missing is not installed" "$test_tmp/missing-output" >/dev/null ||
fail "agent launcher explains when the default command is missing"
pass "agent launcher reports a missing default command"
# OpenClaw comes from its pacman package, not mise: choosing it must route
# through omarchy-install-openclaw-cli and never touch a mise environment.
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == openclaw && ${OMARCHY_TEST_OPENCLAW_INSTALLED:-false} == "true" ]]
SH
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
printf '%s\n' "pkg-add $*" >>"$OMARCHY_TEST_STUB_LOG"
SH
cat >"$mock_bin/omarchy-launch-openclaw" <<'SH'
#!/bin/bash
printf '%s\0' omarchy-launch-openclaw "$@" >"$OMARCHY_TEST_AGENT_INLINE_LOG"
SH
cat >"$mock_bin/openclaw" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin/omarchy-pkg-present" "$mock_bin/omarchy-pkg-add" \
"$mock_bin/omarchy-launch-openclaw" "$mock_bin/openclaw"
: >"$launch_log"
: >"$terminal_log"
: >"$mise_history"
OMARCHY_TEST_OPENCLAW_INSTALLED=true omarchy-default-agent openclaw
read -r chosen <"$agent_file"
[[ $chosen == openclaw ]] || fail "choosing OpenClaw records it as the default agent"
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent omarchy-launch-openclaw --tui" ]] ||
fail "choosing OpenClaw launches its terminal UI"
[[ ! -s $terminal_log ]] || fail "an installed OpenClaw needs no install terminal"
! grep -q 'use -g openclaw' "$mise_history" || fail "OpenClaw never installs through mise"
pass "choosing OpenClaw uses the package and launches its terminal UI"
: >"$terminal_log"
OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent openclaw
mapfile -d '' -t terminal_args <"$terminal_log"
[[ ${terminal_args[*]} == "omarchy-default-agent --install openclaw" ]] ||
fail "a missing OpenClaw routes through the install terminal"
pass "a missing OpenClaw routes through the install terminal"
: >"$stub_log"
: >"$inline_log"
OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent --install openclaw >/dev/null
grep -Fx "pkg-add openclaw" "$stub_log" >/dev/null ||
fail "installing OpenClaw as default agent adds its package"
mapfile -d '' -t inline_args <"$inline_log"
[[ ${inline_args[*]} == "omarchy-launch-openclaw --tui" ]] ||
fail "installing OpenClaw as default agent hands over to its terminal UI"
pass "installing OpenClaw as default agent adds its package"
: >"$launch_log"
omarchy agent prompt "Review this project"
mapfile -d '' -t launch_args <"$launch_log"
# Element-wise: the prompt must travel as one argv entry, which a space-joined
# comparison could not tell apart from a prompt split into words.
[[ ${#launch_args[@]} == 5 &&
${launch_args[0]} == "--app-id=org.omarchy.agent" &&
${launch_args[1]} == "omarchy-launch-openclaw" &&
${launch_args[2]} == "--tui" &&
${launch_args[3]} == "--message" &&
${launch_args[4]} == "Review this project" ]] ||
fail "OpenClaw receives prompts through --message" "argv: ${launch_args[*]}"
pass "OpenClaw receives prompts through --message"
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
#
# The fingerprint driver migration only repairs a machine an earlier version of
# it left with fprintd and no libfprint; any installed driver is left alone.
# The real package helpers run over a stubbed pacman.
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1785090473.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin"
export CALL_LOG="$scratch/calls"
export PATH="$scratch/bin:$ROOT/bin:$PATH"
cat > "$scratch/bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
# INSTALLED lists the installed package names, one per line; an install adds
# its packages to INSTALLED_LOG so omarchy-pkg-add's follow-up query sees them.
cat > "$scratch/bin/pacman" <<'STUB'
#!/bin/bash
case "$1" in
-Q) grep -qx "$2" <<< "${INSTALLED:-}" || grep -qx "$2" "$INSTALLED_LOG" ;;
-S)
printf 'pacman %s\n' "$*" >> "$CALL_LOG"
for arg in "$@"; do
[[ $arg == -* ]] || printf '%s\n' "$arg" >> "$INSTALLED_LOG"
done
;;
*) printf 'pacman %s\n' "$*" >> "$CALL_LOG" ;;
esac
STUB
chmod +x "$scratch/bin/"*
export INSTALLED_LOG="$scratch/installed"
run_migration() {
: > "$CALL_LOG"
: > "$INSTALLED_LOG"
bash -euo pipefail "$migration" > /dev/null
}
INSTALLED='fprintd' run_migration
grep -qx 'pacman -S --noconfirm --needed libfprint-git' "$CALL_LOG" || fail "fprintd without a library gets libfprint-git"
pass "fprintd without a library gets libfprint-git"
INSTALLED=$'libfprint-git\nfprintd' run_migration
[[ ! -s $CALL_LOG ]] || fail "an installed libfprint-git is left alone" "$(<"$CALL_LOG")"
pass "an installed libfprint-git is left alone"
INSTALLED=$'libfprint\nfprintd' run_migration
[[ ! -s $CALL_LOG ]] || fail "an installed stock libfprint is left alone" "$(<"$CALL_LOG")"
pass "an installed stock libfprint is left alone"
INSTALLED='' run_migration
[[ ! -s $CALL_LOG ]] || fail "a machine without fprintd is left alone" "$(<"$CALL_LOG")"
pass "a machine without fprintd is left alone"
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
#
# The fingerprint setup installs libfprint-git in place of stock libfprint. The
# two conflict, so the swap has to happen inside one --ask 4 transaction, and a
# rerun with everything installed must not touch pacman at all. The real
# omarchy-pkg-missing runs; pacman and the privileged calls are stubbed.
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin"
export CALL_LOG="$scratch/calls"
export PATH="$scratch/bin:$ROOT/bin:$PATH"
cat > "$scratch/bin/omarchy-hw-fingerprint" <<'STUB'
#!/bin/bash
exit "${HARDWARE_STATUS:-0}"
STUB
cat > "$scratch/bin/sudo" <<'STUB'
#!/bin/bash
case "$1" in
pacman | fprintd-enroll) exec "$@" ;;
*) echo "Unexpected privileged call: $*" >> "$CALL_LOG"; exit 99 ;;
esac
STUB
# INSTALLED lists the installed package names, one per line.
cat > "$scratch/bin/pacman" <<'STUB'
#!/bin/bash
case "$1" in
-Q) grep -qx "$2" <<< "${INSTALLED:-}" ;;
-S)
printf 'pacman %s\n' "$*" >> "$CALL_LOG"
exit "${INSTALL_STATUS:-0}"
;;
*) printf 'pacman %s\n' "$*" >> "$CALL_LOG"; exit 99 ;;
esac
STUB
cat > "$scratch/bin/fprintd-enroll" <<'STUB'
#!/bin/bash
# Stop before verification/PAM; no host authentication files may be changed.
echo enroll >> "$CALL_LOG"
exit 1
STUB
cat > "$scratch/bin/fprintd-verify" <<'STUB'
#!/bin/bash
echo verify >> "$CALL_LOG"
exit 1
STUB
chmod +x "$scratch/bin/"*
run_setup() {
: > "$CALL_LOG"
if "$ROOT/bin/omarchy-setup-security-fingerprint" > "$scratch/output" 2>&1; then
fail "setup stops on the simulated enrollment or installation failure"
fi
if grep -q 'Unexpected privileged call' "$CALL_LOG"; then
fail "setup does not change PAM after failed enrollment"
fi
}
assert_installs() {
grep -qx 'pacman -S --needed --noconfirm --ask 4 libfprint-git fprintd usbutils' "$CALL_LOG" || fail "$1"
(( $(grep -c '^pacman ' "$CALL_LOG") == 1 )) || fail "$1: one pacman transaction"
}
run_setup
assert_installs "a fresh machine installs libfprint-git, fprintd and usbutils"
grep -qx enroll "$CALL_LOG" || fail "installation is followed by enrollment"
pass "a fresh machine installs libfprint-git and reaches enrollment"
INSTALLED=$'libfprint\nfprintd\nusbutils' run_setup
assert_installs "installed stock libfprint is replaced in the same transaction"
pass "installed stock libfprint is replaced without a removal step"
INSTALLED=$'libfprint-git\nfprintd\nusbutils' run_setup
if grep -q '^pacman' "$CALL_LOG"; then
fail "a rerun with everything installed does not touch pacman"
fi
grep -qx enroll "$CALL_LOG" || fail "a rerun with everything installed reaches enrollment"
pass "a rerun with everything installed goes straight to enrollment"
INSTALL_STATUS=1 run_setup
if grep -qx enroll "$CALL_LOG"; then
fail "a failed package transaction prevents enrollment"
fi
pass "a failed installation stops before enrollment"
HARDWARE_STATUS=1 run_setup
[[ ! -s $CALL_LOG ]] || fail "missing hardware stops before package operations"
pass "missing hardware performs no package operations"
@@ -0,0 +1,58 @@
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from kitty.config import load_config
from kitty.options.utils import parse_map
root = Path(os.environ['ROOT'])
system = root / 'etc/xdg/kitty/kitty.conf'
template = root / 'config/kitty/kitty.conf'
legacy = root / 'test/shell.d/fixtures/kitty/legacy.conf'
active_lines = [line for line in template.read_text().splitlines() if line and not line.startswith('#')]
assert active_lines == ['include ~/.local/state/omarchy/current/theme/kitty.conf']
with TemporaryDirectory() as tmp:
user = Path(tmp) / 'kitty.conf'
# Use a local theme to avoid depending on the developer's generated state.
theme = Path(tmp) / 'theme.conf'
theme.write_text('background #123456\n')
themed = f'include {theme}\n'
user.write_text(themed)
errors = []
opts = load_config(str(system), str(user), accumulate_bad_lines=errors)
assert not errors, errors
assert opts.allow_remote_control == 'socket-only'
assert opts.listen_on == 'unix:${XDG_RUNTIME_DIR}/omarchy-kitty-{kitty_pid}'
old = Path(tmp) / 'legacy.conf'
old.write_text(legacy.read_text().replace(active_lines[0], themed.strip()))
before = load_config(str(old), accumulate_bad_lines=errors)
# Moving defaults must preserve appearance and behavior except remote control.
for key in ('font_family', 'bold_italic_font', 'font_size', 'window_padding_width',
'hide_window_decorations', 'confirm_os_window_close', 'cursor_shape',
'cursor_blink_interval', 'shell_integration', 'enable_audio_bell',
'tab_bar_edge', 'tab_bar_style', 'tab_powerline_style',
'tab_title_template', 'background'):
assert getattr(opts, key) == getattr(before, key), key
def binding(options, shortcut):
trigger = next(parse_map(shortcut)).trigger
return [entry.definition for entry in options.keyboard_modes[''].keymap.get(trigger, [])]
for shortcut in ('ctrl+insert', 'shift+insert', 'shift+enter', 'alt+shift+enter'):
assert binding(opts, shortcut) == binding(before, shortcut), shortcut
user.write_text(themed + 'font_size 15\nmap ctrl+insert\nmap shift+insert copy_to_clipboard\n')
opts = load_config(str(system), str(user), accumulate_bad_lines=errors)
assert opts.font_size == 15
assert not any(binding(opts, 'ctrl+insert'))
assert binding(opts, 'shift+insert')[-1] == 'copy_to_clipboard'
user.write_text(themed + 'clear_all_shortcuts yes\nmap f1 new_window\n')
opts = load_config(str(system), str(user), accumulate_bad_lines=errors)
assert len(opts.keyboard_modes[''].keymap) == 1
assert binding(opts, 'f1') == ['new_window']
assert not errors, errors
print('ok - Kitty loads defaults and theme, preserves appearance, and supports user overrides and unmapping')
+35
View File
@@ -0,0 +1,35 @@
include ~/.local/state/omarchy/current/theme/kitty.conf
# Font
font_family JetBrainsMono Nerd Font
bold_italic_font auto
font_size 9.0
# Window
window_padding_width 14
hide_window_decorations yes
confirm_os_window_close 0
# Keybindings
map ctrl+insert copy_to_clipboard
map shift+insert paste_from_clipboard
# Send Shift+Enter as CSI-u so TUIs can distinguish it from Enter.
map shift+enter send_text all \e[13;2u
# Kitty legacy encoding sends Alt+Shift+Enter the same as Alt+Enter; send CSI-u so tmux can match M-S-Enter.
map alt+shift+enter send_text all \e[13;4u
# Allow remote access
allow_remote_control yes
listen_on unix:${XDG_RUNTIME_DIR}/omarchy-kitty-{kitty_pid}
# Aesthetics
cursor_shape block
cursor_blink_interval 0
shell_integration no-cursor
enable_audio_bell no
# Minimal Tab bar styling
tab_bar_edge bottom
tab_bar_style powerline
tab_powerline_style slanted
tab_title_template {title}{' :{}:'.format(num_windows) if num_windows > 1 else ''}
@@ -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() }
}
@@ -0,0 +1,20 @@
import QtQuick
import "services/AuthServiceStore.js" as AuthServiceStore
QtObject {
function retain(id, service) {
AuthServiceStore.put(id, service)
}
function has(id) {
return AuthServiceStore.has(id)
}
function isTrusted(id) {
return AuthServiceStore.isTrusted(id)
}
function updateManifest(id, manifest) {
AuthServiceStore.updateManifest(id, manifest)
}
}
@@ -0,0 +1,8 @@
import QtQuick
import "services/AuthServiceStore.js" as AuthServiceStore
QtObject {
function has(id) {
return AuthServiceStore.has(id)
}
}
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "services"
ShellRoot {
id: root
property var calls: []
property QtObject ownService: QtObject {
property string marker: "own"
property var manifest: null
}
AuthStoreOwner { id: authStoreOwner }
AuthStoreReader { id: authStoreReader }
Component {
id: apiComponent
PluginShellApi { }
}
FileView {
id: resultFile
path: Quickshell.env("OMARCHY_QML_TEST_RESULT")
atomicWrites: true
}
Component.onCompleted: {
var caller = "example.safe"
authStoreOwner.retain("omarchy.lock", root.ownService)
authStoreOwner.updateManifest("omarchy.lock", { version: "kept" })
var api = apiComponent.createObject(null, {
pluginId: caller,
idleConfig: { screensaver: 60, lock: 120 },
_serviceLookup: function(requestedId) {
return requestedId === caller ? root.ownService : null
},
_summon: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["summon"])
return true
},
_hide: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["hide"])
return true
},
_toggle: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["toggle"])
return true
},
_isOpen: function(requestedId) { return requestedId === caller },
_updateSettings: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["settings"])
return true
}
})
var own = api.serviceFor(caller)
var result = {
detached: api.parent === undefined || api.parent === null,
ownService: own && own.marker === "own",
foreignService: api.serviceFor("omarchy.lock") === null,
firstPartyService: api.firstPartyServiceFor("omarchy.polkit") === null,
ownSummon: api.summon(caller, "{}") === true,
foreignSummon: api.summon("omarchy.lock", "{}") === false,
ownHide: api.hide(caller) === true,
foreignHide: api.hide("omarchy.lock") === false,
ownToggle: api.toggle(caller, "{}") === true,
foreignToggle: api.toggle("omarchy.lock", "{}") === false,
ownOpen: api.isPluginOpen(caller) === true,
foreignOpen: api.isPluginOpen("omarchy.lock") === false,
ownSettings: api.updateEntryInline(caller, {}) === true,
foreignSettings: api.updateEntryInline("omarchy.lock", {}) === false,
detachedIdleConfig: api.idleConfig.screensaver === 60 && api.idleConfig.lock === 120,
authStoreOwnerRetains: authStoreOwner.has("omarchy.lock") === true,
authStoreOwnerRemembersTrust: authStoreOwner.isTrusted("omarchy.lock") === true,
authStoreOwnerUpdatesManifest: root.ownService.manifest
&& root.ownService.manifest.version === "kept",
authStoreImportIsolated: authStoreReader.has("omarchy.lock") === false,
noGenericPluginShellFactory: typeof api.pluginShellForId !== "function",
calls: root.calls
}
result.ok = Object.keys(result).every(function(key) {
return key === "ok" || key === "calls" || result[key] === true
}) && JSON.stringify(result.calls) === JSON.stringify(["summon", "hide", "toggle", "settings"])
resultFile.setText(JSON.stringify(result))
}
}
@@ -83,6 +83,9 @@ ShellRoot {
scan += block("firstparty", "/first/bar", manifest("omarchy.bar", ["bar"], { bar: "Bar.qml" }))
scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" }))
scan += block("firstparty", "/first/hybrid", manifest("omarchy.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.qml" }))
var futureAuth = manifest("omarchy.future-auth", ["service"], { service: "Service.qml" })
futureAuth.omarchy = { capabilities: ["authentication"] }
scan += block("firstparty", "/first/future-auth", futureAuth)
scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "left" }))
scan += block("thirdparty", "/third/center-widget", manifest("third.center-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
@@ -103,6 +106,12 @@ ShellRoot {
localBar.omarchy = { clonedFrom: "omarchy.bar" }
scan += block("thirdparty", "/third/local-bar", localBar)
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
var localFutureAuth = manifest("local.future-auth", ["service"], { service: "Service.qml" })
localFutureAuth.omarchy = { clonedFrom: "omarchy.future-auth" }
scan += block("thirdparty", "/third/local-future-auth", localFutureAuth)
var spoofedAuth = manifest("third.spoofed-auth", ["service"], { service: "Service.qml" })
spoofedAuth.omarchy = { capabilities: ["authentication"] }
scan += block("thirdparty", "/third/spoofed-auth", spoofedAuth)
scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/unsafe", manifest("third.unsafe", ["panel"], { panel: "../Panel.qml" }))
@@ -116,22 +125,28 @@ ShellRoot {
root.assertDeepEqual(pluginIds(), [
"local.bar",
"local.first-widget",
"local.future-auth",
"local.grouped-panel",
"local.hybrid",
"local.weather",
"omarchy.bar",
"omarchy.first-widget",
"omarchy.future-auth",
"omarchy.grouped-panel",
"omarchy.hybrid",
"third.bar",
"third.center-widget",
"third.panel",
"third.right-widget",
"third.spoofed-auth",
"third.widget"
], "registry merges valid first-party and third-party manifests")
root.assertTrue(registry.installedPlugins["omarchy.first-widget"].__isFirstParty === true, "first-party manifests are stamped")
root.assertTrue(registry.installedPlugins["third.panel"].__isFirstParty === false, "third-party manifests are stamped")
root.assertDeepEqual(registry.installedPlugins["omarchy.future-auth"].__hostCapabilities, ["authentication"], "trusted manifests stamp authentication capability")
root.assertDeepEqual(registry.installedPlugins["local.future-auth"].__hostCapabilities, ["authentication"], "clones inherit trusted host capabilities")
root.assertDeepEqual(registry.installedPlugins["third.spoofed-auth"].__hostCapabilities, [], "third-party manifests cannot self-grant host capabilities")
root.assertEqual(registry.installedPlugins["omarchy.grouped-panel"].__sourceDir, "/first/panels/grouped", "grouped plugin source paths are preserved")
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.panel"], "panel"), "file:///third/panel/Panel.qml", "entryPointUrl resolves plugin-relative paths")
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.widget"], "barWidget"), "file:///third/widget/Widget.qml", "entryPointUrl resolves bar widget paths")
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1787760281.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
hermes="$test_home/.local/bin/hermes"
marker="# Written by omarchy-install-hermes-cli."
mkdir -p "$mock_bin" "$test_home/.local/bin" "$test_home/.local/state/omarchy"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
#!/bin/bash
! command -v "$1" >/dev/null 2>&1
SH
mise_log="$test_tmp/mise-log"
cat >"$mock_bin/mise" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
[[ $1 != "where" ]]
SH
chmod +x "$mock_bin"/*
# The real installer is on PATH so the migration writes today's stub, not a
# copy of it.
run_migration() {
OMARCHY_TEST_DESKTOP_INSTALLED="${1:-0}" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$test_home" \
PATH="$mock_bin:$ROOT/bin:$PATH" \
bash -euo pipefail "$migration" >/dev/null 2>&1
}
run_migration || fail "the migration installs the wrapper on a plain install"
[[ -x $hermes ]] && grep -qxF "$marker" "$hermes" || fail "the migration writes the Omarchy wrapper"
pass "the migration installs the Hermes wrapper"
before=$(cat "$hermes")
run_migration || fail "rerunning the migration succeeds"
[[ $(cat "$hermes") == "$before" ]] || fail "rerunning the migration leaves the same wrapper"
pass "the migration is idempotent"
chmod -x "$hermes"
run_migration || fail "the migration repairs a non-executable Omarchy wrapper"
[[ -x $hermes ]] && grep -qxF "$marker" "$hermes" ||
fail "the migration restores a non-executable Omarchy wrapper"
pass "the migration repairs a non-executable Omarchy wrapper"
rm -f "$hermes"
touch "$test_home/.local/state/omarchy/preinstalls-removed"
run_migration || fail "the migration succeeds for users who removed the preinstalls"
[[ ! -e $hermes ]] || fail "the migration respects the preinstalls opt-out"
pass "the migration skips users who removed the preinstalls"
rm -f "$test_home/.local/state/omarchy/preinstalls-removed"
run_migration 1 || fail "the migration succeeds when Hermes Desktop owns Hermes"
[[ ! -e $hermes ]] || fail "the migration writes nothing when Hermes Desktop owns Hermes"
pass "the migration stands aside for Hermes Desktop"
# Standing aside is not the same as leaving a second Hermes behind: the wrapper
# an earlier install wrote and the mise copy it points at both go when the
# desktop app owns Hermes, even though the app has not finished setting up.
printf '%s\n' "#!/bin/bash" "$marker" >"$hermes"
chmod +x "$hermes"
: >"$mise_log"
run_migration 1 || fail "the migration succeeds when Hermes Desktop owns Hermes and the old wrapper is present"
[[ ! -e $hermes ]] || fail "the migration removes the Omarchy wrapper when Hermes Desktop owns Hermes"
mise_calls=$(tr '\0' ' ' <"$mise_log")
[[ $mise_calls == *"rm -g "* ]] || fail "the migration removes the global mise Hermes for Hermes Desktop"
[[ $mise_calls == *"uninstall --all "* ]] || fail "the migration uninstalls the mise Hermes for Hermes Desktop"
pass "the migration clears the old Omarchy Hermes for Hermes Desktop"
# ...while anyone else's hermes stays exactly where it is, and is not run.
foreign_ran="$test_tmp/foreign-ran"
foreign_body="#!/bin/bash
touch $foreign_ran
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$foreign_body" >"$hermes"
chmod +x "$hermes"
run_migration 1 || fail "the migration succeeds over a foreign hermes when Hermes Desktop owns Hermes"
[[ -x $hermes && $(cat "$hermes") == "$foreign_body" ]] ||
fail "the migration leaves a foreign hermes alone when Hermes Desktop owns Hermes"
[[ ! -e $foreign_ran ]] || fail "the migration does not run a foreign hermes"
pass "the migration preserves a foreign hermes for Hermes Desktop"
rm -f "$hermes"
official_body="#!/bin/bash
unset PYTHONPATH
unset PYTHONHOME
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$official_body" >"$hermes"
chmod +x "$hermes"
run_migration || fail "the migration succeeds over a foreign hermes command"
[[ $(cat "$hermes") == "$official_body" ]] || fail "the migration leaves a foreign hermes command alone"
pass "the migration preserves a foreign hermes command"
chmod -x "$hermes"
run_migration || fail "the migration succeeds over a non-executable foreign hermes"
[[ -f $hermes && ! -x $hermes && $(cat "$hermes") == "$official_body" ]] ||
fail "the migration leaves a non-executable foreign hermes alone"
pass "the migration preserves a non-executable foreign hermes"
rm -f "$hermes"
ln -s "$test_home/nowhere/hermes" "$hermes"
run_migration || fail "the migration succeeds over a dangling hermes link"
[[ -L $hermes && $(readlink "$hermes") == "$test_home/nowhere/hermes" ]] ||
fail "the migration leaves a dangling hermes link alone"
pass "the migration preserves a dangling hermes link"
rm -f "$hermes"
mkdir "$hermes"
run_migration || fail "the migration succeeds over a directory at the hermes path"
[[ -d $hermes ]] || fail "the migration leaves a directory at the hermes path alone"
pass "the migration preserves a directory at the hermes path"
rmdir "$hermes"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
chmod +x "$hermes"
run_migration || fail "the migration succeeds over a wrapper that mentions the installer"
grep -qxF "$marker" "$hermes" && fail "the migration does not rewrite a wrapper that merely mentions the installer"
pass "the migration preserves a wrapper that merely mentions the installer"
+576
View File
@@ -0,0 +1,576 @@
#!/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
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mise_log="$test_tmp/mise-log"
mkdir -p "$mock_bin" "$test_home/.local/bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
#!/bin/bash
! command -v "$1" >/dev/null 2>&1
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"
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
chmod +x "$mock_bin"/*
run_installer() {
OMARCHY_TEST_DESKTOP_INSTALLED="$1" \
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="$test_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" ${2:+"$2"} >/dev/null 2>&1
}
stub_marker="# Written by omarchy-install-hermes-cli."
python_pin="3.13"
app_stub_body='#!/bin/bash
exec /home/x/.hermes/hermes-agent/venv/bin/hermes "$@"'
# Writing the stub must not provision anything: user setup calls this on every
# machine, including the ones that never run Hermes.
: >"$mise_log"
rm -f "$test_home/.local/bin/hermes"
run_installer 0 || fail "installer failed with no desktop installed"
[[ -x $test_home/.local/bin/hermes ]] || fail "installer writes a hermes stub when the desktop is absent"
grep -qxF "$stub_marker" "$test_home/.local/bin/hermes" || fail "the stub records which command wrote it"
tr '\0' ' ' <"$mise_log" | grep -q "use -g --quiet uv" &&
fail "writing the stub does not install uv"
pass "writing the Hermes stub provisions nothing"
# The desktop app owns Hermes, so our own stub must go rather than sit there
# answering `hermes` until the app's bootstrap replaces it.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 || true
[[ ! -e $test_home/.local/bin/hermes ]] ||
fail "the desktop taking over removes the stub this command wrote"
pass "installing the desktop app removes the CLI stub"
# ...but the app's own hermes is not ours to delete.
printf '%s\n' "$app_stub_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 || true
[[ -x $test_home/.local/bin/hermes ]] ||
fail "the desktop app's own hermes command survives"
pass "the app's own hermes command is left alone"
# A copy mise cannot vouch for is still a second Hermes.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 1 || true
tr '\0' '\n' <"$mise_log" | grep -q "uninstall" ||
fail "takeover removes a mise copy even when it is not healthy"
pass "takeover removes an unhealthy mise copy"
# --check answers about Hermes being usable, not about the venv appearing. The
# venv exists from the python-deps stage, several stages before the command.
rm -rf "$test_home/.hermes"
rm -f "$test_home/.local/bin/hermes"
run_installer 1 --check && fail "--check reports Hermes missing before the app installs it"
# The venv command answers the readiness probes, as the real one does: foreign
# wrappers below exec it, and the installer runs both before trusting them.
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 "[-q QUERY, --query QUERY] [--tui]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$test_home/.hermes/hermes-agent/venv/bin/hermes"
run_installer 1 --check && fail "--check waits for the install to finish, not just the venv"
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 --check || fail "--check reports Hermes present once the app has finished"
pass "--check follows the app's completed install"
# An executable called hermes that belongs to something else is not this
# install being ready.
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/somebody-elses-hermes \"\$@\"" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 --check && fail "--check rejects a hermes command belonging to something else"
pass "--check rejects a foreign hermes command"
# A hermes the user installed themselves -- the official installer, a wrapper of
# their own -- is not ours to replace. --check follows whether it runs, and
# installing steps aside so the default agent uses it.
official_body="#!/bin/bash
unset PYTHONPATH
unset PYTHONHOME
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 --check || fail "--check accepts a working foreign hermes command"
run_installer 0 || fail "installing over a foreign hermes command returns success"
run_installer 0 --now || fail "--now over a foreign hermes command returns success"
[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] ||
fail "a foreign hermes command is left untouched"
pass "a foreign hermes command is preserved and satisfies --check"
OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 --check &&
fail "--check rejects a foreign Hermes without native prompted sessions"
OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 &&
fail "installing refuses a foreign Hermes without native prompted sessions"
[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] ||
fail "an older foreign Hermes command is left untouched"
pass "a foreign Hermes must support native prompted sessions"
# Broken foreign paths are still foreign. They cannot be used, so --check says
# so and the installer refuses rather than replacing them.
printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes"
chmod -x "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a non-executable foreign hermes"
run_installer 0 && fail "the installer does not succeed over a non-executable foreign hermes"
[[ -f $test_home/.local/bin/hermes && ! -x $test_home/.local/bin/hermes ]] ||
fail "a non-executable foreign hermes is left untouched"
pass "a non-executable foreign hermes is preserved"
# The executable bit is not enough: a wrapper whose interpreter is gone passes
# -x and still cannot run. The probe has to run it to find out, and finding
# out never touches the file.
broken_interp_body="#!$test_home/nowhere/python3
print('hermes')"
printf '%s\n' "$broken_interp_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a foreign hermes whose interpreter is missing"
run_installer 0 && fail "the installer does not succeed over a foreign hermes whose interpreter is missing"
run_installer 0 --now && fail "--now does not succeed over a foreign hermes whose interpreter is missing"
[[ -x $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$broken_interp_body" ]] ||
fail "a foreign hermes whose interpreter is missing is left untouched"
pass "a foreign hermes with a missing interpreter is preserved and rejected"
# Likewise a wrapper that execs a target that is no longer there.
broken_target_body="#!/bin/bash
exec $test_home/nowhere/hermes \"\$@\""
printf '%s\n' "$broken_target_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a foreign hermes whose target is missing"
run_installer 0 && fail "the installer does not succeed over a foreign hermes whose target is missing"
run_installer 0 --now && fail "--now does not succeed over a foreign hermes whose target is missing"
[[ -x $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$broken_target_body" ]] ||
fail "a foreign hermes whose target is missing is left untouched"
pass "a foreign hermes with a missing target is preserved and rejected"
foreign_target="$test_home/foreign/hermes"
mkdir -p "$(dirname "$foreign_target")"
printf '%s\n' "$official_body" >"$foreign_target"
chmod +x "$foreign_target"
rm -f "$test_home/.local/bin/hermes"
ln -s "$foreign_target" "$test_home/.local/bin/hermes"
run_installer 0 --check || fail "--check accepts a foreign link to a working hermes command"
run_installer 0 || fail "the installer succeeds over a foreign link to a working hermes command"
run_installer 0 --now || fail "--now succeeds over a foreign link to a working hermes command"
[[ -L $test_home/.local/bin/hermes && $(readlink "$test_home/.local/bin/hermes") == "$foreign_target" ]] ||
fail "a foreign link to a working hermes command is left untouched"
pass "a foreign link to a working hermes command is preserved"
rm -f "$test_home/.local/bin/hermes"
ln -s "$test_home/nowhere/hermes" "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a dangling hermes link"
run_installer 0 && fail "the installer does not succeed over a dangling hermes link"
[[ -L $test_home/.local/bin/hermes && $(readlink "$test_home/.local/bin/hermes") == "$test_home/nowhere/hermes" ]] ||
fail "a dangling hermes link is left untouched"
pass "a dangling hermes link is preserved"
# A directory passes -x on search permission alone. It is still not a command.
rm -f "$test_home/.local/bin/hermes"
mkdir "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a directory at the hermes path"
run_installer 0 && fail "the installer does not succeed over a directory at the hermes path"
[[ -d $test_home/.local/bin/hermes ]] || fail "a directory at the hermes path is left untouched"
pass "a directory at the hermes path is preserved and rejected"
# Mentioning the installer is not the same as being written by it.
rmdir "$test_home/.local/bin/hermes"
mentions_body="#!/bin/bash
# Replaces the stub omarchy-install-hermes-cli used to write.
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$mentions_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 || fail "installing over a wrapper that mentions the installer returns success"
[[ $(cat "$test_home/.local/bin/hermes") == "$mentions_body" ]] ||
fail "a wrapper that merely mentions the installer is left untouched"
pass "ownership needs the exact marker line, not a mention"
# Our own stub is ours to rewrite, so reinstalling refreshes it to the current
# template.
rm -f "$test_home/.local/bin/hermes"
printf '%s\n' "#!/bin/bash" "$stub_marker" "# stale template" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 || fail "reinstalling over our own stub succeeds"
grep -qxF "$stub_marker" "$test_home/.local/bin/hermes" || fail "the refreshed stub still carries the marker"
grep -q "stale template" "$test_home/.local/bin/hermes" && fail "reinstalling rewrites our own stub"
grep -q "exec env -u UV_PYTHON mise x" "$test_home/.local/bin/hermes" || fail "the refreshed stub is the current template"
pass "reinstalling refreshes the Omarchy stub"
mkdir -p "$test_tmp/mise/hermes-agent/lib/python$python_pin"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 || fail "reinstalling replaces an older owned Hermes environment"
tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "an older owned Hermes environment is removed from mise config"
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 &&
fail "installing refuses to claim an unmarked Hermes mise environment"
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
fail "an unmarked Hermes mise environment is never removed"
[[ ! -e $test_home/.local/bin/hermes ]] ||
fail "an unmarked Hermes mise environment is not given an Omarchy wrapper"
pass "a Hermes mise environment needs wrapper ownership before replacement"
# install/user/mise.sh is sourced by install/user/all.sh through run_logged,
# which runs it under `bash -eE` and hands its exit code back to
# omarchy-provision-user's `set -euo pipefail`. Everything that finalizes a user
# -- the default browser, the mailto handler, the first-install migration
# markers, the finalize-user marker -- runs after that source, so this leaf
# returning non-zero costs the user all of it. The Hermes installer is the only
# line in it that can fail, and it does exactly that whenever hermes-desktop is
# installed but the app has not been launched yet: the case a second user on a
# shared machine hits on their first login.
mise_sh_home="$test_tmp/mise-sh-home"
mkdir -p "$mise_sh_home/.local/bin"
cat >"$mock_bin/omarchy-mise-install" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin/omarchy-mise-install"
# Desktop installed, nothing bootstrapped: omarchy-install-hermes-cli exits 1.
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$mise_sh_home" \
PATH="$mock_bin:$ROOT/bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" >/dev/null 2>&1 &&
fail "the Hermes installer exits non-zero when the desktop app has not set Hermes up"
# Sourced exactly as run_logged does it.
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$mise_sh_home" \
PATH="$mock_bin:$ROOT/bin:$PATH" \
bash -eE -c 'source "$1"' bash "$ROOT/install/user/mise.sh" >/dev/null 2>&1 ||
fail "user setup survives a Hermes install that cannot finish"
pass "user setup survives a Hermes install that cannot finish"
# UV_PYTHON pins the interpreter Hermes is built against. Left in the
# environment it reaches Hermes itself and every command the agent shells out
# to, so a `uv` run in the user's own project resolves 3.13 there as well --
# uv only warns that this contradicts the project's requires-python, then
# builds the venv anyway. The stub drops it before handing over.
leak_home="$test_tmp/leak-home"
leak_bin="$test_tmp/leak-bin"
leak_log="$test_tmp/leak-log"
leak_prefix="$test_tmp/leak-prefix"
mkdir -p "$leak_home/.local/bin" "$leak_bin" "$leak_prefix/hermes-agent/lib/python$python_pin"
# A mise whose `where` satisfies the stub's probe, so the stub goes straight to
# handing over, and whose `x` records the UV_PYTHON it was handed.
cat >"$leak_bin/mise" <<SH
#!/bin/bash
case \$1 in
where) echo "$leak_prefix" ;;
x) printf '%s' "\${UV_PYTHON-}" >"$leak_log" ;;
esac
SH
chmod +x "$leak_bin/mise"
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$leak_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" >/dev/null 2>&1 ||
fail "the installer writes a stub for the leak check"
HOME="$leak_home" PATH="$leak_bin:$mock_bin:$PATH" \
"$leak_home/.local/bin/hermes" --version >/dev/null 2>&1
[[ -f $leak_log ]] || fail "the stub reaches the command it wraps"
[[ -z $(cat "$leak_log") ]] ||
fail "the interpreter pin does not follow Hermes into the commands it runs"
pass "the interpreter pin does not follow Hermes into the commands it runs"
# --owns is the one answer to whether the wrapper on PATH is this installer's.
# Remove Preinstalls and the migration both ask it rather than carrying their
# own copy of the marker, so a change to what ownership means reaches them.
owns_home="$test_tmp/owns-home"
mkdir -p "$owns_home/.local/bin"
run_owns() {
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$owns_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --owns
}
rm -f "$owns_home/.local/bin/hermes"
run_owns && fail "--owns says no when there is no wrapper at all"
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$owns_home/.local/bin/hermes"
chmod +x "$owns_home/.local/bin/hermes"
run_owns || fail "--owns recognises the stub this installer wrote"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." \
>"$owns_home/.local/bin/hermes"
run_owns && fail "--owns needs the exact marker line, not a mention"
# Quoting the marker inside a longer line is not the same as carrying it: the
# match is whole-line, so a wrapper describing what it replaced stays the
# user's.
printf '%s\n' "#!/bin/bash" "# Replaced '$stub_marker' with my own." \
>"$owns_home/.local/bin/hermes"
run_owns && fail "--owns needs the marker to be the whole line, not part of one"
rm -f "$owns_home/.local/bin/hermes"
ln -s "$test_home/.local/bin/hermes" "$owns_home/.local/bin/hermes"
run_owns && fail "--owns disclaims a symlink, whatever it resolves to"
rm -f "$owns_home/.local/bin/hermes"
pass "--owns answers for the wrapper this installer wrote and nothing else"
# The marker lives in exactly one place. Every other caller asks --owns, so a
# second copy is drift waiting to happen.
marker_copies=$(grep -rl "Written by omarchy-install-hermes-cli" \
"$ROOT/bin" "$ROOT/install" "$ROOT/migrations" 2>/dev/null | wc -l)
(( marker_copies == 1 )) ||
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.
ready_home="$test_tmp/ready-home"
mkdir -p "$ready_home/.hermes/hermes-agent/venv/bin" "$ready_home/.local/bin"
touch "$ready_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
printf '%s\n' "#!/bin/bash" "exec $ready_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$ready_home/.local/bin/hermes"
chmod +x "$ready_home/.local/bin/hermes"
run_ready_check() {
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$ready_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --check >/dev/null 2>&1
}
run_ready_check && fail "--check rejects the app's wrapper when its runtime is gone"
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "[-q QUERY, --query QUERY] [--tui]"
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 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"
+359
View File
@@ -0,0 +1,359 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
for command in git jq python3; do require_command "$command"; done
test_tmp=$(mktemp -d)
trap 'rm -rf -- "$test_tmp"' EXIT
export OMARCHY_TEST_ROOT="$test_tmp"
mkdir -p "$test_tmp/bin" "$test_tmp/package/resources" "$test_tmp/share" "$test_tmp/seed"
# Real Git exercises patch checks and preservation; all package, desktop and
# service commands are mocks. No command reaches the live user installation.
git -C "$test_tmp/seed" init -q -b main
printf 'venv/\n.hermes-bootstrap-complete\napps/desktop/release/\n__pycache__/\n' >"$test_tmp/seed/.gitignore"
printf 'before\n' >"$test_tmp/seed/runtime.txt"
mkdir -p "$test_tmp/seed/apps/desktop/src"
printf 'desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
mkdir -p "$test_tmp/seed/hermes_cli"
cat >"$test_tmp/seed/hermes_cli/main.py" <<'PY'
import os
from pathlib import Path
def _write_desktop_build_stamp(project_root, *, source_mode):
home = Path(os.environ['HERMES_HOME'])
assert project_root == home / 'hermes-agent'
assert source_mode is False
assert (project_root / 'apps/desktop/release/linux-unpacked/resources/app.asar').is_file()
(home / 'desktop-build-stamp.json').write_text('upstream build stamp')
with (Path(os.environ['OMARCHY_TEST_ROOT']) / 'events').open('a') as log:
log.write('build-stamp\n')
PY
git -C "$test_tmp/seed" add .
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm fixture
release_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
printf 'after\n' >"$test_tmp/seed/runtime.txt"
git -C "$test_tmp/seed" diff >"$test_tmp/share/runtime.patch"
printf 'before\n' >"$test_tmp/seed/runtime.txt"
printf 'newer desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
git -C "$test_tmp/seed" add apps/desktop/src/main.js
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm newer-main
origin_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
export OMARCHY_TEST_RELEASE_COMMIT="$release_commit"
printf '{"branch":"main","commit":"%s"}\n' "$release_commit" >"$test_tmp/package/resources/install-stamp.json"
printf 'packaged app\n' >"$test_tmp/package/resources/app.asar"
printf '#!/bin/bash\nexit 0\n' >"$test_tmp/package/Hermes"
touch "$test_tmp/package/chrome-sandbox"
chmod 755 "$test_tmp/package/Hermes"
chmod 4755 "$test_tmp/package/chrome-sandbox"
cat >"$test_tmp/share/install.sh" <<'MOCK'
#!/bin/bash
set -e
printf 'bootstrap\n' >>"$OMARCHY_TEST_ROOT/events"
printf '%s\n' "$@" >"$OMARCHY_TEST_ROOT/install-args"
[[ ${OMARCHY_TEST_INSTALL_FAIL:-0} != 1 ]] || exit 7
commit=$OMARCHY_TEST_RELEASE_COMMIT
force=false
while (( $# )); do
case "$1" in
--dir) runtime=$2; shift ;;
--commit) commit=$2; shift ;;
--force-commit) force=true ;;
--hermes-home) [[ $2 == "$HERMES_HOME" ]] ;;
esac
shift
done
mkdir -p -- "${runtime%/*}"
if [[ ! -d $runtime ]]; then
git clone -q --depth 1 "file://$OMARCHY_TEST_ROOT/seed" "$runtime"
else
git -C "$runtime" checkout -q main
git -C "$runtime" pull -q --ff-only origin main
fi
git -C "$runtime" fetch -q origin "$commit"
if [[ $force == true ]] || ! git -C "$runtime" merge-base --is-ancestor "$commit" HEAD; then
git -C "$runtime" checkout -q --detach "$commit"
fi
mkdir -p "$runtime/venv/bin"
git -C "$runtime" rev-parse HEAD >"$runtime/venv/dependency-commit"
printf '#!/bin/bash\nexit 0\n' >"$runtime/venv/bin/hermes"
chmod +x "$runtime/venv/bin/hermes"
printf '#!/bin/bash\nexec /usr/bin/python3 "$@"\n' >"$runtime/venv/bin/python"
chmod +x "$runtime/venv/bin/python"
[[ ${OMARCHY_TEST_NO_MARKER:-0} == 1 ]] || touch "$runtime/.hermes-bootstrap-complete"
mkdir -p "$HOME/.local/bin"
for command in hermes hermes-agent hermes-acp; do
rm -f "$HOME/.local/bin/$command"
printf 'native runtime shim\n' >"$HOME/.local/bin/$command"
done
MOCK
cat >"$test_tmp/bin/omarchy-pkg-add" <<'MOCK'
#!/bin/bash
printf 'package %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events"
[[ ${OMARCHY_TEST_PACKAGE_FAIL:-0} != 1 ]]
MOCK
cat >"$test_tmp/bin/git" <<'MOCK'
#!/bin/bash
if [[ ${OMARCHY_TEST_FETCH_FAIL:-0} == 1 && " $* " == *" --unshallow "* ]]; then exit 8; fi
exec /usr/bin/git "$@"
MOCK
cat >"$test_tmp/bin/omarchy-install-hermes-cli" <<'MOCK'
#!/bin/bash
printf 'handoff\n' >>"$OMARCHY_TEST_ROOT/events"
exit 1
MOCK
cat >"$test_tmp/bin/setsid" <<'MOCK'
#!/bin/bash
exec "$@"
MOCK
cat >"$test_tmp/bin/cp" <<'MOCK'
#!/bin/bash
if [[ ${OMARCHY_TEST_COPY_FAIL:-0} == 1 ]]; then
touch "${@: -1}/partial-copy"
exit 9
fi
exec /usr/bin/cp "$@"
MOCK
cat >"$test_tmp/bin/mv" <<'MOCK'
#!/bin/bash
if [[ ${OMARCHY_TEST_COPY_RACE:-0} == 1 && $1 == -T ]]; then
mkdir -p "${@: -1}"
fi
exec /usr/bin/mv "$@"
MOCK
cat >"$test_tmp/bin/uwsm-app" <<'MOCK'
#!/bin/bash
[[ $1 == -- ]] || exit 1
shift
exec "$@"
MOCK
cat >"$test_tmp/bin/hermes-desktop" <<'MOCK'
#!/bin/bash
sleep 0.05
native="$HERMES_HOME/hermes-agent/apps/desktop/release/linux-unpacked"
if [[ -x $native/Hermes && -f $native/resources/app.asar ]]; then
printf 'launch\n' >>"$OMARCHY_TEST_ROOT/events"
else
printf 'launch-before-copy\n' >>"$OMARCHY_TEST_ROOT/events"
fi
MOCK
cat >"$test_tmp/bin/systemctl" <<'MOCK'
#!/bin/bash
printf 'theme-stop\n' >>"$OMARCHY_TEST_ROOT/events"
MOCK
cat >"$test_tmp/bin/systemd-run" <<'MOCK'
#!/bin/bash
printf 'theme-start\n' >>"$OMARCHY_TEST_ROOT/events"
# Join the mock asynchronous launch so every test owns its full lifetime.
for (( attempt=0; attempt<100; attempt++ )); do
if grep -q '^launch' "$OMARCHY_TEST_ROOT/events"; then exit 0; fi
sleep 0.01
done
exit 1
MOCK
chmod +x "$test_tmp/bin/"*
# Substitute only system package paths in a scratch copy of the actual script.
python3 - "$ROOT/bin/omarchy-install-ai-hermes" "$test_tmp" <<'PY'
from pathlib import Path
import sys
source, scratch = Path(sys.argv[1]), Path(sys.argv[2])
script = source.read_text()
for original, replacement in {
'/opt/hermes-desktop': str(scratch / 'package'),
'/usr/share/hermes-desktop': str(scratch / 'share'),
'/usr/bin/hermes-desktop': str(scratch / 'bin/hermes-desktop'),
}.items():
script = script.replace(original, replacement)
(scratch / 'installer').write_text(script)
PY
new_home() {
test_home="$test_tmp/$1"
hermes_home="$test_home/.hermes"
runtime="$hermes_home/hermes-agent"
native="$runtime/apps/desktop/release/linux-unpacked"
mkdir -p "$test_home"
: >"$test_tmp/events"
}
run_installer() {
HOME="$test_home" HERMES_HOME="${OMARCHY_TEST_HOME:-$hermes_home}" PATH="$test_tmp/bin:$PATH" \
bash "$test_tmp/installer" >"$test_tmp/output" 2>&1
}
assert_stopped() {
if grep -Eq '^(launch|theme-|build-stamp)' "$test_tmp/events"; then fail "$1"; fi
}
new_home fresh
run_installer || fail "fresh setup succeeds" "$(cat "$test_tmp/output")"
expected=$(printf '%s\n' --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$hermes_home")
[[ $(cat "$test_tmp/install-args") == "$expected" ]] || fail "upstream installer receives the pinned main arguments"
[[ $(head -3 "$test_tmp/events") == $'package hermes-desktop\nhandoff\nbootstrap' ]] || fail "package and CLI handoff precede runtime bootstrap"
grep -qx launch "$test_tmp/events" || fail "native app is copied before launch"
[[ $(sed -n '4p' "$test_tmp/events") == build-stamp ]] || fail "upstream build stamp follows the app copy and precedes launch"
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "the upstream helper records the completed packaged build"
[[ $(cat "$runtime/runtime.txt") == after ]] || fail "the release runtime receives its patch"
[[ $(stat -c %a "$native/chrome-sandbox") == 755 ]] || fail "the user sandbox is not setuid"
[[ $(stat -c %a "$test_tmp/package/chrome-sandbox") == 4755 ]] || fail "package sandbox permissions remain unchanged"
[[ $(git -C "$runtime" symbolic-ref --short HEAD) == main && $(git -C "$runtime" rev-parse main) == "$release_commit" ]] || fail "main starts at the release rather than the clone tip"
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "dependencies are installed for the release"
[[ $(git -C "$runtime" rev-parse --is-shallow-repository) == false ]] || fail "first update has connected history"
# Reproduce the updater's checkout/count/pull sequence while origin stays put.
git clone -q "$runtime" "$test_tmp/first-update"
git -C "$test_tmp/first-update" remote set-url origin "file://$test_tmp/seed"
git -C "$test_tmp/first-update" fetch -q origin main
git -C "$test_tmp/first-update" checkout -q main
[[ $(git -C "$test_tmp/first-update" rev-list HEAD..origin/main --count) == 1 ]] || fail "first update detects work even when origin has not moved since install"
git -C "$test_tmp/first-update" pull -q --ff-only origin main
[[ $(git -C "$test_tmp/first-update" rev-parse HEAD) == "$origin_commit" ]] || fail "first update fast-forwards to origin"
pass "fresh setup pins main, patches the matching runtime and copies the complete app before launch"
printf 'user app\n' >"$native/resources/app.asar"
printf 'user build stamp\n' >"$hermes_home/desktop-build-stamp.json"
: >"$test_tmp/events"
run_installer || fail "repeat setup succeeds" "$(cat "$test_tmp/output")"
! grep -qx bootstrap "$test_tmp/events" || fail "repeat setup does not bootstrap again"
! grep -qx build-stamp "$test_tmp/events" || fail "existing app never reruns the build stamp writer"
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'user build stamp' ]] || fail "existing native build stamp remains unchanged"
[[ $(cat "$native/resources/app.asar") == 'user app' ]] || fail "existing native app remains unchanged"
pass "repeat setup accepts the applied patch and preserves the existing native app"
# Advancing the runtime must never reinstall the release or reapply its patch.
printf 'new main\n' >"$runtime/runtime.txt"
git -C "$runtime" add runtime.txt
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm update
: >"$test_tmp/events"
run_installer || fail "a complete updated runtime and native app are reused"
[[ $(cat "$runtime/runtime.txt") == 'new main' ]] || fail "updated runtime is not release-patched"
mv "$native" "$test_tmp/saved-native"
: >"$test_tmp/events"
run_installer && fail "a newer runtime cannot receive an older native app"
[[ ! -e $native ]] || fail "no mismatched native app was copied"
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance"
assert_stopped "a missing updated app prevents launch and theme setup"
pass "updated runtimes are preserved and never seeded with the old packaged app"
new_home dirty-desktop
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
printf 'local desktop edit\n' >"$runtime/apps/desktop/src/main.js"
: >"$test_tmp/events"
run_installer && fail "modified desktop sources cannot be certified as the packaged build"
[[ ! -e $native && ! -e $hermes_home/desktop-build-stamp.json ]] || fail "modified desktop sources receive neither packaged app nor build stamp"
[[ $(cat "$runtime/apps/desktop/src/main.js") == 'local desktop edit' ]] || fail "desktop source edits are preserved"
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "modified desktop sources have build guidance"
assert_stopped "modified desktop sources prevent stamping, launch and theme setup"
pass "a matching commit with modified desktop sources is preserved without seeding or stamping"
for failure in package install marker; do
new_home "$failure-failure"
case "$failure" in
package) OMARCHY_TEST_PACKAGE_FAIL=1 run_installer && fail "package failure stops setup" ;;
install) OMARCHY_TEST_INSTALL_FAIL=1 run_installer && fail "installer failure stops setup" ;;
marker) OMARCHY_TEST_NO_MARKER=1 run_installer && fail "missing marker stops setup" ;;
esac
[[ ! -e $native ]] || fail "failed setup does not seed the app"
assert_stopped "failed setup prevents launch and theme setup"
done
pass "package, upstream installer and readiness failures stop before launch"
for failure in copy race; do
new_home "$failure-failure"
if [[ $failure == "copy" ]]; then
OMARCHY_TEST_COPY_FAIL=1 run_installer && fail "copy failure stops setup"
[[ ! -e $native ]] || fail "partial copy is never published"
else
OMARCHY_TEST_COPY_RACE=1 run_installer && fail "concurrent native app stops publication"
[[ -d $native && -z $(ls -A "$native") ]] || fail "concurrent empty app directory is preserved"
fi
[[ -z $(find "${native%/*}" -maxdepth 1 -name '.linux-unpacked.*' -print) ]] || fail "owned staging directory is cleaned up"
assert_stopped "publication failure prevents launch"
done
pass "failed copies and concurrent app creation preserve existing work and clean only staging"
new_home incomplete-native
run_installer || fail "incomplete native fixture sets up"
rm "$native/resources/app.asar"
: >"$test_tmp/events"
run_installer && fail "incomplete existing app requires repair"
[[ ! -e $native/resources/app.asar ]] || fail "incomplete existing app is not overwritten"
assert_stopped "incomplete native app prevents launch"
pass "an incomplete existing native app is preserved"
new_home patch-conflict
run_installer || fail "patch conflict fixture sets up"
printf 'local edit\n' >"$runtime/runtime.txt"
: >"$test_tmp/events"
run_installer && fail "unexpected patch conflict stops setup"
[[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "conflicting runtime changes are preserved"
assert_stopped "patch conflict prevents launch"
rm "$runtime/.hermes-bootstrap-complete"
: >"$test_tmp/events"
run_installer && fail "incomplete modified runtime cannot be reset by upstream installer"
! grep -qx bootstrap "$test_tmp/events" || fail "modified runtime never reaches upstream installer"
pass "patch conflicts and incomplete modified runtimes retain local changes and stop safely"
new_home full-history-retry
git clone -q "$test_tmp/seed" "$runtime"
git -C "$runtime" checkout -q --detach "$release_commit"
run_installer || fail "clean incomplete full-history release checkout is repaired" "$(cat "$test_tmp/output")"
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "full-history retry pins before installing dependencies"
[[ $(git -C "$runtime" rev-parse HEAD) == "$release_commit" && -f $native/resources/app.asar ]] || fail "full-history retry seeds the matching release"
pass "full-history retries force the guarded release pin before dependency setup"
new_home local-main
git clone -q "$test_tmp/seed" "$runtime"
printf 'local branch work\n' >"$runtime/keep"
git -C "$runtime" add keep
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm local-work
local_main=$(git -C "$runtime" rev-parse main)
git -C "$runtime" checkout -q --detach "$release_commit"
run_installer && fail "local main commits cannot be reset by upstream installation"
! grep -qx bootstrap "$test_tmp/events" || fail "local main is checked before upstream installer"
[[ $(git -C "$runtime" rev-parse main) == "$local_main" ]] || fail "local main commit stays referenced"
pass "detached release checkouts do not hide local main work from the installer guard"
new_home deepen-retry
OMARCHY_TEST_FETCH_FAIL=1 run_installer && fail "history fetch failure stops setup"
[[ ! -e $native ]] || fail "failed history fetch does not seed the app"
assert_stopped "failed history fetch prevents launch"
: >"$test_tmp/events"
run_installer || fail "history fetch can be retried after runtime setup" "$(cat "$test_tmp/output")"
! grep -qx bootstrap "$test_tmp/events" || fail "history retry does not repeat upstream installation"
pass "a history fetch failure can be retried without reinstalling the ready runtime"
new_home existing-commands
mkdir -p "$test_home/.local/bin"
printf 'foreign wrapper\n' >"$test_home/.local/bin/hermes"
printf 'symlink target\n' >"$test_home/target"
ln -s "$test_home/target" "$test_home/.local/bin/hermes-agent"
ln -s "$test_home/missing" "$test_home/.local/bin/hermes-acp"
run_installer || fail "existing commands are preserved before upstream replaces them" "$(cat "$test_tmp/output")"
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
[[ ${#backups[@]} == 1 && -d ${backups[0]} ]] || fail "one backup directory preserves existing command names"
[[ $(cat "${backups[0]}/hermes") == 'foreign wrapper' ]] || fail "foreign wrapper bytes are saved"
[[ $(readlink "${backups[0]}/hermes-agent") == "$test_home/target" && $(readlink "${backups[0]}/hermes-acp") == "$test_home/missing" ]] || fail "working and broken symlinks are saved as links"
[[ $(cat "$test_home/target") == 'symlink target' ]] || fail "upstream does not overwrite the original symlink target"
grep -qF "${backups[0]}" "$test_tmp/output" || fail "backup location is reported"
pass "pre-existing command files and symlinks are backed up before replacement"
new_home old-package
mv "$test_tmp/package/resources/install-stamp.json" "$test_tmp/saved-install-stamp.json"
run_installer && fail "an old installed package cannot bootstrap"
grep -q 'omarchy update' "$test_tmp/output" || fail "old package has actionable upgrade guidance"
! grep -qx handoff "$test_tmp/events" || fail "old package is rejected before CLI handoff"
! grep -qx bootstrap "$test_tmp/events" || fail "old package never reaches upstream installer"
mv "$test_tmp/saved-install-stamp.json" "$test_tmp/package/resources/install-stamp.json"
pass "old package fails with upgrade guidance before changing the runtime or CLI"
new_home custom-profile
hermes_home="$test_home/custom home"
runtime="$hermes_home/hermes-agent"
OMARCHY_TEST_HOME="$hermes_home/PrOfIlEs/coder/../coder/" run_installer || fail "profile setup succeeds"
[[ -x $runtime/apps/desktop/release/linux-unpacked/Hermes ]] || fail "profile uses the canonical root runtime"
grep -qxF "$hermes_home" "$test_tmp/install-args" || fail "canonical custom home reaches upstream installer"
pass "custom profile paths normalize to the shared Hermes home"
+384
View File
@@ -0,0 +1,384 @@
#!/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
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mkdir -p "$mock_bin"
# Keep package-path checks scoped to the fixture, even with a live app open.
python3 - "$ROOT/bin/omarchy-remove-ai-hermes" "$test_tmp" <<'PY'
from pathlib import Path
import sys
source, scratch = map(Path, sys.argv[1:])
(scratch / 'remover').write_text(source.read_text().replace('/opt/hermes-desktop', str(scratch / 'package')))
PY
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"
if [[ -n ${OMARCHY_TEST_PROMPT_GATE:-} ]]; then
touch "$OMARCHY_TEST_PROMPT_GATE.started"
for (( attempt=0; attempt<500; attempt++ )); do
[[ ! -e $OMARCHY_TEST_PROMPT_GATE.continue ]] || exit 0
sleep 0.01
done
exit 1
fi
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() {
rm -rf "$test_home"
mkdir -p "$test_home/.hermes/hermes-agent" "$test_home/.hermes/bootstrap-cache" \
"$test_home/.hermes/bin" "$test_home/.hermes/node/bin" \
"$test_home/.hermes/memories" "$test_home/.hermes/sessions" \
"$test_home/.config/Hermes" "$test_home/.local/bin"
printf 'chat\n' >"$test_home/.hermes/sessions/one.json"
printf 'memory\n' >"$test_home/.hermes/memories/one.md"
printf 'soul\n' >"$test_home/.hermes/SOUL.md"
printf 'uv\n' >"$test_home/.hermes/bin/uv"
ln -sf "$test_home/.hermes/node/bin/node" "$test_home/.local/bin/node"
ln -sf "$test_home/.hermes/node/bin/npm" "$test_home/.local/bin/npm"
ln -sf /usr/bin/npx "$test_home/.local/bin/npx"
printf 'node\n' >"$test_home/.hermes/node/bin/node"
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() {
: >"$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 "$test_tmp/remover" </dev/null >"$test_tmp/output" 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 '$test_tmp/remover'" /dev/null >"$test_tmp/output" 2>&1
}
# The app brings its own uv and its own node; both are runtime, not data.
seed_install
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds"
[[ ! -d $test_home/.hermes/hermes-agent ]] || fail "the runtime checkout is removed"
[[ ! -d $test_home/.hermes/bin ]] || fail "the uv the app installed is removed"
[[ ! -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"
# -L, not -e: a dangling symlink fails -e while very much still being there.
[[ ! -L $test_home/.local/bin/node ]] || fail "a node symlink into ~/.hermes is removed"
[[ ! -L $test_home/.local/bin/npm ]] || fail "an npm symlink into ~/.hermes is removed"
[[ -L $test_home/.local/bin/npx ]] || fail "an npx symlink pointing elsewhere survives"
pass "removal clears only the managed Node links it stranded"
[[ -f $test_home/.hermes/sessions/one.json ]] || fail "chats survive removal"
[[ -f $test_home/.hermes/memories/one.md ]] || fail "memories survive removal"
[[ -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
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/my-own-hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds with a foreign hermes present"
[[ -f $test_home/.local/bin/hermes ]] ||
fail "a hermes command the app did not write survives removal"
pass "removal leaves a hermes it does not own"
# Installed but never launched. The app provisions its runtime on first launch
# and marks it complete when it lands, so without that marker everything under
# ~/.hermes predates the app -- an official install, or one built by hand -- and
# the paths are identical either way. Dropping the package is the whole job.
seed_install
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
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 ]] ||
fail "local changes to a runtime the app never installed survive removal"
[[ -d $test_home/.hermes/bin && -d $test_home/.hermes/node ]] ||
fail "the rest of a runtime the app never installed survives removal"
[[ -f $test_home/.local/bin/hermes ]] ||
fail "the command a runtime the app never installed put on PATH survives removal"
[[ -L $test_home/.local/bin/node ]] ||
fail "node links belonging to a runtime the app never installed survive removal"
pass "removal leaves a Hermes the app never installed"
# ~/.hermes carries a dot, so a pattern rather than a plain string would also
# claim a wrapper pointing at a sibling directory that merely looks like it.
seed_install
mkdir -p "$test_home/xhermes/bin"
sibling_body="#!/bin/bash
exec $test_home/xhermes/bin/hermes \"\$@\""
printf '%s\n' "$sibling_body" >"$test_home/.local/bin/hermes"
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"
# Real SQLite writers exercise the kernel's live/deleted file descriptors.
# Package, service and confirmation commands remain confined to the mocks.
python3 - "$test_tmp" <<'PY'
import os
from pathlib import Path
import pty
import subprocess
import sys
import time
scratch = Path(sys.argv[1])
writer_code = '''import os, sqlite3, sys
c = sqlite3.connect(os.environ['TEST_DB'])
c.execute('pragma journal_mode=wal')
c.execute('create table fixture(value)')
c.execute("insert into fixture values ('keep')")
c.commit()
print('ready', flush=True)
sys.stdin.readline()
c.close()
'''
def setup(name):
home = scratch / name
runtime = home / '.hermes/hermes-agent'
runtime.mkdir(parents=True)
(runtime / '.hermes-bootstrap-complete').touch()
(home / '.config/Hermes').mkdir(parents=True)
env = {**os.environ, 'HOME': str(home), 'PATH': f"{scratch / 'bin'}:/usr/bin:/bin",
'OMARCHY_TEST_GUM_STATUS': '0'}
for key in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'):
log = home / (key + '.log')
log.touch()
env['OMARCHY_TEST_' + key + '_LOG'] = str(log)
return home, runtime, env
def writer(db):
child = subprocess.Popen([sys.executable, '-u', '-c', writer_code],
env={**os.environ, 'TEST_DB': str(db)},
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
assert child.stdout.readline().strip() == 'ready'
return child
def stop(child):
if child.poll() is None:
child.stdin.write('\n')
child.stdin.flush()
child.wait(timeout=5)
def remove(env):
master, slave = pty.openpty()
try:
return subprocess.run(['bash', str(scratch / 'remover')], env=env,
stdin=slave, capture_output=True, text=True, timeout=10)
finally:
os.close(master)
os.close(slave)
def blocked(result, home, runtime, child):
assert result.returncode != 0 and str(child.pid) in result.stderr, result
assert 'Close Hermes' in result.stderr, result.stderr
assert (runtime / '.hermes-bootstrap-complete').exists()
assert all((home / (name + '.log')).stat().st_size == 0
for name in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'))
assert child.poll() is None, 'remover must not kill sessions'
for deleted in (False, True):
home, runtime, env = setup('deleted-writer' if deleted else 'live-writer')
db = home / '.hermes/state.db'
child = writer(db)
try:
if deleted:
for suffix in ('', '-wal', '-shm'):
Path(str(db) + suffix).unlink()
db.write_bytes(b'new database generation')
blocked(remove(env), home, runtime, child)
if deleted:
assert db.read_bytes() == b'new database generation'
finally:
stop(child)
assert remove(env).returncode == 0, 'removal succeeds once the writer closes'
assert not (home / '.hermes').exists()
print('ok - live and deleted SQLite holders block removal before any side effects; closing them allows retry')
for kind in ('terminal', 'desktop', 'working-directory'):
home, runtime, env = setup(kind)
executable_name = str(scratch / 'package/Hermes') if kind == 'desktop' else str(runtime / 'hermes')
args = ['sleep', '30'] if kind == 'working-directory' else [executable_name, '30']
child = subprocess.Popen(args, executable='/usr/bin/sleep',
cwd=runtime if kind == 'working-directory' else scratch)
try:
blocked(remove(env), home, runtime, child)
finally:
child.terminate()
child.wait(timeout=5)
print('ok - terminal, packaged desktop and runtime working-directory processes are detected without a database')
home, runtime, env = setup('unrelated-writer')
sibling = home / '.hermes-other'
sibling.mkdir()
child = writer(sibling / 'state.db')
try:
assert remove(env).returncode == 0, 'a sibling database does not block Hermes removal'
assert child.poll() is None
finally:
stop(child)
print('ok - unrelated database holders are left alone')
home, runtime, env = setup('prompt-race')
gate = home / 'prompt'
env['OMARCHY_TEST_PROMPT_GATE'] = str(gate)
master, slave = pty.openpty()
remover = subprocess.Popen(['bash', str(scratch / 'remover')], env=env, stdin=slave,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
os.close(slave)
child = None
try:
deadline = time.monotonic() + 5
while not Path(str(gate) + '.started').exists():
assert remover.poll() is None and time.monotonic() < deadline, 'prompt was not reached'
time.sleep(0.01)
child = writer(home / '.hermes/state.db')
Path(str(gate) + '.continue').touch()
stdout, stderr = remover.communicate(timeout=10)
assert remover.returncode != 0 and str(child.pid) in stderr, (stdout, stderr)
assert (home / '.hermes/state.db-wal').exists()
assert (home / '.config/Hermes').exists()
finally:
if child is not None:
stop(child)
if remover.poll() is None:
remover.terminate()
remover.wait(timeout=5)
os.close(master)
print('ok - a writer started during confirmation blocks data deletion')
PY
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1787843905.sh"
[[ -f $migration ]] || fail "Hermes skills migration exists"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
home="$test_dir/home"
run_migration() {
HOME="$home" OMARCHY_PATH="$ROOT" bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean"
}
assert_link() {
local link="$1"
local skill="$2"
local description="$3"
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
fail "$description" "$link -> $(readlink "$link" 2>/dev/null || echo missing)"
}
# ------------------------------------------------------------------ default home, no profiles
rm -rf "$home"
mkdir -p "$home"
run_migration
for skill in omarchy diagnose-crash; do
assert_link "$home/.hermes/skills/$skill" "$skill" "migration links $skill into the default Hermes home"
done
[[ -e $home/.hermes/profiles ]] && fail "migration does not create Hermes profiles"
pass "migration links the default Hermes home and does not create profiles"
run_migration
for skill in omarchy diagnose-crash; do
assert_link "$home/.hermes/skills/$skill" "$skill" "migration is idempotent on the default home for $skill"
done
pass "migration is idempotent on the default home"
# ------------------------------------------------------------------ pre-existing profile
rm -rf "$home"
mkdir -p "$home/.hermes/profiles/james"
run_migration
for skill in omarchy diagnose-crash; do
assert_link "$home/.hermes/skills/$skill" "$skill" "migration links $skill into the default Hermes home when a profile exists"
assert_link "$home/.hermes/profiles/james/skills/$skill" "$skill" "migration links $skill into a pre-existing Hermes profile"
done
[[ -d $home/.hermes/profiles/james ]] || fail "migration leaves the pre-existing profile in place"
profile_count=$(find "$home/.hermes/profiles" -mindepth 1 -maxdepth 1 -type d | wc -l)
(( profile_count == 1 )) || fail "migration does not create extra profiles" "count=$profile_count"
pass "migration links a pre-existing Hermes profile and does not create extras"
run_migration
for skill in omarchy diagnose-crash; do
assert_link "$home/.hermes/skills/$skill" "$skill" "migration is idempotent on the default home when a profile exists for $skill"
assert_link "$home/.hermes/profiles/james/skills/$skill" "$skill" "migration is idempotent on a pre-existing profile for $skill"
done
pass "migration is idempotent on a pre-existing profile"
# ------------------------------------------------------------------ missing skill source
rm -rf "$home"
mkdir -p "$home" "$test_dir/empty-omarchy"
HOME="$home" OMARCHY_PATH="$test_dir/empty-omarchy" bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean when the skill source is missing"
[[ -e $home/.hermes ]] && fail "migration no-ops when the skill source is missing"
pass "migration no-ops when the skill source is missing"
@@ -0,0 +1,59 @@
#!/bin/bash
set -euo pipefail
# The migration hands an existing Hermes Desktop install the Omarchy skin. It
# is exercised here with the package probe and the skin hook stubbed, so a
# migration that reached a Hermes Omarchy did not install, or that marked a
# failed hand-over done, shows up in what it ran.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1788619462.sh"
[[ -f $migration ]] || fail "Hermes skin migration exists"
[[ $(stat -c %a "$migration") == "644" ]] || fail "migration is a plain 0644 file"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
calls="$test_tmp/calls"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-theme-set-hermes" <<'SH'
#!/bin/bash
echo "omarchy-theme-set-hermes $*" >>"$OMARCHY_TEST_CALLS"
[[ ${OMARCHY_TEST_HOOK_FAILS:-0} == 0 ]]
SH
chmod +x "$mock_bin"/*
run_migration() {
: >"$calls"
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
OMARCHY_TEST_HOOK_FAILS="${OMARCHY_TEST_HOOK_FAILS:-0}" \
OMARCHY_TEST_CALLS="$calls" \
PATH="$mock_bin:$PATH" \
HOME="$test_tmp/home" \
OMARCHY_PATH="$ROOT" \
bash -euo pipefail "$migration" >/dev/null
}
OMARCHY_TEST_DESKTOP_INSTALLED=0 run_migration || fail "migration exits clean without Hermes Desktop"
[[ ! -s $calls ]] || fail "a machine without Hermes Desktop is left alone" "$(cat "$calls")"
pass "migration only applies where Omarchy installed Hermes Desktop"
run_migration || fail "migration exits clean with Hermes Desktop installed"
[[ $(cat "$calls") == "omarchy-theme-set-hermes --activate" ]] ||
fail "the skin is rendered, published and activated through the hook's deliberate form" "$(cat "$calls")"
pass "migration hands the skin over through the hook"
if OMARCHY_TEST_HOOK_FAILS=1 run_migration; then
fail "a hand-over that failed on Omarchy's side stays pending"
fi
pass "migration stays pending when the hand-over fails"
+401
View File
@@ -0,0 +1,401 @@
#!/bin/bash
set -euo pipefail
# omarchy-theme-set-hermes writes a file another program parses and asks that
# program to switch to it while it is still on its default. Both are exercised
# here against a throwaway HOME with the Hermes readiness probe, the hermes
# command and the theme refresh stubbed, so a skin that stopped being
# validated, a write into a Hermes that was never set up, or an activation
# that trampled a chosen skin shows up in what landed on disk and what was run.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH'
#!/bin/bash
echo "check" >>"$OMARCHY_TEST_HERMES_CALLS"
[[ $1 == "--check" && ${OMARCHY_TEST_HERMES_READY:-0} == "1" ]]
SH
# A theme switch finishes the hand-over only for the desktop app Omarchy
# installed; --activate is asked for by name and does not look.
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-1} == "1" ]]
SH
# The hook runs the hermes the probe vets, ~/.local/bin/hermes, not one on PATH;
# reset_home installs this stub there and a decoy on PATH that must never run.
cat >"$mock_bin/hermes" <<'SH'
#!/bin/bash
echo "PATH hermes ran: $*" >>"$OMARCHY_TEST_HERMES_CALLS"
exit 1
SH
cat >"$mock_bin/hermes-stub" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_HERMES_CALLS"
if [[ $1 == "config" && $2 == "get" ]]; then
[[ ${OMARCHY_TEST_HERMES_GET_FAILS:-0} == 0 ]] || exit 1
printf '%s\n' "${OMARCHY_TEST_HERMES_SKIN-default}"
fi
if [[ $1 == "config" && $2 == "set" ]]; then
[[ ${OMARCHY_TEST_HERMES_SET_FAILS:-0} == 0 ]] || exit 1
fi
SH
# A refresh re-stages the current theme, which is where the skin gets rendered.
cat >"$mock_bin/omarchy-theme-refresh" <<'SH'
#!/bin/bash
echo "refresh" >>"$OMARCHY_TEST_HERMES_CALLS"
printf 'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\n' \
>"$HOME/.local/state/omarchy/current/theme/hermes.yaml"
SH
# --wait sleeps between its polls and once more after activating; the stub
# records the delays it was asked for and returns at once.
cat >"$mock_bin/sleep" <<'SH'
#!/bin/bash
printf 'sleep %s\n' "$1" >>"$OMARCHY_TEST_HERMES_CALLS"
if [[ $1 == 60 && -n ${OMARCHY_TEST_SWAP_SOURCE:-} ]]; then
printf '%s\n' "$OMARCHY_TEST_SWAP_SOURCE" >"$HOME/.local/state/omarchy/current/theme/hermes.yaml"
fi
if [[ $1 == 60 && ${OMARCHY_TEST_DROP_SKINS:-0} == 1 ]]; then
rm -f "$HOME/.hermes/skins/omarchy.yaml" "$HOME"/.hermes/profiles/*/skins/omarchy.yaml
fi
SH
chmod +x "$mock_bin"/*
good_skin='name: omarchy
description: Omarchy system theme
colors:
background: "#1a1b26"
ui_text: "#a9b1d6"
ui_accent: "#7aa2f7"'
test_home="$test_tmp/home"
hermes_home="$test_home/.hermes"
skin="$hermes_home/skins/omarchy.yaml"
hermes_calls="$test_tmp/hermes-calls"
# Each case gets a fresh HOME so no file survives from the one before. The
# Hermes home is created the way provisioning does on every machine; only
# --set-up adds the config that says Hermes itself has run, on its default
# skin unless --on names another.
reset_home() {
local source="$good_skin"
rm -rf "$test_home"
mkdir -p "$test_home/.local/state/omarchy/current/theme" "$test_home/.local/bin" "$hermes_home/skills"
cp "$mock_bin/hermes-stub" "$test_home/.local/bin/hermes"
: >"$hermes_calls"
while (( $# > 0 )); do
case "$1" in
--set-up) printf 'display:\n skin: default\n' >"$hermes_home/config.yaml" ;;
--on) printf 'display:\n skin: %s\n' "$2" >"$hermes_home/config.yaml"; shift ;;
*) source="$1" ;;
esac
shift
done
printf '%s\n' "$source" >"$test_home/.local/state/omarchy/current/theme/hermes.yaml"
}
run_hook() {
OMARCHY_TEST_HERMES_READY="${OMARCHY_TEST_HERMES_READY:-0}" \
OMARCHY_TEST_HERMES_SKIN="${OMARCHY_TEST_HERMES_SKIN-default}" \
OMARCHY_TEST_HERMES_GET_FAILS="${OMARCHY_TEST_HERMES_GET_FAILS:-0}" \
OMARCHY_TEST_HERMES_SET_FAILS="${OMARCHY_TEST_HERMES_SET_FAILS:-0}" \
OMARCHY_TEST_HERMES_CALLS="$hermes_calls" \
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
OMARCHY_TEST_SWAP_SOURCE="${OMARCHY_TEST_SWAP_SOURCE:-}" \
OMARCHY_TEST_DROP_SKINS="${OMARCHY_TEST_DROP_SKINS:-0}" \
PATH="$mock_bin:$PATH" \
HOME="$test_home" \
HERMES_HOME='' \
"$ROOT/bin/omarchy-theme-set-hermes" "$@"
}
# -- publishing ---------------------------------------------------------------
reset_home
run_hook
[[ ! -e $hermes_home/skins ]] || fail "a Hermes home that only holds the Omarchy skill gets no skin"
[[ ! -s $hermes_calls ]] || fail "nothing is run for a Hermes that never ran" "$(cat "$hermes_calls")"
pass "a theme switch leaves a machine that never ran Hermes alone"
reset_home --set-up
mkdir -p "$hermes_home/profiles/work"
run_hook 2>"$test_tmp/stderr"
diff -q "$test_home/.local/state/omarchy/current/theme/hermes.yaml" "$skin" >/dev/null ||
fail "the generated skin is published to ~/.hermes/skins/omarchy.yaml"
diff -q "$skin" "$hermes_home/profiles/work/skins/omarchy.yaml" >/dev/null ||
fail "an existing Hermes profile gets the skin too"
[[ $(ls "$hermes_home/skins") == "omarchy.yaml" ]] || fail "no temporary file is left beside the skin"
[[ $(cat "$hermes_calls") == "check" ]] || fail "a Hermes that is not ready is asked nothing more" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch says nothing about Hermes" "$(cat "$test_tmp/stderr")"
pass "the skin is published to the Hermes home and every profile"
reset_home --set-up 'name: omarchy
description: Omarchy system theme
colors:
background: "{{ background }}"'
mkdir -p "$hermes_home/skins"
printf 'name: omarchy\ncolors:\n background: "#000000"\n' >"$skin"
run_hook 2>"$test_tmp/stderr"
grep -q '#000000' "$skin" || fail "an unresolved placeholder keeps the previous skin in place"
grep -q 'not a plain color palette' "$test_tmp/stderr" || fail "an unresolved placeholder is reported"
pass "a skin with unresolved colors is not published"
# mv would otherwise move the temp file inside a directory at the skin's path,
# leaving Hermes a directory to read and the temp file behind.
reset_home --set-up
mkdir -p "$skin"
if run_hook 2>/dev/null; then
fail "a directory at the skin's path is an error, not a place to put the skin"
fi
[[ -z $(ls -A "$skin") && $(ls "$hermes_home/skins") == "omarchy.yaml" ]] ||
fail "nothing is left inside or beside a directory at the skin's path" "$(ls -R "$hermes_home/skins")"
pass "a directory at the skin's path is refused cleanly"
for bad in \
$'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\nbanner_logo: "[link=file:///etc/passwd]x[/link]"' \
$'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26\\"\\n ui_text: \\"#ffffff"' \
$'name: nord\ndescription: Nord\ncolors:\n background: "#2e3440"' \
$'description: Omarchy system theme\ncolors:\n background: "#1a1b26"' \
$'name: omarchy\ndescription: Nord: arctic palette\ncolors:\n background: "#2e3440"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\rbanner_logo: "[link=file:///etc/passwd]x[/link]"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\xe2\x80\xa8banner_logo: "evil"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\xc2\x85banner_logo: "evil"' \
$'name: omarchy\n background: "#1a1b26"\ncolors:' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\ncolors:' \
$'colors:\n background: "#1a1b26"\nname: omarchy' \
$'name: omarchy\ncolors:'; do
reset_home --set-up "$bad"
run_hook 2>/dev/null
[[ ! -e $skin ]] || fail "a skin that is not exactly a named palette of hex colors is not published" "$bad"
done
pass "a skin is held to the shape Hermes loads, on the lines Hermes' YAML reader sees"
# A NUL cannot travel through a shell string, so it is written straight to the
# source; grep reads past one where YAML stops.
reset_home --set-up
printf 'name: omarchy\ncolors:\n background: "#1a1b26"\0\n' >"$test_home/.local/state/omarchy/current/theme/hermes.yaml"
run_hook 2>/dev/null
[[ ! -e $skin ]] || fail "a NUL byte in the skin is rejected"
pass "a skin carrying a NUL byte is not published"
reset_home --set-up $'# rendered by Omarchy\nname: omarchy\n\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\n'
run_hook 2>/dev/null
[[ -f $skin ]] || fail "comments and blank lines are allowed around the palette"
pass "a well-formed skin with comments and blank lines is published"
# -- a theme switch finishes a missed hand-over ---------------------------------
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 run_hook 2>"$test_tmp/stderr"
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy' ]] ||
fail "a ready Hermes still on its default is switched by a theme switch" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch activates quietly" "$(cat "$test_tmp/stderr")"
pass "a theme switch activates the skin on a Hermes still on its default"
reset_home --on omarchy
OMARCHY_TEST_HERMES_READY=1 run_hook
[[ -f $skin ]] || fail "the skin is published when it is already active"
[[ ! -s $hermes_calls ]] || fail "a Hermes already on the skin is not started" "$(cat "$hermes_calls")"
pass "a theme switch does not start a Hermes already on the skin"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DESKTOP_INSTALLED=0 run_hook
[[ -f $skin ]] || fail "a Hermes installed some other way still gets the skin published"
[[ ! -s $hermes_calls ]] || fail "a theme switch does not touch a Hermes Omarchy did not install as the app" "$(cat "$hermes_calls")"
pass "a theme switch activates only for the desktop app Omarchy installed"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DESKTOP_INSTALLED=0 run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "--activate switches whichever Hermes it is asked about" "$(cat "$hermes_calls")"
pass "--activate does not ask which Hermes it is"
reset_home --on ares
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "a chosen skin still gets the Omarchy skin published beside it"
[[ ! -s $hermes_calls ]] || fail "a theme switch does not start a Hermes whose config names a chosen skin" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a chosen skin is left without comment on a theme switch" "$(cat "$test_tmp/stderr")"
pass "a theme switch leaves a skin the user chose in Hermes"
# Hermes reads the config of the profile named in active_profile, so that is
# the config that says whether there is anything left to do.
reset_home --on omarchy
mkdir -p "$hermes_home/profiles/work"
printf 'display:\n skin: default\n' >"$hermes_home/profiles/work/config.yaml"
echo work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "an active profile still on its default is switched even when the root config names the skin" "$(cat "$hermes_calls")"
pass "a theme switch follows the active Hermes profile"
reset_home --set-up
mkdir -p "$hermes_home/profiles/work"
printf 'display:\n skin: ares\n' >"$hermes_home/profiles/work/config.yaml"
echo work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
[[ ! -s $hermes_calls ]] || fail "a skin chosen in the active profile is not replaced" "$(cat "$hermes_calls")"
pass "a theme switch leaves a skin chosen in the active Hermes profile"
# Hermes selects a profile once its directory exists; without a config of its
# own it is on the default skin whatever the root config says.
reset_home --on omarchy
mkdir -p "$hermes_home/profiles/work"
echo Work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "an active profile without a config of its own is on the default and gets switched" "$(cat "$hermes_calls")"
pass "a theme switch follows an active profile that has no config yet"
# Only a plainly named skin ends a switch early; anything Hermes might read as
# its default is left for Hermes to answer.
for line in 'skin: "default"' 'skin: default # chosen long ago' 'skin: null' 'skin: false'; do
reset_home --set-up
printf 'display:\n %s\n' "$line" >"$hermes_home/config.yaml"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "a config line Hermes reads as the default still gets the skin activated" "$line: $(cat "$hermes_calls")"
done
pass "a theme switch asks Hermes about any skin line that is not a plain name"
reset_home --set-up
mkdir -p "$hermes_home/profiles/broken"
: >"$hermes_home/profiles/broken/skins"
OMARCHY_TEST_HERMES_READY=1 run_hook 2>"$test_tmp/stderr" || fail "a profile that cannot take the skin does not fail the hook"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch stays quiet about a profile it could not reach" "$(cat "$test_tmp/stderr")"
[[ -f $skin ]] || fail "the Hermes home still gets the skin beside a broken profile"
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "activation still happens beside a broken profile" "$(cat "$hermes_calls")"
pass "a profile that cannot take the skin costs nobody else"
# -- activation ---------------------------------------------------------------
reset_home
run_hook --activate 2>"$test_tmp/stderr"
[[ ! -e $hermes_home/skins && ! -e $hermes_home/config.yaml ]] ||
fail "--activate writes nothing into a Hermes that has never run"
grep -q 'not set up yet' "$test_tmp/stderr" || fail "--activate says why nothing happened"
pass "--activate waits for Hermes to have been set up"
reset_home --set-up
run_hook --activate 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "--activate publishes the skin when Hermes is not ready"
[[ $(cat "$hermes_calls") == "check" ]] || fail "a Hermes that is not ready is not run" "$(cat "$hermes_calls")"
grep -q 'hermes config set display.skin omarchy' "$test_tmp/stderr" || fail "an unready Hermes gets the command that finishes the job"
pass "--activate publishes but does not run a Hermes that is not ready"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 run_hook --activate 2>"$test_tmp/stderr"
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy' ]] ||
fail "a ready Hermes is asked for its skin and then to switch" "$(cat "$hermes_calls")"
grep -q 'on the Omarchy skin' "$test_tmp/stderr" || fail "--activate reports success"
pass "--activate goes through hermes config set when Hermes runs"
reset_home --on omarchy
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=omarchy run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "--activate goes through Hermes even when the config already names the skin" "$(cat "$hermes_calls")"
pass "--activate always asks Hermes to switch"
for chosen in omarchy ""; do
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN="$chosen" run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "the default and the omarchy skin are both replaced" "skin='$chosen': $(cat "$hermes_calls")"
done
pass "--activate replaces Hermes' default skin"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook --activate 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "a chosen skin still gets the Omarchy skin published beside it"
! grep -q 'config set' "$hermes_calls" || fail "a skin the user chose is not replaced" "$(cat "$hermes_calls")"
grep -q "'ares' skin" "$test_tmp/stderr" || fail "leaving a chosen skin is reported"
pass "--activate leaves a skin the user chose in Hermes"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_GET_FAILS=1 run_hook --activate 2>"$test_tmp/stderr" || fail "a Hermes that does not answer is not an error"
! grep -q 'config set' "$hermes_calls" || fail "no answer from Hermes is not taken for the default" "$(cat "$hermes_calls")"
grep -q 'did not say' "$test_tmp/stderr" || fail "an unanswered question is reported"
pass "--activate does not switch a Hermes that did not say which skin it is on"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SET_FAILS=1 run_hook --wait 2>"$test_tmp/stderr" || fail "a Hermes that refuses the write is not an error"
grep -q 'refused' "$test_tmp/stderr" || fail "a refused write is reported"
! grep -q 'sleep 60' "$hermes_calls" || fail "nothing is announced for a write that did not happen" "$(cat "$hermes_calls")"
pass "--activate reports a Hermes that refused the skin and moves on"
# -- a skin the current theme has not rendered yet -------------------------------
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
run_hook
[[ ! -e $hermes_home/skins && ! -s $hermes_calls ]] ||
fail "a theme switch without a rendered skin publishes nothing" "$(cat "$hermes_calls")"
pass "a theme switch has nothing to do without a rendered skin"
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
if run_hook --activate 2>"$test_tmp/stderr"; then
fail "--activate fails when no theme has been selected"
fi
grep -q 'Select an Omarchy theme' "$test_tmp/stderr" || fail "a missing theme is reported"
pass "--activate fails without a current theme to render the skin from"
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
echo tokyo-night >"$test_home/.local/state/omarchy/current/theme.name"
OMARCHY_TEST_HERMES_READY=1 run_hook --activate 2>/dev/null
[[ $(head -1 "$hermes_calls") == "refresh" ]] || fail "a theme applied before the template existed is re-staged" "$(cat "$hermes_calls")"
[[ -f $skin ]] || fail "the freshly rendered skin is published"
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" || fail "the freshly rendered skin is activated" "$(cat "$hermes_calls")"
pass "--activate renders the skin for a theme that predates it"
# -- waiting for the desktop app's first launch ----------------------------------
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>/dev/null
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy\nsleep 60' ]] ||
fail "--wait activates as soon as the runtime marker is there, then republishes after the gateway is up" "$(cat "$hermes_calls")"
[[ -f $skin ]] || fail "--wait publishes the skin"
pass "--wait activates once the desktop app has built its runtime"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent" "$hermes_home/profiles/work"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_SWAP_SOURCE=$'name: omarchy\ncolors:\n background: "#1a1b26"\nbanner_logo: "evil"' run_hook --wait 2>/dev/null
! grep -q 'banner_logo' "$skin" || fail "the republish after the gateway is up does not publish a theme that changed underneath into something rejected"
pass "--wait checks the skin again before republishing it"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent" "$hermes_home/profiles/work"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DROP_SKINS=1 run_hook --wait 2>/dev/null
[[ -f $skin && -f $hermes_home/profiles/work/skins/omarchy.yaml ]] ||
fail "the republish after the gateway is up writes the skin to the Hermes home and every profile again" "$(ls -R "$hermes_home")"
pass "--wait republishes the skin everywhere once the gateway is up"
reset_home
OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>"$test_tmp/stderr"
[[ $(head -1 "$hermes_calls") == "sleep 10" && ! -e $hermes_home/skins ]] ||
fail "--wait polls for the runtime instead of running Hermes" "$(head -3 "$hermes_calls")"
[[ $(grep -c 'sleep 10' "$hermes_calls") == 180 ]] || fail "--wait gives up after 30 minutes" "$(grep -c 'sleep 10' "$hermes_calls")"
grep -q 'did not finish setting up' "$test_tmp/stderr" || fail "giving up is reported"
pass "--wait polls until the desktop app has built its runtime and gives up in time"
@@ -40,6 +40,13 @@ hl = setmetatable({
release = opts.release == true,
})
end,
unbind = function(keys)
for index = #bindings, 1, -1 do
if bindings[index].keys == keys then
table.remove(bindings, index)
end
end
end,
config = function() end,
env = function() end,
monitor = function() end,
@@ -188,3 +195,17 @@ probe=$(PATH="$stub_bin:$PATH" list_bindings "$home" \
grep -Fqx "ALT+SHIFT+SUPER+RIGHT" <<<"$probe" ||
fail "the conflict check ignores modifier order"
pass "the conflict check catches collisions across keycodes and modifier order"
rebound=$(PATH="$stub_bin:$PATH" list_bindings "$home" \
'o.rebind("SUPER + SHIFT + F", "Flea", { launch = "flea" })' | \
awk -F'\t' '$1 == "SHIFT+SUPER+F"')
[[ $rebound == $'SHIFT+SUPER+F\tSUPER + SHIFT + F\tFlea' ]] ||
fail "rebinding replaces the default file manager without stacking actions" "$rebound"
pass "rebinding replaces the default file manager without stacking actions"
rebound=$(PATH="$stub_bin:$PATH" list_bindings "$home" \
'o.rebind("F9", "Dictation on release", "voxtype record toggle", { release = true })' | \
awk -F'\t' '$2 == "F9"')
[[ $rebound == $'F9 (release)\tF9\tDictation on release' ]] ||
fail "rebinding replaces all bindings for a key and preserves binding options" "$rebound"
pass "rebinding replaces all bindings for a key and preserves binding options"
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
test_home="$test_dir/home"
kitty_config="$test_home/.config/kitty/kitty.conf"
legacy="$ROOT/test/shell.d/fixtures/kitty/legacy.conf"
migration="$ROOT/migrations/1788745941.sh"
mkdir -p "$(dirname "$kitty_config")" "$test_dir/bin"
run_migration() {
env HOME="$test_home" OMARCHY_PATH="$ROOT" PATH="$ROOT/bin:$PATH" bash -euo pipefail "$migration"
}
cp "$legacy" "$kitty_config"
output=$(run_migration)
cmp -s "$ROOT/config/kitty/kitty.conf" "$kitty_config" || fail "stock config becomes the user template"
backups=("$kitty_config".bak.*)
cmp -s "$legacy" "${backups[0]}" || fail "refresh backs up the original config"
[[ $output == *"Close and reopen all Kitty windows"* ]] || fail "migration requires a full restart"
pass "stock config is refreshed with a backup and restart guidance"
output=$(run_migration)
cmp -s "$ROOT/config/kitty/kitty.conf" "$kitty_config" || fail "stock migration is idempotent"
[[ $output != *"Close and reopen"* ]] || fail "rerun does not repeat restart guidance"
pass "stock migration is idempotent"
cat >"$kitty_config" <<'CONF'
# Keep this comment and my theme choice
include my-theme.conf
font_family My Font
font_size 13
map ctrl+insert
include shortcuts.conf
map shift+insert paste_from_clipboard
allow_remote_control yes
allow_remote_control y
allow_remote_control true
# allow_remote_control yes
listen_on unix:/tmp/my-kitty
CONF
printf 'allow_remote_control yes \n' >>"$kitty_config"
cp "$kitty_config" "$test_dir/custom-original"
cat >"$test_dir/expected" <<'CONF'
# Keep this comment and my theme choice
include my-theme.conf
font_family My Font
font_size 13
map ctrl+insert
include shortcuts.conf
map shift+insert paste_from_clipboard
# allow_remote_control yes
# allow_remote_control y
# allow_remote_control true
# allow_remote_control yes
listen_on unix:/tmp/my-kitty
CONF
printf '# allow_remote_control yes \n' >>"$test_dir/expected"
chmod 600 "$kitty_config"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "customizations survive the security repair"
[[ $(stat -c %a "$kitty_config") == "600" ]] || fail "migration preserves config permissions"
backup=$(rg -l 'allow_remote_control true' "$kitty_config".bak.* | tail -1)
cmp -s "$test_dir/custom-original" "$backup" || fail "custom config is backed up"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "custom migration is idempotent"
pass "custom config repair preserves ordering, mappings, theme, permissions, and original backup"
for mode in no n false socket-only socket password; do
printf 'allow_remote_control %s\nfont_size 13\n' "$mode" >"$kitty_config"
cp "$kitty_config" "$test_dir/expected"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "migration preserves $mode"
done
pass "explicit restricted remote-control modes are preserved"
printf 'font_size 13\n' >"$kitty_config"
cp "$kitty_config" "$test_dir/expected"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "omitted setting stays omitted"
rm "$kitty_config"
run_migration >/dev/null
[[ ! -e $kitty_config ]] || fail "absent config stays absent"
pass "migration leaves omitted settings and absent user configs alone"
printf 'allow_remote_control yes\nfont_size 13\n' >"$test_dir/dotfiles.conf"
ln -s "$test_dir/dotfiles.conf" "$kitty_config"
run_migration >/dev/null
[[ -L $kitty_config ]] || fail "migration preserves a dotfile symlink"
grep -qx '# allow_remote_control yes' "$test_dir/dotfiles.conf" || fail "symlink target is repaired"
pass "custom dotfile symlinks survive the repair"
rm "$kitty_config"
# Exercise the real font commands without changing the running desktop.
for command in pkill omarchy-restart-shell omarchy-hook omarchy-notification-send; do
printf '#!/bin/bash\nexit 0\n' >"$test_dir/bin/$command"
done
printf '#!/bin/bash\nexit 1\n' >"$test_dir/bin/pgrep"
printf '#!/bin/bash\nprintf "Test Font\\n"\n' >"$test_dir/bin/fc-list"
printf '#!/bin/bash\nexit 0\n' >"$test_dir/bin/kitty"
cat >"$test_dir/bin/gsettings" <<'SH'
#!/bin/bash
if [[ $1 == "get" ]]; then
if [[ $3 == "font-name" ]]; then
echo "'Sans 11'"
else
echo 1.0
fi
fi
SH
chmod +x "$test_dir/bin/"*
run_command() {
env HOME="$test_home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" "$ROOT/bin/$@"
}
cp "$ROOT/config/kitty/kitty.conf" "$kitty_config"
output=$(run_command omarchy-display-text-size)
[[ $output == *"terminal font: 9 pt"* ]] || fail "size report accounts for inherited Kitty default"
run_command omarchy-font-set 'Test Font'
run_command omarchy-display-text-size 16
grep -qx 'font_family Test Font' "$kitty_config" || fail "font command creates family override"
run_command omarchy-font-set Font
grep -qx 'font_family Font' "$kitty_config" || fail "font command updates family override"
[[ $(grep -c '^font_family ' "$kitty_config") == "1" ]] || fail "font update avoids duplicate overrides"
grep -qx 'font_size 12.0' "$kitty_config" || fail "size command creates size override"
grep -qx '# font_size 12' "$kitty_config" || fail "font commands keep commented instructions"
run_command omarchy-display-text-size 18
[[ $(grep -c '^font_size ' "$kitty_config") == "1" ]] || fail "size update avoids duplicate overrides"
run_command omarchy-display-text-size reset
grep -qx 'font_size 9.0' "$kitty_config" || fail "size reset restores default"
pass "font controls add and update overrides in the minimal template"
rm "$kitty_config"
output=$(run_command omarchy-display-text-size)
[[ $output == *"terminal font: 9 pt"* ]] || fail "size report handles absent Kitty config"
run_command omarchy-font-set 'Test Font'
run_command omarchy-display-text-size 16
grep -qx 'font_family Test Font' "$kitty_config" || fail "font command handles absent config"
grep -qx 'font_size 12.0' "$kitty_config" || fail "size command handles absent setting"
! grep -q '^include ' "$kitty_config" || fail "font controls must not opt users back into theming"
rm "$kitty_config"
run_command omarchy-display-text-size 16
grep -qx 'font_size 12.0' "$kitty_config" || fail "size command handles absent config"
pass "font controls create missing Kitty overrides without restoring the theme include"
if "$ROOT/bin/omarchy-cmd-present" kitty; then
kitty +runpy "$(cat "$ROOT/test/shell.d/fixtures/kitty/check-config.py")"
else
pass "Kitty not installed; skipping native config parser checks"
fi
+3 -2
View File
@@ -31,8 +31,9 @@ chmod +x "$mock_bin"/*
launch_log="$test_tmp/launch-log"
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=true OMARCHY_TEST_LOG="$launch_log" \
bash "$ROOT/bin/omarchy-launch-1password"
grep -Fxq 'launch:-- 1password' "$launch_log" || fail "1Password launcher starts the installed app"
pass "1Password launcher starts the installed app"
grep -Fxq 'launch:-- 1password --force-device-scale-factor=1' "$launch_log" ||
fail "1Password launcher starts the installed app at a fixed scale factor"
pass "1Password launcher starts the installed app at a fixed scale factor"
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=false OMARCHY_TEST_LOG="$launch_log" \
bash "$ROOT/bin/omarchy-launch-1password"
+189
View File
@@ -0,0 +1,189 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
export HOME="$tmp_dir/home"
for stub in omarchy-launch-webapp omarchy-launch-floating-terminal-with-presentation omarchy-openclaw-onboard; do
cat >"$tmp_dir/bin/$stub" <<SCRIPT
#!/bin/bash
printf '$stub:%s\n' "\$*" >>"\$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/$stub"
done
# The retry loop sleeps between polls; a no-op keeps the suite fast.
printf '#!/bin/bash\n' >"$tmp_dir/bin/sleep"
chmod +x "$tmp_dir/bin/sleep"
# The launcher asks systemd whether the gateway unit is enabled; a flag file
# stands in for the user manager.
cat >"$tmp_dir/bin/systemctl" <<SCRIPT
#!/bin/bash
printf 'systemctl:%s\\n' "\$*" >>"\$TEST_LOG"
[[ \$* == *is-enabled* ]] && [[ -f $tmp_dir/gateway-enabled ]]
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
# A machine that never onboarded gets the wizard, not a dashboard probe.
"$ROOT/bin/omarchy-launch-openclaw"
# Through omarchy-openclaw-onboard, never bare `openclaw onboard`, which as of
# 2026.9.1 ends in a browser tab with a foreground gateway and never returns.
grep -q '^omarchy-launch-floating-terminal-with-presentation:omarchy-openclaw-onboard && \[\[ -f \$HOME/.openclaw/openclaw.json \]\] && omarchy-launch-openclaw$' "$TEST_LOG" ||
fail "OpenClaw launch hands a never-onboarded machine to the wizard" "$(grep floating "$TEST_LOG")"
! grep -q '^omarchy-launch-webapp:' "$TEST_LOG" ||
fail "OpenClaw launch hands a never-onboarded machine to the wizard" "webapp opened anyway"
pass "OpenClaw launch hands a never-onboarded machine to the wizard"
mkdir -p "$HOME/.openclaw"
touch "$HOME/.openclaw/openclaw.json"
# A running gateway answers the first probe; its handoff URL opens as the app.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $* == *--json* ]] &&
echo '{"ok":true,"url":"http://127.0.0.1:18789/?token=shared","browserUrl":"http://127.0.0.1:18789/#handoff"}'
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
"$ROOT/bin/omarchy-launch-openclaw"
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#handoff$' "$TEST_LOG" ||
fail "OpenClaw launch opens the running gateway's handoff URL"
pass "OpenClaw launch opens the running gateway's handoff URL"
# A machine whose gateway unit was never enabled (never installed, or an
# install that died after writing the unit) gets `gateway install --force`,
# then is polled until the dashboard answers. Never `dashboard --yes`: since
# 2026.9.1 that neither installs nor starts anything and, once the gateway is
# up, pushes a one-time pairing URL into the clipboard.
cat >"$tmp_dir/bin/openclaw" <<SCRIPT
#!/bin/bash
printf 'openclaw:%s\\n' "\$*" >>"\$TEST_LOG"
if [[ \$* == *--json* ]]; then
[[ -f $tmp_dir/gateway-up ]] || { echo '{"ok":false,"reason":"Gateway is not running."}'; exit 1; }
echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#cold-start"}'
elif [[ \$1 == gateway ]]; then
touch "$tmp_dir/gateway-up"
fi
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rm -f "$tmp_dir/gateway-up" "$tmp_dir/gateway-enabled"
mkdir -p "$HOME/.config/systemd/user"
touch "$HOME/.config/systemd/user/openclaw-gateway.service"
: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-openclaw"
grep -q '^openclaw:gateway install --force$' "$TEST_LOG" ||
fail "OpenClaw launch installs a never-enabled gateway before opening the app"
! grep -q '^openclaw:gateway start$' "$TEST_LOG" ||
fail "OpenClaw launch installs a never-enabled gateway before opening the app" "started the half-installed unit instead"
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#cold-start$' "$TEST_LOG" ||
fail "OpenClaw launch installs a never-enabled gateway before opening the app" "webapp never opened"
pass "OpenClaw launch installs a never-enabled gateway before opening the app"
# An enabled but stopped unit is started, not reinstalled.
touch "$tmp_dir/gateway-enabled"
rm -f "$tmp_dir/gateway-up"
: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-openclaw"
grep -q '^openclaw:gateway start$' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app"
! grep -q '^openclaw:gateway install' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app" "reinstalled the unit"
! grep -q -- '--yes' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app" "fell back to dashboard --yes"
grep -q '^omarchy-launch-webapp:http://127.0.0.1:18789/#cold-start$' "$TEST_LOG" ||
fail "OpenClaw launch starts a stopped gateway before opening the app" "webapp never opened"
pass "OpenClaw launch starts a stopped gateway before opening the app"
rm -f "$tmp_dir/gateway-enabled"
# A dashboard probe that hangs is cut off, so the launch still fails cleanly
# instead of sitting forever behind an app-grid icon with no terminal.
cat >"$tmp_dir/bin/timeout" <<SCRIPT
#!/bin/bash
printf 'timeout:%s\\n' "\$1" >>"\$TEST_LOG"
exit 124
SCRIPT
chmod +x "$tmp_dir/bin/timeout"
: >"$TEST_LOG"
rc=0
"$ROOT/bin/omarchy-launch-openclaw" >/dev/null 2>&1 || rc=$?
[[ $rc != 0 ]] || fail "OpenClaw launch bounds a hanging dashboard probe"
grep -q '^timeout:10$' "$TEST_LOG" || fail "OpenClaw launch bounds a hanging dashboard probe" "probe ran without a timeout"
pass "OpenClaw launch bounds a hanging dashboard probe"
rm -f "$tmp_dir/bin/timeout"
# A gateway that never answers fails the launch instead of opening a dead page.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $* == *--json* ]] && exit 1
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
webapp_calls_before=$(grep -c '^omarchy-launch-webapp:' "$TEST_LOG" || true)
rc=0
"$ROOT/bin/omarchy-launch-openclaw" >/dev/null 2>&1 || rc=$?
webapp_calls_after=$(grep -c '^omarchy-launch-webapp:' "$TEST_LOG" || true)
[[ $rc != 0 ]] || fail "OpenClaw launch fails cleanly when the gateway never comes up"
[[ $webapp_calls_before == "$webapp_calls_after" ]] ||
fail "OpenClaw launch fails cleanly when the gateway never comes up" "webapp opened anyway"
pass "OpenClaw launch fails cleanly when the gateway never comes up"
# --tui runs onboarding in the terminal it is already in, then attaches to the
# gateway with `openclaw tui` -- never the embedded chat, which the running
# gateway's state-directory lock would refuse.
rm -f "$HOME/.openclaw/openclaw.json"
cat >"$tmp_dir/bin/omarchy-openclaw-onboard" <<SCRIPT
#!/bin/bash
printf 'omarchy-openclaw-onboard:%s\n' "\$*" >>"\$TEST_LOG"
mkdir -p "\$HOME/.openclaw" && touch "\$HOME/.openclaw/openclaw.json"
SCRIPT
chmod +x "$tmp_dir/bin/omarchy-openclaw-onboard"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $* == *--json* ]] && echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#tui"}'
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
floating_calls_before=$(grep -c '^omarchy-launch-floating-terminal-with-presentation:' "$TEST_LOG" || true)
"$ROOT/bin/omarchy-launch-openclaw" --tui
grep -q '^omarchy-openclaw-onboard:$' "$TEST_LOG" ||
fail "--tui onboards in the current terminal" "$(grep 'onboard' "$TEST_LOG")"
floating_calls_after=$(grep -c '^omarchy-launch-floating-terminal-with-presentation:' "$TEST_LOG" || true)
[[ $floating_calls_before == "$floating_calls_after" ]] ||
fail "--tui onboards in the current terminal" "spawned a floating terminal"
grep -q '^openclaw:tui$' "$TEST_LOG" || fail "--tui attaches to the gateway"
pass "--tui onboards in place and attaches to the gateway"
# The stub also logs argv one entry per line, so a prompt split into words
# would show up as extra lines rather than pass a space-joined comparison.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == tui ]] && printf 'argv:[%s]\n' "$@" >>"$TEST_LOG"
[[ $* == *--json* ]] && echo '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#tui"}'
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-openclaw" --tui --message "Review this project"
[[ $(grep -c '^argv:' "$TEST_LOG") == 3 ]] && grep -q '^argv:\[Review this project\]$' "$TEST_LOG" ||
fail "--tui seeds the session through --message" "$(grep '^argv:' "$TEST_LOG" | tr '\n' ' ')"
! grep -q '^omarchy-launch-webapp:' "$TEST_LOG" ||
fail "--tui seeds the session through --message" "webapp opened instead"
pass "--tui seeds the session through --message"
@@ -1,111 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1786482992.sh"
packaged_defaults="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" ||
fail "the packaged Limine defaults still unpack the initramfs synchronously"
pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin"
: >"$calls"
cat >"$stub_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
(( ${LIMINE_MKINITCPIO_INSTALLED:-1} == 1 ))
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo' >>"$TEST_LOG"
printf '\t%s' "$@" >>"$TEST_LOG"
printf '\n' >>"$TEST_LOG"
"$@"
SH
cat >"$stub_bin/limine-mkinitcpio" <<'SH'
#!/bin/bash
echo 'limine-mkinitcpio' >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
defaults_conf="$test_tmp/omarchy-defaults.conf"
running_cmdline="$test_tmp/cmdline"
rebuild_marker="$test_tmp/rebuild-complete"
cp "$packaged_defaults" "$defaults_conf"
# A boot image baked before quattro's defaults landed: the pre-quattro command
# line plus the machine's own root parameters, and none of the new ones.
stale_cmdline='quiet splash cryptdevice=PARTUUID=fake:root root=/dev/mapper/root rw'
# Everything the packaged defaults ask for, as a rebuilt image would carry it.
configured=$(sed -n 's/^KERNEL_CMDLINE\[default\]+="\(.*\)"[[:space:]]*$/\1/p' "$defaults_conf" | tr '\n' ' ')
current_cmdline="cryptdevice=PARTUUID=fake:root root=/dev/mapper/root rw $configured"
run_migration() {
PATH="$stub_bin:$PATH" \
TEST_LOG="$calls" \
OMARCHY_LIMINE_DEFAULTS_CONF="$defaults_conf" \
OMARCHY_RUNNING_CMDLINE="$running_cmdline" \
OMARCHY_LIMINE_REBUILD_MARKER="$rebuild_marker" \
bash -euo pipefail "$migration" >/dev/null
}
echo "$stale_cmdline" >"$running_cmdline"
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "a boot image older than the Limine defaults is rebuilt"
[[ -f $rebuild_marker ]] || fail "the rebuild records the machine-wide repair"
pass "migration rebuilds a boot image that predates the Limine defaults"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a recorded rebuild is not repeated" "$(cat "$calls")"
pass "migration is machine-idempotent before reboot"
rm -f "$rebuild_marker"
: >"$calls"
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "an interrupted rebuild is retried"
[[ -f $rebuild_marker ]] || fail "a retried rebuild records completion"
pass "migration retries an interrupted rebuild"
echo "$current_cmdline" >"$running_cmdline"
rm -f "$rebuild_marker"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a boot image matching the defaults is left alone" "$(cat "$calls")"
[[ ! -e $rebuild_marker ]] || fail "an untouched machine is not marked as repaired"
pass "migration skips a boot image that already carries the defaults"
echo "$stale_cmdline" >"$running_cmdline"
: >"$calls"
LIMINE_MKINITCPIO_INSTALLED=0 run_migration
[[ ! -s $calls ]] || fail "installs without limine-mkinitcpio are skipped" "$(cat "$calls")"
mv "$defaults_conf" "$defaults_conf.away"
run_migration
mv "$defaults_conf.away" "$defaults_conf"
[[ ! -s $calls ]] || fail "installs without the Limine defaults are skipped" "$(cat "$calls")"
pass "migration skips installs it does not apply to"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
packaged_defaults="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" ||
fail "the packaged Limine defaults still unpack the initramfs synchronously"
pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt"
+86 -157
View File
@@ -4,166 +4,95 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
config_script="$ROOT/install/config/locate.sh"
require_command python3
require_command updatedb
require_command plocate
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
python3 - <<'PY'
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
stock_conf() {
cat >"$1" <<'CONF'
PRUNE_BIND_MOUNTS = "yes"
PRUNEFS = "9p afs autofs cifs fuse nfs nfs4 proc sysfs tmpfs"
PRUNENAMES = ".git .hg .svn"
PRUNEPATHS = "/afs /media /mnt /net /sfs /tmp /udev /var/cache /var/lib/pacman/local /var/lock /var/run /var/spool /var/tmp"
CONF
}
root = Path(os.environ["ROOT"])
# updatedb dies on a config that defines a variable twice, so hand every
# rewritten file to the real parser rather than trusting the greps below.
empty_tree="$test_tmp/empty-tree"
mkdir -p "$empty_tree"
def check(condition, description):
if not condition:
raise SystemExit("not ok - " + description)
print("ok - " + description, flush=True)
assert_conf_parses() {
command -v updatedb >/dev/null || return 0
drop_in = root / "default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf"
directives = [line.strip() for line in drop_in.read_text().splitlines() if line.strip() and not line.startswith("#")]
check(len(directives) == 3 and directives[:2] == ["[Service]", "ExecStart="] and directives[2].startswith("ExecStart="),
"locate drop-in replaces the command and preserves upstream service restrictions")
command = shlex.split(directives[2].removeprefix("ExecStart="))
options = ["--prune-bind-mounts=no", "--add-prunepaths=/.snapshots"]
check(command == ["/usr/bin/updatedb", *options],
"locate service runs updatedb directly with fixed Btrfs options")
check("ConditionACPower=true" in (root / "etc/systemd/system/plocate-updatedb.service.d/ac-only.conf").read_text(),
"scheduled locate indexing keeps its AC-power condition")
check(not (root / "install/config/locate.sh").exists() and not (root / "migrations/1784809451.sh").exists(),
"the retired locate configuration helper and migration are absent")
for directory in ("bin", "install", "migrations"):
for path in (root / directory).rglob("*"):
if path.is_file():
content = path.read_text()
if "OMARCHY_UPDATEDB_CONF_PATH" in content or "config/locate.sh" in content:
raise SystemExit("not ok - retired locate configuration path remains in " + str(path))
check(True, "runtime and installation no longer reference the configuration rewrite")
local errors
errors=$(updatedb --config-file "$1" -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null | grep -F "$1:" || true)
[[ -z $errors ]] || fail "updatedb accepts the rewritten config" "$errors"
}
with tempfile.TemporaryDirectory(prefix="omarchy-locate-") as scratch:
scratch = Path(scratch)
fake_bin = scratch / "bin"
fake_bin.mkdir()
stubs = {
"updatedb": 'printf "%s\\n" "$@" >"$TEST_CALLS"',
"sudo": 'exec "$@"',
"fzf": 'cat >/dev/null\nprintf "%s\\n" test-package',
"yay": 'if [[ ${1:-} == "-Slqa" ]]; then printf "%s\\n" test-package; fi',
"omarchy-sudo-keepalive": ':',
"omarchy-show-done": ':',
}
for name, body in stubs.items():
path = fake_bin / name
path.write_text("#!/bin/bash\n" + body + "\n")
path.chmod(0o755)
calls = scratch / "updatedb-arguments"
env = dict(os.environ, PATH=str(fake_bin) + ":" + os.environ["PATH"], TEST_CALLS=str(calls))
for relative in ("install/post-install/localdb.sh", "bin/omarchy-pkg-aur-install"):
subprocess.run(["bash", "-euo", "pipefail", str(root / relative)], env=env, check=True)
check(calls.read_text().splitlines() == options,
relative + " passes the scheduled service options directly")
calls.unlink()
conf="$test_tmp/updatedb.conf"
stock_conf "$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config indexes Btrfs subvolume mounts like /home"
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate config prunes /.snapshots"
assert_conf_parses "$conf"
pass "locate config skips Btrfs snapshots and indexes Btrfs subvolumes"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
[[ $(grep -o '/\.snapshots' "$conf" | wc -l) -eq 1 ]] || fail "locate config is idempotent"
assert_conf_parses "$conf"
pass "locate config leaves an already-configured file alone"
OMARCHY_UPDATEDB_CONF_PATH="$test_tmp/missing.conf" bash -euo pipefail "$config_script" >/dev/null
pass "locate config tolerates a missing updatedb.conf"
# A hand-edited updatedb.conf may drop the settings entirely, or write them
# without the spaces around the "=" or the quotes that the stock Arch file uses.
conf="$test_tmp/sparse-updatedb.conf"
printf '%s\n' 'PRUNENAMES = ".git .hg .svn"' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config adds a missing PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config adds a missing PRUNEPATHS"
assert_conf_parses "$conf"
pass "locate config adds settings a hand-edited updatedb.conf is missing"
conf="$test_tmp/unspaced-updatedb.conf"
printf '%s\n' 'PRUNE_BIND_MOUNTS="yes"' 'PRUNEPATHS="/tmp /var/tmp"' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an unspaced PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp /var/tmp"' "$conf" || fail "locate config prunes /.snapshots in an unspaced PRUNEPATHS"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config keeps a single PRUNEPATHS setting"
assert_conf_parses "$conf"
pass "locate config handles updatedb.conf written without spaces around ="
# updatedb allows a comment after a value and indented settings, and defining
# either setting twice makes it refuse to run at all.
conf="$test_tmp/commented-updatedb.conf"
printf '%s\n' ' PRUNE_BIND_MOUNTS = "yes" # subvolumes look like bind mounts' \
'PRUNEPATHS = "/tmp" # scratch' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an indented PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp"' "$conf" || fail "locate config keeps the paths a commented PRUNEPATHS already prunes"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces a commented PRUNEPATHS instead of adding a second one"
assert_conf_parses "$conf"
pass "locate config handles indented settings and trailing comments"
# A hand-edited file may have dropped the quotes updatedb requires, which
# leaves it unparseable until something writes the setting out properly.
conf="$test_tmp/unquoted-updatedb.conf"
printf '%s\n' 'PRUNEPATHS = /tmp' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config repairs an unquoted PRUNEPATHS"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces an unquoted PRUNEPATHS instead of adding a second one"
assert_conf_parses "$conf"
pass "locate config handles updatedb.conf written without quotes"
# A path that merely ends in /.snapshots is not the root snapshot directory.
conf="$test_tmp/nested-snapshots-updatedb.conf"
printf '%s\n' 'PRUNEPATHS = "/var/lib/machines/.snapshots"' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNEPATHS = "/.snapshots /var/lib/machines/.snapshots"' "$conf" || fail "locate config prunes /.snapshots alongside a path that ends in it"
assert_conf_parses "$conf"
pass "locate config tells /.snapshots apart from a path that ends in it"
locate_migration=$(grep -rl 'Configure locate to skip Btrfs snapshots' "$ROOT/migrations" | head -n 1 || true)
[[ -n $locate_migration ]] || fail "locate migration exists"
fake_bin="$test_tmp/bin"
mkdir -p "$fake_bin"
cat >"$fake_bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
chmod +x "$fake_bin/sudo"
cat >"$fake_bin/systemctl" <<'STUB'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"$TEST_LOG"
STUB
chmod +x "$fake_bin/systemctl"
conf="$test_tmp/migration-updatedb.conf"
stock_conf "$conf"
TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$ROOT" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate migration rewrites updatedb.conf"
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate migration prunes /.snapshots"
grep -qFx 'systemctl restart --no-block plocate-updatedb.service' "$test_tmp/calls.log" || fail "locate migration replaces an in-flight run and rebuilds the index without blocking"
pass "locate migration fixes existing installs and rebuilds the index"
: >"$test_tmp/calls.log"
TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$ROOT" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration skips already-configured installs"
pass "locate migration is a no-op once updatedb.conf is configured"
# A dev checkout carries migrations from a release whose install scripts the
# checked-out tree may not have yet, and omarchy-migrate runs under set -e.
: >"$test_tmp/calls.log"
conf="$test_tmp/no-config-script-updatedb.conf"
stock_conf "$conf"
TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$test_tmp/empty" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null ||
fail "locate migration survives a tree without the locate config script"
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration touches nothing without the locate config script"
pass "locate migration is a no-op when the locate config script is missing"
tree = scratch / "tree"
visible = tree / "home/current-file"
excluded = tree / "private&pipe|directory"
hidden = excluded / "private-file"
visible.parent.mkdir(parents=True)
excluded.mkdir()
visible.touch()
hidden.touch()
conf = scratch / "updatedb.conf"
conf.write_text('PRUNE_BIND_MOUNTS = "yes"\nPRUNEPATHS = "' + str(excluded) + '"\n')
conf.chmod(0o640)
original = conf.read_bytes()
metadata = conf.stat()
database = scratch / "plocate.db"
run = [*command, "--config-file", str(conf), "--database-root", str(tree),
"--output", str(database), "--require-visibility", "no", "--debug-pruning"]
result = subprocess.run(run, capture_output=True, text=True, check=True)
debug = result.stdout + result.stderr
check("prune_bind_mounts\\000\n0\\000" in debug and "/.snapshots\\000" in debug,
"real updatedb overrides bind-mount pruning and adds root snapshots to exclusions")
entries = subprocess.check_output(["plocate", "--database", str(database), ""], text=True).splitlines()
check(str(visible) in entries and str(hidden) not in entries,
"real locate indexes current files and preserves literal administrator exclusions")
check(conf.read_bytes() == original and (conf.stat().st_mode, conf.stat().st_uid, conf.stat().st_gid, conf.stat().st_mtime_ns)
== (metadata.st_mode, metadata.st_uid, metadata.st_gid, metadata.st_mtime_ns),
"indexing preserves configuration bytes, permissions, ownership, and modification time")
subprocess.run(run, capture_output=True, check=True)
repeated = subprocess.check_output(["plocate", "--database", str(database), ""], text=True).splitlines()
check(repeated == entries, "repeated indexing retains the same results and exclusions")
PY
+1 -1
View File
@@ -69,7 +69,7 @@ PATH="$stub_bin:$PATH" XDG_CACHE_HOME="$cache_home" \
fail "image menu recovers thumbnails from stranded locks"
(( $(awk 'END { print NR }' "$cache_dir/$cache_key.rows") == 3 )) ||
fail "image menu rebuilds every row after cache invalidation"
[[ $(head -n 1 "$cache_dir/$cache_key.signature") == "v3" ]] ||
[[ $(head -n 1 "$cache_dir/$cache_key.signature") == "v4" ]] ||
fail "image menu invalidates stale row caches"
[[ ! -e $stale_tmp ]] ||
fail "image menu clears partial thumbnails left by killed generators"
+8 -3
View File
@@ -224,8 +224,13 @@ const expectedAgents = {
claude: { icon: '󰛄', label: 'Claude' },
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]) => {
@@ -238,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', '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 = {
@@ -636,5 +641,5 @@ assert(
JS
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
[[ $font_charset == *"e900-e909"* ]] || 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"
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
home="$tmpdir/home"
stub_bin="$tmpdir/bin"
mkdir -p "$home" "$stub_bin"
# Stands in for the real mise so a generated wrapper can be run and asked what
# arguments it passed on.
cat >"$stub_bin/mise" <<'SH'
#!/bin/bash
printf 'mise' >>"$OMARCHY_MISE_TEST_LOG"
for arg in "$@"; do
printf '\t%s' "$arg" >>"$OMARCHY_MISE_TEST_LOG"
done
printf '\n' >>"$OMARCHY_MISE_TEST_LOG"
SH
chmod +x "$stub_bin/mise"
install_wrapper() {
HOME="$home" "$ROOT/bin/omarchy-mise-install" "$@"
}
# The ordinary case still works, and every call site in install/user/mise.sh
# passes names of this shape.
install_wrapper npm:playwright playwright >/dev/null
[[ -x $home/.local/bin/playwright ]] ||
fail "a normal install writes an executable wrapper"
log="$tmpdir/normal.log"
: >"$log"
OMARCHY_MISE_TEST_LOG="$log" PATH="$stub_bin:$PATH" "$home/.local/bin/playwright" >/dev/null
grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:playwright' "$log" ||
fail "the wrapper asks mise for the package it was given" "$(cat "$log")"
pass "a normal install writes a wrapper that names its package"
# A package name is data. Quoted with %q it reaches mise as one argument
# instead of being read as shell source when the wrapper runs.
install_wrapper 'npm:pkg$(touch '"$tmpdir"'/PWNED)end' hostile >/dev/null
log="$tmpdir/hostile.log"
: >"$log"
OMARCHY_MISE_TEST_LOG="$log" PATH="$stub_bin:$PATH" "$home/.local/bin/hostile" >/dev/null
[[ -e $tmpdir/PWNED ]] &&
fail "a package name with shell characters does not run when the wrapper does" \
"wrapper: $(cat "$home/.local/bin/hostile")"
grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:pkg$(touch '"$tmpdir"'/PWNED)end' "$log" ||
fail "the package reaches mise whole" "$(cat "$log")"
pass "a package name with shell characters reaches mise as one argument"
# The command name is a file name under ~/.local/bin. These shapes escape it,
# hide it, make something that reads as an option, or carry characters that have
# no business in a file name. Labelled so a newline in the value does not end up
# inside the test output.
refused=(
"a slash" "../escaped"
"a leading dot" ".hidden"
"a leading dash" "-dash"
"a newline" $'with\nnewline'
"a tab" $'with\ttab'
)
for (( i = 0; i < ${#refused[@]}; i += 2 )); do
label=${refused[i]}
name=${refused[i + 1]}
if install_wrapper somepkg "$name" >/dev/null 2>"$tmpdir/err"; then
fail "a command name with $label is refused"
fi
grep -Fq 'is not usable as a command name' "$tmpdir/err" ||
fail "the refusal says why for a command name with $label" "$(cat "$tmpdir/err")"
done
pass "command names that are not plain file names are refused"
# The refusal has to land before the rm, which would otherwise delete the
# escaped path on its way to failing.
victim="$tmpdir/victim"
printf 'keep me\n' >"$victim"
if install_wrapper somepkg "../../../..$victim" >/dev/null 2>&1; then
fail "an escaping command name is refused"
fi
[[ -f $victim ]] ||
fail "an escaping command name removes nothing outside ~/.local/bin"
pass "an escaping command name removes nothing outside ~/.local/bin"
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const network = requireFromRoot('shell/plugins/panels/network/Model.js')
const states = { Unknown: 0, None: 1, Portal: 2, Limited: 3, Full: 4 }
for (const kind of ['wifi', 'ethernet']) {
for (const [native, expected] of [
['Unknown', 'unknown'], ['None', 'none'], ['Portal', 'portal'],
['Limited', 'limited'], ['Full', 'full']
]) {
assertEqual(network.connectivityState(kind, states[native], states, true), expected,
`${kind} maps native ${native} connectivity without confusing an outage with a portal`)
}
assertEqual(network.connectivityState(kind, 99, states, true), 'unknown', `${kind} handles unknown connectivity`)
for (const native of Object.values(states)) {
assertEqual(network.connectivityState(kind, native, states, false), 'unknown', `${kind} ignores stale results with probing disabled (${native})`)
}
}
for (const native of Object.values(states)) {
assertEqual(network.connectivityState('disconnected', native, states, true), 'none', `disconnect clears stale connectivity (${native})`)
}
for (const state of ['portal', 'limited']) {
assertEqual(network.connectionIcon('wifi', 80, state), '󰤩', `${state} uses a blocked Wi-Fi icon`)
assertEqual(network.connectionIcon('ethernet', 80, state), '󰈂', `${state} uses a blocked Ethernet icon`)
assertEqual(network.connectionIcon('disconnected', 80, state), '󰤮', `${state} does not override the disconnected icon`)
}
for (const state of ['full', 'unknown', 'none', undefined]) {
for (const signal of [-1, 0, 20, 40, 60, 80, 100]) {
assertEqual(network.connectionIcon('wifi', signal, state), network.wifiIconFor(signal), `${state} preserves Wi-Fi strength ${signal}`)
}
assertEqual(network.connectionIcon('ethernet', -1, state), '󰈀', `${state} preserves the Ethernet icon`)
}
const url = new URL(network.captivePortalUrl)
assertEqual(url.protocol, 'http:', 'browser entry point uses plain HTTP so a portal can intercept it')
assertEqual(url.hostname, 'ping.archlinux.org', 'browser entry point is fixed rather than portal-supplied')
assertEqual(url.username + url.password, '', 'browser entry point contains no credentials')
JS
require_compositor "network captive-portal runtime test"
require_command quickshell
stage=$(mktemp -d)
trap 'rm -rf -- "$stage"' EXIT
fixture="$SHELL_TEST_DIR/fixtures/network-captive-portal"
mkdir -p "$stage/network" "$stage/bin" "$stage/home"
ln -s "$ROOT/shell/Ui" "$stage/Ui"
ln -s "$ROOT/shell/Commons" "$stage/Commons"
cp -r "$fixture/mocks" "$stage/mocks"
cp "$fixture/shell.qml" "$stage/shell.qml"
cp "$ROOT/shell/plugins/panels/network/Model.js" "$stage/network/Model.js"
node - "$ROOT" "$stage" <<'JS'
const fs = require('fs')
const [root, stage] = process.argv.slice(2)
let source = fs.readFileSync(`${root}/shell/plugins/panels/network/Panel.qml`, 'utf8')
// Keep installed enum values and actual UI bindings. Only replace the singleton
// and expose private IDs in the disposable copy, never in production code.
source = source.replace('import Quickshell.Networking', 'import Quickshell.Networking\nimport "../mocks"')
source = source.replace(/\bNetworking\./g, 'NetworkMock.')
source = source.replace(' id: root', ` id: root
property alias testButton: portalAction
property alias testKeys: keyCatcher
property alias testMeta: heroMeta
property alias testTitle: heroSsid
property alias testPoll: connectivityPoll
property alias testBarButton: button`)
fs.writeFileSync(`${stage}/network/Panel.qml`, source)
JS
printf '#!/bin/bash\nexit 0\n' > "$stage/bin/noop"
chmod +x "$stage/bin/noop"
for command in omarchy-dns omarchy-network-band; do
ln -s noop "$stage/bin/$command"
done
# Preview uses only synthetic details, never the host's SSID or addresses.
# Normal assertions keep the details empty to exercise missing-route handling.
printf '#!/bin/bash\nif [[ -n ${NETWORK_TEST_PREVIEW:-} ]]; then\n printf "type\\twifi\\niface\\ttest-wifi\\nssid\\tGuest Wi-Fi\\nip\\t192.0.2.10\\ngateway\\t192.0.2.1\\n"\nfi\n' > "$stage/bin/omarchy-network-status"
chmod +x "$stage/bin/omarchy-network-status"
printf '#!/bin/bash\nprintf "%%s\\n" "$@" >> "$NETWORK_TEST_BROWSER_LOG"\n' > "$stage/bin/omarchy-launch-browser"
chmod +x "$stage/bin/omarchy-launch-browser"
# All networking and external actions are mocked; the real connection and
# browser are never touched, and the fixture writes only to its scratch HOME.
output=$(HOME="$stage/home" OMARCHY_PATH="$ROOT" PATH="$stage/bin:$PATH" \
NETWORK_TEST_BROWSER_LOG="$stage/browser.log" \
timeout 30 quickshell -p "$stage" --no-color 2>&1) || fail "network portal fixture exits cleanly" "$output"
[[ $output == *"RESULT pass"* ]] || fail "network portal runtime assertions pass" "$output"
if rg -q 'RESULT fail|ReferenceError|TypeError|Error:|Unable to assign|Binding loop' <<< "$output"; then
fail "network portal fixture has no QML errors" "$output"
fi
[[ -f $stage/browser.log ]] || fail "portal action launches the browser"
[[ $(<"$stage/browser.log") == "http://ping.archlinux.org/nm-check.txt" ]] || fail "portal opens exactly one fixed HTTP URL"
pass "network portal, recovery, disabled checks, outage, disconnect, keyboard navigation, and browser argv work in QML"
@@ -6,7 +6,6 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
dns="$ROOT/bin/omarchy-dns"
hardware_network="$ROOT/install/hardware/network.sh"
migration="$ROOT/migrations/1782002156.sh"
! grep -F 'systemd-networkd' "$dns" >/dev/null || fail "omarchy-dns no longer restarts systemd-networkd"
grep -F 'NetworkManager/conf.d/20-omarchy-dns.conf' "$dns" >/dev/null
@@ -26,10 +25,3 @@ grep -F 'systemd-networkd.socket' "$hardware_network" >/dev/null
grep -F '20-wlan.network' "$hardware_network" >/dev/null
grep -F 'omarchy-networkd-retired' "$hardware_network" >/dev/null
pass "hardware setup retires archinstall networkd state"
grep -F 'OMARCHY_UPGRADE_TO_QUATTRO_LIVE' "$migration" >/dev/null
grep -F 'systemctl disable --now "$unit"' "$migration" >/dev/null
grep -F 'systemctl stop systemd-networkd.service' "$migration" >/dev/null
grep -F 'NetworkManager.service' "$migration" >/dev/null
grep -F '20-wlan.network' "$migration" >/dev/null
pass "migration repairs upgraded systems with networkd still active"
-131
View File
@@ -1,131 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1786605598.sh"
packaged_hooks="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin"
: >"$calls"
cat >"$stub_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
(( ${LIMINE_MKINITCPIO_INSTALLED:-1} == 1 ))
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo' >>"$TEST_LOG"
printf '\t%s' "$@" >>"$TEST_LOG"
printf '\n' >>"$TEST_LOG"
"$@"
SH
cat >"$stub_bin/limine-mkinitcpio" <<'SH'
#!/bin/bash
echo 'limine-mkinitcpio' >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
hooks_conf="$test_tmp/omarchy_hooks.conf"
nvidia_conf="$test_tmp/nvidia.conf"
rebuild_marker="$test_tmp/rebuild-complete"
cp "$packaged_hooks" "$hooks_conf"
echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' >"$nvidia_conf"
# Each argument is a PCI device as "vendor:class", in sysfs's own format.
write_pci_devices() {
rm -rf "$test_tmp/devices"
mkdir -p "$test_tmp/devices"
local index=0
local spec
for spec in "$@"; do
local slot
slot=$(printf '0000:%02x:00.0' "$index")
mkdir -p "$test_tmp/devices/$slot"
printf '%s\n' "${spec%%:*}" >"$test_tmp/devices/$slot/vendor"
printf '%s\n' "${spec##*:}" >"$test_tmp/devices/$slot/class"
index=$((index + 1))
done
}
run_migration() {
PATH="$stub_bin:$PATH" \
TEST_LOG="$calls" \
OMARCHY_MKINITCPIO_HOOKS_CONF="$hooks_conf" \
OMARCHY_MKINITCPIO_NVIDIA_CONF="$nvidia_conf" \
OMARCHY_KMS_REBUILD_MARKER="$rebuild_marker" \
OMARCHY_PCI_DEVICES_PATH="$test_tmp/devices" \
bash -euo pipefail "$migration" >/dev/null
}
# NVIDIA-only machine with the proprietary driver: the packaged conditional
# drops kms, so the stale initramfs must be rebuilt once.
write_pci_devices 0x10de:0x030000
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "an NVIDIA-only machine rebuilds its initramfs"
[[ -f $rebuild_marker ]] || fail "the rebuild records the machine-wide repair"
pass "migration rebuilds the initramfs on an NVIDIA-only machine"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a recorded rebuild is not repeated" "$(cat "$calls")"
pass "migration is machine-idempotent across users"
rm -f "$rebuild_marker"
: >"$calls"
run_migration
grep -Fxq 'limine-mkinitcpio' "$calls" ||
fail "an interrupted rebuild is retried"
pass "migration retries an interrupted rebuild"
# Hybrid machine: the conditional keeps kms, so the initramfs already matches.
write_pci_devices 0x1002:0x030000 0x10de:0x030200
rm -f "$rebuild_marker"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a hybrid machine is left alone" "$(cat "$calls")"
[[ ! -e $rebuild_marker ]] || fail "an untouched machine is not marked as repaired"
pass "migration skips a hybrid machine that keeps kms"
# A user-edited hooks conf predating the conditional (the packaged update sits
# in a .pacnew) still carries kms unconditionally: nothing to rebuild for.
write_pci_devices 0x10de:0x030000
echo 'HOOKS=(base udev autodetect modconf kms block filesystems fsck)' >"$hooks_conf"
: >"$calls"
run_migration
[[ ! -s $calls ]] || fail "a user-edited hooks conf is left alone" "$(cat "$calls")"
pass "migration skips a hooks conf without the conditional"
cp "$packaged_hooks" "$hooks_conf"
: >"$calls"
LIMINE_MKINITCPIO_INSTALLED=0 run_migration
[[ ! -s $calls ]] || fail "installs without limine-mkinitcpio are skipped" "$(cat "$calls")"
mv "$nvidia_conf" "$nvidia_conf.away"
run_migration
mv "$nvidia_conf.away" "$nvidia_conf"
[[ ! -s $calls ]] || fail "machines without the proprietary NVIDIA driver are skipped" "$(cat "$calls")"
pass "migration skips installs it does not apply to"
+288
View File
@@ -0,0 +1,288 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
export HOME="$tmp_dir/home"
export OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS=0
unit="$HOME/.config/systemd/user/openclaw-gateway.service"
export unit
# systemd and ss stand in for themselves. Once the wizard has written the unit
# it is running as pid 4242, and that same process holds the gateway port
# unless a test plants a different listener (an orphan) in $listener_file.
listener_file="$tmp_dir/listener"
export listener_file
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
if [[ $* == *MainPID* ]]; then
[[ -f $unit ]] && echo 4242 || echo 0
exit 0
fi
[[ $* == *is-active* ]] && [[ -f $unit ]]
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
cat >"$tmp_dir/bin/ss" <<'SCRIPT'
#!/bin/bash
pid=
[[ -f $listener_file ]] && pid=$(cat "$listener_file")
[[ -z $pid && -f $unit ]] && pid=4242
[[ -n $pid ]] && echo "LISTEN 0 511 127.0.0.1:18789 0.0.0.0:* users:((\"node\",pid=$pid,fd=3))"
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/ss"
export OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT=1
# A wizard that configures OpenClaw, prints its outro, and then lingers forever
# on an open handle: the 2026.9.1 --skip-ui behaviour. It "installs the
# gateway" by writing the config a moment in, so the gateway only starts
# answering partway through, never at once.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard)
( sleep 1; mkdir -p "$HOME/.openclaw" "${unit%/*}"; touch "$HOME/.openclaw/openclaw.json" "$unit" ) &
trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM
while :; do sleep 0.2; done
;;
dashboard)
[[ -f $HOME/.openclaw/openclaw.json ]] && echo '{"ok":true,"port":18789}' || { echo '{"ok":false}'; exit 1; }
;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
start=$SECONDS
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
elapsed=$((SECONDS - start))
grep -q '^openclaw:onboard --flow quickstart --install-daemon --skip-ui$' "$TEST_LOG" ||
fail "onboarding runs the classic quickstart wizard with the service install" "$(grep '^openclaw:onboard' "$TEST_LOG" || true)"
pass "onboarding runs the classic quickstart wizard with the service install"
[[ $rc == 0 ]] || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" ||
fail "a wizard that lingers after the gateway is up is stopped and counts as success" "wizard was never signalled"
(( elapsed < 30 )) || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "took ${elapsed}s"
pass "a wizard that lingers after the gateway is up is stopped and counts as success"
# The wizard only starts being stopped once the gateway actually answers: a
# stub that never writes the config is left alone and must be ended by its own
# exit, not the watcher.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == onboard ]] && { echo "Skipped for now."; exit 3; }
echo '{"ok":false}'; exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 3 ]] || fail "an abandoned wizard passes its exit code through" "rc=$rc"
[[ ! -f $HOME/.openclaw/openclaw.json ]] || fail "an abandoned wizard passes its exit code through" "config appeared"
! grep -q '^terminated$' "$TEST_LOG" || fail "an abandoned wizard passes its exit code through" "watcher signalled it anyway"
pass "an abandoned wizard passes its exit code through"
# A wizard that finishes and exits on its own is simply waited for.
: >"$TEST_LOG"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == onboard ]] && { mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"; exit 0; }
echo '{"ok":true,"port":18789}'
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "a wizard that exits cleanly is waited for" "rc=$rc"
pass "a wizard that exits cleanly is waited for"
# Input reaches the wizard: it runs in the foreground, not backgrounded (which
# would stop it on SIGTTIN the moment it read stdin).
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard)
read -r answer
printf 'answer:%s\n' "$answer" >>"$TEST_LOG"
mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"
exit 0
;;
dashboard) echo '{"ok":true,"port":18789}' ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
printf 'gpt-5\n' | "$ROOT/bin/omarchy-openclaw-onboard" >/dev/null 2>&1
grep -q '^answer:gpt-5$' "$TEST_LOG" ||
fail "the wizard reads terminal input" "$(grep '^answer' "$TEST_LOG" || echo 'no input reached it')"
pass "the wizard reads terminal input"
# An already-configured machine whose gateway answers is not onboarded again:
# the wizard's repair pass would otherwise be stopped mid-prompt the moment
# the watcher saw the pre-existing gateway.
: >"$TEST_LOG"
mkdir -p "$HOME/.openclaw" && touch "$HOME/.openclaw/openclaw.json"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
[[ $1 == onboard ]] && { echo wizard-ran >>"$TEST_LOG"; exit 0; }
echo '{"ok":true,"port":18789}'
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "an already-running OpenClaw is left alone" "rc=$rc"
! grep -q '^wizard-ran$' "$TEST_LOG" || fail "an already-running OpenClaw is left alone" "wizard ran anyway"
pass "an already-running OpenClaw is left alone"
# Setup applied but the gateway never answers: the wizard would linger
# forever, so it is stopped after the deadline and that is reported as failure.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard)
mkdir -p "$HOME/.openclaw"; touch "$HOME/.openclaw/openclaw.json"
trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM
while :; do sleep 0.2; done
;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
start=$SECONDS
rc=0
err=$("$ROOT/bin/omarchy-openclaw-onboard" </dev/null 2>&1 >/dev/null) || rc=$?
elapsed=$((SECONDS - start))
[[ $rc == 1 ]] || fail "a gateway that never comes up ends the wait with a failure" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" || fail "a gateway that never comes up ends the wait with a failure" "wizard left running"
[[ $err == *"gateway status"* ]] || fail "a gateway that never comes up ends the wait with a failure" "no diagnostic: $err"
(( elapsed < 30 )) || fail "a gateway that never comes up ends the wait with a failure" "took ${elapsed}s"
pass "a gateway that never comes up ends the wait with a failure"
# A signal at this script alone takes the wizard down with it.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 &
wrapper=$!
sleep 1
kill -TERM "$wrapper"
wait "$wrapper" 2>/dev/null || true
sleep 1
grep -q '^terminated$' "$TEST_LOG" || fail "stopping the wrapper stops the wizard" "wizard survived"
! pgrep -f "$tmp_dir/bin/openclaw onboard" >/dev/null || fail "stopping the wrapper stops the wizard" "wizard process still alive"
pass "stopping the wrapper stops the wizard"
# A config left by an earlier, incomplete setup does not start the gateway
# deadline: only this run applying setup does. The wizard here takes longer
# than the deadline at its prompts, never touches the old config, and must be
# left to finish on its own.
: >"$TEST_LOG"
mkdir -p "$HOME/.openclaw" && touch "$HOME/.openclaw/openclaw.json"
sleep 1
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; sleep 4; echo finished >>"$TEST_LOG"; exit 0 ;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "a stale config does not start the gateway deadline" "rc=$rc"
grep -q '^finished$' "$TEST_LOG" || fail "a stale config does not start the gateway deadline" "wizard was cut short"
! grep -q '^terminated$' "$TEST_LOG" || fail "a stale config does not start the gateway deadline" "wizard was signalled"
pass "a stale config does not start the gateway deadline"
# ...while the same stale config being rewritten by this run does arm it.
: >"$TEST_LOG"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) touch "$HOME/.openclaw/openclaw.json"; trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
dashboard) echo '{"ok":false}'; exit 1 ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 1 ]] || fail "a config rewritten by this run arms the gateway deadline" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" || fail "a config rewritten by this run arms the gateway deadline" "wizard left running"
pass "a config rewritten by this run arms the gateway deadline"
# A gateway that answers while no config exists is not this run's: with no
# config `openclaw dashboard --json` still probes the default port, so a
# process left behind by something else (an earlier guided onboarding's
# foreground gateway) would otherwise end a wizard still at its first prompt.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; sleep 4; echo finished >>"$TEST_LOG"; exit 0 ;;
dashboard) echo '{"ok":true,"port":18789}' ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 0 ]] || fail "an orphaned gateway answering with no config does not end the wizard" "rc=$rc"
grep -q '^finished$' "$TEST_LOG" || fail "an orphaned gateway answering with no config does not end the wizard" "wizard was cut short"
! grep -q '^terminated$' "$TEST_LOG" || fail "an orphaned gateway answering with no config does not end the wizard" "wizard was signalled"
pass "an orphaned gateway answering with no config does not end the wizard"
# And after setup, an orphan still bound to the port is not success even
# while the unit reports active: Type=simple counts it active from the fork,
# before it has discovered the port is taken. The port's owner has to be the
# unit's own main process. Here it never is, so the deadline ends the wait as
# a failure rather than opening the app on the orphan.
: >"$TEST_LOG"
rm -rf "$HOME/.openclaw" "$unit" "$listener_file"
echo 999 >"$listener_file"
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
case $1 in
onboard) mkdir -p "$HOME/.openclaw" "${unit%/*}"; touch "$HOME/.openclaw/openclaw.json" "$unit"; trap 'echo terminated >>"$TEST_LOG"; exit 143' TERM; while :; do sleep 0.2; done ;;
dashboard) echo '{"ok":true,"port":18789}' ;;
esac
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
rc=0
"$ROOT/bin/omarchy-openclaw-onboard" </dev/null >/dev/null 2>&1 || rc=$?
[[ $rc == 1 ]] || fail "an orphan holding the port is not mistaken for the active unit" "rc=$rc"
grep -q '^terminated$' "$TEST_LOG" || fail "an orphan holding the port is not mistaken for the active unit" "wizard left running"
grep -q '^systemctl:--user show -p MainPID --value openclaw-gateway.service$' "$TEST_LOG" ||
fail "an orphan holding the port is not mistaken for the active unit" "unit ownership never checked"
pass "an orphan holding the port is not mistaken for the active unit"
rm -f "$listener_file"
+201
View File
@@ -0,0 +1,201 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=""
QS_PID=""
cleanup() {
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
kill "$QS_PID" 2>/dev/null || true
wait "$QS_PID" 2>/dev/null || true
fi
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
rm -rf "$TMPDIR"
fi
}
trap cleanup EXIT
shell_qml="$ROOT/shell/shell.qml"
bar_qml="$ROOT/shell/plugins/bar/Bar.qml"
plugin_shell_api="$ROOT/shell/services/PluginShellApi.qml"
idle_service="$ROOT/shell/plugins/services/idle/Service.qml"
# Normalize horizontal and vertical whitespace so the wiring assertions survive
# harmless QML reflow. The runtime fixture below behaviorally covers
# PluginShellApi and AuthServiceStore; these checks remain the guard for their
# integration through shell.qml and Bar.qml, including without a compositor.
qml_matches() {
local file=$1
local pattern=$2
tr '\n\r\t' ' ' < "$file" | grep -Eq "$pattern"
}
qml_matches "$shell_qml" 'comp\.createObject\( *manifest\.__isFirstParty *&& *!authenticationService *\? *serviceHost *: *null *\)' ||
fail "third-party and authentication services are detached from the host object tree"
qml_matches "$shell_qml" 'AuthServiceStore\.put\( *key, *inst *\)' ||
fail "authentication services are retained outside the host service map"
qml_matches "$shell_qml" 'AuthServiceStore\.isTrusted\( *key *\)' ||
fail "live authentication classification survives public manifest mutation"
qml_matches "$shell_qml" 'AuthServiceStore\.updateManifest\( *id, *shell\.publicPluginManifest\( *m *\) *\)' ||
fail "kept authentication services receive only a public manifest snapshot"
qml_matches "$shell_qml" 'if *\( *!serviceKeepLoaded\( *authenticationId *\) *\) *AuthServiceStore\.destroy\( *authenticationId *\)' ||
fail "keepLoaded authentication services survive plugin rescans"
pass "third-party and authentication services are detached from the host object tree"
run_node_test <<'JS'
const fs = require('fs')
const vm = require('vm')
const store = {}
vm.createContext(store)
vm.runInContext(
fs.readFileSync(path.join(root, 'shell/services/AuthServiceStore.js'), 'utf8'),
store
)
const service = { destroy() {} }
store.put('omarchy.lock', service)
store.destroy('omarchy.lock')
assert(
!store.has('omarchy.lock') && store.isTrusted('omarchy.lock'),
'authentication classification survives service teardown'
)
JS
qml_matches "$shell_qml" 'inst\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
fail "service plugins receive a scoped shell facade"
qml_matches "$shell_qml" 'item\.shell *= *shell\.pluginShellFor\( *panelEntry\.manifest *\)' ||
fail "panel plugins receive a scoped shell facade"
qml_matches "$shell_qml" 'target\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
fail "full-bar plugins receive a scoped shell facade"
pass "third-party entry points receive scoped shell facades"
if qml_matches "$plugin_shell_api" 'function +pluginShellForId\('; then
fail "replacement-bar facade exposes a generic plugin-shell factory"
fi
qml_matches "$bar_qml" 'else if *\( *root\.shell *&& *typeof root\.shell\.pluginShellForBarEntry *=== *"function" *\) *\{[^}]*pluginShell *= *root\.shell\.pluginShellForBarEntry\( *key, *moduleName *\)' ||
fail "replacement bars do not fall back to a service-less entry facade"
pass "replacement bars cannot manufacture another plugin's service facade"
qml_matches "$shell_qml" 'target\.barConfig *= *shell\.barConfigFor\( *manifest *\)' ||
fail "initial replacement-bar configuration is not detached"
qml_matches "$shell_qml" 'bar\.barConfig *= *shell\.barConfigFor\( *shell\.activeBarManifest *\)' ||
fail "replacement-bar configuration updates are not detached"
pass "replacement bars receive detached configuration snapshots"
qml_matches "$bar_qml" 'target\.bar *= *firstParty *\? *root *: *root\.pluginBarApiFor\( *pluginApiId, *moduleName, *registered *\)' ||
fail "third-party widgets receive a bar facade instead of the host bar"
qml_matches "$bar_qml" 'api\.clickTargets *= *root\.pluginClickTargets\( *api\.pluginId *\)' ||
fail "third-party bar facades exclude other widgets from their object graph"
pass "third-party widgets receive a bar facade instead of the host bar"
qml_matches "$shell_qml" 'widgets: *shell\.publicBarWidgetSnapshot\( *\)' ||
fail "third-party widget registries receive detached snapshots"
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *target, *"clickTarget" *\)' ||
fail "third-party bar-object ownership is stamped by the host callback"
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *owner, *"popout" *\)' ||
fail "owner-less popouts receive trusted ownership before activation"
qml_matches "$shell_qml" 'manifest\.__hostCapabilities\.indexOf\( *"authentication" *\)' ||
fail "authentication isolation follows host-stamped capabilities"
pass "registry mutation and ownership boundaries are host-controlled"
qml_matches "$bar_qml" 'root\.moduleWidgets\( *moduleName *\)' ||
fail "custom bar module widget lookups use their real module name"
qml_matches "$shell_qml" 'shell\.pluginShellForBarEntry\( *cacheKey *\+ *":" *\+ *ownerId, *moduleName *\)' ||
fail "full-bar plugins receive a scoped settings facade for custom modules"
pass "custom bar modules retain settings and popout identity"
if qml_matches "$bar_qml" 'on(Foreground|BarForeground|Background|Urgent|FontFamily|Vertical|BarSize|Transparent)Changed: *sync'; then
fail "animated scalar properties still trigger full facade resyncs"
fi
qml_matches "$bar_qml" 'api\.foreground *= *Qt\.binding\( *function\( *\) *\{ *return root\.foreground *\} *\)' ||
fail "third-party bar scalar mirrors use bindings"
qml_matches "$shell_qml" 'shell\.prunePluginApis\( *\)' ||
fail "disabled plugin facade caches are pruned"
pass "plugin facade synchronization is bounded"
qml_matches "$shell_qml" 'descriptor\.profile *!== *expectedProfile[^}]*shell\.revokePluginShellApi\( *shellKey *\)' ||
fail "manifest capability changes do not revoke cached plugin facades"
qml_matches "$shell_qml" 'shell\.barPluginMayControl\( *currentManifest\( *\), *requestedId *\)' ||
fail "bar lifecycle callbacks do not validate the current manifest"
qml_matches "$shell_qml" 'return hasCurrentBarCapabilities\( *\) *\? *shell\.mutatePluginBarConfig\( *mutator *\) *: *false' ||
fail "bar configuration mutation does not validate the current manifest"
pass "manifest changes revoke cached facade capabilities"
qml_matches "$shell_qml" 'idleConfig: *shell\.publicIdleConfigFor\( *manifest *\)' ||
fail "cloned idle services do not receive their configured timeouts"
qml_matches "$shell_qml" 'shellApi\.idleConfig *= *shell\.publicIdleConfigFor\( *shellManifest *\)' ||
fail "cloned idle service configuration does not refresh"
qml_matches "$idle_service" 'shell *&& *shell\.idleConfig *\? *shell\.idleConfig *: *\(\{\}\)' ||
fail "the idle service does not consume its scoped configuration"
bar_entry_shell=$(sed -n '/^ function pluginShellForBarEntry(/,/^ function pluginShellFor(/p' "$shell_qml")
tr '\n\r\t' ' ' <<<"$bar_entry_shell" |
grep -Eq 'var id *= *shell\.pluginRegistry\.resolveEnabledId\( *target *\)[^}]*return shell\.pluginRegistry\.installedPlugins\[id\] *\|\| *null' ||
fail "replacement-bar clone authorization does not follow the enabled implementation"
tr '\n\r\t' ' ' <<<"$bar_entry_shell" |
grep -Eq 'shell\.pluginCloneMaySummon\( *currentManifest\( *\), *requestedId *\)' ||
fail "built-in clones in replacement bars cannot summon their existing auxiliary UI"
qml_matches "$shell_qml" 'shell\.pluginCloneMaySummon\( *currentManifest\( *\), *requestedId *\)' ||
fail "built-in clones cannot summon their existing auxiliary UI"
qml_matches "$shell_qml" '"omarchy\.media": *\["omarchy\.osd"\]' ||
fail "media clones cannot summon their existing OSD target"
qml_matches "$shell_qml" '"omarchy\.network": *\["omarchy\.speedtest", *"omarchy\.wifiqr"\]' ||
fail "network clones cannot summon their existing auxiliary panels"
pass "built-in service and widget clones retain narrow configuration and UI integration"
qml_matches "$shell_qml" 'shell\.serviceFor\( *shell\.pluginRegistry\.resolveEnabledId\( *id *\) *\)' ||
fail "narrow first-party service proxies do not resolve enabled clones"
qml_matches "$shell_qml" 'return serviceFor\( *shell\.pluginRegistry\.resolveEnabledId\( *pluginId *\) *\)' ||
fail "trusted first-party service lookups do not resolve enabled clones"
qml_matches "$shell_qml" 'allowOwnService *&& *shell\.pluginOwnsTarget\( *key, *requestedId *\)[^}]*return shell\.pluginServiceFor\( *key, *requestedId *\)' ||
fail "cloned widgets cannot use a source id to reach their own service"
pass "service facades resolve enabled clones without widening replacement-bar access"
require_compositor "plugin authentication boundary runtime test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping plugin authentication boundary runtime test"
exit 0
fi
require_command jq
TMPDIR=$(mktemp -d)
result="$TMPDIR/result.json"
log="$TMPDIR/quickshell.log"
config_dir="$TMPDIR/plugin-auth-boundary"
mkdir -p "$config_dir" "$TMPDIR/home"
cp "$SHELL_TEST_DIR/fixtures/plugin-auth-boundary/"*.qml "$config_dir/"
ln -s "$ROOT/shell/services" "$config_dir/services"
OMARCHY_QML_TEST_RESULT="$result" \
HOME="$TMPDIR/home" \
XDG_CONFIG_HOME="$TMPDIR/home/.config" \
XDG_CACHE_HOME="$TMPDIR/home/.cache" \
XDG_STATE_HOME="$TMPDIR/home/.local/state" \
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..80}; do
[[ -s $result ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary fixture exited before writing result"
fi
sleep 0.1
done
[[ -s $result ]] || {
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary runtime test timed out"
}
if ! jq -e '.ok == true' "$result" >/dev/null; then
jq . "$result" >&2
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary runtime behavior"
fi
pass "plugin authentication boundary runtime behavior"
+13
View File
@@ -197,5 +197,18 @@ for (const [id, section] of Object.entries({
}
check(byId['omarchy.media']?.barWidget?.defaultSection === undefined, 'omarchy.media must use the center fallback')
for (const id of ['omarchy.lock', 'omarchy.idle', 'omarchy.polkit', 'omarchy.notifications', 'omarchy.media']) {
check(byId[id]?.keepLoaded === true, `${id} must stay loaded across plugin reloads`)
}
const shellSource = fs.readFileSync(path.join(root, 'shell/shell.qml'), 'utf8')
const unloadMatch = shellSource.match(/function unloadPluginServices\(\) \{[\s\S]*?\n \}/)
check(!!unloadMatch, 'unloadPluginServices is defined')
check(!!unloadMatch && /serviceKeepLoaded/.test(unloadMatch[0]), 'unloadPluginServices honors keepLoaded')
check(
/function _syncServices\(\) \{[\s\S]*Drop services for plugins that have been disabled/.test(shellSource),
'_syncServices still drops disabled or removed services'
)
assert(errors.length === 0, 'plugin manifests match shell registry contract', errors.join('\n'))
JS
+44 -1
View File
@@ -36,7 +36,10 @@ SH
chmod +x "$mock_bin"/*
export PATH="$mock_bin:$PATH"
# $ROOT/bin after the mocks: Remove Preinstalls asks omarchy-install-hermes-cli
# whether the wrapper is Omarchy's rather than matching the marker itself, and
# that is the real command at runtime. The mocks still shadow what they name.
export PATH="$mock_bin:$ROOT/bin:$PATH"
export HOME="$test_home"
export OMARCHY_TEST_PKG_LOG="$pkg_log"
@@ -89,3 +92,43 @@ pass "declining Remove Preinstalls changes nothing"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -f $marker ]] || fail "Remove Preinstalls records the opt-out"
pass "Remove Preinstalls records the opt-out"
# Hermes' wrapper is only a preinstall when omarchy-install-hermes-cli wrote it.
# The desktop app's command and an official install live at the same path and
# are the user's, whether or not any package says so.
hermes="$test_home/.local/bin/hermes"
mkdir -p "$(dirname "$hermes")"
printf '%s\n' "#!/bin/bash" "# Written by omarchy-install-hermes-cli." >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ ! -e $hermes ]] || fail "Remove Preinstalls deletes the Omarchy Hermes wrapper"
pass "Remove Preinstalls deletes the Omarchy Hermes wrapper"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -x $hermes ]] || fail "Remove Preinstalls keeps the desktop app's Hermes command"
pass "Remove Preinstalls keeps the desktop app's Hermes command"
official_body="#!/bin/bash
unset PYTHONPATH
unset PYTHONHOME
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$official_body" >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -x $hermes && $(cat "$hermes") == "$official_body" ]] || fail "Remove Preinstalls keeps an official Hermes install"
pass "Remove Preinstalls keeps an official Hermes install"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -x $hermes ]] || fail "Remove Preinstalls keeps a wrapper that merely mentions the installer"
pass "Remove Preinstalls keeps a wrapper that merely mentions the installer"
rm -f "$hermes"
ln -s "$test_home/nowhere/hermes" "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -L $hermes ]] || fail "Remove Preinstalls keeps a foreign hermes link"
pass "Remove Preinstalls keeps a foreign hermes link"
+10 -2
View File
@@ -8,7 +8,7 @@ test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin" "$test_tmp/home"
mkdir -p "$mock_bin" "$test_tmp/home" "$test_tmp/home/.hermes/profiles/james"
for command in xdg-user-dirs-update xdg-settings xdg-mime; do
printf '#!/bin/bash\nexit 0\n' >"$mock_bin/$command"
@@ -30,6 +30,14 @@ for skill in omarchy diagnose-crash; do
link="$test_tmp/home/.gemini/config/skills/$skill"
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
fail "omarchy-provision-user provisions the $skill skill for Antigravity"
link="$test_tmp/home/.hermes/skills/$skill"
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
fail "omarchy-provision-user provisions the $skill skill for Hermes"
link="$test_tmp/home/.hermes/profiles/james/skills/$skill"
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
fail "omarchy-provision-user provisions the $skill skill for a Hermes profile"
done
pass "omarchy-provision-user provisions Antigravity skills"
pass "omarchy-provision-user provisions Antigravity and Hermes skills"
+256
View File
@@ -15,6 +15,22 @@ printf 'drop:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/omarchy-pkg-drop"
# omarchy-remove-ai-perplexity asks through gum whether the user's data goes
# too. The stub answers "no" unless a test says otherwise and logs the call: a
# real gum would hang the run, and one that answered "yes" on its own would be
# the data loss the default-no exists to prevent. It logs to its own file
# because the OpenClaw section below greps TEST_LOG to prove gum was never
# reached, and the pty runs here call it on purpose.
cat >"$tmp_dir/bin/gum" <<'SCRIPT'
#!/bin/bash
printf 'gum:%s\n' "$*" >>"$TEST_GUM_LOG"
exit "${TEST_GUM_STATUS:-1}"
SCRIPT
chmod +x "$tmp_dir/bin/gum"
export TEST_GUM_LOG="$tmp_dir/gum-log"
touch "$TEST_GUM_LOG"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
@@ -75,6 +91,88 @@ for kept in .grok .claude.json .npm .local/share/opencode; do
done
pass "T3 Code removal keeps the agent state it bootstrapped"
# Perplexity's other products (the CLI, Comet) share the perplexity-* prefix;
# the desktop app owns only its rpc-server runtime and Electron caches. The
# logins, vault, and flags are the user's and only go when a terminal said so.
seed_perplexity() {
fresh_home
mkdir -p "$HOME/.config/Perplexity" "$HOME/.cache/Perplexity" \
"$HOME/.cache/perplexity-rpc-server" "$HOME/.local/share/perplexity-rpc-server" \
"$HOME/.local/state/perplexity" "$HOME/.cache/perplexity-personal-computer-poc"
touch "$HOME/.config/perplexity-flags.conf"
}
# </dev/null pins stdin off a terminal, so this run exercises the
# non-interactive path no matter where the suite itself is running.
seed_perplexity
"$ROOT/bin/omarchy-remove-ai-perplexity" </dev/null >/dev/null
for gone in .cache/Perplexity .cache/perplexity-rpc-server .local/share/perplexity-rpc-server; do
[[ ! -e $HOME/$gone ]] || fail "Perplexity removal deletes the app's runtime and caches" "$gone"
done
pass "Perplexity removal deletes the app's runtime and caches"
for kept in .config/Perplexity .local/state/perplexity .config/perplexity-flags.conf .cache/perplexity-personal-computer-poc; do
[[ -e $HOME/$kept ]] || fail "Perplexity removal keeps the user's data and other products' caches" "$kept"
done
pass "Perplexity removal keeps the user's data and other products' caches"
grep -qx 'drop:perplexity' "$TEST_LOG" || fail "Perplexity removal drops the perplexity package"
pass "Perplexity removal drops the perplexity package"
# Without a terminal there is nobody to ask, so gum must not even be reached:
# one that answered "yes" on its own would be a data loss.
! grep -q '^gum:' "$TEST_GUM_LOG" || fail "Perplexity removal keeps the user's data unasked when there is no terminal"
pass "Perplexity removal keeps the user's data unasked when there is no terminal"
# script(1) puts the remover on a pty, the only way -t 0 answers true in a
# test; the stubbed gum then supplies the answer.
seed_perplexity
script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity'" /dev/null >/dev/null 2>&1
[[ -d $HOME/.config/Perplexity && -d $HOME/.local/state/perplexity && -f $HOME/.config/perplexity-flags.conf ]] ||
fail "Perplexity removal keeps the user's data when the answer is no"
pass "Perplexity removal keeps the user's data when the answer is no"
# The stub answers "no" on its own, so only the logged arguments prove the
# real gum would not default to yes on a bare Enter.
grep -q '^gum:confirm --default=false ' "$TEST_GUM_LOG" ||
fail "Perplexity removal asks with the destructive answer defaulted off"
pass "Perplexity removal asks with the destructive answer defaulted off"
# A partial install has no user data to measure; du failing must not abort
# the removal between the package and the prompt.
seed_perplexity
rm -rf "$HOME/.config/Perplexity" "$HOME/.local/state/perplexity"
script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity'" /dev/null >/dev/null 2>&1 ||
fail "Perplexity removal survives user-data directories that are already gone"
pass "Perplexity removal survives user-data directories that are already gone"
# gum draws its prompt on stderr; with stderr redirected the question would be
# invisible, so the remover must keep the data instead of blocking on it.
seed_perplexity
gum_calls_before=$(wc -l <"$TEST_GUM_LOG")
script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity' 2>/dev/null" /dev/null >/dev/null 2>&1 ||
fail "Perplexity removal completes when stderr is not a terminal"
[[ -d $HOME/.config/Perplexity ]] && (( $(wc -l <"$TEST_GUM_LOG") == gum_calls_before )) ||
fail "Perplexity removal keeps the user's data unasked when stderr is not a terminal"
pass "Perplexity removal keeps the user's data unasked when stderr is not a terminal"
seed_perplexity
TEST_GUM_STATUS=0 script -qec "'$ROOT/bin/omarchy-remove-ai-perplexity'" /dev/null >/dev/null 2>&1
for gone in .config/Perplexity .local/state/perplexity .config/perplexity-flags.conf; do
[[ ! -e $HOME/$gone ]] || fail "Perplexity removal deletes the user's data on an explicit yes" "$gone"
done
pass "Perplexity removal deletes the user's data on an explicit yes"
# Without HOME the rm -rf paths would degrade to /-rooted ones; -u makes that a
# refusal instead.
if env -u HOME "$ROOT/bin/omarchy-remove-ai-perplexity" </dev/null >/dev/null 2>&1; then
fail "Perplexity removal refuses to run without HOME"
fi
pass "Perplexity removal refuses to run without HOME"
# ~/.grok belongs to the Grok CLI that omarchy-default-agent installs.
fresh_home
mkdir -p "$HOME/.config/Grok Bot" "$HOME/.grokbot" "$HOME/.grok"
@@ -93,3 +191,161 @@ ollama_row=$(grep '^ "remove.ai.ollama":' "$ROOT/default/omarchy/omarchy-menu.j
[[ $ollama_row == *'"when":"omarchy-pkg-present ollama"'* ]] ||
fail "Ollama removal is offered only where the package is installed" "$ollama_row"
pass "Ollama removal is offered only where the package is installed"
# OpenClaw's gateway unit and web app launcher are the app's own; the agent
# state in ~/.openclaw is the user's. systemctl and openclaw are stubbed so the
# sandbox never reaches the real user manager or a real gateway.
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
# Default openclaw stub: the packaged CLI predates `gateway uninstall`, so the
# remover has to fall back to its manual systemd path.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
fresh_openclaw_home() {
fresh_home
mkdir -p "$HOME/.config/systemd/user/default.target.wants" "$HOME/.openclaw" \
"$HOME/.local/share/applications" "$HOME/.local/share/icons/hicolor/256x256/apps"
touch "$HOME/.config/systemd/user/openclaw-gateway.service" \
"$HOME/.config/systemd/user/openclaw-gateway.service.bak" \
"$HOME/.config/systemd/user/openclaw-node.service" \
"$HOME/.openclaw/openclaw.json" \
"$HOME/.local/share/applications/OpenClaw.desktop" \
"$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
ln -s ../openclaw-gateway.service \
"$HOME/.config/systemd/user/default.target.wants/openclaw-gateway.service"
ln -s ../openclaw-node.service \
"$HOME/.config/systemd/user/default.target.wants/openclaw-node.service"
}
# gum would ask about ~/.openclaw; the tests never run on a terminal, so it
# must not even be reached. Logging it proves that.
cat >"$tmp_dir/bin/gum" <<'SCRIPT'
#!/bin/bash
printf 'gum:%s\n' "$*" >>"$TEST_LOG"
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/gum"
fresh_openclaw_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
for gone in .config/systemd/user/openclaw-gateway.service \
.config/systemd/user/openclaw-gateway.service.bak \
.config/systemd/user/default.target.wants/openclaw-gateway.service \
.config/systemd/user/openclaw-node.service \
.config/systemd/user/default.target.wants/openclaw-node.service \
.local/share/applications/OpenClaw.desktop \
.local/share/icons/hicolor/256x256/apps/openclaw.png; do
[[ ! -e $HOME/$gone && ! -L $HOME/$gone ]] || fail "OpenClaw removal deletes the service and launcher it installed" "$gone"
done
pass "OpenClaw removal deletes the service and launcher it installed"
grep -q '^systemctl:--user disable --now openclaw-gateway.service$' "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service"
grep -q '^systemctl:--user disable --now openclaw-node.service$' "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service" "node host service left running"
for unit in openclaw-gateway.service openclaw-node.service; do
grep -q "^systemctl:--user reset-failed $unit\$" "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service" "$unit left in systemd's failed list"
done
pass "OpenClaw removal stops the gateway service"
# Without a terminal there is nobody to ask, so the state stays and gum is
# never invoked (a gum that answered "yes" on its own would be a data loss).
[[ -f $HOME/.openclaw/openclaw.json ]] || fail "OpenClaw removal keeps the user's agent state"
! grep -q '^gum:' "$TEST_LOG" ||
fail "OpenClaw removal keeps the user's agent state" "asked about ~/.openclaw without a terminal"
pass "OpenClaw removal keeps the user's agent state"
# A CLI that knows `gateway uninstall` owns the teardown; the manual systemd
# fallback must not run.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
rm -f "$HOME/.config/systemd/user/openclaw-$1.service" \
"$HOME/.config/systemd/user/default.target.wants/openclaw-$1.service"
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
: >"$TEST_LOG"
fresh_openclaw_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
grep -q '^openclaw:gateway uninstall$' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown"
grep -q '^openclaw:node uninstall$' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown" "node host not handed to upstream"
! grep -q '^systemctl:--user disable' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown" "manual disable ran too"
pass "OpenClaw removal prefers upstream's own gateway teardown"
# Without the unit file there is nothing of ours registered, so systemd stays untouched.
systemctl_calls_before=$(grep -c '^systemctl:' "$TEST_LOG" || true)
fresh_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
systemctl_calls_after=$(grep -c '^systemctl:' "$TEST_LOG" || true)
[[ $systemctl_calls_before == "$systemctl_calls_after" ]] ||
fail "OpenClaw removal leaves systemd alone when onboarding never ran"
pass "OpenClaw removal leaves systemd alone when onboarding never ran"
# A gateway that will not stop aborts the removal before the package drop:
# pacman would otherwise strand the live process on deleted code.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
[[ $2 == disable ]] && exit 1
[[ $2 == is-active ]] && exit 0
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
drop_calls_before=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
fresh_openclaw_home
rc=0
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
drop_calls_after=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when the gateway cannot be stopped"
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "unit file deleted"
[[ $drop_calls_before == "$drop_calls_after" ]] ||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "package dropped anyway"
pass "OpenClaw removal aborts when the gateway cannot be stopped"
# An unreachable user manager is not a stopped gateway: every probe failing
# with no answer must still abort, not read as "confirmed inactive".
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
echo "Failed to connect to user scope bus" >&2
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
: >"$TEST_LOG"
fresh_openclaw_home
rc=0
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when systemd cannot be reached"
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
fail "OpenClaw removal aborts when systemd cannot be reached" "unit file deleted"
! grep -q '^drop:openclaw$' "$TEST_LOG" ||
fail "OpenClaw removal aborts when systemd cannot be reached" "package dropped anyway"
pass "OpenClaw removal aborts when systemd cannot be reached"
+385
View File
@@ -74,6 +74,238 @@ 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
# A replacement bar must not receive a generic factory for another plugin's
# live service, and its barConfig must be a detached snapshot on both initial
# injection and later host-config updates.
victim_service_id="acme.victim-service"
victim_service_dir="$test_home/.config/omarchy/plugins/$victim_service_id"
mkdir -p "$victim_service_dir"
cat >"$victim_service_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$victim_service_id",
"name": "Victim Service",
"version": "1.0.0",
"kinds": ["service"],
"entryPoints": {"service": "Service.qml"}
}
JSON
cat >"$victim_service_dir/Service.qml" <<'QML'
import QtQuick
Item {
property string privateValue: "victim-secret"
}
QML
# A clone of the built-in media service exercises both supported service paths:
# its own widget receives the raw companion service under the trusted bar, while
# a replacement bar receives only the narrow media proxy resolved to the clone.
media_clone_id="acme.media-clone"
media_clone_dir="$test_home/.config/omarchy/plugins/$media_clone_id"
mkdir -p "$media_clone_dir"
cat >"$media_clone_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$media_clone_id",
"name": "Media Clone",
"version": "1.0.0",
"kinds": ["service", "bar-widget"],
"entryPoints": {"service": "Service.qml", "barWidget": "BarWidget.qml"},
"barWidget": {"defaultSection": "center"},
"omarchy": {"clonedFrom": "omarchy.media"}
}
JSON
cat >"$media_clone_dir/Service.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property string marker: "clone-service"
property bool enabled: true
property var activePlayer: null
property var sourcePlayers: []
property var shell: null
function runAction(action, showFeedback, targetKey) {}
function playerKey(player) { return "" }
function selectPlayer(playerKey) {}
IpcHandler {
target: "acme-media-clone-service"
function ping(): string { return marker }
function summonOsd(): string {
return root.shell && root.shell.summon("omarchy.osd", "{}") ? "true" : "false"
}
}
}
QML
cat >"$media_clone_dir/BarWidget.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var bar: null
IpcHandler {
target: "acme-media-clone-widget"
function probeOwnService(): string {
var service = root.bar && root.bar.shell
? root.bar.shell.firstPartyServiceFor("omarchy.media") : null
return JSON.stringify({
reachable: !!service,
marker: service ? String(service.marker || "") : ""
})
}
}
}
QML
review_bar_id="acme.review-bar"
review_bar_dir="$test_home/.config/omarchy/plugins/$review_bar_id"
mkdir -p "$review_bar_dir"
cat >"$review_bar_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$review_bar_id",
"name": "Review Bar",
"version": "1.0.0",
"kinds": ["bar", "service"],
"keepLoaded": true,
"entryPoints": {"bar": "Bar.qml", "service": "Service.qml"}
}
JSON
cat >"$review_bar_dir/Bar.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var shell: null
property var barConfig: ({})
IpcHandler {
target: "acme-review-bar"
function probeVictim(): string {
var genericFactory = root.shell
&& typeof root.shell.pluginShellForId === "function"
var entryFacade = root.shell
&& typeof root.shell.pluginShellForBarEntry === "function"
? root.shell.pluginShellForBarEntry("probe", "acme.victim-service") : null
var victim = entryFacade && typeof entryFacade.serviceFor === "function"
? entryFacade.serviceFor("acme.victim-service") : null
return JSON.stringify({
genericFactory: !!genericFactory,
entryFacade: !!entryFacade,
victimServiceReachable: !!victim
})
}
function snapshot(): string {
return JSON.stringify(root.barConfig || {})
}
function probeMediaProxy(): string {
var service = root.shell
? root.shell.firstPartyServiceFor("omarchy.media") : null
return JSON.stringify({ reachable: !!service, enabled: service ? service.enabled === true : false })
}
function probeMediaWidgetSummon(): string {
var entryFacade = root.shell
&& typeof root.shell.pluginShellForBarEntry === "function"
? root.shell.pluginShellForBarEntry("probe-media", "acme.media-clone") : null
return JSON.stringify({
entryFacade: !!entryFacade,
osdSummoned: entryFacade ? entryFacade.summon("omarchy.osd", "{}") : false,
foreignSummoned: entryFacade ? entryFacade.summon("omarchy.lock", "{}") : false
})
}
function mutateSnapshot(): string {
if (root.barConfig && root.barConfig.layout
&& root.barConfig.layout.left && root.barConfig.layout.left.length > 0)
root.barConfig.layout.left[0].id = "tampered.by.review-bar"
return snapshot()
}
}
}
QML
cat >"$review_bar_dir/Service.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var shell: null
property var retainedShell: null
onShellChanged: if (!retainedShell && shell) retainedShell = shell
function mutationAllowed(candidate) {
if (!candidate) return false
try {
return typeof candidate.mutateShellConfig === "function"
&& candidate.mutateShellConfig(function(config) {}) === true
} catch (e) {
return false
}
}
IpcHandler {
target: "acme-review-capability"
function probe(): string {
return JSON.stringify({
currentAllowed: root.mutationAllowed(root.shell),
retainedAllowed: root.mutationAllowed(root.retainedShell)
})
}
}
}
QML
cat >"$stub_bin/omarchy-update-available" <<'SH'
#!/bin/bash
echo "Omarchy update available (test)"
@@ -188,6 +420,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 +456,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
@@ -362,3 +628,122 @@ jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.keyboard-layout")' \
<<<"$(shell_ipc shell listShellConfig)" >/dev/null ||
fail_with_log "bar put added a second copy of a widget already on the bar"
pass "bar put leaves a widget already on the bar alone"
# Run the replacement-bar probes last: switching bar loaders can transiently
# leave bar-aware panels without a visual host, which should not add noise to
# the default-bar assertions above.
[[ $(shell_ipc shell setPluginEnabled "$media_clone_id" true) == "ok" ]] ||
fail_with_log "media clone fixture could not be enabled"
clone_widget_probe=""
for _ in {1..80}; do
clone_widget_probe=$(shell_ipc acme-media-clone-widget probeOwnService 2>/dev/null || true)
if jq -e '.reachable == true and .marker == "clone-service"' \
<<<"$clone_widget_probe" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
jq -e '.reachable == true and .marker == "clone-service"' \
<<<"$clone_widget_probe" >/dev/null || {
printf 'Clone own-service probe: %s\n' "$clone_widget_probe" >&2
fail_with_log "a cloned widget resolves its source id to its own companion service"
}
pass "trusted bar gives a cloned widget its own companion service"
[[ $(shell_ipc acme-media-clone-service summonOsd) == "true" ]] ||
fail_with_log "a cloned media service cannot summon its existing OSD target"
pass "a cloned built-in service retains its auxiliary UI integration"
[[ $(shell_ipc shell setPluginEnabled "$victim_service_id" true) == "ok" ]] ||
fail_with_log "victim service fixture could not be enabled"
[[ $(shell_ipc shell enablePlugin "$review_bar_id" '{}') == "ok" ]] ||
fail_with_log "replacement-bar fixture could not be enabled"
review_probe=""
for _ in {1..80}; do
review_probe=$(shell_ipc acme-review-bar probeVictim 2>/dev/null || true)
if jq -e '.genericFactory == false and .entryFacade == false and .victimServiceReachable == false' \
<<<"$review_probe" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while loading the replacement-bar fixture"
fi
sleep 0.1
done
jq -e '.genericFactory == false and .entryFacade == false and .victimServiceReachable == false' \
<<<"$review_probe" >/dev/null || {
printf 'Replacement-bar service probe: %s\n' "$review_probe" >&2
fail_with_log "replacement bar cannot recover another plugin's live service"
}
media_proxy_probe=$(shell_ipc acme-review-bar probeMediaProxy)
jq -e '.reachable == true and .enabled == true' <<<"$media_proxy_probe" >/dev/null || {
printf 'Replacement-bar media proxy probe: %s\n' "$media_proxy_probe" >&2
fail_with_log "replacement-bar service proxies resolve enabled clones"
}
media_summon_probe=$(shell_ipc acme-review-bar probeMediaWidgetSummon)
jq -e '.entryFacade == true and .osdSummoned == true and .foreignSummoned == false' \
<<<"$media_summon_probe" >/dev/null || {
printf 'Replacement-bar media summon probe: %s\n' "$media_summon_probe" >&2
fail_with_log "replacement-bar clone facades retain only their auxiliary UI integration"
}
bar_config_before=$(shell_ipc shell listShellConfig | jq -c '.bar')
shell_ipc acme-review-bar mutateSnapshot >/dev/null
bar_config_after=$(shell_ipc shell listShellConfig | jq -c '.bar')
[[ $bar_config_after == "$bar_config_before" ]] ||
fail_with_log "replacement bar mutated the initially injected host configuration"
[[ $(shell_ipc shell setBarWidget omarchy.clock format '"HH:mm:ss"' '{}') == "ok" ]] ||
fail_with_log "host bar configuration could not be updated for snapshot testing"
updated_snapshot=""
for _ in {1..80}; do
updated_snapshot=$(shell_ipc acme-review-bar snapshot 2>/dev/null || true)
if jq -e 'any(.layout.center[]; (.id // .) == "omarchy.clock" and .format == "HH:mm:ss")' \
<<<"$updated_snapshot" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
jq -e 'any(.layout.center[]; (.id // .) == "omarchy.clock" and .format == "HH:mm:ss")' \
<<<"$updated_snapshot" >/dev/null ||
fail_with_log "replacement bar did not receive the refreshed configuration snapshot"
bar_config_before=$(shell_ipc shell listShellConfig | jq -c '.bar')
shell_ipc acme-review-bar mutateSnapshot >/dev/null
bar_config_after=$(shell_ipc shell listShellConfig | jq -c '.bar')
[[ $bar_config_after == "$bar_config_before" ]] ||
fail_with_log "replacement bar mutated a refreshed host configuration"
pass "replacement-bar service and configuration boundaries hold at runtime"
capability_before=$(shell_ipc acme-review-capability probe)
jq -e '.currentAllowed == true and .retainedAllowed == true' \
<<<"$capability_before" >/dev/null ||
fail_with_log "bar service fixture did not initially receive bar capabilities"
# Keep the same enabled plugin ID and service instance while dropping the bar
# kind. Both the currently injected facade and a reference retained by the
# plugin must lose the old configuration capability after the manifest rescan.
jq '.kinds = ["service"] | .entryPoints = {"service": "Service.qml"}' \
"$review_bar_dir/manifest.json" >"$review_bar_dir/manifest.json.tmp"
mv "$review_bar_dir/manifest.json.tmp" "$review_bar_dir/manifest.json"
capability_after=""
for _ in {1..80}; do
capability_after=$(shell_ipc acme-review-capability probe 2>/dev/null || true)
if jq -e '.currentAllowed == false and .retainedAllowed == false' \
<<<"$capability_after" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while revoking changed manifest capabilities"
fi
sleep 0.1
done
jq -e '.currentAllowed == false and .retainedAllowed == false' \
<<<"$capability_after" >/dev/null || {
printf 'Capability revocation probe: %s\n' "$capability_after" >&2
fail_with_log "cached plugin facades revoke capabilities removed from the manifest"
}
pass "manifest reload revokes cached facade capabilities"
@@ -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"
@@ -0,0 +1,705 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1788662350.sh"
test_tmp=$(mktemp -d -p /tmp)
trap 'rm -rf "$test_tmp"' EXIT
mock_omarchy="$test_tmp/omarchy"
sleep_dir="$test_tmp/system-sleep"
systemd_dir="$test_tmp/systemd"
drop_in="$systemd_dir/supergfxd.service.d/delay-start.conf"
quarantine="$test_tmp/quarantine"
reload_needed_marker="$test_tmp/reload-needed"
migration_copy="$test_tmp/migration.sh"
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls"
mkdir -p "$mock_omarchy/default/systemd/system-sleep" \
"$mock_omarchy/default/systemd/system/supergfxd.service.d" \
"$sleep_dir" "${drop_in%/*}" "$stub_bin"
cp "$ROOT/default/systemd/system-sleep/keyboard-backlight" \
"$mock_omarchy/default/systemd/system-sleep/keyboard-backlight"
cp "$ROOT/default/systemd/system-sleep/force-igpu" \
"$mock_omarchy/default/systemd/system-sleep/force-igpu"
cp "$ROOT/default/systemd/system/supergfxd.service.d/delay-start.conf" \
"$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf"
[[ $(grep -Fxc 'system_sleep_dir=/usr/lib/systemd/system-sleep' "$migration") == 1 ]] ||
fail "migration fixes one literal system-sleep directory"
[[ $(grep -Fxc 'supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf' "$migration") == 1 ]] ||
fail "migration fixes one literal supergfxd drop-in"
sed \
-e "s|system_sleep_dir=/usr/lib/systemd/system-sleep|system_sleep_dir=$sleep_dir|" \
-e "s|supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf|supergfxd_drop_in=$drop_in|" \
-e "s|quarantine_root=/var/lib/omarchy/migrations/1788662350-system-sleep|quarantine_root=$quarantine|" \
-e "s|/var/lib/omarchy/migrations/1788662350-systemd-reload-needed|$reload_needed_marker|" \
-e "s|/usr/bin/stat|$stub_bin/stat|g" \
-e "s|/usr/bin/readlink|$stub_bin/readlink|g" \
"$migration" >"$migration_copy"
cat >"$stub_bin/stat" <<'SH'
#!/bin/bash
path=${!#}
if [[ :${INACCESSIBLE_AS_USER:-}: == *":$path:"* && ${FAKE_SUDO:-0} == 0 ]]; then
exit 13
fi
actual_file_mode=$(/usr/bin/stat -c '%f' -- "$path") || exit 1
actual_mode=$(/usr/bin/stat -c '%a' -- "$path") || exit 1
if [[ :${FAKE_ROOT_DIRS:-}: == *":$path:"* ]]; then
uid=0
gid=0
mode=$(printf '%o' "$((8#$actual_mode & ~8#022))")
elif [[ :${FAKE_ROOT_FILES:-}: == *":$path:"* ]]; then
uid=0
gid=${FAKE_ROOT_GID:-0}
mode=${FAKE_ROOT_MODE:-$actual_mode}
else
exec /usr/bin/stat "$@"
fi
file_type=$((16#$actual_file_mode & 16#f000))
file_mode=$(printf '%x' "$((file_type | 8#$mode))")
case "$*" in
*"%f %u %g %a"*) printf '%s %s %s %s\n' "$file_mode" "$uid" "$gid" "$mode" ;;
*"%u %g %a"*) printf '%s %s %s\n' "$uid" "$gid" "$mode" ;;
*"%a"*) printf '%s\n' "$mode" ;;
*) exec /usr/bin/stat "$@" ;;
esac
SH
cat >"$stub_bin/readlink" <<'SH'
#!/bin/bash
path=${!#}
if [[ :${INACCESSIBLE_AS_USER:-}: == *":$path:"* && ${FAKE_SUDO:-0} == 0 ]]; then
exit 13
fi
exec /usr/bin/readlink "$@"
SH
chmod +x "$stub_bin/stat" "$stub_bin/readlink"
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
set -euo pipefail
printf 'sudo' >>"$CALLS"
printf '\t%s' "$@" >>"$CALLS"
printf '\n' >>"$CALLS"
case "$1" in
*/stat | */readlink)
FAKE_SUDO=1 exec "$@"
;;
/usr/bin/test)
shift
if [[ $1 == "-x" && :${FAKE_ROOT_DIRS:-}: == *":$2:"* ]]; then
exit 0
else
exec /usr/bin/test "$@"
fi
;;
/usr/bin/mktemp | /usr/bin/mv | /usr/bin/chmod | /usr/bin/cp | /usr/bin/rm)
exec "$@"
;;
/usr/bin/systemctl)
if [[ -n ${SYSTEMCTL_FAIL_ONCE_FILE:-} && -e $SYSTEMCTL_FAIL_ONCE_FILE ]]; then
/usr/bin/rm -f -- "$SYSTEMCTL_FAIL_ONCE_FILE"
exit 1
fi
exit 0
;;
/usr/bin/install)
shift
args=()
while (($#)); do
case "$1" in
-o | -g)
shift 2
;;
*)
args+=("$1")
shift
;;
esac
done
exec /usr/bin/install "${args[@]}"
;;
*)
printf 'unexpected sudo command: %s\n' "$*" >&2
exit 97
;;
esac
SH
chmod +x "$stub_bin/sudo"
run_migration() {
local fake_root_dirs
: >"$calls"
fake_root_dirs="/:/tmp:$test_tmp:$sleep_dir:$systemd_dir:${drop_in%/*}"
[[ -z ${EXTRA_FAKE_ROOT_DIRS:-} ]] || fake_root_dirs+=":$EXTRA_FAKE_ROOT_DIRS"
CALLS="$calls" \
FAKE_ROOT_DIRS="$fake_root_dirs" \
FAKE_ROOT_FILES="${FAKE_ROOT_FILES:-${2:-}}" \
FAKE_ROOT_MODE="${FAKE_ROOT_MODE:-${3:-}}" \
FAKE_ROOT_GID="${FAKE_ROOT_GID:-0}" \
INACCESSIBLE_AS_USER="${INACCESSIBLE_AS_USER:-}" \
SYSTEMCTL_FAIL_ONCE_FILE="${SYSTEMCTL_FAIL_ONCE_FILE:-}" \
OMARCHY_PATH="$mock_omarchy" \
PATH="$stub_bin:$PATH" bash -euo pipefail "$migration_copy" >/dev/null
}
printf 'attacker keyboard\n' >"$sleep_dir/keyboard-backlight"
printf 'attacker gpu\n' >"$sleep_dir/force-igpu"
printf 'attacker drop-in\n' >"$drop_in"
chmod 0777 "$sleep_dir/keyboard-backlight" "$sleep_dir/force-igpu"
chmod 0666 "$drop_in"
exec 9>>"$sleep_dir/keyboard-backlight"
run_migration Integrated
printf 'write through stale attacker descriptor\n' >&9
exec 9>&-
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" ||
fail "migration replaces the user-owned keyboard hook with trusted content"
cmp -s "$mock_omarchy/default/systemd/system-sleep/force-igpu" "$sleep_dir/force-igpu" ||
fail "migration replaces the user-owned GPU hook with trusted content"
cmp -s "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" "$drop_in" ||
fail "migration replaces the user-owned root service drop-in with trusted content"
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration activates the repaired keyboard hook"
[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] ||
fail "migration activates force-igpu only in Integrated mode"
[[ $(stat -c '%a' "$drop_in") == 644 ]] ||
fail "migration installs the service drop-in as configuration"
grep -Fx $'sudo\t/usr/bin/systemctl\tdaemon-reload' "$calls" >/dev/null ||
fail "migration reloads systemd after repairing its root service drop-in"
[[ ! -e $reload_needed_marker ]] ||
fail "migration leaves a reload marker after systemd accepted the repaired drop-in"
[[ $(stat -c '%a' "$quarantine") == 700 ]] ||
fail "migration keeps preserved unsafe custom content in a root-only directory"
keyboard_backup=$(find "$quarantine" -path '*/keyboard-backlight.*/original' -type f -print -quit)
force_backup=$(find "$quarantine" -path '*/force-igpu.*/original' -type f -print -quit)
drop_in_backup=$(find "$quarantine" -path '*/delay-start.conf.*/original' -type f -print -quit)
grep -Fxq 'attacker keyboard' "$keyboard_backup" ||
fail "migration preserves unknown keyboard-hook content before replacing it"
grep -Fxq 'attacker gpu' "$force_backup" ||
fail "migration preserves unknown force-iGPU content before replacing it"
grep -Fxq 'attacker drop-in' "$drop_in_backup" ||
fail "migration preserves unknown service-drop-in content before replacing it"
pass "migration replaces writable privileged files with trusted root-owned copies"
backup_count=$(find "$quarantine" -mindepth 2 -maxdepth 2 -name original | wc -l)
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ ! -s $calls ]] ||
fail "migration changes already-repaired privileged files on a second run" "$(<"$calls")"
[[ $(find "$quarantine" -mindepth 2 -maxdepth 2 -name original | wc -l) == "$backup_count" ]] ||
fail "migration creates duplicate quarantines on a second run"
pass "migration is idempotent after repairing unsafe privileged files"
printf 'attacker drop-in\n' >"$drop_in"
chmod 0666 "$drop_in"
reload_failure="$test_tmp/fail-systemd-reload-once"
touch "$reload_failure"
set +e
SYSTEMCTL_FAIL_ONCE_FILE="$reload_failure" \
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu" \
run_migration Integrated
reload_status=$?
set -e
(( reload_status != 0 )) ||
fail "migration reports success after systemd rejects the repaired drop-in"
cmp -s "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" "$drop_in" ||
fail "migration does not repair the drop-in before the simulated reload failure"
[[ -e $reload_needed_marker && $(stat -c '%a' "$reload_needed_marker") == 644 ]] ||
fail "migration does not persist the reload requirement before replacing the drop-in"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
grep -Fx $'sudo\t/usr/bin/systemctl\tdaemon-reload' "$calls" >/dev/null ||
fail "migration does not retry a failed reload after the drop-in is already safe"
[[ ! -e $reload_needed_marker ]] ||
fail "migration does not clear the reload requirement after a successful retry"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ ! -s $calls ]] ||
fail "migration repeats a successfully completed reload repair" "$(<"$calls")"
pass "migration persists and retries systemd reload after failure or interruption"
keyboard_backup_count=$(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l)
cp "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/force-igpu:$drop_in" run_migration Integrated
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration does not safely activate a user-owned canonical hook"
[[ $(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) == "$keyboard_backup_count" ]] ||
fail "migration quarantines an exact legacy artifact as administrator content"
pass "migration replaces exact vulnerable installer artifacts without inventing backups"
legacy_keyboard="$test_tmp/legacy-keyboard-backlight"
cat >"$legacy_keyboard" <<'SH'
#!/bin/bash
# Turn off keyboard backlight before hibernate to prevent hang on power-off.
# The ASUS keyboard controller can block S4 shutdown if LEDs are active.
if [[ $1 == "pre" && $2 == "hibernate" ]]; then
device=""
for candidate in /sys/class/leds/*kbd_backlight*; do
if [[ -e "$candidate" ]]; then
device="$(basename "$candidate")"
break
fi
done
if [[ -n "$device" ]]; then
brightnessctl -d "$device" set 0 >/dev/null 2>&1
fi
fi
SH
[[ $(sha256sum "$legacy_keyboard" | cut -d' ' -f1) == f313a81e47401f0d38b8602e5997f52c5286d5e97f74027564ddd515b3d16511 ]] ||
fail "keyboard-backlight legacy fixture no longer matches the migration fingerprint"
keyboard_backup_count=$(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l)
cp "$legacy_keyboard" "$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" ||
fail "migration does not upgrade the released keyboard-backlight hook"
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration leaves the released keyboard-backlight hook non-executable"
[[ $(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) == "$keyboard_backup_count" ]] ||
fail "migration quarantines the released keyboard hook as administrator content"
pass "migration activates the released root-owned keyboard-backlight hook"
cp "$legacy_keyboard" "$sleep_dir/keyboard-backlight"
chmod 0755 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" ||
fail "migration mistakes executable released hook bytes for a current artifact"
pass "migration refreshes recognized legacy hook contents at the final mode"
printf 'attacker gpu\n' >"$sleep_dir/force-igpu"
chmod 0777 "$sleep_dir/force-igpu"
run_migration Hybrid
[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] ||
fail "migration does not activate the trusted self-guarding force-igpu hook"
pass "migration repairs force-igpu without depending on a live GPU-mode query"
printf 'administrator customization\n' >"$sleep_dir/keyboard-backlight"
chmod 0755 "$sleep_dir/keyboard-backlight"
run_migration Integrated "$sleep_dir/keyboard-backlight" 755
grep -Fxq 'administrator customization' "$sleep_dir/keyboard-backlight" ||
fail "migration preserves a secure administrator-owned custom hook"
cp "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
run_migration Integrated "$sleep_dir/keyboard-backlight" 644
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration leaves an exact packaged keyboard hook non-executable"
printf 'administrator customization\n' >"$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
run_migration Integrated "$sleep_dir/keyboard-backlight" 644
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 644 ]] ||
fail "migration changes the mode of a safe noncanonical administrator hook"
grep -Fxq 'administrator customization' "$sleep_dir/keyboard-backlight" ||
fail "migration replaces a safe noncanonical administrator hook"
pass "migration activates only exact packaged hooks while preserving safe custom files"
legacy_force_igpu="$test_tmp/legacy-force-igpu"
cat >"$legacy_force_igpu" <<'SH'
#!/bin/bash
# Use the Vfio to Integrated trick to turn off NVIDIA dgpu when in integrated mode
# without needing to restart the computer. This is needed because computers like the Asus G14
# will wake after suspend in Hybrid mode, even if the system was in Integrated mode before
# suspending.
case "$1" in
pre)
# Before hibernating, switch to Vfio so the nvidia driver is detached from the dGPU.
# Without this, hibernate resume fails because the nvidia driver can't freeze a
# powered-off dGPU (returns -EIO), which aborts the entire resume.
if [[ $2 == "hibernate" ]]; then
/usr/bin/supergfxctl -m Vfio
sleep 1
fi
;;
post)
# small delay so the device is fully re-enumerated
sleep 4
# force-bind dGPU to vfio (fully detached from nvidia)
/usr/bin/supergfxctl -m Vfio
sleep 1
# then go back to Integrated, which powers it off again
/usr/bin/supergfxctl -m Integrated
;;
esac
SH
[[ $(sha256sum "$legacy_force_igpu" | cut -d' ' -f1) == d604e7c4903829563e45fc52188fc5602c3f1bc66e247f0a2cc0a974ed6e57db ]] ||
fail "force-igpu legacy fixture no longer matches the migration fingerprint"
cp "$legacy_force_igpu" "$sleep_dir/force-igpu"
chmod 0644 "$sleep_dir/force-igpu"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
cmp -s "$mock_omarchy/default/systemd/system-sleep/force-igpu" "$sleep_dir/force-igpu" ||
fail "migration does not upgrade the exact legacy force-igpu hook"
[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] ||
fail "migration leaves the exact legacy force-igpu hook non-executable"
pass "migration activates the exact legacy force-igpu artifact with its new guard"
printf 'wheel-managed customization\n' >"$sleep_dir/keyboard-backlight"
chmod 0755 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
FAKE_ROOT_GID=10 run_migration Integrated
grep -Fxq 'wheel-managed customization' "$sleep_dir/keyboard-backlight" ||
fail "migration replaces a safe root:wheel administrator hook"
[[ ! -s $calls ]] ||
fail "migration escalates while preserving safe root:wheel entries"
pass "migration treats non-writable root-owned files as safe regardless of group"
admin_dir="$test_tmp/admin-hooks"
admin_keyboard="$admin_dir/keyboard"
admin_delay="$admin_dir/delay.conf"
mkdir -p "$admin_dir"
printf 'protected keyboard customization\n' >"$admin_keyboard"
printf 'protected delay customization\n' >"$admin_delay"
chmod 0755 "$admin_keyboard"
chmod 0644 "$admin_delay"
rm -f "$sleep_dir/keyboard-backlight" "$drop_in"
ln -s "$admin_keyboard" "$sleep_dir/keyboard-backlight"
ln -s "$admin_delay" "$drop_in"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_keyboard:$admin_delay:$sleep_dir/force-igpu" \
FAKE_ROOT_GID=10 run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$admin_keyboard" ]] ||
fail "migration replaces a safe administrator-managed keyboard-hook symlink"
[[ -L $drop_in && $(readlink "$drop_in") == "$admin_delay" ]] ||
fail "migration replaces a safe administrator-managed service-drop-in symlink"
[[ ! -s $calls ]] ||
fail "migration escalates while preserving safe administrator symlinks"
pass "migration preserves symlinks whose full target paths are root-controlled"
dangling_target="$admin_dir/future-keyboard"
rm -f "$sleep_dir/keyboard-backlight" "$dangling_target"
ln -s "$dangling_target" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$dangling_target" ]] ||
fail "migration replaces a safe dangling administrator symlink"
[[ ! -s $calls ]] ||
fail "migration asks for sudo to verify an absent target below a searchable root-controlled directory"
pass "migration handles safe dangling administrator symlinks without sudo"
escaping_user_dir="$test_tmp/escaping-user-hooks"
escaping_user_hook="$escaping_user_dir/keyboard"
escaping_missing_dir="$admin_dir/future"
escaping_target="$escaping_missing_dir/../../escaping-user-hooks/keyboard"
mkdir -p "$escaping_user_dir"
printf 'future unsafe keyboard customization\n' >"$escaping_user_hook"
chmod 0755 "$escaping_user_hook"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$escaping_target" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ ! -L $sleep_dir/keyboard-backlight ]] ||
fail "migration trusts a dangling symlink whose unresolved suffix escapes to a user-controlled path"
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight" ||
fail "migration does not replace a future user-controlled dangling symlink"
pass "migration resolves the full dangling-symlink suffix before trusting it"
protected_dir="$test_tmp/root-only-hooks"
protected_target="$protected_dir/target"
protected_bridge="$protected_dir/bridge"
mkdir -p "$protected_dir"
printf 'root-only administrator customization\n' >"$protected_target"
ln -s "$protected_target" "$protected_bridge"
chmod 0700 "$protected_dir"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$protected_bridge" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir:$protected_dir" \
FAKE_ROOT_FILES="$protected_target:$admin_delay:$sleep_dir/force-igpu:$drop_in" \
INACCESSIBLE_AS_USER="$protected_bridge:$protected_target" \
run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$protected_bridge" ]] ||
fail "migration replaces a safe symlink whose target is hidden by a root-only directory"
grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-hooks/bridge$' "$calls" ||
fail "migration does not inspect inaccessible symlink metadata with privilege"
grep -q $'^sudo\t.*/readlink\t--\t.*/root-only-hooks/bridge$' "$calls" ||
fail "migration does not resolve an inaccessible administrator symlink with privilege"
grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-hooks/target$' "$calls" ||
fail "migration does not inspect an inaccessible administrator target with privilege"
pass "migration preserves root-controlled symlink chains hidden from the invoking user"
protected_dangling_dir="$test_tmp/root-only-dangling"
protected_dangling_target="$protected_dangling_dir/future-keyboard"
mkdir -p "$protected_dangling_dir"
chmod 0000 "$protected_dangling_dir"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$protected_dangling_target" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir:$protected_dangling_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \
INACCESSIBLE_AS_USER="$protected_dangling_target" \
run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$protected_dangling_target" ]] ||
fail "migration replaces a safe dangling symlink below a root-only directory"
grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-dangling/future-keyboard$' "$calls" ||
fail "migration does not inspect a protected dangling target with privilege"
grep -q $'^sudo\t/usr/bin/test\t-x\t.*/root-only-dangling$' "$calls" ||
fail "migration does not distinguish a protected missing target from an inaccessible parent"
pass "migration preserves dangling administrator symlinks below root-only directories"
user_dir="$test_tmp/user-hooks"
user_keyboard="$user_dir/keyboard"
mkdir -p "$user_dir"
printf 'unsafe symlink customization\n' >"$user_keyboard"
chmod 0755 "$user_keyboard"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$user_keyboard" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu" run_migration Integrated
[[ ! -L $sleep_dir/keyboard-backlight ]] ||
fail "migration leaves a user-controlled keyboard-hook symlink active"
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight" ||
fail "migration does not replace an unsafe symlink with trusted hook content"
symlink_backup=$(find "$quarantine" -path '*/keyboard-backlight.*/original' -type l -print -quit)
[[ -n $symlink_backup && $(readlink "$symlink_backup") == "$user_keyboard" ]] ||
fail "migration discards an unsafe custom symlink instead of preserving it"
pass "migration quarantines unsafe symlinks outside the active systemd directory"
bridge="$user_dir/bridge"
ln -s "$admin_keyboard" "$bridge"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$bridge" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_keyboard:$admin_delay:$sleep_dir/force-igpu" \
run_migration Integrated
[[ ! -L $sleep_dir/keyboard-backlight ]] ||
fail "migration trusts a symlink chain routed through a user-controlled directory"
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight" ||
fail "migration does not repair an indirectly user-controlled symlink"
pass "migration checks every intermediate component in a symlink chain"
hook_copy="$test_tmp/force-igpu-hook"
hook_calls="$test_tmp/force-igpu-calls"
hook_queries="$test_tmp/force-igpu-queries"
hook_config="$test_tmp/supergfxd.conf"
hook_marker="$test_tmp/force-igpu-restore"
hook_pending="$test_tmp/force-igpu-pending"
sed \
-e "s|/usr/bin/supergfxctl|$stub_bin/hook-supergfxctl|g" \
-e "s|/usr/bin/install|$stub_bin/hook-install|g" \
-e "s|/etc/supergfxd.conf|$hook_config|g" \
-e "s|/run/omarchy-force-igpu-integrated|$hook_marker|g" \
"$ROOT/default/systemd/system-sleep/force-igpu" >"$hook_copy"
cat >"$stub_bin/hook-supergfxctl" <<'SH'
#!/bin/bash
case "$1" in
-m)
printf '%s\n' "$*" >>"$HOOK_CALLS"
if [[ ${HOOK_BLOCK_MODE:-} == "$2" ]]; then
trap '' TERM
/usr/bin/sleep 30
fi
current=$(sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$HOOK_CONFIG")
if [[ $current != "$2" ]]; then
printf '%s %s\n' "$2" "${HOOK_CONFIRM_AFTER:-1}" >"$HOOK_PENDING"
fi
;;
-g)
printf '%s\n' "$*" >>"$HOOK_QUERIES"
if [[ -f $HOOK_PENDING ]]; then
read -r pending remaining <"$HOOK_PENDING"
if [[ ${HOOK_FAIL_MODE:-} != "$pending" ]]; then
remaining=$((remaining - 1))
if (( remaining <= 0 )); then
sed -i "s/\"mode\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"mode\": \"$pending\"/" "$HOOK_CONFIG"
rm -f -- "$HOOK_PENDING"
else
printf '%s %s\n' "$pending" "$remaining" >"$HOOK_PENDING"
fi
fi
fi
sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$HOOK_CONFIG"
;;
esac
SH
cat >"$stub_bin/hook-install" <<'SH'
#!/bin/bash
args=()
while (($#)); do
case "$1" in
-o | -g)
shift 2
;;
*)
args+=("$1")
shift
;;
esac
done
exec /usr/bin/install "${args[@]}"
SH
cat >"$stub_bin/sleep" <<'SH'
#!/bin/bash
:
SH
chmod +x "$stub_bin/hook-supergfxctl" "$stub_bin/hook-install" "$stub_bin/sleep"
hook_env=(
"HOOK_CALLS=$hook_calls"
"HOOK_QUERIES=$hook_queries"
"HOOK_CONFIG=$hook_config"
"HOOK_PENDING=$hook_pending"
"PATH=$stub_bin:$PATH"
)
printf '{ "mode": "Hybrid" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ ! -e $hook_calls ]] || fail "force-igpu runs while the root-owned config says Hybrid"
[[ ! -e $hook_marker ]] || fail "force-igpu records restore intent while configured for Hybrid mode"
rm -f "$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ ! -e $hook_calls ]] || fail "force-igpu runs when its mode config is unavailable"
[[ ! -e $hook_marker ]] || fail "force-igpu records restore intent without a mode config"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
[[ -f $hook_marker && $(stat -c '%a' "$hook_marker") == 600 ]] ||
fail "force-igpu does not securely record Integrated restore intent during pre-suspend"
HOOK_CONFIRM_AFTER=2 env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ $(wc -l <"$hook_calls") == 2 ]] ||
fail "force-igpu does not run both GPU transitions in Integrated mode"
grep -Fqx -- '-m Integrated' "$hook_calls" ||
fail "force-igpu does not restore Integrated mode after suspend"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after suspend"
(( $(wc -l <"$hook_queries") >= 4 )) ||
fail "force-igpu does not wait for asynchronous GPU transitions"
pass "force-igpu confirms asynchronous transitions for Integrated sleep cycles"
: >"$hook_calls"
: >"$hook_queries"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
set +e
HOOK_CONFIRM_AFTER=2 HOOK_FAIL_MODE=Integrated env "${hook_env[@]}" \
bash "$hook_copy" post suspend >/dev/null 2>&1
restore_status=$?
set -e
(( restore_status != 0 )) || fail "force-igpu reports success without confirming Integrated mode"
grep -Fq '"mode": "Vfio"' "$hook_config" ||
fail "force-igpu failure test does not leave the transition in Vfio mode"
[[ -f $hook_marker ]] || fail "force-igpu discards restore intent after an asynchronous transition failure"
HOOK_CONFIRM_AFTER=2 env "${hook_env[@]}" bash "$hook_copy" post suspend
grep -Fq '"mode": "Integrated"' "$hook_config" ||
fail "force-igpu does not recover the Integrated transition on the next sleep cycle"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves restore intent after a confirmed retry"
pass "force-igpu retains restore intent until Integrated mode is confirmed"
: >"$hook_calls"
: >"$hook_queries"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
set +e
HOOK_BLOCK_MODE=Vfio env "${hook_env[@]}" \
bash "$hook_copy" post suspend >/dev/null 2>&1
blocked_request_status=$?
set -e
(( blocked_request_status != 0 )) || fail "force-igpu waits forever for a blocked GPU transition request"
[[ -f $hook_marker ]] || fail "force-igpu discards restore intent after a blocked transition request"
[[ ! -s $hook_queries ]] || fail "force-igpu polls before a blocked transition request returns"
env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ ! -e $hook_marker ]] || fail "force-igpu cannot retry after a blocked transition request"
pass "force-igpu bounds blocked transition requests and retains retry intent"
: >"$hook_calls"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre hibernate
grep -Fq '"mode": "Vfio"' "$hook_config" ||
fail "force-igpu test double does not model the pre-hibernate Vfio persistence"
[[ -f $hook_marker ]] || fail "force-igpu loses restore intent during the Vfio transition"
env "${hook_env[@]}" bash "$hook_copy" post hibernate
[[ $(wc -l <"$hook_calls") == 3 ]] ||
fail "force-igpu skips the post-hibernate transitions after Vfio changes the config"
[[ $(tail -1 "$hook_calls") == "-m Integrated" ]] ||
fail "force-igpu does not finish post-hibernate restoration in Integrated mode"
grep -Fq '"mode": "Integrated"' "$hook_config" ||
fail "force-igpu leaves supergfxd configured for Vfio after hibernation"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after hibernation"
pass "force-igpu restores Integrated mode after pre-hibernate persists Vfio"
: >"$hook_calls"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
SYSTEMD_SLEEP_ACTION=suspend env "${hook_env[@]}" bash "$hook_copy" pre suspend-then-hibernate
SYSTEMD_SLEEP_ACTION=suspend env "${hook_env[@]}" bash "$hook_copy" post suspend-then-hibernate
[[ $(wc -l <"$hook_calls") == 2 ]] ||
fail "force-igpu does not complete the initial suspend phase of suspend-then-hibernate"
SYSTEMD_SLEEP_ACTION=hibernate env "${hook_env[@]}" bash "$hook_copy" pre suspend-then-hibernate
[[ $(wc -l <"$hook_calls") == 3 && $(tail -1 "$hook_calls") == "-m Vfio" ]] ||
fail "force-igpu skips the Vfio transition before compound hibernation"
grep -Fq '"mode": "Vfio"' "$hook_config" ||
fail "force-igpu does not detach the dGPU during the hibernate phase"
[[ -f $hook_marker ]] || fail "force-igpu loses restore intent during compound hibernation"
SYSTEMD_SLEEP_ACTION=hibernate env "${hook_env[@]}" bash "$hook_copy" post suspend-then-hibernate
[[ $(wc -l <"$hook_calls") == 5 && $(tail -1 "$hook_calls") == "-m Integrated" ]] ||
fail "force-igpu does not restore Integrated mode after compound hibernation"
grep -Fq '"mode": "Integrated"' "$hook_config" ||
fail "force-igpu leaves supergfxd configured for Vfio after compound hibernation"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after compound hibernation"
pass "force-igpu handles both phases of suspend-then-hibernate"
keyboard_hook_copy="$test_tmp/keyboard-backlight-hook"
keyboard_calls="$test_tmp/keyboard-backlight-calls"
keyboard_led_dir="$test_tmp/leds"
mkdir -p "$keyboard_led_dir/asus::kbd_backlight"
sed "s|/sys/class/leds/\*kbd_backlight\*|$keyboard_led_dir/*kbd_backlight*|" \
"$ROOT/default/systemd/system-sleep/keyboard-backlight" >"$keyboard_hook_copy"
cat >"$stub_bin/brightnessctl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$KEYBOARD_CALLS"
SH
chmod +x "$stub_bin/brightnessctl"
SYSTEMD_SLEEP_ACTION=suspend KEYBOARD_CALLS="$keyboard_calls" PATH="$stub_bin:$PATH" \
bash "$keyboard_hook_copy" pre suspend-then-hibernate
[[ ! -e $keyboard_calls ]] || fail "keyboard-backlight runs during the suspend phase of compound sleep"
SYSTEMD_SLEEP_ACTION=hibernate KEYBOARD_CALLS="$keyboard_calls" PATH="$stub_bin:$PATH" \
bash "$keyboard_hook_copy" pre suspend-then-hibernate
grep -Fqx -- '-d asus::kbd_backlight set 0' "$keyboard_calls" ||
fail "keyboard-backlight skips the hibernate phase of compound sleep"
pass "keyboard-backlight handles the hibernate phase of suspend-then-hibernate"
+1 -15
View File
@@ -71,14 +71,6 @@ grep -Fx 'WantedBy=graphical-session.target' "$fcitx_service" >/dev/null ||
grep -Fx 'ConditionEnvironment=WAYLAND_DISPLAY' "$fcitx_service" >/dev/null ||
fail "an update over SSH has a live user manager and no display; starting fcitx5 there wedges the unit active-but-blind, and Wants= will not replace it at graphical login"
fcitx_migration="$ROOT/migrations/1785167800.sh"
grep -F 'is-active --quiet graphical-session.target' "$fcitx_migration" >/dev/null ||
fail "migration kills fcitx5 and starts the unit outside a graphical session"
grep -F 'systemctl --user enable omarchy-fcitx5.service' "$fcitx_migration" >/dev/null ||
fail "migration must enable without --now; --now starts the unit before the session-gate check"
grep -F 'Could not start omarchy-fcitx5.service' "$fcitx_migration" >/dev/null ||
fail "migration pkills a working fcitx5, so a failed handover must be reported instead of marked complete"
grep -F 'pkill -x fcitx5' "$ROOT/bin/omarchy-restart-xcompose" >/dev/null ||
fail "restart-xcompose cannot reload a fcitx5 running outside the unit, so it silently keeps serving the old table"
@@ -111,10 +103,4 @@ pass "systemd-oomd acts on sustained memory stall"
grep -Fx 'systemctl enable systemd-oomd.service' "$ROOT/install/config/enable-services.sh" >/dev/null ||
fail "new installs ship the oomd drop-ins with the daemon that reads them disabled"
oomd_migration=$(grep -rl 'systemd-oomd.service' "$ROOT/migrations" | head -n 1 || true)
[[ -n $oomd_migration ]] ||
fail "existing installs never enable systemd-oomd; enable-services.sh only runs at install time"
grep -F 'systemctl --user daemon-reload' "$oomd_migration" >/dev/null ||
fail "migration leaves the user manager unaware of app.slice candidacy until the next login"
pass "existing installs enable systemd-oomd and report app.slice without a relogin"
pass "new installs enable systemd-oomd"
+81
View File
@@ -0,0 +1,81 @@
#!/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
test_home="$test_tmp/home"
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin"
# Run the real theme refresh, renderer and publisher in a throwaway home.
# Only package installation, the T3 CLI and desktop launch are stubbed.
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
[[ $1 == "t3code-bin" ]]
SH
cat >"$mock_bin/t3" <<'SH'
#!/bin/bash
[[ $1 == "theme" && $2 == "set" && $3 == "omarchy" && $4 == "--base-dir" && $5 == "$T3CODE_HOME" ]] || exit 1
palette="$T3CODE_HOME/userdata/themes/omarchy.json"
if [[ ! -f $palette ]] || ! jq -e '.name == "Omarchy" and (.accent | test("^#[0-9a-fA-F]{6}$"))' "$palette" >/dev/null; then
echo "No published Omarchy theme"
exit 1
fi
if [[ ${OMARCHY_TEST_T3_FAIL:-0} == "1" ]]; then
echo "T3 theme selection failed"
exit 1
fi
echo "T3 selected Omarchy"
SH
cat >"$mock_bin/setsid" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin"/*
run_installer() {
HOME="$test_home" T3CODE_HOME="$test_tmp/custom t3 home" \
OMARCHY_PATH="$ROOT" PATH="$mock_bin:$ROOT/bin:$PATH" \
OMARCHY_THEME_HEADLESS=1 XDG_RUNTIME_DIR="$test_tmp" \
"$ROOT/bin/omarchy-install-ai-t3-code" >"$test_tmp/output" 2>&1
}
state="$test_home/.local/state/omarchy/current"
palette="$test_tmp/custom t3 home/userdata/themes/omarchy.json"
settings="$test_tmp/custom t3 home/userdata/settings.json"
mkdir -p "$state/theme"
echo 'tokyo-night' >"$state/theme.name"
cp "$ROOT/themes/tokyo-night/colors.toml" "$state/theme/colors.toml"
run_installer || fail "installing after an update succeeds" "$(cat "$test_tmp/output")"
[[ -f $state/theme/t3code.json && -f $palette ]] || fail "the missing palette is rendered and published"
cmp -s "$state/theme/t3code.json" "$palette" || fail "T3 receives the current theme's palette"
grep -q 'T3 selected Omarchy' "$test_tmp/output" || fail "the published palette is selected"
grep -q 'Opening T3 Code' "$test_tmp/output" || fail "the themed app opens"
[[ ! -e $test_home/.t3 ]] || fail "the custom T3 home is respected"
pass "an update with no staged T3 palette renders, publishes and selects it before launch"
# Preserve an already rendered palette, including a user-supplied color.
jq '.accent = "#abcdef"' "$state/theme/t3code.json" >"$test_tmp/palette.json"
cp "$test_tmp/palette.json" "$state/theme/t3code.json"
run_installer || fail "installing with an existing palette succeeds" "$(cat "$test_tmp/output")"
cmp -s "$test_tmp/palette.json" "$palette" || fail "an existing palette is published without re-staging"
pass "an existing palette is used without refreshing the current theme"
printf '{"defaultTheme":"dark","keep":"existing settings"}\n' >"$settings"
cp "$settings" "$test_tmp/settings-before.json"
if OMARCHY_TEST_T3_FAIL=1 run_installer; then
fail "a failed T3 theme selection fails the installer"
fi
grep -q 'T3 theme selection failed' "$test_tmp/output" || fail "the CLI failure is visible"
if grep -Eq 'Opening T3 Code|T3 Code has been installed' "$test_tmp/output"; then
fail "a failed selection does not report successful setup"
fi
cmp -s "$test_tmp/settings-before.json" "$settings" || fail "a failed selection does not rewrite settings"
pass "a CLI failure remains visible and leaves the existing settings intact"
@@ -1,59 +0,0 @@
#!/bin/bash
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
migration=$(grep -rl 'Make Shift+Enter distinguishable for terminals and Codex' "$ROOT/migrations" | head -n 1 || true)
[[ -n $migration ]] || fail "terminal Shift+Enter migration exists"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
test_home="$TMPDIR/home"
mkdir -p \
"$test_home/.config/alacritty" \
"$test_home/.config/foot" \
"$test_home/.config/ghostty" \
"$test_home/.config/kitty"
cp "$ROOT/config/alacritty/alacritty.toml" "$test_home/.config/alacritty/alacritty.toml"
sed -i 's/\\u001B\[13;2u/\\u001B\\r/' "$test_home/.config/alacritty/alacritty.toml"
cp "$ROOT/config/kitty/kitty.conf" "$test_home/.config/kitty/kitty.conf"
sed -i '/shift+enter/d' "$test_home/.config/kitty/kitty.conf"
cp "$ROOT/config/ghostty/config" "$test_home/.config/ghostty/config"
sed -i '/shift+enter/d' "$test_home/.config/ghostty/config"
cp "$ROOT/config/foot/foot.ini" "$test_home/.config/foot/foot.ini"
sed -i '/text-bindings/,$d' "$test_home/.config/foot/foot.ini"
HOME="$test_home" bash "$migration" >/dev/null
grep -qxF '{ key = "Return", mods = "Shift", chars = "\u001B[13;2u" },' "$test_home/.config/alacritty/alacritty.toml" ||
fail "migration updates Alacritty Shift+Return to CSI-u"
pass "migration updates Alacritty Shift+Return to CSI-u"
grep -qxF 'map shift+enter send_text all \e[13;2u' "$test_home/.config/kitty/kitty.conf" ||
fail "migration adds Kitty Shift+Enter CSI-u binding"
grep -qxF 'map alt+shift+enter send_text all \e[13;4u' "$test_home/.config/kitty/kitty.conf" ||
fail "migration adds Kitty Alt+Shift+Enter CSI-u binding"
pass "migration adds Kitty CSI-u bindings"
grep -qxF 'keybind = shift+enter=csi:13;2u' "$test_home/.config/ghostty/config" ||
fail "migration adds Ghostty Shift+Enter CSI-u binding"
grep -qxF 'keybind = alt+shift+enter=csi:13;4u' "$test_home/.config/ghostty/config" ||
fail "migration adds Ghostty Alt+Shift+Enter CSI-u binding"
pass "migration adds Ghostty CSI-u bindings"
grep -qxF '\x1b[13;2u=Shift+Return' "$test_home/.config/foot/foot.ini" ||
fail "migration adds Foot Shift+Return CSI-u binding"
grep -qxF '\x1b[13;4u=Mod1+Shift+Return' "$test_home/.config/foot/foot.ini" ||
fail "migration adds Foot Alt+Shift+Return CSI-u binding"
pass "migration adds Foot CSI-u bindings"
before=$(find "$test_home/.config" -type f -print0 | sort -z | xargs -0 sha256sum)
HOME="$test_home" bash "$migration" >/dev/null
after=$(find "$test_home/.config" -type f -print0 | sort -z | xargs -0 sha256sum)
[[ $before == "$after" ]] || fail "terminal Shift+Enter migration is idempotent"
pass "terminal Shift+Enter migration is idempotent"
+1 -1
View File
@@ -207,7 +207,7 @@ pass "a theme name cannot climb out of the theme directories"
# generates. Every generated theme file is either denied to an installed theme or
# recorded here as carrying colour, so a new template fails until it is placed.
denied=(alacritty.toml foot.ini ghostty.conf kitty.conf gum_env.lua hyprland.lua neovim.lua vscode.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml t3code.json vscode-theme.json)
for tpl in "$ROOT"/default/themed/*.tpl; do
generated=$(basename "$tpl" .tpl)
+33
View File
@@ -51,3 +51,36 @@ pass "bar on is idempotent"
HOME="$test_home" omarchy-toggle-bar off
[[ ! -f $bar_flag ]] || fail "bar off disables bar-off toggle"
pass "bar off disables bar-off toggle"
# The gaps half of full screen copies a flag file in and reloads Hyprland, so
# give it this checkout to copy from and a hyprctl that answers without a
# compositor.
export OMARCHY_PATH="$ROOT"
stub_bin="$TMPDIR/bin"
mkdir -p "$stub_bin"
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/hyprctl"
chmod +x "$stub_bin/hyprctl"
export PATH="$stub_bin:$PATH"
gaps_flag="$test_home/.local/state/omarchy/toggles/hypr/window-no-gaps.lua"
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle hides the bar and the gaps together"
pass "fullscreen toggle hides the bar and the gaps together"
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen toggle restores the bar and the gaps together"
pass "fullscreen toggle restores the bar and the gaps together"
HOME="$test_home" omarchy-toggle-bar on
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle pulls a half-hidden desktop into full screen"
pass "fullscreen toggle pulls a half-hidden desktop into full screen"
HOME="$test_home" omarchy-toggle-fullscreen-desktop off
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen off leaves full screen"
pass "fullscreen off leaves full screen"
HOME="$test_home" omarchy-toggle-fullscreen-desktop on
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen on enters full screen"
pass "fullscreen on enters full screen"
+44
View File
@@ -132,3 +132,47 @@ if problems:
PYTHON
pass "no Omarchy script writes a path under /usr that no package owns"
for script in bin/omarchy-hibernation-setup bin/omarchy-toggle-hybrid-gpu; do
grep -F '"${destination%/*}/.${destination##*/}.omarchy.XXXXXX"' "$ROOT/$script" >/dev/null ||
fail "$script reserves a hidden sibling for the privileged replacement"
grep -F 'sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage"' "$ROOT/$script" >/dev/null ||
fail "$script prepares privileged files with final root ownership and mode"
grep -F 'sudo /usr/bin/mv -Tf -- "$stage" "$destination"' "$ROOT/$script" >/dev/null ||
fail "$script atomically replaces the privileged destination"
if grep -F 'sudo /usr/bin/chmod "$mode" "$destination"' "$ROOT/$script" >/dev/null; then
fail "$script changes mode after publishing the privileged destination"
fi
done
grep -F ' /usr/lib/systemd/system-sleep/keyboard-backlight 0755' "$ROOT/bin/omarchy-hibernation-setup" >/dev/null ||
fail "hibernation setup installs keyboard-backlight as a root-owned executable"
hook_install_line=$(rg -n '^if ! install_root_file .*keyboard-backlight' "$ROOT/bin/omarchy-hibernation-setup" | cut -d: -f1)
resume_marker_line=$(rg -n '^echo "HOOKS\+=\(resume\)"' "$ROOT/bin/omarchy-hibernation-setup" | cut -d: -f1)
[[ -n $hook_install_line && -n $resume_marker_line ]] ||
fail "hibernation setup keeps recognizable hook-install and resume-marker steps"
(( hook_install_line < resume_marker_line )) ||
fail "hibernation setup marks completion before a failed hook install can be retried"
grep -F ' /usr/lib/systemd/system-sleep/force-igpu 0755' "$ROOT/bin/omarchy-toggle-hybrid-gpu" >/dev/null ||
fail "hybrid GPU setup installs force-igpu as a root-owned executable"
grep -F ' /etc/systemd/system/supergfxd.service.d/delay-start.conf 0644' "$ROOT/bin/omarchy-toggle-hybrid-gpu" >/dev/null ||
fail "hybrid GPU setup installs its root service drop-in as root-owned configuration"
delay_install_line=$(rg -n '^ if ! install_root_file .*delay-start\.conf' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | cut -d: -f1)
force_install_line=$(rg -n '^ if ! install_root_file .*force-igpu' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | cut -d: -f1)
config_switch_line=$(rg -n '^ sudo sed -i \\' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | tail -1 | cut -d: -f1)
[[ -n $delay_install_line && -n $force_install_line && -n $config_switch_line ]] ||
fail "hybrid GPU setup keeps recognizable support-file and config-switch steps"
(( delay_install_line < config_switch_line && force_install_line < config_switch_line )) ||
fail "hybrid GPU setup switches config before every required file is installed"
grep -Fq '/usr/bin/grep -Eq' "$ROOT/default/systemd/system-sleep/force-igpu" ||
fail "force-igpu does not guard execution with the configured GPU mode"
if rg -n 'cp -p.*(system-sleep|supergfxd\.service\.d)' "$ROOT/bin/omarchy-hibernation-setup" "$ROOT/bin/omarchy-toggle-hybrid-gpu"; then
fail "privileged sleep and hybrid GPU files are never copied with source ownership"
fi
pass "system-sleep hooks and the hybrid GPU drop-in enforce root ownership"
+42 -4
View File
@@ -6,6 +6,10 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
upgrade_to_quattro="$ROOT/bin/omarchy-upgrade-to-quattro"
function_body() {
awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro"
}
snapshot_line=$(grep -n '^create_pre_upgrade_snapshot$' "$upgrade_to_quattro" | cut -d: -f1)
pacman_line=$(grep -n '^configure_pacman_channel$' "$upgrade_to_quattro" | cut -d: -f1)
[[ -n $snapshot_line && -n $pacman_line ]] || fail "upgrade snapshot and first mutation calls exist"
@@ -73,6 +77,44 @@ grep -F 'OMARCHY_INSTALL_USER="$target_user"' "$upgrade_to_quattro" >/dev/null
grep -F '"$apply_lock"' "$upgrade_to_quattro" >/dev/null
pass "Omarchy 4 upgrade configures lock screen authentication for the target user"
root_path_count=$(awk '/^root_path=/{ count++ } END { print count + 0 }' "$upgrade_to_quattro")
(( root_path_count == 1 )) || fail "Omarchy 4 upgrade defines exactly one root command path"
grep -Fx 'root_path=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade limits root command lookup to trusted system directories"
if grep -E '^root_path=.*(target_home|\.local/bin)' "$upgrade_to_quattro" >/dev/null; then
fail "Omarchy 4 upgrade does not put the target user's bin directory on the root command path"
fi
grep -Fx 'package_path="$root_path:$target_home/.local/bin"' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade retains the target user's bin directory for user commands"
lock_authentication_body=$(function_body configure_lock_authentication)
lock_path_assignment_count=$(awk '{ count += gsub(/(^|[[:space:]])PATH=/, "") } END { print count + 0 }' <<<"$lock_authentication_body")
(( lock_path_assignment_count == 1 )) ||
fail "Omarchy 4 upgrade gives the privileged lock helper exactly one command path"
grep -Fx ' PATH="$root_path" \' <<<"$lock_authentication_body" >/dev/null ||
fail "Omarchy 4 upgrade gives the privileged lock helper the trusted root path"
if grep -E '(package_path|target_home|\.local/bin)' <<<"$lock_authentication_body" >/dev/null; then
fail "Omarchy 4 upgrade does not give the privileged lock helper the target user's path"
fi
firewall_body=$(function_body apply_firewall_defaults)
firewall_path_assignment_count=$(awk '{ count += gsub(/(^|[[:space:]])PATH=/, "") } END { print count + 0 }' <<<"$firewall_body")
(( firewall_path_assignment_count == 1 )) ||
fail "Omarchy 4 upgrade gives the privileged firewall helper exactly one command path"
grep -Fx ' as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$root_path" \' <<<"$firewall_body" >/dev/null ||
fail "Omarchy 4 upgrade gives the privileged firewall helper the trusted root path"
if grep -E '(package_path|target_home|\.local/bin)' <<<"$firewall_body" >/dev/null; then
fail "Omarchy 4 upgrade does not give the privileged firewall helper the target user's path"
fi
user_omarchy_body=$(function_body run_as_user_omarchy)
grep -F 'PATH="$package_path"' <<<"$user_omarchy_body" >/dev/null ||
fail "Omarchy 4 upgrade retains the package and user path for target-user commands"
if grep -F 'PATH="$root_path"' <<<"$user_omarchy_body" >/dev/null; then
fail "Omarchy 4 upgrade does not narrow target-user commands to the root-only path"
fi
pass "Omarchy 4 upgrade separates privileged and target-user command paths"
grep -F 'install/helpers/browser-policy.sh' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade uses the shared browser-policy helper"
grep -F 'as_root test -f "$browser_policy_helper"' "$upgrade_to_quattro" >/dev/null ||
@@ -100,10 +142,6 @@ pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager"
# Booting with both managers enabled leaves them fighting over the Wi-Fi
# adapter, so enabling NetworkManager and disabling iwd cannot be separated by
# any step that might abort in between.
function_body() {
awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro"
}
migrations_body=$(function_body run_post_upgrade_migrations)
grep -F 'fail "Omarchy migrations did not complete.' <<<"$migrations_body" >/dev/null ||
fail "Omarchy 4 upgrade fails when a migration cannot complete"
+366
View File
@@ -0,0 +1,366 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8')
const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8')
const videoQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundVideo.qml'), 'utf8')
const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8')
const lockQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const themeSwitcher = fs.readFileSync(path.join(root, 'bin/omarchy-theme-switcher'), 'utf8')
const quattroUpgrade = fs.readFileSync(path.join(root, 'bin/omarchy-upgrade-to-quattro'), 'utf8')
const multimediaMigration = fs.readFileSync(path.join(root, 'migrations/1786609204.sh'), 'utf8')
const barTextColor = fs.readFileSync(path.join(root, 'bin/omarchy-bar-text-color'), 'utf8')
const menuImages = fs.readFileSync(path.join(root, 'bin/omarchy-menu-images'), 'utf8')
const lockView = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const lockService = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8')
const batteryService = fs.readFileSync(path.join(root, 'shell/plugins/services/battery/Service.qml'), 'utf8')
const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8')
const directImageList = fs.readFileSync(path.join(root, 'shell/plugins/image-picker/list.sh'), 'utf8')
assert(
/function isVideoPath\(path\)[\s\S]*\.test\(String\(path \|\| ""\)\)/.test(utilQml) &&
!utilQml.includes('split(/[?#]/)'),
'shared media helper identifies video paths without truncating valid local names'
)
assert(
videoQml.includes('loops: MediaPlayer.Infinite') &&
videoQml.includes('autoPlay: root.playbackEnabled') &&
videoQml.includes('fillMode: VideoOutput.PreserveAspectCrop') &&
/imageUrl: path && !Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
/videoUrl: path && Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) : ""/.test(mediaQml),
'background media plays aspect-cropped videos on a loop, and hands each loader only its own kind of file'
)
assert(
videoQml.includes('MediaPlayer.LoadedMedia') &&
videoQml.includes('primePauseTimer') &&
videoQml.includes('mediaGeneration') &&
videoQml.includes('videoSink') &&
videoQml.includes('onVideoFrameChanged') &&
videoQml.includes('MediaPlayer.BufferedMedia') &&
videoQml.includes('mediaStatus !== MediaPlayer.BufferedMedia') &&
videoQml.includes('interval: 1000') &&
videoQml.includes('interval: 50') &&
videoQml.includes('frameReceived') &&
videoQml.includes('output.clearOutput()') &&
!videoQml.includes('KeepLastFrame') &&
/onPlaybackEnabledChanged:[\s\S]*?if \(playbackEnabled\) player\.play\(\)[\s\S]*?else player\.pause\(\)/.test(videoQml) &&
videoQml.includes('primingGeneration') &&
videoQml.includes('player.play()') &&
videoQml.includes('player.pause()'),
'paused video sources are primed to display their first frame'
)
assert(
!/^\s*import QtMultimedia/m.test(mediaQml) &&
mediaQml.includes('source: "BackgroundVideo.qml"'),
'the still-image path never imports QtMultimedia, so image-only sessions do not map it'
)
assert(
!/^\s*Video\s*\{/m.test(videoQml) &&
/property bool audioEnabled: false/.test(videoQml) &&
/property bool audioEnabled: false/.test(mediaQml) &&
/active: root\.audioEnabled && player\.hasAudio/.test(videoQml) &&
/audioOutput: audioLoader\.item/.test(videoQml) &&
/muted: root\.priming \|\| !root\.playbackEnabled/.test(videoQml) &&
/property: "audioEnabled"\s*\n\s*value: root\.audioEnabled/.test(mediaQml) &&
/firstScreen: Quickshell\.screens\.length > 0\s*\n\s*&& String\(Quickshell\.screens\[0\]\.name/.test(backgroundQml) &&
backgroundQml.includes('audioEnabled: panel.firstScreen') &&
!lockQml.includes('audioEnabled'),
'a sound track plays from the first monitor only, a silent file builds no audio output, and the lock stays quiet'
)
assert(
/property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) &&
!videoQml.includes('Component.onDestruction'),
'a player on its way out keeps its source, so nothing is left loading for its destructor to cancel'
)
assert(
!mediaQml.includes('mipmap'),
'the shared image path leaves mipmapping off, as the desktop background had it'
)
assert(
/instant \|\| !displayedBackground \|\| isVideo\(path\) \|\| isVideo\(displayedBackground\)[\s\S]*displayedBackground = finalPath/.test(backgroundQml),
'video switches bypass the image-only reveal stack and use the durable background path'
)
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering')
assert(
lockQml.includes('source: wallpaper.video ? null : wallpaper') &&
lockQml.includes('visible: !wallpaper.video') &&
lockQml.includes('visible: wallpaper.video'),
'lock screen bypasses its image effect for video output'
)
assert(
/sessionObscured:\s*lockActive \|\| screensaverActive/.test(backgroundQml) &&
backgroundQml.includes('playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere') &&
backgroundQml.includes('omarchy.lock') &&
backgroundQml.includes('omarchy.idle') &&
backgroundQml.includes('omarchy.battery'),
'desktop playback stops while covered or on battery power-saver'
)
assert(
backgroundQml.includes('Hyprland.monitorFor(modelData)') &&
/fullscreenHere: visibleWorkspace \? visibleWorkspace\.hasFullscreen : false/.test(backgroundQml) &&
!backgroundQml.includes('ToplevelManager.activeToplevel'),
'a fullscreen window pauses only the output it covers, wherever focus is'
)
assert(
/if \(displayedBackground === finalPath\) displayedReloads \+= 1/.test(backgroundQml) &&
backgroundQml.includes('reloads: root.displayedReloads') &&
/active: root\.path !== "" && root\.video && !root\.reloading/.test(mediaQml) &&
/onReloadsChanged: \{[\s\S]*?reloading = true/.test(mediaQml),
'a theme switch that keeps the video path still reopens the replaced file'
)
assert(
barTextColor.includes('magick "$background_path[0]"'),
'bar colour sampling reads one frame instead of decoding a whole video'
)
assert(
menuImages.includes('pending_video_file') && /video_jobs=\$\(\( \$\(nproc\) \/ 4 \)\)/.test(menuImages),
'video thumbnails fan out narrower than single-threaded vips jobs'
)
assert(
themeSwitcher.includes('fast_signature="v2"'),
'the theme preview cache rebuilds after preview discovery learned about video'
)
assert(
/lazy_thumbnails == true && \$cache_only != true \]\] && ! is_video_path/.test(menuImages),
'a video never stands in as its own lazy thumbnail, so the fan out cap always applies'
)
assert(
/thumbnail_command=\(timeout -k \d+ \d+ ffmpegthumbnailer/.test(menuImages),
'a stalled video cannot hold the picker shut, because its generator is time bounded'
)
assert(
directImageList.includes('generate_video_thumbnail') &&
/timeout -k \d+ \d+ ffmpegthumbnailer/.test(directImageList) &&
/if \[\[ ! -f \$thumbnail \]\] && ! is_video_path/.test(directImageList),
'a direct picker scan generates bounded video thumbnails without content-hashing the media'
)
assert(
themeSet.includes('choose_staged_theme_background') &&
themeSet.includes('background_transition_uses_snapshots') &&
themeSet.includes('BACKGROUND_TRANSITION_SNAPSHOTS=false') &&
/if \[\[ \$BACKGROUND_TRANSITION_SNAPSHOTS == "true" \]\]/.test(themeSet) &&
/if \[\[ -z \$CHOSEN_THEME_BACKGROUND \|\| ! -f \$CHOSEN_THEME_BACKGROUND \]\]/.test(themeSet),
'theme changes disable both transition snapshots whenever either side is a video'
)
assert(
/function onScreensChanged\(\) \{[\s\S]*?root\.displaysBlank = false/.test(lockService),
'a display coming back gives up the blank state instead of freezing a visible wallpaper'
)
assert(
lockService.includes('function screenBlank(screenName)') &&
lockService.includes('function applyMonitorDpms(text)') &&
lockService.includes('command: ["hyprctl", "monitors", "-j"]') &&
lockService.includes('running: root.locked && root.videoBackground') &&
/displaysBlank: root\.screenBlank\(lockSurface\.screen/.test(lockService) &&
/function runWake\(\) \{[\s\S]*?root\.monitorDpmsKnown = false/.test(lockService) &&
/function runBlank\(\) \{[\s\S]*?root\.monitorDpmsKnown = false/.test(lockService),
'a locked video wallpaper follows what each panel actually did, not only what the lock asked for'
)
assert(
lockView.includes('playbackEnabled: root.loadBackground && !root.displaysBlank') &&
lockView.includes('&& !root.powerSaverActive') &&
/displaysBlank: root\.screenBlank\(/.test(lockService) &&
/powerSaverActive: root\.powerSaverActive/.test(lockService) &&
/function runBlank\(\) \{\s*\n\s*root\.displaysBlank = true/.test(lockService) &&
/function runWake\(\) \{\s*\n\s*root\.displaysBlank = false/.test(lockService),
'the lock screen stops playback once displays go dark or power-saver is active'
)
assert(
batteryService.includes('property string activePowerProfile') &&
batteryService.includes('UPower.onBattery && activePowerProfile === "power-saver"') &&
batteryService.includes('["powerprofilesctl", "get"]') &&
batteryService.includes('interval: 2000'),
'the battery service tracks the active power-saver profile'
)
assert(
themeSwitcher.includes("-iname '*.mp4'") &&
themeSwitcher.includes('mp4 m4v mov webm mkv avi') &&
themeSwitcher.includes('preview.mp4'),
'theme switcher previews video-only themes, named preview files included'
)
assert(quattroUpgrade.includes("-iname '*.mp4'"), 'Quattro upgrade can seed a video-only theme background')
assert(
multimediaMigration.includes('omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg'),
'existing Quattro installations receive video playback dependencies'
)
JS
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin" "$test_tmp/backgrounds" "$test_tmp/generator-cache" "$test_tmp/direct-cache"
printf 'not a real video\n' >"$test_tmp/backgrounds/sample.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
while (( $# > 0 )); do
case "$1" in
-i) input=$2; shift 2 ;;
-o) output=$2; shift 2 ;;
*) shift ;;
esac
done
printf 'thumbnail for %s\n' "$input" >"$output"
SH
chmod +x "$test_tmp/bin/ffmpegthumbnailer"
cat >"$test_tmp/bin/md5sum" <<'SH'
#!/bin/bash
if (( $# > 0 )); then
printf 'unexpected file hash: %s\n' "$*" >>"$MD5_FILE_CALLS"
fi
exec /usr/bin/md5sum "$@"
SH
chmod +x "$test_tmp/bin/md5sum"
md5_file_calls="$test_tmp/md5-file-calls"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/generator-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$test_tmp/backgrounds"
generator_thumbnail=$(find "$test_tmp/generator-cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.jpg' -print -quit)
[[ -s $generator_thumbnail ]] || fail "menu image generator creates a video thumbnail"
generator_row=$(XDG_CACHE_HOME="$test_tmp/generator-cache" "$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
IFS=$'\t' read -r generator_row_path generator_row_thumbnail <<<"$generator_row"
[[ $generator_row_path == "$test_tmp/backgrounds/sample.mp4" && $generator_row_thumbnail == "$generator_thumbnail" ]] || \
fail "direct picker consumes the menu image generator thumbnail" "$generator_row"
row=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/direct-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
IFS=$'\t' read -r row_path row_thumbnail <<<"$row"
[[ $row_path == "$test_tmp/backgrounds/sample.mp4" && $row_thumbnail == *.jpg && -s $row_thumbnail ]] || \
fail "image picker lists videos with their cached thumbnail" "$row"
[[ ! -s $md5_file_calls ]] || fail "direct video scans avoid hashing the complete media file" "$(<"$md5_file_calls")"
failed_backgrounds="$test_tmp/failed-backgrounds"
failed_cache="$test_tmp/failed-cache"
mkdir -p "$failed_backgrounds" "$failed_cache"
printf 'broken video\n' >"$failed_backgrounds/broken.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
exit 1
SH
cached_row=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/direct-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
[[ $cached_row == "$row" ]] || fail "direct picker reuses its cached video thumbnail" "$cached_row"
failed_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$failed_cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$failed_backgrounds")
[[ -z $failed_rows ]] || fail "direct picker omits a video whose thumbnail fails" "$failed_rows"
failed_marker=$(find "$failed_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -n $failed_marker ]] || fail "direct picker remembers a video the converter rejected"
thumbnailer_calls="$test_tmp/thumbnailer-calls"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$THUMBNAILER_CALLS"
exit 1
SH
failed_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$failed_backgrounds")
[[ -z $failed_rows && ! -e $thumbnailer_calls ]] || fail "direct picker skips a rejected video on the next scan" "$(cat "$thumbnailer_calls" 2>/dev/null)"
generator_failed_cache="$test_tmp/generator-failed-cache"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ -s $thumbnailer_calls ]] || fail "menu image generator tries a video it has not seen"
generator_marker=$(find "$generator_failed_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -n $generator_marker ]] || fail "menu image generator remembers a video the converter rejected"
rm -f "$thumbnailer_calls"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ ! -e $thumbnailer_calls ]] || fail "menu image generator skips a rejected video on the next open" "$(<"$thumbnailer_calls")"
# A repaired file gets a fresh key, so the old marker no longer applies, and
# the rows are not cached over its absence, so an in-place repair that leaves
# the directory's mtime alone is still noticed.
touch -d '2 minutes' "$failed_backgrounds/broken.mp4"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ -s $thumbnailer_calls ]] || fail "menu image generator retries a video that changed since it was rejected"
timeout_backgrounds="$test_tmp/timeout-backgrounds"
timeout_cache="$test_tmp/timeout-cache"
mkdir -p "$timeout_backgrounds"
printf 'slow video\n' >"$timeout_backgrounds/slow.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
exit 124
SH
timeout_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$timeout_cache" \
"$ROOT/shell/plugins/image-picker/list.sh" "$timeout_backgrounds")
[[ -z $timeout_rows ]] || fail "direct picker omits a video whose thumbnail timed out" "$timeout_rows"
timeout_marker=$(find "$timeout_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -z $timeout_marker ]] || fail "a timed out video is left to retry rather than remembered as failed"
grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia runtime is a base package"
grep -qx 'qt6-multimedia-ffmpeg' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia FFmpeg backend is a base package"
pass "menu image generator creates thumbnails consumed by the picker"
pass "direct picker generates and reuses still thumbnails"
pass "direct picker omits videos whose thumbnails cannot be generated"
pass "a rejected video is remembered so it costs nothing on the next open"
pass "a timed out video is left to retry"
pass "a locked video wallpaper follows the panels' real DPMS state"
pass "Qt Multimedia playback dependencies are declared"
source <(awk '
/^(is_video_path|snapshot_background_path|background_transition_uses_snapshots|choose_theme_background|choose_staged_theme_background|set_theme_background)\(\) \{/ { copying=1 }
copying { print }
copying && /^}$/ { copying=0 }
' "$ROOT/bin/omarchy-theme-set")
transition_home="$test_tmp/transition-home"
CURRENT_THEME_PATH="$transition_home/.local/state/omarchy/current/theme"
NEXT_THEME_PATH="$transition_home/.local/state/omarchy/current/next-theme"
CURRENT_BACKGROUND_LINK="$transition_home/.local/state/omarchy/current/background"
BACKGROUND_TRANSITION_CACHE="$transition_home/.cache/omarchy/background-transitions"
THEME_NAME="video-test"
HOME="$transition_home"
mkdir -p "$CURRENT_THEME_PATH/backgrounds" "$NEXT_THEME_PATH/backgrounds" "$HOME/.config/omarchy/backgrounds/$THEME_NAME"
printf 'old image\n' >"$CURRENT_THEME_PATH/backgrounds/old.png"
printf 'old image staged\n' >"$NEXT_THEME_PATH/backgrounds/old.png"
printf 'new video\n' >"$NEXT_THEME_PATH/backgrounds/new.mp4"
ln -s "$CURRENT_THEME_PATH/backgrounds/old.png" "$CURRENT_BACKGROUND_LINK"
choose_staged_theme_background || fail "staged video background is selected before the theme swap"
expected_staged_background="$CURRENT_THEME_PATH/backgrounds/new.mp4"
[[ $CHOSEN_THEME_BACKGROUND == $expected_staged_background ]] || \
fail "staged background resolves to its durable post-swap path" "$CHOSEN_THEME_BACKGROUND"
if background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
fail "image to video theme transitions skip snapshots"
fi
background_transition_uses_snapshots "$CURRENT_THEME_PATH/backgrounds/new.png" || \
fail "image to image theme transitions retain snapshots"
rm "$CURRENT_BACKGROUND_LINK"
printf 'old video\n' >"$CURRENT_THEME_PATH/backgrounds/old.mp4"
ln -s "$CURRENT_THEME_PATH/backgrounds/old.mp4" "$CURRENT_BACKGROUND_LINK"
if background_transition_uses_snapshots "$CURRENT_THEME_PATH/backgrounds/new.png"; then
fail "video to image theme transitions skip snapshots"
fi
video_snapshot=$(snapshot_background_path "$CURRENT_THEME_PATH/backgrounds/old.mp4" "video")
[[ -z $video_snapshot && ! -e $BACKGROUND_TRANSITION_CACHE ]] || fail "video files are never snapshotted"
CHOSEN_THEME_BACKGROUND="$transition_home/disappeared.mp4"
BACKGROUND_TRANSITION_SNAPSHOTS=false
OLD_BACKGROUND_SNAPSHOT=""
colors_payload=""
shell_payload=""
shell_ipc() { :; }
set_theme_background
[[ -f $CHOSEN_THEME_BACKGROUND && $(readlink "$CURRENT_BACKGROUND_LINK") == "$CHOSEN_THEME_BACKGROUND" ]] || \
fail "theme changes recover when a preselected background disappears" "$CHOSEN_THEME_BACKGROUND"
pass "theme transitions skip snapshots whenever either side is a video"
pass "theme changes recover from a missing preselected background"
-104
View File
@@ -1,104 +0,0 @@
#!/bin/bash
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
migration=$(grep -rl 'Move zram tuning to a vendor drop-in' "$ROOT/migrations" | head -n 1 || true)
[[ -n $migration ]] || fail "zram drop-in migration exists"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# The migration shells out to pacman (ownership check) and sudo (removal).
# Stub both so the test never touches the real system, and let each case pick
# what `pacman -Qo` reports through PACMAN_OWNS.
stub_bin="$TMPDIR/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/pacman" <<'STUB'
#!/bin/bash
[[ ${PACMAN_OWNS:-0} == 1 ]]
STUB
cat >"$stub_bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
chmod +x "$stub_bin/pacman" "$stub_bin/sudo"
# The migration removes the /etc copy only once the drop-in that replaces it is
# installed. Point that at a fixture so the result does not depend on whether
# the machine running the tests happens to carry the real one.
dropin="$TMPDIR/90-omarchy.conf"
: >"$dropin"
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
# whole chain on a non-zero exit, so match that invocation exactly.
run_migration() {
local conf="$1"
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean for $(basename "$conf")"
}
# archinstall's own output: a [zram0] section with nothing but the algorithm.
conf="$TMPDIR/archinstall.conf"
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration removes archinstall's generated config"
pass "migration removes archinstall's generated config"
# Same shape, different algorithm, plus comments and blank lines.
conf="$TMPDIR/commented.conf"
printf '# written by archinstall\n\n[zram0]\ncompression-algorithm = lz4\n\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration ignores comments and a non-zstd algorithm"
pass "migration ignores comments and a non-zstd algorithm"
# A config that sets nothing decides nothing, and must not take the migration
# chain down with it.
conf="$TMPDIR/comments-only.conf"
printf '# nothing to see here\n\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration removes a config that sets nothing"
pass "migration removes a config that sets nothing"
conf="$TMPDIR/empty.conf"
: >"$conf"
run_migration "$conf"
[[ -f $conf ]] && fail "migration removes an empty config"
pass "migration removes an empty config"
# A local override must survive.
conf="$TMPDIR/local.conf"
printf '[zram0]\ncompression-algorithm = zstd\nzram-size = ram / 4\n' >"$conf"
run_migration "$conf"
[[ -f $conf ]] || fail "migration keeps a locally edited config"
pass "migration keeps a locally edited config"
# Package-owned copies go away with their package; the migration must not touch
# them.
conf="$TMPDIR/owned.conf"
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
PATH="$stub_bin:$PATH" PACMAN_OWNS=1 OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean for a package-owned config"
[[ -f $conf ]] || fail "migration keeps a package-owned config"
pass "migration keeps a package-owned config"
# Nothing to do, and running twice must stay clean.
conf="$TMPDIR/absent.conf"
run_migration "$conf"
run_migration "$conf"
pass "migration no-ops when the config is already gone"
# Without the drop-in installed, the /etc copy is the only thing configuring
# zram at all. Removing it would leave the machine with no zram device, so the
# migration has to leave it alone and stay clean doing it.
conf="$TMPDIR/no-dropin.conf"
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$TMPDIR/absent-dropin.conf" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean when the drop-in is missing"
[[ -f $conf ]] || fail "migration keeps the config when the drop-in is missing"
pass "migration keeps the config until the drop-in is installed"