Merge quattro and route refreshes through omarchy-update-pacman

Upstream now runs every Omarchy-owned pacman transaction through the
hidden omarchy-update-pacman helper so a mid-transaction systemd reexec
cannot kill it. Keep the deferred pre-refresh-pacman hook and the
command-scoped sudo wrapper, and call the helper from the refresh and
channel commands; the wrapper still applies to the helper's own sudo.

The sudo boundary fixture copies the helper into its root and runs a
systemd-run stand-in that execs the wrapped pacman step in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Afonso OliveiraandClaude Fable 5.1 committed 2026-09-16 21:54:06 +01:00
commit 4bd593f4ed
105 files changed
+2541 -361

No files matched your search

+13 -7
View File
@@ -46,6 +46,12 @@ printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
cp "$stub_bin/sudo" "$SUDO_TEST_ROOT/mock/sudo"
write_stub omarchy-update-pacman '#!/bin/bash
printf "update-pacman" >>"$OMARCHY_CHANNEL_TEST_LOG"
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
'
write_stub omarchy-dev-unlink '#!/bin/bash
printf "unlink" >>"$OMARCHY_CHANNEL_TEST_LOG"
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
@@ -122,7 +128,7 @@ assert_log_line() {
run_channel stable
assert_log_line $'refresh\tstable\tdefer-hook' "stable refreshes the stable pacman channel"
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt"
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "stable runs the normal update pipeline from the package-backed path"
if grep -q $'^state\tset\treboot-required$' "$log_file"; then
@@ -132,7 +138,7 @@ pass "stable does not require reboot when already package-backed"
run_channel rc
assert_log_line $'refresh\trc\tdefer-hook' "rc refreshes the rc pacman channel"
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt"
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "rc runs the normal update pipeline from the package-backed path"
@@ -140,11 +146,11 @@ active_checkout="$test_tmp/active-checkout"
cp -a "$package_root" "$active_checkout"
OMARCHY_TEST_PATH="$active_checkout" run_channel edge
assert_log_line $'refresh\tedge\tdefer-hook' "edge refreshes the edge pacman channel"
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt"
assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev"
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "edge runs the normal update pipeline from the package-backed path"
[[ $(grep -E '^(unlink|state|update)' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH='"$package_root" ]] ||
[[ $(grep -E $'^(unlink|state|update)\t' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH='"$package_root" ]] ||
fail "edge defers the reboot prompt until the update restart stage" "$(cat "$log_file")"
pass "edge defers the reboot prompt until the update restart stage"
@@ -164,12 +170,12 @@ rmdir "$checkout"
run_channel dev
assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation"
assert_log_line $'refresh\tedge\tdefer-hook' "dev refreshes the edge pacman channel"
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
assert_log_line $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
assert_log_line $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt"
assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline"
assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout"
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nsudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nupdate-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")"
pass "dev activates the checkout before changing or updating packages"
[[ $(tail -1 "$log_file") == $'refresh\tedge\trun-deferred' ]] || fail "channel refresh hook must run after the complete update"
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Exercise the privileged installer without writing to the host's /usr/share.
if ! command -v bwrap >/dev/null || ! bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true 2>/dev/null; then
pass "user namespaces unavailable; skipping isolated Claude extension installation"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/share" "$test_tmp/bin"
installer="$ROOT/bin/omarchy-install-chromium-claude"
extension_id=fcoeoabgfenejglbffodgkkbkcdhcgfn
sandbox=(bwrap --ro-bind / / --bind "$test_tmp" "$test_tmp" --dev /dev --proc /proc --unshare-user)
"${sandbox[@]}" --uid 0 --gid 0 --bind "$test_tmp/share" /usr/share bash "$installer"
for browser in chromium google-chrome microsoft-edge; do
file="$test_tmp/share/$browser/extensions/$extension_id.json"
jq -e '.external_update_url == "https://clients2.google.com/service/update2/crx"' "$file" >/dev/null ||
fail "$browser registers the official Claude Web Store extension"
[[ $(stat -c '%a' "$file") == "644" ]] || fail "$browser extension registration is readable"
done
pass "Claude extension installer registers all supported browser families"
cat >"$test_tmp/bin/pkexec" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$AUTH_LOG"
exit 42
SH
chmod +x "$test_tmp/bin/pkexec"
export AUTH_LOG="$test_tmp/auth-log"
export PATH="$test_tmp/bin:$PATH"
# A read-only /usr/share and a failing auth stub prove that a repeated run
# neither rewrites the files nor requests authentication.
"${sandbox[@]}" --uid 1000 --gid 1000 --ro-bind "$test_tmp/share" /usr/share bash "$installer" </dev/null
[[ ! -e $AUTH_LOG ]] || fail "configured Claude extensions need no authentication"
pass "configured Claude extensions need neither writes nor authentication"
rm "$test_tmp/share/google-chrome/extensions/$extension_id.json"
status=0
"${sandbox[@]}" --uid 1000 --gid 1000 --ro-bind "$test_tmp/share" /usr/share bash "$installer" </dev/null || status=$?
[[ $status == 42 ]] || fail "Claude extension installer propagates authentication failure"
[[ $(cat "$AUTH_LOG") == "/usr/bin/omarchy-install-chromium-claude" ]] ||
fail "menu installation elevates only the packaged installer"
pass "missing registration uses pkexec and propagates authentication failure"
+24
View File
@@ -22,6 +22,15 @@ menu_log="$test_tmp/menu"
muse_login_log="$test_tmp/muse-login"
mkdir -p "$mock_bin" "$test_home"
cat >"$mock_bin/omarchy-install-chromium-claude" <<'SH'
#!/bin/bash
echo claude-extension >>"$OMARCHY_TEST_STUB_LOG"
if [[ ${OMARCHY_TEST_EXTENSION_FAIL:-false} == "true" ]]; then
echo "Extension installation failed" >&2
exit 1
fi
SH
cat >"$mock_bin/omarchy-notification-send" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_NOTIFICATION_HISTORY"
@@ -406,9 +415,16 @@ declare -A expected_packages=(
for selection in "${!expected_agents[@]}"; do
expected=${expected_agents[$selection]}
: >"$agent_open_log"
: >"$stub_log"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent "$selection"
[[ $(omarchy-default-agent) == $expected ]] || fail "default agent canonicalizes $selection"
if [[ $expected == "claude" ]]; then
grep -qx claude-extension "$stub_log" || fail "Claude selection installs the browser extension"
else
[[ ! -s $stub_log ]] || fail "other agents do not install the Claude extension"
fi
mapfile -d '' -t mise_args <"$mise_log"
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" ]] ||
fail "default agent installs $selection globally through mise"
@@ -426,6 +442,14 @@ pass "default agent selects and opens every supported provider and alias"
fail "default agent stores its selection in Omarchy user config"
pass "default agent stores its selection in Omarchy user config"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
: >"$agent_open_log"
OMARCHY_TEST_AGENT_INSTALLED=true OMARCHY_TEST_EXTENSION_FAIL=true omarchy-default-agent claude >"$test_tmp/extension-failure" 2>&1
[[ $(omarchy-default-agent) == "claude" ]] || fail "extension installation failure still selects Claude"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${agent_open_args[*]} == "omarchy-agent" ]] || fail "extension installation failure still launches Claude"
[[ ! -s $test_tmp/extension-failure ]] || fail "extension installation failure is silent"
pass "extension installation failure silently continues selecting and launching Claude"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
: >"$notification_history"
: >"$agent_open_log"
+148
View File
@@ -0,0 +1,148 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if (( EUID != 0 )); then
if unshare --user --map-root-user true 2>/dev/null; then
exec unshare --user --map-root-user bash "$0"
fi
pass "no unprivileged user namespace; skipping factory account cleanup"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
# Load the production functions without self-elevation or the reset entrypoint.
awk '
/^[a-z_]+\(\) \{/ { copying = 1 }
copying { print }
/^}/ { copying = 0 }
' "$ROOT/bin/omarchy-system-factory-reset" >"$test_tmp/functions"
cat >"$test_tmp/reset" <<'SH'
#!/bin/bash
set -euo pipefail
source "$1/functions"
TOP_MNT="$2"
NEXT_NAME=@omarchy-reset-next
PROVISIONING_DIR=/var/lib/omarchy/provisioning
LOG_FILE="$TOP_MNT/reset.log"
log() { printf '%s\n' "$1" >>"$LOG_FILE"; }
fail() { log "$1"; exit 1; }
# Account tools are real. Only snapshots, boot rebuilding, and system services
# are replaced: all writes stay inside this test's disposable directory.
btrfs() {
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then
mkdir -p "$4"
cp -a "$3/." "$4/"
elif [[ $1 == "property" ]]; then
printf '%s\n' "$6" >"$4/read-only"
else
return 1
fi
}
systemd-id128() { printf '%032d\n' 1; }
install_provisioning_units() { :; }
encrypted_install() { return 1; }
rebuild_next_boot() { touch "$TOP_MNT/rebuilt"; }
sync() { :; }
userdel() {
[[ ${FAIL_COMMAND:-} == "userdel" && $2 == "$FAIL_ROOT" ]] && return 42
command userdel "$@"
}
usermod() {
[[ ${FAIL_COMMAND:-} == "usermod" && $2 == "$FAIL_ROOT" ]] && return 42
command usermod "$@"
}
rm() {
[[ ${FAIL_COMMAND:-} == "rm" && $* == *"$FAIL_ROOT/etc/shadow-"* ]] && return 42
command rm "$@"
}
stage_full_reset
SH
make_fixture() {
local top="$1" root_hash="${2:-original-root-hash}"
local factory="$top/@factory"
mkdir -p "$top/@" "$factory/etc" "$factory/home/seller" \
"$factory/usr/bin" "$factory/usr/share/omarchy/install/provisioning" \
"$factory/var/lib/omarchy/provisioning/packages"
touch "$top/@/old-system" "$factory/home/seller/private-file" \
"$factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
"$factory/var/lib/omarchy/provisioning/packages/node-v0.tar.gz"
printf '#!/bin/bash\n' >"$factory/usr/bin/omarchy-provision-owner"
chmod +x "$factory/usr/bin/omarchy-provision-owner"
printf 'true\n' >"$factory/read-only"
cat >"$factory/etc/passwd" <<'EOF'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/:/usr/bin/nologin
seller:x:1000:1000:Seller:/home/seller:/bin/bash
EOF
printf 'root:%s:20000:0:99999:7:::\ndaemon:*:20000:0:99999:7:::\nseller:original-user-hash:20000:0:99999:7:::\n' \
"$root_hash" >"$factory/etc/shadow"
printf 'root:x:0:\ndaemon:x:1:\nseller:x:1000:\nwheel:x:998:seller\n' >"$factory/etc/group"
printf 'root:!::\ndaemon:!::\nseller:!::\nwheel:!::seller\n' >"$factory/etc/gshadow"
printf 'USERGROUPS_ENAB yes\n' >"$factory/etc/login.defs"
printf 'seller:100000:65536\n' >"$factory/etc/subuid"
printf 'seller:100000:65536\n' >"$factory/etc/subgid"
chmod 600 "$factory/etc/"{shadow,gshadow}
for file in passwd shadow group gshadow subuid subgid; do
cp "$factory/etc/$file" "$factory/etc/$file-"
done
}
assert_scrubbed() {
local root="$1" file
[[ $(awk -F: '$1 == "root" { print $2 }' "$root/etc/shadow") == "!" ]] ||
fail "reset erases the root hash while keeping the account locked"
! grep -q 'original-.*-hash\|seller' "$root/etc/"{passwd,shadow,group,gshadow} ||
fail "reset removes seller account credentials and group membership"
[[ ! -e $root/home/seller ]] || fail "reset removes the seller's baseline home"
grep -q '^daemon:\*:' "$root/etc/shadow" || fail "reset preserves service accounts"
[[ $(stat -c '%a' "$root/etc/shadow") == "600" ]] || fail "shadow stays private"
for file in passwd shadow group gshadow subuid subgid; do
[[ ! -e $root/etc/$file- ]] || fail "reset removes the $file backup"
done
}
for scenario in normal locked; do
top="$test_tmp/$scenario"
if [[ $scenario == "locked" ]]; then
make_fixture "$top" '!'
else
make_fixture "$top"
fi
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset stages successfully"
assert_scrubbed "$top/@factory"
assert_scrubbed "$top/@"
[[ $(cat "$top/@factory/read-only") == "true" ]] || fail "baseline returns to read-only"
[[ -f $top/@/var/lib/omarchy/provisioning/pending && -f $top/rebuilt ]] ||
fail "reset reaches provisioning after cleanup"
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset can be repeated"
assert_scrubbed "$top/@factory"
assert_scrubbed "$top/@"
pass "$scenario reset scrubs both roots, preserves service accounts, and can be repeated"
done
for target in @omarchy-reset-next @factory; do
for command in userdel usermod rm; do
top="$test_tmp/fail-$target-$command"
make_fixture "$top"
if FAIL_COMMAND="$command" FAIL_ROOT="$top/$target" bash "$test_tmp/reset" "$test_tmp" "$top"; then
fail "reset accepted failed $command in $target"
fi
[[ -f $top/@/old-system && ! -e $top/rebuilt ]] ||
fail "failed cleanup must not activate or rebuild the reset system"
[[ $(cat "$top/@factory/read-only") == "true" ]] ||
fail "failed cleanup must leave the baseline read-only"
pass "failed $command in $target aborts reset before activation"
done
done
@@ -0,0 +1,3 @@
Stock Omarchy icon font from `config/omarchy.ttf` at `babfafa5^`, before fonts moved into the settings package. SHA-256: `e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966`.
The migration test uses the real font so it exercises the exact-content guard without mocking `sha256sum`.
Binary file not shown.
+8 -1
View File
@@ -30,6 +30,7 @@ PY
}
copy_boundary_file bin/omarchy-security-functions
copy_boundary_file bin/omarchy-update-pacman
copy_boundary_file default/omarchy/sudo-no-update/sudo
cat >"$SUDO_TEST_ROOT/mock/sudo" <<'STUB'
@@ -82,6 +83,12 @@ cat >"$SUDO_TEST_ROOT/bin/test-step" <<'STUB'
set -euo pipefail
step=${0##*/}
printf 'step:%s %s\n' "$step" "$*" >>"$SUDO_TEST_LOG"
if [[ $step == "systemd-run" ]]; then
# omarchy-update-pacman registers the transaction as a PID 1 scope on booted
# hosts. Run the wrapped command in place so the pacman step still executes.
while (( $# )) && [[ $1 == -* ]]; do shift; done
exec "$@"
fi
if [[ $step == "omarchy-hook" || $step == "omarchy-update-mise" ]]; then
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
fi
@@ -108,7 +115,7 @@ case "$step" in
esac
STUB
chmod +x "$SUDO_TEST_ROOT/bin/test-step"
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman cp yay; do
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman systemd-run cp yay; do
ln -s test-step "$SUDO_TEST_ROOT/bin/$step"
done
ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
@@ -357,3 +357,18 @@ OMARCHY_TEST_HOME="$hermes_home/PrOfIlEs/coder/../coder/" run_installer || fail
[[ -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"
# New releases moved the stamp writer out of main. Keep the earlier cases on
# the old layout and exercise a fresh installation with the relocated helper.
git -C "$test_tmp/seed" mv hermes_cli/main.py hermes_cli/main_desktop.py
printf 'raise AssertionError("legacy module imported after desktop split")\n' >"$test_tmp/seed/hermes_cli/main.py"
git -C "$test_tmp/seed" add hermes_cli/main.py
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm split-desktop
release_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"
new_home split-desktop
run_installer || fail "setup supports the relocated desktop helper" "$(cat "$test_tmp/output")"
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "relocated helper writes the build stamp"
grep -qx launch "$test_tmp/events" || fail "setup launches after the relocated helper writes the stamp"
pass "new releases use the relocated desktop stamp writer"
+158
View File
@@ -0,0 +1,158 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
work_dir=$(mktemp -d)
trap 'rm -rf "$work_dir"' EXIT
fake_home="$work_dir/home"
mkdir -p "$fake_home/.config/omarchy/hooks" "$fake_home/.local/state/omarchy"
# --- omarchy-hook --------------------------------------------------------------
# A hook name is a label, not a path. One carrying a slash, or one that is a
# bare `.` or `..`, would run a script from outside the hooks directory.
cat >"$fake_home/.config/omarchy/hooks/test-hook" <<'SH'
touch "$HOME/hook-ran"
SH
HOME="$fake_home" "$ROOT/bin/omarchy-hook" test-hook
[[ -f $fake_home/hook-ran ]] ||
fail "omarchy hook runs a named hook from the hooks directory"
pass "omarchy hook runs a named hook from the hooks directory"
# Dots inside a name are not a path. a..b stays inside the hooks directory.
cat >"$fake_home/.config/omarchy/hooks/a..b" <<'SH'
touch "$HOME/dotted-hook-ran"
SH
HOME="$fake_home" "$ROOT/bin/omarchy-hook" a..b
[[ -f $fake_home/dotted-hook-ran ]] ||
fail "omarchy hook accepts a hook name with dots in the middle"
pass "omarchy hook accepts a hook name with dots in the middle"
for name in . ..; do
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "$name" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook refuses a hook name of $name" "exit: $status"
pass "omarchy hook refuses a hook name of $name"
done
# This file sits where a name of ../../evil would resolve: hooks/../.. is
# ~/.config.
cat >"$fake_home/.config/evil" <<'SH'
touch "$HOME/escape-ran"
SH
chmod +x "$fake_home/.config/evil"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "../../evil" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook refuses a hook name with a dot-dot" "exit: $status"
[[ ! -e $fake_home/escape-ran ]] ||
fail "omarchy hook runs nothing when it refuses the name"
pass "omarchy hook refuses a hook name with a dot-dot"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "sub/dir" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook refuses a hook name with a slash" "exit: $status"
pass "omarchy hook refuses a hook name with a slash"
# --- omarchy-hook-install ------------------------------------------------------
# The installer joins the type into ~/.config/omarchy/hooks/<type>.d before
# mkdir/cp. The runner already refuses a slashed type; install must too, or a
# name the runner will not run still lands on disk.
source_hook="$work_dir/source-hook"
cat >"$source_hook" <<'SH'
#!/bin/bash
true
SH
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" post-update "$source_hook" >/dev/null
[[ -f $fake_home/.config/omarchy/hooks/post-update.d/source-hook ]] ||
fail "omarchy hook install still installs a named hook"
pass "omarchy hook install still installs a named hook"
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" a..b "$source_hook" >/dev/null
[[ -f $fake_home/.config/omarchy/hooks/a..b.d/source-hook ]] ||
fail "omarchy hook install accepts a hook name with dots in the middle"
pass "omarchy hook install accepts a hook name with dots in the middle"
for name in . ..; do
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "$name" "$source_hook" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook install refuses a hook name of $name" "exit: $status"
[[ ! -e $fake_home/.config/omarchy/hooks/${name}.d ]] ||
fail "omarchy hook install creates no directory for a hook name of $name"
pass "omarchy hook install refuses a hook name of $name"
done
# hooks/../../evil.d is ~/.config/evil.d. The guard must fire before mkdir.
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "../../evil" "$source_hook" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook install refuses a hook name with a dot-dot" "exit: $status"
[[ ! -e $fake_home/.config/evil.d ]] ||
fail "omarchy hook install creates nothing outside the hooks directory"
pass "omarchy hook install refuses a hook name with a dot-dot"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "sub/dir" "$source_hook" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook install refuses a hook name with a slash" "exit: $status"
[[ ! -e $fake_home/.config/omarchy/hooks/sub ]] ||
fail "omarchy hook install creates no nested directory from a slashed name"
pass "omarchy hook install refuses a hook name with a slash"
# --- omarchy-state -------------------------------------------------------------
state_dir="$fake_home/.local/state/omarchy"
HOME="$fake_home" "$ROOT/bin/omarchy-state" set reboot-required
[[ -f $state_dir/reboot-required ]] ||
fail "omarchy state set still creates a plain state file"
pass "omarchy state set still creates a plain state file"
HOME="$fake_home" "$ROOT/bin/omarchy-state" set v1..2
[[ -f $state_dir/v1..2 ]] ||
fail "omarchy state set accepts a state name with dots in the middle"
pass "omarchy state set accepts a state name with dots in the middle"
for name in . ..; do
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "$name" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy state set refuses a state name of $name" "exit: $status"
pass "omarchy state set refuses a state name of $name"
done
# state/../.. is ~/.local. The guard must fire before touch gets there.
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "../../escape" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy state set refuses a state name with a dot-dot" "exit: $status"
[[ ! -e $fake_home/.local/escape ]] ||
fail "omarchy state set creates nothing outside the state directory"
pass "omarchy state set refuses a state name with a dot-dot"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "sub/dir" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy state set refuses a state name with a slash" "exit: $status"
pass "omarchy state set refuses a state name with a slash"
# clear takes patterns by design ("state-name-or-pattern") and matches
# basenames through find -name, so it can never walk out of the directory.
touch "$state_dir/restart-a-required" "$state_dir/restart-b-required" "$state_dir/keep-me"
HOME="$fake_home" "$ROOT/bin/omarchy-state" clear "restart-*-required"
[[ ! -e $state_dir/restart-a-required && ! -e $state_dir/restart-b-required && -f $state_dir/keep-me ]] ||
fail "omarchy state clear still clears matching patterns only"
pass "omarchy state clear still clears matching patterns only"
+204 -15
View File
@@ -4,15 +4,16 @@ source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command lua
# The console is sized by the gap underneath it, recomputed from the monitor,
# The console is sized by the gaps around it, recomputed from the monitor,
# because a window rule's size would freeze at whatever the screen measured when
# the console first opened. The arithmetic is what keeps it half a screen on a
# scaled display, so it is worth pinning down.
# the console first opened. The arithmetic is what keeps it a half-height panel
# on a scaled display, so it is worth pinning down.
# base-test.sh does not set -e, so the assertions have to fail the file
# themselves rather than leaving the pass below to run regardless.
OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "the console covers half the work area at any monitor scale"
OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "the console is a centered panel until a second app joins it"
local rules, handlers = {}, {}
local monitor = nil
local workspace = nil
hl = {
config = function() end,
@@ -20,6 +21,16 @@ hl = {
workspace_rule = function(rule) table.insert(rules, rule) end,
on = function(event, callback) handlers[event] = callback end,
get_active_monitor = function() return monitor end,
get_workspace = function() return workspace end,
-- workspace.windows is the tiled count here and workspace.floats the floating
-- one, so the fixtures can say which kind of window is on the console.
get_workspace_windows = function()
local list = {}
for _ = 1, workspace and workspace.windows or 0 do table.insert(list, { floating = false }) end
for _ = 1, workspace and workspace.floats or 0 do table.insert(list, { floating = true }) end
return list
end,
exec_scheduled_prop_refresh_immediately = function() end,
}
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
@@ -29,6 +40,11 @@ local function current()
return rules[#rules]
end
local function gaps()
local g = current().gaps_out
return g.top, g.right, g.bottom, g.left
end
-- Config loads before the outputs are up, so the first pass has no monitor to
-- read. It still has to leave a rule behind, or the console would open unseeded.
assert(#rules > 0, "console is ruled even before a monitor can be read")
@@ -38,7 +54,7 @@ assert(current().on_created_empty:find("^%[workspace special:scratchpad silent%]
assert(current().workspace == "special:scratchpad")
local function rescale(height, scale, bar)
monitor = { height = height, scale = scale, reserved = { top = bar, bottom = 0, left = 0, right = 0 } }
monitor = { width = 1920, height = height, scale = scale, transform = 0, reserved = { top = bar, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
return current().gaps_out.bottom
end
@@ -52,11 +68,9 @@ assert(rescale(2160, 1.5, 40) == 700, "and at a fractional scale")
-- console short.
assert(rescale(1440, 1, 0) == 720, "a monitor with nothing reserved")
-- The console stays flush with the top and the sides, the way a Quake console
-- drops in, and keeps its seed across every refit.
local final = current()
assert(final.gaps_out.top == 0 and final.gaps_out.left == 0 and final.gaps_out.right == 0,
"the console is flush to the top and sides")
assert(final.gaps_out.top == 0, "the console stays flush with the top, the way a drop-down arrives")
assert(final.gaps_out.left == final.gaps_out.right, "the panel is centered")
assert(final.on_created_empty:find("omarchy%-agent"), "refitting keeps the console seeded")
assert(final.no_border == true, "the console drops the active window border")
@@ -69,22 +83,197 @@ assert(current().gaps_out.bottom == before, "an absent monitor leaves the consol
-- A monitor handle outliving its output answers nil to everything, which is
-- what a layout change looks like mid-flight. Reading height or reserved off
-- that would throw, so the scale guard has to catch it first.
monitor = setmetatable({}, { __index = function() return nil end })
local expired = setmetatable({}, { __index = function() return nil end })
monitor = expired
handlers["monitor.layout_changed"]()
assert(current().gaps_out.bottom == before, "an expired monitor handle is not read to pieces")
-- Refitting to the size it already is would still cost a state refresh, and
-- monitor.focused fires on every hop between screens.
monitor = { height = 1440, scale = 1, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
local written = #rules
handlers["monitor.focused"]()
handlers["monitor.layout_changed"]()
assert(#rules == written, "refitting to the same size does not rewrite the rule")
monitor.scale = 2
-- A centered 2:1 panel, not a full-width drop-down.
monitor = { width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
assert(#rules == written + 1, "a real change still rewrites it")
assert(current().gaps_out.bottom == 360, "and lands on half the rescaled screen")
local top, right, bottom, left = gaps()
assert(top == 0 and left == 420 and right == 420 and bottom == 540, "16:9 leaves a 1080x540 panel")
local dell = { name = "DP-1", width = 6144, height = 2560, scale = 1, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
monitor = dell
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 1807 and right == 1807 and bottom == 1265, "the same 2:1 panel on 6K")
-- Same logical box at scale 2x (physical 12288x5120): scale does not change the
-- panel's logical size.
monitor = { name = "DP-1", width = 12288, height = 5120, scale = 2, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 1807 and right == 1807 and bottom == 1265, "the 6K box is in logical pixels")
-- Same height, different width: the sides have to move even though the bottom
-- gap is identical, so the cache cannot key on height alone.
monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
written = #rules
monitor = { width = 3440, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
assert(#rules == written + 1, "a same-height ultrawide hop still rewrites the sides")
top, right, bottom, left = gaps()
assert(left == 1000 and right == 1000 and bottom == 720, "3440x1440 leaves a 1440x720 box")
-- A panel wider than the screen is just the screen: a portrait monitor has no
-- room for a 2:1 box and falls back to the full width rather than a negative gap.
monitor = { width = 1080, height = 1920, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 0 and right == 0 and bottom == 960, "a portrait monitor keeps the full width")
assert(left >= 0 and right >= 0 and bottom >= 0, "gaps are never negative")
-- A monitor turned on its side still reports the panel's own pixels, so the
-- work area has to be turned with it. Measured against Hyprland 0.56.2: a
-- rotated 1920x1080 lays its windows out in 1080x1920 while width and height
-- still read 1920 and 1080. Quarter turns are the odd transforms; a half turn
-- leaves the shape alone.
monitor = { width = 1920, height = 1080, scale = 1, transform = 1, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 0 and right == 0 and bottom == 945, "a quarter-turned monitor is sized portrait")
monitor = { width = 1920, height = 1080, scale = 1, transform = 3, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 0 and right == 0 and bottom == 945, "and so is the other quarter turn")
monitor = { width = 1920, height = 1080, scale = 1, transform = 2, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435 and bottom == 525, "a half turn is still landscape")
-- Special workspaces open on the monitor they are toggled on, not on whichever
-- output was focused when the rule was last written. Opening on 1080p after a
-- 6K fit has to resize the box.
local acer = { name = "HDMI-A-1", width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
monitor = dell
handlers["monitor.layout_changed"]()
handlers["workspace.special_active"]({ name = "special:scratchpad" }, acer)
top, right, bottom, left = gaps()
assert(left == 435 and right == 435 and bottom == 525, "opening on 1080p after a 6K fit resizes the box")
assert(1920 - left - right > 0 and 1080 - 30 - bottom > 0, "1080p leftover is never negative")
-- follow_mouse onto the 6K while the console is already showing on 1080p must
-- not steal the rule; that is what oversized the Dell after a hop.
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
monitor = dell
written = #rules
handlers["monitor.focused"](dell)
assert(#rules == written, "focus on another output does not rewrite an open console")
-- The output the console is showing on can go away mid-layout-change. Its
-- handle then answers nil to everything, and preferring it blindly would leave
-- the console stranded at the gaps of the monitor that is gone. The rule is
-- still the 1080p one here, so only refitting on the Dell can satisfy this.
workspace = { name = "special:scratchpad", visible = true, monitor = expired, windows = 1 }
monitor = dell
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 1807 and right == 1807 and bottom == 1265,
"a console whose output vanished refits on the monitor that is still there")
-- Back onto the 1080p panel for the window-count checks below.
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
monitor = acer
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435 and bottom == 525, "and refits again once it is back on a live output")
-- One window reads as a console and keeps the panel. A second app has turned
-- the scratchpad into a workspace, and a workspace wants the whole width.
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 2 }
handlers["window.open"]()
top, right, bottom, left = gaps()
assert(left == 0 and right == 0, "a second app on the scratchpad restores the full width")
assert(bottom == 525, "and the console keeps its half-height drop")
workspace.windows = 3
written = #rules
handlers["window.open"]()
assert(#rules == written, "a third app changes nothing that is already full width")
workspace.windows = 1
handlers["window.destroy"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435, "closing back down to one window recenters the panel")
-- An empty scratchpad is about to be seeded with a single agent, so it is sized
-- as a console rather than as a workspace.
workspace.windows = 0
handlers["window.destroy"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435, "an empty console is still a console")
-- A hidden console is refitted on its way back in, so the count does not have to
-- be chased while it is off screen; every window on the desktop would otherwise
-- rewrite the rule.
workspace = { name = "special:scratchpad", visible = false, monitor = acer, windows = 4 }
monitor = dell
written = #rules
handlers["window.open"]()
assert(#rules == written, "a window opening elsewhere does not rewrite a hidden console")
-- A scratchpad nothing has opened yet has no workspace to read at all, and is
-- sized as the console the seed is about to put a single agent into.
workspace = nil
monitor = { width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 420 and right == 420 and bottom == 540, "a scratchpad that does not exist yet is sized as a console")
-- Back to a console on screen for the move and float checks.
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
monitor = acer
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435, "a single tiled window is a console")
-- A floating window on top of the console is not laid out by the gaps, so it
-- must not stretch the panel out from under the agent.
workspace.floats = 1
handlers["window.open"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435, "a floating window on the console keeps the panel")
-- Tiling that float (Super+T) makes it a second app, and floating it again
-- gives the panel back. Both arrive as window.update_rules.
workspace.floats, workspace.windows = 0, 2
handlers["window.update_rules"]()
top, right, bottom, left = gaps()
assert(left == 0 and right == 0, "tiling a float on the console restores the full width")
workspace.floats, workspace.windows = 1, 1
handlers["window.update_rules"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435, "floating it again recenters the panel")
-- Sending an app onto the scratchpad (Super+Alt+S) or off it (Super+Shift+1)
-- does not open or destroy anything, so the move itself has to refit.
workspace.floats, workspace.windows = 0, 2
handlers["window.move_to_workspace"]()
top, right, bottom, left = gaps()
assert(left == 0 and right == 0, "moving a second app onto the console restores the full width")
workspace.windows = 1
handlers["window.move_to_workspace"]()
top, right, bottom, left = gaps()
assert(left == 435 and right == 435, "moving it back off recenters the panel")
-- window.close still counts the window on its way out, and window.destroy
-- follows it with the settled count, so only destroy is hooked.
assert(handlers["window.close"] == nil, "window.close counts the window on its way out")
LUA
pass "the console covers half the work area at any monitor scale"
pass "the console is a centered panel until a second app joins it"
+58
View File
@@ -0,0 +1,58 @@
#!/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"
export INSTALLED_PACKAGES="$tmp_dir/installed" CALL_LOG="$tmp_dir/calls"
export PATH="$tmp_dir/bin:$ROOT/bin:$PATH"
# Keep the real package helpers, but contain every pacman transaction here.
cat > "$tmp_dir/bin/pacman" <<'SH'
#!/bin/bash
case "$1" in
-Q) grep -Fxq -- "$2" "$INSTALLED_PACKAGES" ;;
-S)
[[ ${FAIL_INSTALL:-0} == 0 ]] || exit 1
shift 3 # -S --noconfirm --needed
printf '%s\n' "$@" >> "$INSTALLED_PACKAGES"
printf '%s\n' "$@" >> "$CALL_LOG"
;;
*) exit 1 ;;
esac
SH
cat > "$tmp_dir/bin/sudo" <<'SH'
#!/bin/bash
[[ $1 == "pacman" ]] || exit 1
"$@"
SH
chmod +x "$tmp_dir/bin/"*
migration="$ROOT/migrations/1789444024.sh"
for kernels in linux-omarchy linux-t2 'linux-omarchy linux-t2'; do
read -ra installed <<< "$kernels"
printf '%s\n' linux linux-headers "${installed[@]}" > "$INSTALLED_PACKAGES"
: > "$CALL_LOG"
bash -euo pipefail "$migration" >/dev/null
for kernel in "${installed[@]}"; do
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "$kernel gets its headers"
done
: > "$CALL_LOG"
bash -euo pipefail "$migration" >/dev/null
[[ ! -s $CALL_LOG ]] || fail "header repair is idempotent"
pass "missing headers are repaired once for $kernels"
done
printf '%s\n' linux linux-aarch64 > "$INSTALLED_PACKAGES"
: > "$CALL_LOG"
bash -euo pipefail "$migration" >/dev/null
[[ ! -s $CALL_LOG ]] || fail "header repair skips unrelated kernels"
pass "header repair skips unrelated kernels"
echo linux-omarchy > "$INSTALLED_PACKAGES"
if FAIL_INSTALL=1 bash -euo pipefail "$migration" >/dev/null; then
fail "a failed header installation must leave the migration pending"
fi
pass "header installation failure is propagated"
@@ -0,0 +1,106 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1788848726.sh"
fixture="$ROOT/test/shell.d/fixtures/legacy-icon-font/omarchy.ttf"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
export FONT_TEST_HOME="$test_dir/home with spaces"
export FONT_TEST_PACKAGE="$test_dir/packaged-font.ttf"
export FONT_TEST_CACHE_LOG="$test_dir/cache.log"
legacy_font="$FONT_TEST_HOME/.local/share/fonts/omarchy.ttf"
# Redirect only the filesystem roots; run the real hash check and removal.
# Never change the developer's HOME or refresh their real font cache.
python3 - "$migration" "$test_dir/migration.sh" <<'PY'
import pathlib
import sys
source = pathlib.Path(sys.argv[1]).read_text()
source = source.replace('$HOME', '$FONT_TEST_HOME')
source = source.replace('/usr/share/fonts/omarchy/omarchy.ttf', '$FONT_TEST_PACKAGE')
pathlib.Path(sys.argv[2]).write_text(source)
PY
mkdir -p "$test_dir/bin"
cat > "$test_dir/bin/fc-cache" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >> "$FONT_TEST_CACHE_LOG"
exit "${FONT_TEST_CACHE_STATUS:-0}"
SH
chmod +x "$test_dir/bin/fc-cache"
reset_fonts() {
rm -rf "$FONT_TEST_HOME"
mkdir -p "$FONT_TEST_HOME/.local/share/fonts" "$FONT_TEST_HOME/.config"
cp "$ROOT/default/fonts/omarchy/omarchy.ttf" "$FONT_TEST_PACKAGE"
: > "$FONT_TEST_CACHE_LOG"
}
run_migration() {
PATH="$test_dir/bin:$PATH" bash -euo pipefail "$test_dir/migration.sh" > "$test_dir/output" 2>&1
}
reset_fonts
cp "$fixture" "$legacy_font"
cp "$fixture" "$FONT_TEST_HOME/.config/omarchy.ttf"
run_migration
[[ ! -e $legacy_font ]] || fail "stock font is removed from the actual user font directory"
cmp "$fixture" "$FONT_TEST_HOME/.config/omarchy.ttf" || fail "unrelated config path is untouched"
cmp "$ROOT/default/fonts/omarchy/omarchy.ttf" "$FONT_TEST_PACKAGE" || fail "packaged font is untouched"
[[ $(cat "$FONT_TEST_CACHE_LOG") == "-f" ]] || fail "font cache is refreshed after retirement"
pass "retire the known stock font at its real path and refresh the cache"
run_migration
[[ ! -e $legacy_font ]] || fail "a second run leaves the stock font retired"
pass "font retirement is idempotent"
reset_fonts
cp "$fixture" "$legacy_font"
printf 'custom modification\n' >> "$legacy_font"
cp "$legacy_font" "$test_dir/custom-font.ttf"
run_migration
cmp "$test_dir/custom-font.ttf" "$legacy_font" || fail "custom font is preserved"
pass "preserve a modified font with the legacy filename"
reset_fonts
cp "$fixture" "$test_dir/symlink-target.ttf"
ln -s "$test_dir/symlink-target.ttf" "$legacy_font"
run_migration
[[ -L $legacy_font ]] || fail "user font symlink is preserved"
cmp "$fixture" "$test_dir/symlink-target.ttf" || fail "symlink target is untouched"
pass "preserve user font symlinks even when they point to the stock font"
reset_fonts
run_migration
[[ ! -e $legacy_font ]] || fail "an absent user font is left absent"
pass "handle installs without a legacy user font"
reset_fonts
cp "$fixture" "$legacy_font"
rm "$FONT_TEST_PACKAGE"
if run_migration; then
fail "missing packaged font keeps the repair pending"
fi
cmp "$fixture" "$legacy_font" || fail "keep the stock font until its replacement is present"
[[ ! -s $FONT_TEST_CACHE_LOG ]] || fail "missing replacement stops before cache refresh"
pass "preserve the stock font and fail when the packaged replacement is missing"
reset_fonts
cp "$fixture" "$legacy_font"
if FONT_TEST_CACHE_STATUS=17 run_migration; then
fail "font cache failure keeps the repair pending"
fi
[[ ! -e $legacy_font ]] || fail "cache failure follows successful retirement"
run_migration
(( $(wc -l < "$FONT_TEST_CACHE_LOG") == 2 )) || fail "retry refreshes the cache after the file was removed"
pass "retry a failed cache refresh after successful font retirement"
if grep -q $'^retire\tomarchy.ttf\t' "$ROOT/bin/omarchy-upgrade-to-quattro"; then
fail "upgrader no longer treats the user font as a config file"
fi
pass "upgrader leaves font retirement to its post-upgrade migrations"
+4
View File
@@ -9,3 +9,7 @@ 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"
grep -Fxq 'BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' "$packaged_defaults" ||
fail "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"
pass "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"
+1 -1
View File
@@ -641,5 +641,5 @@ assert(
JS
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
[[ $font_charset == *"e900-e90d"* ]] || fail "Omarchy icon font includes every custom menu glyph"
[[ $font_charset == *"e900-e90e"* ]] || fail "Omarchy icon font includes every custom menu glyph"
pass "Omarchy icon font includes the official agent marks"
+270
View File
@@ -0,0 +1,270 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command mise
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
migration="$ROOT/migrations/1789095456.sh"
run_migration() {
local test_home="$1"
env -i \
HOME="$test_home" \
XDG_CACHE_HOME="$test_home/.cache" \
XDG_CONFIG_HOME="$test_home/.config" \
XDG_DATA_HOME="$test_home/.local/share" \
XDG_STATE_HOME="$test_home/.local/state" \
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
PATH=/usr/bin \
bash -euo pipefail "$migration"
}
run_mise() {
local test_home="$1"
shift
env -i \
HOME="$test_home" \
XDG_CACHE_HOME="$test_home/.cache" \
XDG_CONFIG_HOME="$test_home/.config" \
XDG_DATA_HOME="$test_home/.local/share" \
XDG_STATE_HOME="$test_home/.local/state" \
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
PATH=/usr/bin \
mise "$@"
}
mise_environment() {
local test_home="$1"
local project="$2"
(
cd "$project"
run_mise "$test_home" env -s bash
)
}
mise_path_active() {
local test_home="$1"
local project="$2"
local output
if ! output=$(mise_environment "$test_home" "$project" 2>/dev/null); then
return 1
fi
grep -F "$project/bin" <<<"$output" >/dev/null
}
assert_unsafe_variant_removed() {
local variant="$1"
local assignment="$2"
local variant_home="$test_dir/$variant-home"
local variant_config="$variant_home/Work/.mise.toml"
local variant_project="$variant_home/Work/tries/untrusted-repository"
mkdir -p "$variant_project/bin"
printf '[env]\n%s\n' "$assignment" >"$variant_config"
run_mise "$variant_home" trust "$variant_config" >/dev/null
mise_path_active "$variant_home" "$variant_project" || fail "$variant legacy config prepends the repository bin directory"
run_migration "$variant_home" >/dev/null
if mise_path_active "$variant_home" "$variant_project"; then
fail "$variant repository bin directory remains in PATH after migration"
fi
}
install_home="$test_dir/install-home"
install_log="$test_dir/install-mise.log"
mkdir -p "$install_home" "$test_dir/bin"
cat >"$test_dir/bin/mise" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$MISE_TEST_LOG"
SH
chmod +x "$test_dir/bin/mise"
env \
HOME="$install_home" \
MISE_TEST_LOG="$install_log" \
OMARCHY_SETUP_CONTEXT=runtime \
PATH="$test_dir/bin:/usr/bin" \
bash -euo pipefail -c 'source "$1"' bash "$ROOT/install/user/mise-work.sh"
[[ -d $install_home/Work/tries ]] || fail "installer creates the work and tries directories"
[[ ! -e $install_home/Work/.mise.toml ]] || fail "installer does not create a trusted Work Mise config"
[[ $(<"$install_log") == "use -g node@latest" ]] || fail "installer only invokes Mise for the global Node setup"
pass "new installs do not add project bin directories to PATH"
stock_home="$test_dir/stock-home"
stock_config="$stock_home/Work/.mise.toml"
stock_project="$stock_home/Work/tries/untrusted-repository"
mkdir -p "$stock_project/bin"
cat >"$stock_config" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
TOML
run_mise "$stock_home" trust "$stock_config" >/dev/null
mise_path_active "$stock_home" "$stock_project" || fail "legacy config prepends the repository bin directory"
run_migration "$stock_home" >/dev/null
[[ ! -e $stock_config ]] || fail "migration removes the stock Work Mise config"
cat >"$stock_config" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
TOML
if mise_path_active "$stock_home" "$stock_project"; then
fail "recreated Work config remains trusted after migration"
fi
run_migration "$stock_home" >/dev/null
[[ ! -e $stock_config ]] || fail "stock migration is idempotent"
pass "migration removes the repository bin directory and revokes the Work trust root"
assert_unsafe_variant_removed inline-comment '_.path = "{{ cwd }}/bin" # Omarchy default'
assert_unsafe_variant_removed single-quoted "_.path = '{{ cwd }}/bin'"
pass "migration removes annotated and single-quoted project bin paths"
custom_home="$test_dir/custom-home"
custom_config="$custom_home/Work/.mise.toml"
mkdir -p "$(dirname "$custom_config")"
cat >"$custom_config" <<'TOML'
[env]
KEEP = "yes"
_.path = "{{ cwd }}/bin"
# _.path = "{{ cwd }}/bin"
[tools]
ruby = "latest"
[other]
_.path = "{{ cwd }}/bin"
TOML
cp "$custom_config" "$test_dir/custom-original"
cat >"$test_dir/custom-expected" <<'TOML'
[env]
KEEP = "yes"
# _.path = "{{ cwd }}/bin"
[tools]
ruby = "latest"
[other]
_.path = "{{ cwd }}/bin"
TOML
chmod 600 "$custom_config"
run_mise "$custom_home" trust "$custom_config" >/dev/null
custom_output=$(run_migration "$custom_home")
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "migration preserves unrelated custom Mise settings"
[[ $(stat -c %a "$custom_config") == "600" ]] || fail "migration preserves custom config permissions"
grep -F "mise trust $custom_config" <<<"$custom_output" >/dev/null || fail "migration explains how to review and re-trust a custom config"
custom_backups=("$custom_config".bak.*)
[[ -f ${custom_backups[0]} ]] || fail "migration backs up a customized Mise config"
(( ${#custom_backups[@]} == 1 )) || fail "migration creates one custom config backup"
cmp -s "$test_dir/custom-original" "${custom_backups[0]}" || fail "custom config backup preserves the original"
run_migration "$custom_home" >/dev/null
custom_backups=("$custom_config".bak.*)
(( ${#custom_backups[@]} == 1 )) || fail "custom migration does not create another backup on rerun"
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "custom migration is idempotent"
pass "custom Mise settings, permissions, and original backup survive the repair"
unrelated_home="$test_dir/unrelated-home"
unrelated_config="$unrelated_home/Work/.mise.toml"
unrelated_project="$unrelated_home/Work/tries/untrusted-repository"
mkdir -p "$unrelated_project/bin"
printf '[env]\nKEEP = "yes"\n' >"$unrelated_config"
cp "$unrelated_config" "$test_dir/unrelated-original"
run_mise "$unrelated_home" trust "$unrelated_config" >/dev/null
run_migration "$unrelated_home" >/dev/null
cmp -s "$test_dir/unrelated-original" "$unrelated_config" || fail "unrelated Mise config remains unchanged"
unrelated_backups=("$unrelated_config".bak.*)
[[ ! -e ${unrelated_backups[0]} ]] || fail "unchanged Mise config is not backed up"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$unrelated_config"
if mise_path_active "$unrelated_home" "$unrelated_project"; then
fail "safe Work config retains its old trust grant"
fi
absent_home="$test_dir/absent-home"
absent_config="$absent_home/Work/.mise.toml"
absent_project="$absent_home/Work/tries/untrusted-repository"
mkdir -p "$(dirname "$absent_config")"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
run_mise "$absent_home" trust "$absent_config" >/dev/null
rm "$absent_config"
rmdir "$absent_home/Work"
run_migration "$absent_home" >/dev/null
[[ ! -e $absent_home/Work ]] || fail "migration does not retain a temporary Work directory"
mkdir -p "$absent_project/bin"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
if mise_path_active "$absent_home" "$absent_project"; then
fail "deleted Work directory retains its stale trust grant"
fi
pass "migration leaves unrelated configs alone and revokes dangling Work trust"
paranoid_home="$test_dir/paranoid-home"
paranoid_work="$paranoid_home/Work"
paranoid_config="$paranoid_work/.mise.toml"
paranoid_project="$paranoid_work/tries/untrusted-repository"
mkdir -p "$paranoid_project/bin"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
OMARCHY_TEST_MISE_PARANOID=true run_mise "$paranoid_home" trust "$paranoid_config" >/dev/null
OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project" || fail "paranoid legacy config prepends the repository bin directory"
rm -r "$paranoid_work"
OMARCHY_TEST_MISE_PARANOID=true run_migration "$paranoid_home" >/dev/null
[[ ! -e $paranoid_work ]] || fail "paranoid migration removes its temporary Work directory"
mkdir -p "$paranoid_project/bin"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
if OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project"; then
fail "paranoid migration retains content-bound trust for the deleted legacy config"
fi
pass "migration revokes stale content-bound trust in Mise paranoid mode"
ignored_home="$test_dir/ignored-home"
ignored_config="$ignored_home/Work/.mise.toml"
ignored_project="$ignored_home/Work/tries/untrusted-repository"
mkdir -p "$ignored_project/bin"
cat >"$ignored_config" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
KEEP = "yes"
TOML
run_mise "$ignored_home" trust "$ignored_config" >/dev/null
run_mise "$ignored_home" trust --ignore "$ignored_config" >/dev/null
ignored_output=$(run_migration "$ignored_home")
grep -F '{{ cwd }}/bin' "$ignored_config" >/dev/null && fail "ignored config retains the unsafe path"
grep -Fx 'KEEP = "yes"' "$ignored_config" >/dev/null || fail "ignored config keeps unrelated settings"
ignored_entries=("$ignored_home/.local/state/mise/ignored-configs/"*)
[[ -L ${ignored_entries[0]} ]] || fail "migration preserves the explicit Mise ignore marker"
(( ${#ignored_entries[@]} == 1 )) || fail "migration preserves exactly one Mise ignore marker"
ignored_target=$(readlink "${ignored_entries[0]}")
[[ $ignored_target == $ignored_home/Work || $ignored_target == $ignored_config ]] || fail "preserved Mise ignore marker still targets the Work config"
grep -F "remains ignored by Mise" <<<"$ignored_output" >/dev/null || fail "migration reports that the custom config remains ignored"
if mise_path_active "$ignored_home" "$ignored_project"; then
fail "ignored config becomes active after migration"
fi
pass "migration preserves an explicit decision to ignore the Work config"
symlink_home="$test_dir/symlink-home"
symlink_config="$symlink_home/Work/.mise.toml"
symlink_target="$test_dir/dotfiles-mise.toml"
mkdir -p "$(dirname "$symlink_config")"
cat >"$symlink_target" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
KEEP = "yes"
TOML
ln -s "$symlink_target" "$symlink_config"
run_migration "$symlink_home" >/dev/null
[[ -L $symlink_config ]] || fail "migration preserves a dotfile symlink"
grep -F '{{ cwd }}/bin' "$symlink_target" >/dev/null && fail "symlink target retains the unsafe path"
grep -Fx 'KEEP = "yes"' "$symlink_target" >/dev/null || fail "symlink target keeps unrelated settings"
pass "custom dotfile symlinks survive the repair"
@@ -0,0 +1,248 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1789325478.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin" "$scratch/drop-ins"
export PATH="$scratch/bin:$ROOT/bin:$PATH"
export CALL_LOG="$scratch/calls"
export INSTALLED_PACKAGES="$scratch/packages"
export OMARCHY_KERNEL_LIMINE_CONF="$scratch/limine"
export OMARCHY_KERNEL_LIMINE_DROP_INS="$scratch/drop-ins"
export OMARCHY_KERNEL_REBUILD_MARKER="$scratch/state/1789325478"
kernel="linux-omarchy"
boot_order='BOOT_ORDER="linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"'
# Exercise the real package helpers, including their post-install queries.
cat > "$scratch/bin/pacman" <<'SH'
#!/bin/bash
case "$1" in
-Q) grep -Fxq "$2" "$INSTALLED_PACKAGES" ;;
-S)
printf 'pacman %s\n' "$*" >> "$CALL_LOG"
[[ ${INSTALL_FAIL:-0} == "0" ]] || exit 1
for arg in "$@"; do
[[ $arg == -* ]] || printf '%s\n' "$arg" >> "$INSTALLED_PACKAGES"
done
;;
*) exit 99 ;;
esac
SH
cat > "$scratch/bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo %s\n' "$*" >> "$CALL_LOG"
case "$1" in
pacman | mkdir | touch | sed | tee | install | limine-mkinitcpio | limine-entry-tool) exec "$@" ;;
*) exit 99 ;;
esac
SH
cat > "$scratch/bin/limine-mkinitcpio" <<'SH'
#!/bin/bash
[[ ${REBUILD_FAIL:-0} == "0" ]]
SH
cat > "$scratch/bin/limine-entry-tool" <<'SH'
#!/bin/bash
[[ $* == "--tree" ]] || exit 99
printf '%s\n' 'Omarchy' ' linux-ptl' ' linux-omarchy-ptl-novrr-mm' ' linux-omarchy-bore' ' linux-omarchy-fallback' ' Snapshots'
if [[ ${MISSING_ENTRY:-0} == "0" ]]; then
printf '%s\n' ' linux-omarchy'
fi
SH
cat > "$scratch/bin/omarchy-state" <<'SH'
#!/bin/bash
[[ $* == "set reboot-required" ]] || exit 99
printf 'state %s\n' "$*" >> "$CALL_LOG"
SH
cat > "$scratch/bin/uname" <<'SH'
#!/bin/bash
case "$1" in
-m) printf '%s\n' "${TEST_ARCH:-x86_64}" ;;
-r) printf '%s\n' "${TEST_KERNEL_RELEASE:-7.2.5-arch1-1}" ;;
*) exit 99 ;;
esac
SH
cat > "$scratch/bin/omarchy-notification-dismiss" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$scratch/bin/"*
reset_fixture() {
: > "$CALL_LOG"
printf '%s\n' linux-ptl linux-ptl-headers > "$INSTALLED_PACKAGES"
rm -f "$OMARCHY_KERNEL_REBUILD_MARKER" "$OMARCHY_KERNEL_LIMINE_DROP_INS/"*
cat > "$OMARCHY_KERNEL_LIMINE_CONF" <<'CONF'
KERNEL_CMDLINE[default]="root=UUID=keep-me rw cryptdevice=UUID=keep-me:root"
BOOT_ORDER="*, *fallback, Snapshots"
ENABLE_UKI=yes
CONF
cp "$OMARCHY_KERNEL_LIMINE_CONF" "$scratch/original-limine"
}
run_migration() {
bash -euo pipefail "$migration" > "$scratch/output" 2>&1
}
assert_preferred() {
grep -Fxq "$boot_order" "$1" || fail "Omarchy kernels are preferred in $1"
}
assert_skipped() {
[[ ! -s $CALL_LOG ]] || fail "excluded systems do not change" "$(<"$CALL_LOG")"
cmp -s "$OMARCHY_KERNEL_LIMINE_CONF" "$scratch/original-limine" || fail "excluded systems keep their boot settings"
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "excluded systems do not get a completion marker"
}
for old_kernel in linux linux-lts linux-zen linux-ptl linux-omarchy-ptl-novrr-mm; do
reset_fixture
printf '%s\n' "$old_kernel" "$old_kernel-headers" > "$INSTALLED_PACKAGES"
run_migration
grep -Fxq "$kernel" "$INSTALLED_PACKAGES" || fail "the generic kernel is installed"
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "its headers are installed"
grep -Fxq "$old_kernel" "$INSTALLED_PACKAGES" || fail "the previous kernel remains available"
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
pass "$old_kernel systems receive the generic Omarchy kernel and retain their recovery kernel"
done
for installed in linux-t2 $'linux-t2\nlinux\nlinux-ptl\nlinux-omarchy'; do
reset_fixture
printf '%s\n' "$installed" > "$INSTALLED_PACKAGES"
run_migration
assert_skipped
done
pass "linux-t2 systems are skipped even with other kernels installed"
reset_fixture
TEST_KERNEL_RELEASE=7.2.5-arch1-T2 run_migration
assert_skipped
pass "a running T2 kernel is excluded even if its package is no longer installed"
reset_fixture
TEST_ARCH=aarch64 run_migration
assert_skipped
pass "ARM systems cannot receive an x86_64 kernel"
reset_fixture
printf '%s\n' linux-omarchy-ptl-novrr-mm > "$INSTALLED_PACKAGES"
mkdir -p "$scratch/state" "$scratch/user-markers" "$scratch/omarchy/migrations"
export OMARCHY_PTL_REBUILD_MARKER="$scratch/state/1789095456"
export OMARCHY_MIGRATION_STATE="$scratch/user-markers"
touch "$OMARCHY_PTL_REBUILD_MARKER" "$OMARCHY_MIGRATION_STATE/1789095456.sh"
cp "$migration" "$scratch/omarchy/migrations/"
[[ ! -e $ROOT/migrations/1789095456.sh ]] || fail "the superseded migration must not install the PTL variant first"
pending=$(OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" --pending)
[[ $pending == "1789325478.sh" ]] || fail "the renamed migration is pending after completing the old migration"
OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" > "$scratch/output" 2>&1
grep -Fxq "$kernel" "$INSTALLED_PACKAGES" || fail "the old completion markers cannot skip the generic kernel"
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER && -f $OMARCHY_MIGRATION_STATE/1789325478.sh ]] || fail "new machine and user completion markers are recorded"
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
pass "users who completed the PTL migration run the renamed migration with fresh completion markers"
reset_fixture
for name in dell-xps-panther-lake zz-dell-xps-panther-lake; do
printf '%s\n' 'BOOT_ORDER="linux-ptl*, *fallback, Snapshots"' > "$OMARCHY_KERNEL_LIMINE_DROP_INS/$name.conf"
done
cp "$OMARCHY_KERNEL_LIMINE_CONF" "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf"
run_migration
grep -Fxq "pacman -S --noconfirm --needed $kernel $kernel-headers" "$CALL_LOG" || fail "both new packages are installed"
grep -Fxq linux-ptl "$INSTALLED_PACKAGES" || fail "the old kernel is kept for recovery"
grep -Fxq linux-ptl-headers "$INSTALLED_PACKAGES" || fail "the old kernel headers are kept"
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
diff -u <(sed '/^BOOT_ORDER=/d; /^$/d' "$scratch/original-limine") \
<(sed '/^BOOT_ORDER=/d; /^$/d' "$OMARCHY_KERNEL_LIMINE_CONF") || fail "kernel command line and unrelated settings are preserved"
grep -Fxq "sudo limine-mkinitcpio $kernel" "$CALL_LOG" || fail "the new kernel's boot image is rebuilt"
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "successful completion is recorded"
grep -Fxq 'state set reboot-required' "$CALL_LOG" || fail "the updater must offer a reboot when retaining the old kernel"
pass "new packages install, the central boot order is set, and the old kernel remains available"
: > "$CALL_LOG"
run_migration
[[ ! -s $CALL_LOG ]] || fail "another user's run does not repeat the machine-wide migration"
pass "repeat runs are a no-op after successful completion"
reset_fixture
printf '%s\n' "$kernel" >> "$INSTALLED_PACKAGES"
run_migration
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "missing headers install when the kernel is already present"
pass "a partially installed kernel gets its missing headers"
reset_fixture
printf '%s\n' "$kernel" "$kernel-headers" >> "$INSTALLED_PACKAGES"
run_migration
! grep -q '^pacman -S' "$CALL_LOG" || fail "already installed packages are not reinstalled"
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
pass "existing new packages still receive the config repair and boot rebuild"
reset_fixture
if INSTALL_FAIL=1 run_migration; then
fail "package installation failure must fail the migration"
fi
cmp -s "$OMARCHY_KERNEL_LIMINE_CONF" "$scratch/original-limine" || fail "install failure leaves the config untouched"
! grep -q 'limine-mkinitcpio' "$CALL_LOG" || fail "install failure does not rebuild"
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "install failure stays pending"
pass "package failures leave the old boot setup intact and the migration pending"
reset_fixture
if REBUILD_FAIL=1 run_migration; then
fail "boot rebuild failure must fail the migration"
fi
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "rebuild failure stays pending"
! grep -q '^state ' "$CALL_LOG" || fail "rebuild failure must not request a reboot"
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
: > "$CALL_LOG"
run_migration
grep -Fxq "sudo limine-mkinitcpio $kernel" "$CALL_LOG" || fail "retry must rebuild even after config and packages are repaired"
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "retry records successful completion"
pass "a failed rebuild is retried even after config and package changes succeeded"
reset_fixture
if MISSING_ENTRY=1 run_migration; then
fail "a silently skipped kernel build must fail the migration"
fi
[[ ! -e $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "a missing boot entry stays pending"
! grep -q '^state ' "$CALL_LOG" || fail "a missing boot entry must not request a reboot"
run_migration
[[ -f $OMARCHY_KERNEL_REBUILD_MARKER ]] || fail "a missing boot entry can be repaired on retry"
pass "older Omarchy variants and fallback entries cannot satisfy generic kernel verification"
reset_fixture
cat >> "$OMARCHY_KERNEL_LIMINE_CONF" <<'CONF'
BOOT_ORDER="linux-omarchy-*, *, *fallback, Snapshots"
BOOT_ORDER = 'linux-lts, *, *fallback, Snapshots'
CONF
run_migration
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
[[ $(grep -c '^BOOT_ORDER=' "$OMARCHY_KERNEL_LIMINE_CONF") == "1" ]] || fail "only one boot order is written"
! grep -Eq '^[[:space:]]+BOOT_ORDER' "$OMARCHY_KERNEL_LIMINE_CONF" || fail "a later custom assignment cannot override first position"
pass "the exact generic kernel takes first position over previous PTL and custom orders"
reset_fixture
for name in omarchy-defaults dell-xps-panther-lake zz-dell-xps-panther-lake; do
printf '%s\n' 'BOOT_ORDER="linux-ptl*, *fallback, Snapshots"' > "$OMARCHY_KERNEL_LIMINE_DROP_INS/$name.conf"
done
printf '%s\n' 'ENABLE_SORT=yes' >> "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf"
cp "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf.pacnew"
run_migration
effective_order=$(bash -c 'declare -A KERNEL_CMDLINE; for conf in "$1/"*.conf "$2"; do source "$conf"; done; printf "%s" "$BOOT_ORDER"' -- "$OMARCHY_KERNEL_LIMINE_DROP_INS" "$OMARCHY_KERNEL_LIMINE_CONF")
[[ $effective_order == "linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots" ]] || fail "the central config wins over old and customized drop-ins"
grep -Fxq 'ENABLE_SORT=yes' "$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf" || fail "custom packaged settings survive"
cmp -s "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" \
"$OMARCHY_KERNEL_LIMINE_DROP_INS/omarchy-defaults.conf.pacnew" || fail "the pacnew is left for the administrator to merge"
pass "the central boot order overrides legacy drop-ins and pacnew files without rewriting them"
reset_fixture
rm "$OMARCHY_KERNEL_LIMINE_CONF"
run_migration
assert_preferred "$OMARCHY_KERNEL_LIMINE_CONF"
pass "a missing central config is created with the exact kernel first"
+29
View File
@@ -15,6 +15,14 @@ printf 'drop:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/omarchy-pkg-drop"
# omarchy-remove-ai-claude quits the running app before deleting its state;
# a real pkill here would take the developer's own Claude with it.
cat >"$tmp_dir/bin/pkill" <<'SCRIPT'
#!/bin/bash
printf 'pkill:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/pkill"
# 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
@@ -55,6 +63,27 @@ pass "ChatGPT removal keeps the Codex CLI's runtime cache"
[[ -d $HOME/.codex ]] || fail "ChatGPT removal keeps the Codex CLI's config"
pass "ChatGPT removal keeps the Codex CLI's config"
# The Claude Code CLI ships in its own package and keeps its state in
# ~/.claude, ~/.claude.json, and ~/.cache/claude-cli-nodejs, so removing the
# desktop app must not take it.
fresh_home
mkdir -p "$HOME/.config/Claude" "$HOME/.cache/Claude" "$HOME/.cache/claude-cli-nodejs" "$HOME/.claude"
touch "$HOME/.claude.json"
"$ROOT/bin/omarchy-remove-ai-claude" >/dev/null
for gone in .config/Claude .cache/Claude; do
[[ ! -e $HOME/$gone ]] || fail "Claude removal deletes the desktop app's config and caches" "$gone"
done
pass "Claude removal deletes the desktop app's config and caches"
for kept in .claude .claude.json .cache/claude-cli-nodejs; do
[[ -e $HOME/$kept ]] || fail "Claude removal keeps the Claude Code CLI's state" "$kept"
done
pass "Claude removal keeps the Claude Code CLI's state"
grep -qx 'pkill:-x claude-desktop' "$TEST_LOG" || fail "Claude removal quits the running app before deleting its state"
pass "Claude removal quits the running app before deleting its state"
# LM Studio's models follow a relocatable home, named only by the pointer file.
fresh_home
mkdir -p "$tmp_dir/relocated-models/models"
+111
View File
@@ -299,3 +299,114 @@ grep -F 'move = { "(monitor_w-monitor_h*2/9-40)", "(monitor_h-monitor_h/4-40)" }
grep -F 'move = { "(monitor_w-monitor_h*3/10-40)", "(monitor_h-monitor_h*27/80-40)" }' "$webcam_rules" >/dev/null || \
fail "large webcam starts at its final corner position"
pass "webcam size rules place the initial window in its final corner"
# The stop path reads the recording state file back and uses its contents as a
# path -- ffmpeg writes beside it, `mv` replaces it, `rm -f` deletes its
# preview -- so it has to live in the per-user runtime directory rather than
# under a name in world-writable /tmp that another account can create first.
recording_dir="$tmp_dir/recordings"
mkdir -p "$recording_dir"
cat >"$stub_bin/pgrep" <<'SH'
#!/bin/bash
exit 1
SH
cat >"$stub_bin/omarchy-hyprland-monitor-focused" <<'SH'
#!/bin/bash
printf 'DP-1\n'
SH
cat >"$stub_bin/gpu-screen-recorder" <<'SH'
#!/bin/bash
for i in "$@"; do
[[ -n ${take_next:-} ]] && { : >"$i"; break; }
[[ $i == "-o" ]] && take_next=1
done
sleep 5
SH
cat >"$stub_bin/omarchy-shell" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$stub_bin"/pgrep "$stub_bin"/omarchy-hyprland-monitor-focused \
"$stub_bin"/gpu-screen-recorder "$stub_bin"/omarchy-shell
# Compare that name across the run rather than demanding it be absent: the
# whole point of the finding is that anyone can own it already, and a leftover
# from a pre-fix recording would red-light the fixed script.
tmp_state="/tmp/omarchy-screenrecord-filename"
tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null || true)
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
pkill -f "$stub_bin/gpu-screen-recorder" 2>/dev/null || true
[[ $(stat -c '%y %s' "$tmp_state" 2>/dev/null) == "$tmp_state_before" ]] ||
fail "screen recording keeps no state under a fixed /tmp name"
pass "screen recording keeps no state under a fixed /tmp name"
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-filename ]] ||
fail "the recording state file lives in the per-user runtime directory" \
"$(ls -a "$XDG_RUNTIME_DIR")"
pass "the recording state file lives in the per-user runtime directory"
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-filename") == "$recording_dir"/* ]] ||
fail "the recording state file names the recording that was started" \
"$(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-filename")"
pass "the recording state file names the recording that was started"
# The :-/tmp fallback would reopen the hole this PR closes. A recording
# started without a session runtime dir has to land under the state directory.
state_home="$tmp_dir/state-home"
mkdir -p "$state_home/omarchy" "$tmp_dir/home-fallback"
chmod 755 "$state_home/omarchy"
tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null || true)
env -u XDG_RUNTIME_DIR \
HOME="$tmp_dir/home-fallback" \
XDG_STATE_HOME="$state_home" \
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
pkill -f "$stub_bin/gpu-screen-recorder" 2>/dev/null || true
[[ $(stat -c '%y %s' "$tmp_state" 2>/dev/null) == "$tmp_state_before" ]] ||
fail "without a runtime dir, screen recording still keeps no state under a fixed /tmp name"
pass "without a runtime dir, screen recording still keeps no state under a fixed /tmp name"
fallback_file="$state_home/omarchy/omarchy-screenrecord-filename"
[[ -s $fallback_file ]] ||
fail "without a runtime dir the recording state file lives in the state directory" \
"$(ls -la "$state_home/omarchy" 2>/dev/null || true)"
pass "without a runtime dir the recording state file lives in the state directory"
[[ $(<"$fallback_file") == "$recording_dir"/* ]] ||
fail "the fallback state file names the recording that was started" \
"$(<"$fallback_file")"
pass "the fallback state file names the recording that was started"
# The overlay resizer reads the region file the recorder writes, so the two
# have to resolve the same fallback as well as the same runtime dir.
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
echo "800x600+100+100" >"$state_home/omarchy/omarchy-screenrecord-region"
env -u XDG_RUNTIME_DIR \
HOME="$tmp_dir/home-fallback" \
XDG_STATE_HOME="$state_home" \
"$ROOT/bin/omarchy-capture-webcam-resize" reset
printf '%s\n' \
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 133, y = 150 })' \
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 727, y = 510 })' >"$expected_hyprctl_args"
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
fail "without a runtime dir the webcam anchors to the recorded region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
fi
pass "without a runtime dir the webcam anchors to the recorded region"
mode=$(stat -c '%a' "$state_home/omarchy" 2>/dev/null || stat -f '%Lp' "$state_home/omarchy")
[[ $mode == "700" ]] || fail "fallback directory is private even when it already existed" "mode: $mode"
pass "fallback directory is private even when it already existed"
+5 -2
View File
@@ -31,10 +31,13 @@ allowed = {
# them so the hook does not exist where it does not apply.
"/usr/lib/systemd/system-sleep",
# Written through a variable, so the scan below cannot see them at the point
# they are written. Both drop configuration into another project's tree rather
# than Omarchy's, which is why neither is a candidate for omarchy-settings.
# they are written. These drop configuration into another project's tree
# rather than Omarchy's and are not candidates for omarchy-settings.
"/usr/share/chromium/extensions",
"/usr/lib/firefox/distribution",
# Claude's extension is registered only when the user selects Claude.
"/usr/share/google-chrome/extensions",
"/usr/share/microsoft-edge/extensions",
# Static content that belongs in omarchy-settings. It cannot move there in the
# same release that first ships omarchy-update-system-pkgs-when-conflicted: the
# upgrade carrying the handler is the one that would hit the conflict, and the
+9 -1
View File
@@ -15,6 +15,14 @@ cat >"$stub_bin/sudo" <<'STUB'
exec "$@"
STUB
# omarchy-update-pacman wraps the transaction in a real PID 1 scope; the tests
# must stay inside the fixture, so drop the wrapper's options and run the command.
cat >"$stub_bin/systemd-run" <<'STUB'
#!/bin/bash
while [[ $1 == -* ]]; do shift; done
exec "$@"
STUB
# Fails the first -Syu with the report under test, then succeeds unless the case
# asked for the retry to fail too.
cat >"$stub_bin/pacman" <<'STUB'
@@ -40,7 +48,7 @@ fi
echo "upgrade complete"
STUB
chmod +x "$stub_bin/sudo" "$stub_bin/pacman"
chmod +x "$stub_bin/sudo" "$stub_bin/systemd-run" "$stub_bin/pacman"
replaced="$test_tmp/replaced"
+137
View File
@@ -0,0 +1,137 @@
#!/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"
log_file="$test_tmp/keyring.log"
mkdir -p "$stub_bin"
# Behavior is driven by env vars so each case can pick its failure point:
# KEYRING_TEST_PKG_MISSING exit status of omarchy-pkg-missing (default 1: installed)
# KEYRING_TEST_LIST_FAIL_ON which --list-keys call fails, counted per run (default: none)
# KEYRING_TEST_RECV_STATUS exit status of --recv-keys (default 0)
# KEYRING_TEST_REINSTALL_STATUS exit status of the archlinux-keyring reinstall (default 0)
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo' >>"$KEYRING_TEST_LOG"
for arg in "$@"; do
printf '\t%s' "$arg" >>"$KEYRING_TEST_LOG"
done
printf '\n' >>"$KEYRING_TEST_LOG"
if [[ $1 == "pacman-key" && $2 == "--list-keys" ]]; then
calls_file="$KEYRING_TEST_DIR/list-calls"
calls=$(( $(cat "$calls_file" 2>/dev/null || echo 0) + 1 ))
echo "$calls" >"$calls_file"
if [[ ${KEYRING_TEST_LIST_FAIL_ON:-} == "$calls" ]]; then
exit 1
fi
exit 0
fi
if [[ $1 == "pacman-key" && $2 == "--recv-keys" ]]; then
exit "${KEYRING_TEST_RECV_STATUS:-0}"
fi
if [[ $1 == "pacman-key" && $2 == "--lsign-key" ]]; then
exit 0
fi
if [[ $1 == "pacman" && $* == *archlinux-keyring* ]]; then
exit "${KEYRING_TEST_REINSTALL_STATUS:-0}"
fi
exit 0
SH
chmod +x "$stub_bin/sudo"
cat >"$stub_bin/omarchy-pkg-missing" <<'SH'
#!/bin/bash
exit "${KEYRING_TEST_PKG_MISSING:-1}"
SH
chmod +x "$stub_bin/omarchy-pkg-missing"
cat >"$stub_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
printf 'pkg-add\t%s\n' "$1" >>"$KEYRING_TEST_LOG"
exit 0
SH
chmod +x "$stub_bin/omarchy-pkg-add"
run_keyring() {
KEYRING_TEST_LOG="$log_file" \
KEYRING_TEST_DIR="$test_tmp" \
PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-update-keyring" "$@"
}
# Everything healthy: the key and package are present, the reinstall works.
: >"$log_file"
rm -f "$test_tmp/list-calls"
run_keyring >"$test_tmp/ok.out"
grep -F "Keys are correct" "$test_tmp/ok.out" >/dev/null ||
fail "update-keyring reports success when the keyring is healthy" "$(cat "$test_tmp/ok.out")"
pass "update-keyring reports success when the keyring is healthy"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring$' "$log_file" ||
fail "update-keyring still reinstalls archlinux-keyring" "$(cat "$log_file")"
pass "update-keyring still reinstalls archlinux-keyring"
# Key and package missing: the full populate path runs and verifies at the end.
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_PKG_MISSING=0 run_keyring >"$test_tmp/populate.out"
grep -F "Keys are correct" "$test_tmp/populate.out" >/dev/null ||
fail "update-keyring populates a missing keyring and reports success" "$(cat "$test_tmp/populate.out")"
for expected in 'recv-keys' 'lsign-key' $'pkg-add\tomarchy-keyring'; do
grep -Eq "$expected" "$log_file" ||
fail "update-keyring populates a missing keyring and reports success" "$(cat "$log_file")"
done
pass "update-keyring populates a missing keyring and reports success"
# recv-keys failing must stop the script, not end in "Keys are correct".
: >"$log_file"
rm -f "$test_tmp/list-calls"
if KEYRING_TEST_PKG_MISSING=0 KEYRING_TEST_RECV_STATUS=1 run_keyring >"$test_tmp/recv.out" 2>&1; then
fail "update-keyring fails when recv-keys fails"
fi
if grep -F "Keys are correct" "$test_tmp/recv.out" >/dev/null; then
fail "update-keyring fails when recv-keys fails" "$(cat "$test_tmp/recv.out")"
fi
if grep -q 'lsign-key' "$log_file"; then
fail "update-keyring stops at the failed recv instead of signing anyway" "$(cat "$log_file")"
fi
pass "update-keyring fails when recv-keys fails"
# A failed archlinux-keyring reinstall must not end in success either.
: >"$log_file"
rm -f "$test_tmp/list-calls"
if KEYRING_TEST_REINSTALL_STATUS=1 run_keyring >"$test_tmp/reinstall.out" 2>&1; then
fail "update-keyring fails when the archlinux-keyring reinstall fails"
fi
if grep -F "Keys are correct" "$test_tmp/reinstall.out" >/dev/null; then
fail "update-keyring fails when the archlinux-keyring reinstall fails" "$(cat "$test_tmp/reinstall.out")"
fi
pass "update-keyring fails when the archlinux-keyring reinstall fails"
# The closing check is what backs the success line: the first --list-keys
# passes (key present, populate skipped), the verifying one fails.
: >"$log_file"
rm -f "$test_tmp/list-calls"
if KEYRING_TEST_LIST_FAIL_ON=2 run_keyring >"$test_tmp/verify.out" 2>&1; then
fail "update-keyring fails when the final key check fails"
fi
if grep -F "Keys are correct" "$test_tmp/verify.out" >/dev/null; then
fail "update-keyring fails when the final key check fails" "$(cat "$test_tmp/verify.out")"
fi
pass "update-keyring fails when the final key check fails"
+9 -1
View File
@@ -17,6 +17,14 @@ cat >"$stub_bin/sudo" <<'STUB'
exec "$@"
STUB
# omarchy-update-pacman wraps the transaction in a real PID 1 scope; the tests
# must stay inside the fixture, so drop the wrapper's options and run the command.
cat >"$stub_bin/systemd-run" <<'STUB'
#!/bin/bash
while [[ $1 == -* ]]; do shift; done
exec "$@"
STUB
# Fails the first -Syu with the report under test, then succeeds. Every call
# records its arguments and which of its streams reached a terminal: pacman puts
# its questions on stderr once it is not running --noconfirm, so a retry meant
@@ -39,7 +47,7 @@ fi
echo "upgrade complete"
STUB
chmod +x "$stub_bin/sudo" "$stub_bin/pacman"
chmod +x "$stub_bin/sudo" "$stub_bin/systemd-run" "$stub_bin/pacman"
# Everything a blocked qemu-common upgrade leaves on stderr, and no more. The
# ":: ... Remove qemu-block-gluster? [y/N]" pacman asked is deliberately absent:
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/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/sudo" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >"$SUDO_CALL_LOG"
STUB
chmod +x "$stub_bin/sudo"
run_helper() {
PATH="$stub_bin:$PATH" SUDO_CALL_LOG="$test_tmp/call" "$ROOT/bin/omarchy-update-pacman" "$@"
}
# The scope wrapper only applies on a systemd-booted host, so expect what the
# helper's own booted check would decide for this machine.
if [[ -d /run/systemd/system && ! -L /run/systemd/system ]]; then
expected_scope="systemd-run --scope --quiet --collect "
else
expected_scope=""
fi
run_helper -Syu --noconfirm
[[ $(cat "$test_tmp/call") == "env OMARCHY_UPDATE_PACMAN=1 ${expected_scope}pacman -Syu --noconfirm" ]] ||
fail "helper composes the guarded pacman invocation" "$(cat "$test_tmp/call")"
pass "helper composes the guarded pacman invocation"
LC_ALL=C run_helper -Syu
[[ $(cat "$test_tmp/call") == "env OMARCHY_UPDATE_PACMAN=1 LC_ALL=C ${expected_scope}pacman -Syu" ]] ||
fail "helper forwards LC_ALL to the transaction" "$(cat "$test_tmp/call")"
pass "helper forwards LC_ALL to the transaction"
+2 -1
View File
@@ -174,7 +174,8 @@ assert(
assert(
batteryService.includes('property string activePowerProfile') &&
batteryService.includes('UPower.onBattery && activePowerProfile === "power-saver"') &&
batteryService.includes('["powerprofilesctl", "get"]') &&
batteryService.includes('"get-property", "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", "ActiveProfile"') &&
!batteryService.includes('["powerprofilesctl"') &&
batteryService.includes('interval: 2000'),
'the battery service tracks the active power-saver profile'
)
-8
View File
@@ -13,14 +13,6 @@ grep -q 'run_logged .*hardware/dell-xps13-sidecar-amps.sh' "$all" ||
fail "the sidecar amplifier workaround runs during hardware setup"
pass "the sidecar amplifier workaround runs during hardware setup"
# The apply step rebuilds the boot image, so it has to see the Panther Lake
# kernel that ptl-kernel.sh swaps in rather than the stock one it replaces.
ptl_line=$(grep -n 'hardware/intel/ptl-kernel.sh' "$all" | cut -d: -f1)
amps_line=$(grep -n 'hardware/dell-xps13-sidecar-amps.sh' "$all" | cut -d: -f1)
((ptl_line < amps_line)) ||
fail "the sidecar amplifier workaround runs after the Panther Lake kernel swap"
pass "the sidecar amplifier workaround runs after the Panther Lake kernel swap"
[[ -n $migration ]] || fail "a migration enables the workaround on existing installs"
pass "a migration enables the workaround on existing installs"