Files
omarchycn/test/shell.d/migrate-notify-test.sh
T
David Heinemeier HanssonandClaude Opus 5 03902f2460 Never notify about pending migrations during an update
The retired omarchy-update-user-notify.path stays loaded in sessions that
started before the update removing it, and pacman writes the migrations
directory mid-transaction, so it fired a critical toast for migrations that
omarchy-migrate was about to apply a step later. Migration 1785095882 stops
that watcher, but migrations run after pacman, so it lands 11 seconds too
late to prevent the toast it exists to retire.

Check the lock omarchy-update holds for its whole pipeline instead of
trusting that no trigger exists. That covers the stale watcher and anything
added later: during an update every pending migration is by definition
already being applied. The check repeats after waiting for the notification
server, which is long enough for an update to start underneath it.

Only this user's runtime directory is read, never the /tmp path the updater
falls back to without XDG_RUNTIME_DIR. A shared lock file belongs to whoever
created it first, so honouring it would let one user silence another user's
notification; a redundant toast is the better failure.

The sleep inhibitor now starts with the lock descriptor closed. It outlives
the step that starts it, so an update killed before restore_update_inhibitors
left it holding the flock indefinitely. That already blocked later updates,
and now that the notifier reads the same lock it would have silenced
migration notices at every login.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:41:03 -07:00

150 lines
6.0 KiB
Bash

#!/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"
test_home="$test_tmp/home"
mkdir -p "$stub_bin" "$test_home"
cat >"$stub_bin/omarchy-migrate" <<'SH'
#!/bin/bash
if [[ ${1:-} == "--pending" && ${OMARCHY_TEST_PENDING_MIGRATIONS:-0} == 1 ]]; then
echo 200-migration.sh
exit 0
else
exit 1
fi
SH
chmod +x "$stub_bin/omarchy-migrate"
cat >"$stub_bin/systemd-run" <<'SH'
#!/bin/bash
if [[ ${OMARCHY_TEST_SYSTEMD_RUN:-run} == "fail" ]]; then
exit 1
fi
command=${!#}
bash -c "$command"
SH
chmod +x "$stub_bin/systemd-run"
# Waiting for the notification server is the notifier's one long pause, so it is
# also where an update can start underneath it. Stand one up from inside the
# wait to prove the notifier re-checks afterwards instead of sending a toast it
# decided to send before the update existed.
cat >"$stub_bin/omarchy-notification-wait" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_LOCK_DURING_WAIT:-0} == 1 ]] || exit 0
lock="$XDG_RUNTIME_DIR/omarchy-update.lock"
: >"$lock"
# Hold the lock through a bash-allocated descriptor rather than `flock <file>
# <command>`: bash marks those close-on-exec, so the holder owns the lock alone
# and killing it releases immediately, with no exec'd child to outlive it.
bash -c 'exec {fd}>"$1"; flock -n $fd || exit 1; sleep 60' _ "$lock" &
echo "$!" >"$OMARCHY_TEST_LOCK_HOLDER_PID"
for _ in {1..200}; do
flock -n "$lock" true 2>/dev/null || exit 0
sleep 0.05
done
echo "stub could not establish the update lock" >&2
exit 1
SH
chmod +x "$stub_bin/omarchy-notification-wait"
cat >"$stub_bin/omarchy-notification-send" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"
SH
chmod +x "$stub_bin/omarchy-notification-send"
runtime_dir="$test_tmp/runtime"
mkdir -p "$runtime_dir"
run_notify() {
HOME="$test_home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
XDG_RUNTIME_DIR="$runtime_dir" \
OMARCHY_TEST_PENDING_MIGRATIONS="$1" \
OMARCHY_TEST_NOTIFY_ARGS="$test_tmp/notify-args" \
OMARCHY_TEST_SYSTEMD_RUN="${2:-run}" \
OMARCHY_TEST_LOCK_DURING_WAIT="${OMARCHY_TEST_LOCK_DURING_WAIT:-0}" \
OMARCHY_TEST_LOCK_HOLDER_PID="$test_tmp/lock-holder-pid" \
"$ROOT/bin/omarchy-migrate-notify"
}
run_notify 0 >"$test_tmp/not-pending.out" 2>"$test_tmp/not-pending.err"
[[ ! -s $test_tmp/not-pending.out ]] || fail "migration notifier stays quiet on stdout without pending migrations"
[[ ! -s $test_tmp/not-pending.err ]] || fail "migration notifier stays quiet on stderr without pending migrations"
pass "migration notifier ignores users with no pending migrations"
run_notify 1 fail >"$test_tmp/pending.out" 2>"$test_tmp/pending.err"
grep -q 'Omarchy has pending migrations' "$test_tmp/pending.err" || fail "migration notifier explains pending migrations without notification system"
grep -q '200-migration.sh' "$test_tmp/pending.err" || fail "migration notifier lists pending migration names"
pass "migration notifier reports pending migrations"
run_notify 1 >"$test_tmp/notified.out" 2>"$test_tmp/notified.err"
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null || fail "migration notifier uses pending migrations title"
grep -Fx 'Click to run 1 pending migration.' "$test_tmp/notify-args" >/dev/null || fail "migration notifier describes the pending migration"
grep -Fx '' "$test_tmp/notify-args" >/dev/null || fail "migration notifier includes the large-slot glyph"
pass "migration notifier uses the actionable notification format"
# `omarchy update` applies migrations itself, so nothing may notify about them
# while it holds its lock -- a stale trigger firing mid-transaction is exactly
# how the retired omarchy-update-user-notify.path used to interrupt updates.
rm -f "$test_tmp/notify-args"
update_lock="$runtime_dir/omarchy-update.lock"
: >"$update_lock"
exec {update_lock_fd}>"$update_lock"
flock -n "$update_lock_fd" || fail "test could not hold the update lock"
run_notify 1 >"$test_tmp/during-update.out" 2>"$test_tmp/during-update.err"
[[ ! -s $test_tmp/during-update.out ]] || fail "migration notifier stays quiet on stdout during an update"
[[ ! -s $test_tmp/during-update.err ]] || fail "migration notifier stays quiet on stderr during an update"
[[ ! -e $test_tmp/notify-args ]] || fail "migration notifier sends no notification during an update"
pass "migration notifier stays quiet while omarchy update holds its lock"
exec {update_lock_fd}>&-
run_notify 1 >/dev/null 2>&1
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null ||
fail "migration notifier resumes notifying once the update lock is released"
pass "migration notifier resumes notifying after the update releases its lock"
rm -f "$test_tmp/notify-args"
OMARCHY_TEST_LOCK_DURING_WAIT=1 run_notify 1 >"$test_tmp/raced.out" 2>"$test_tmp/raced.err"
if [[ -s $test_tmp/lock-holder-pid ]]; then
kill "$(<"$test_tmp/lock-holder-pid")" 2>/dev/null || true
for _ in {1..200}; do
flock -n "$runtime_dir/omarchy-update.lock" true 2>/dev/null && break
sleep 0.05
done
fi
[[ ! -e $test_tmp/notify-args ]] ||
fail "migration notifier sends no notification when an update starts while it waits for the notification server"
pass "migration notifier re-checks for an update after waiting for the notification server"
# The guard must never read a lock outside this user's runtime directory: a
# shared /tmp path belongs to whoever created it first, so honouring it would
# let one user silence another user's critical notification.
rm -f "$test_tmp/notify-args" /tmp/omarchy-update.lock
foreign_lock="$test_tmp/foreign/omarchy-update.lock"
mkdir -p "$(dirname "$foreign_lock")"
: >"$foreign_lock"
exec {foreign_lock_fd}>"$foreign_lock"
flock -n "$foreign_lock_fd" || fail "test could not hold the foreign update lock"
run_notify 1 >/dev/null 2>&1
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null ||
fail "migration notifier ignores update locks outside its own runtime directory"
pass "migration notifier ignores update locks outside its own runtime directory"
exec {foreign_lock_fd}>&-