Files
omarchycn/test/shell.d/migrate-notify-test.sh
T
David Heinemeier HanssonandClaude Opus 5 b6e1a43278 Detach the pending-migrations toast from the notifier
omarchy-migrate-notify handed its notification to `systemd-run --scope`,
which is synchronous: the calling process becomes the payload, so
systemd-run does not return until the toast has been answered -- and if
the toast is clicked, not until the migration terminal it opens has been
closed.

omarchy-migrate-notify.service is Type=oneshot, which defaults to
TimeoutStartUSec=infinity, so the unit sat in activating for exactly that
long. On a machine that had not yet picked up c7e327b0, which orders the
notifier after graphical-session.target, that held the target open --
and wayland-wm-app-daemon.service is ordered after the same target, so it
never started. Every keybinding goes through uwsm-app, which waits ten
seconds for that daemon's pipes and then reports "App failure -- Timed
out waiting for pipes!" instead of launching anything. Keybindings were
dead for two minutes and seventeen seconds, until a pacman hook ran
`systemctl reload user@*.service`, whose re-exec of the user manager
broke the scope's bus connection and let the oneshot finish. That also
made systemd-run exit non-zero, so the notifier fell through to the
terminal fallback meant for having no user manager at all, and printed
the migration list into the journal.

Hand the notification to a transient service instead. systemd-run
returns once the unit has started rather than once the payload is done,
so the oneshot completes in milliseconds and nothing ordered after the
target waits on a toast. Put it in background-graphical.slice, which
systemd ships with PartOf=graphical-session.target, so an unanswered
toast ends at logout rather than outliving the session.

The terminal a clicked toast opens is unaffected: uwsm-app re-registers
it into app-graphical.slice, outside this unit's cgroup.

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

192 lines
7.6 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=${!#}
# --scope blocks on the command, a transient service detaches it.
if [[ " $* " == *" --scope "* ]]; then
bash -c "$command"
else
setsid bash -c "$command" >/dev/null 2>&1 &
fi
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
# Written whole so a reader polling for the file never sees half the arguments.
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS.partial"
mv "$OMARCHY_TEST_NOTIFY_ARGS.partial" "$OMARCHY_TEST_NOTIFY_ARGS"
# Stands in for a toast nobody has answered yet, until the test releases it.
if [[ -n ${OMARCHY_TEST_NOTIFY_HOLD:-} ]]; then
for _ in {1..200}; do
[[ -e $OMARCHY_TEST_NOTIFY_HOLD ]] || break
sleep 0.05
done
: >"$OMARCHY_TEST_NOTIFY_ANSWERED"
fi
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" \
OMARCHY_TEST_NOTIFY_HOLD="${OMARCHY_TEST_NOTIFY_HOLD:-}" \
OMARCHY_TEST_NOTIFY_ANSWERED="$test_tmp/notify-answered" \
"$ROOT/bin/omarchy-migrate-notify"
}
# The notification outlives the notifier, so its arguments land after it exits.
wait_for_notify_args() {
for _ in {1..200}; do
[[ -s $test_tmp/notify-args ]] && return 0
sleep 0.05
done
return 1
}
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"
wait_for_notify_args || fail "migration notifier sends a notification for pending migrations"
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
wait_for_notify_args &&
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
wait_for_notify_args &&
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}>&-
# The notifier is a Type=oneshot with no start timeout, so blocking on the toast
# leaves it activating until the user answers.
rm -f "$test_tmp/notify-args" "$test_tmp/notify-answered"
: >"$test_tmp/notify-hold"
OMARCHY_TEST_NOTIFY_HOLD="$test_tmp/notify-hold" run_notify 1 >/dev/null 2>&1
wait_for_notify_args || fail "migration notifier sends the notification it detaches"
[[ ! -e $test_tmp/notify-answered ]] ||
fail "migration notifier waited for the toast to be answered before exiting"
rm -f "$test_tmp/notify-hold"
pass "migration notifier exits while the toast is still unanswered"