#!/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 # `: 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"