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:
commit
4bd593f4ed
105 files changed
+2541
-361
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
Executable
+39
@@ -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"
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in new issue
Block a user