#!/bin/bash set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" upgrade_to_quattro="$ROOT/bin/omarchy-upgrade-to-quattro" function_body() { awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro" } snapshot_line=$(grep -n '^create_pre_upgrade_snapshot$' "$upgrade_to_quattro" | cut -d: -f1) pacman_line=$(grep -n '^configure_pacman_channel$' "$upgrade_to_quattro" | cut -d: -f1) [[ -n $snapshot_line && -n $pacman_line ]] || fail "upgrade snapshot and first mutation calls exist" (( snapshot_line < pacman_line )) || fail "upgrade snapshot runs before pacman configuration" grep -F 'omarchy-snapshot create || (($? == 127))' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade snapshots the system before mutation" # The mirrors are repointed immediately before the keyrings go in, so only a # forced refresh replaces the legacy database and its stale checksums. grep -F 'pacman -Syy --noconfirm archlinux-keyring omarchy-keyring' "$upgrade_to_quattro" >/dev/null if grep -F 'pacman -Sy --noconfirm archlinux-keyring omarchy-keyring' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade forces a database refresh before installing keyrings" fi pass "Omarchy 4 upgrade forces a database refresh before installing keyrings" grep -F 'pacman -Syu --needed' "$upgrade_to_quattro" >/dev/null grep -F 'omarchy-update-aur-pkgs' "$upgrade_to_quattro" >/dev/null grep -F 'omarchy-update-available' "$upgrade_to_quattro" >/dev/null grep -F 'omarchy-update-mise' "$upgrade_to_quattro" >/dev/null grep -F 'run_final_system_package_upgrade' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade completes package update checks" grep -F 'run_post_upgrade_migrations' "$upgrade_to_quattro" >/dev/null grep -F 'omarchy-migrate' "$upgrade_to_quattro" >/dev/null grep -F 'dust' "$upgrade_to_quattro" >/dev/null grep -F 'satty' "$upgrade_to_quattro" >/dev/null final_upgrade_line=$(grep -n '^run_final_system_package_upgrade$' "$upgrade_to_quattro" | cut -d: -f1) migrations_line=$(grep -n '^run_post_upgrade_migrations$' "$upgrade_to_quattro" | cut -d: -f1) [[ -n $final_upgrade_line && -n $migrations_line ]] || fail "final package upgrade and migration calls exist" (( final_upgrade_line < migrations_line )) || fail "Omarchy migrations run after the final package upgrade" pass "Omarchy 4 upgrade applies packaged migrations" if grep -F 'skip-first-run-update-notification' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade does not use notification-specific first-run state" fi pass "Omarchy 4 upgrade completes first-run as one lifecycle" grep -F 'touch "$done_dir/first-run-user" "$done_dir/finalize-user"' "$upgrade_to_quattro" >/dev/null grep -F 'rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done"' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade completes first-run and migrates legacy completion markers" # The script runs from the branch against whatever packaged tree the channel # serves, so a packaged command missing from an older build must never be able # to abort the upgrade partway through. if grep -F '"$root/bin/omarchy-done"' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade writes completion markers without the packaged omarchy-done" fi pass "Omarchy 4 upgrade writes completion markers without the packaged omarchy-done" for guarded_step in omarchy-refresh-applications 'omarchy-bar defaults'; do grep -F "run_as_user_omarchy $guarded_step ||" "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade survives a packaged tree without $guarded_step" done pass "Omarchy 4 upgrade survives a packaged tree missing top-level commands" grep -F 'configure_snapper_policy' "$upgrade_to_quattro" >/dev/null grep -F '/usr/share/omarchy/install/config/snapper.sh' "$upgrade_to_quattro" >/dev/null grep -F 'bash -euo pipefail "$snapper_config_script"' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade normalizes Snapper retention" grep -F 'configure_lock_authentication' "$upgrade_to_quattro" >/dev/null grep -F 'OMARCHY_INSTALL_USER="$target_user"' "$upgrade_to_quattro" >/dev/null grep -F '"$apply_lock"' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade configures lock screen authentication for the target user" root_path_count=$(awk '/^root_path=/{ count++ } END { print count + 0 }' "$upgrade_to_quattro") (( root_path_count == 1 )) || fail "Omarchy 4 upgrade defines exactly one root command path" grep -Fx 'root_path=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade limits root command lookup to trusted system directories" if grep -E '^root_path=.*(target_home|\.local/bin)' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade does not put the target user's bin directory on the root command path" fi grep -Fx 'package_path="$root_path:$target_home/.local/bin"' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade retains the target user's bin directory for user commands" lock_authentication_body=$(function_body configure_lock_authentication) lock_path_assignment_count=$(awk '{ count += gsub(/(^|[[:space:]])PATH=/, "") } END { print count + 0 }' <<<"$lock_authentication_body") (( lock_path_assignment_count == 1 )) || fail "Omarchy 4 upgrade gives the privileged lock helper exactly one command path" grep -Fx ' PATH="$root_path" \' <<<"$lock_authentication_body" >/dev/null || fail "Omarchy 4 upgrade gives the privileged lock helper the trusted root path" if grep -E '(package_path|target_home|\.local/bin)' <<<"$lock_authentication_body" >/dev/null; then fail "Omarchy 4 upgrade does not give the privileged lock helper the target user's path" fi firewall_body=$(function_body apply_firewall_defaults) firewall_path_assignment_count=$(awk '{ count += gsub(/(^|[[:space:]])PATH=/, "") } END { print count + 0 }' <<<"$firewall_body") (( firewall_path_assignment_count == 1 )) || fail "Omarchy 4 upgrade gives the privileged firewall helper exactly one command path" grep -Fx ' as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$root_path" \' <<<"$firewall_body" >/dev/null || fail "Omarchy 4 upgrade gives the privileged firewall helper the trusted root path" if grep -E '(package_path|target_home|\.local/bin)' <<<"$firewall_body" >/dev/null; then fail "Omarchy 4 upgrade does not give the privileged firewall helper the target user's path" fi user_omarchy_body=$(function_body run_as_user_omarchy) grep -F 'PATH="$package_path"' <<<"$user_omarchy_body" >/dev/null || fail "Omarchy 4 upgrade retains the package and user path for target-user commands" if grep -F 'PATH="$root_path"' <<<"$user_omarchy_body" >/dev/null; then fail "Omarchy 4 upgrade does not narrow target-user commands to the root-only path" fi pass "Omarchy 4 upgrade separates privileged and target-user command paths" grep -F 'install/helpers/browser-policy.sh' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade uses the shared browser-policy helper" grep -F 'as_root test -f "$browser_policy_helper"' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade survives a packaged tree without the browser-policy helper" if grep -F 'browser_policy_setup_group' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade does not create a browser-policy group" fi grep -F 'browser_policy_setup_dir /etc/chromium/policies/managed' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade creates a root-owned Chromium policy directory" grep -F 'BROWSER_POLICY_MANAGED_DIRS' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade hardens every Chromium-family policy directory" grep -F 'run_as_user_omarchy omarchy-theme-set-browser' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade rewrites browser theme colour after a headless theme-set" if grep -E 'install -d -m 0?[27]?777 /etc/.*/policies|chmod a\+rw|2775' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade does not create a world-writable Chromium policy directory" fi pass "Omarchy 4 upgrade locks the Chromium policy directory to root" grep -F 'OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd.service' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd.socket' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd-resolve-hook.socket' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager" # Booting with both managers enabled leaves them fighting over the Wi-Fi # adapter, so enabling NetworkManager and disabling iwd cannot be separated by # any step that might abort in between. migrations_body=$(function_body run_post_upgrade_migrations) grep -F 'fail "Omarchy migrations did not complete.' <<<"$migrations_body" >/dev/null || fail "Omarchy 4 upgrade fails when a migration cannot complete" grep -F 'omarchy-migrate --pending' <<<"$migrations_body" >/dev/null || fail "Omarchy 4 upgrade verifies that migrations actually completed" grep -F 'fail "Omarchy migrations are still pending.' <<<"$migrations_body" >/dev/null || fail "Omarchy 4 upgrade fails when a successful migration command leaves pending work" grep -F 'pending_status != 1' <<<"$migrations_body" >/dev/null || fail "Omarchy 4 upgrade distinguishes no pending work from a failed verification" grep -F 'fail "Could not verify that Omarchy migrations completed.' <<<"$migrations_body" >/dev/null || fail "Omarchy 4 upgrade fails when it cannot verify migration state" if grep -F 'return 0' <<<"$migrations_body" >/dev/null || grep -F 'warn ' <<<"$migrations_body" >/dev/null; then fail "Omarchy 4 upgrade does not continue past failed migrations" fi exercise_post_upgrade_migrations() { local stub_migration_status="$1" stub_pending_status="$2" ( log() { :; } fail() { exit 1; } run_as_user_omarchy() { if [[ " $* " == *" --pending "* ]]; then return "$stub_pending_status" else return "$stub_migration_status" fi } eval "run_post_upgrade_migrations() { $migrations_body }" run_post_upgrade_migrations ) } exercise_post_upgrade_migrations 0 1 >/dev/null 2>&1 || fail "Omarchy 4 upgrade accepts a completed migration queue" if exercise_post_upgrade_migrations 1 1 >/dev/null 2>&1; then fail "Omarchy 4 upgrade accepts a failed migration" fi if exercise_post_upgrade_migrations 0 0 >/dev/null 2>&1; then fail "Omarchy 4 upgrade accepts pending migrations" fi if exercise_post_upgrade_migrations 0 2 >/dev/null 2>&1; then fail "Omarchy 4 upgrade accepts a failed pending-state check" fi pass "Omarchy 4 upgrade cannot finish with pending migrations" if function_body cleanup_retired_services | grep -F 'systemctl disable iwd' >/dev/null; then fail "Omarchy 4 upgrade does not retire iwd in a step separate from the NetworkManager enable" fi grep -A1 -F ' enable_system_service NetworkManager.service' "$upgrade_to_quattro" | grep -F 'as_root systemctl disable iwd.service' >/dev/null || fail "Omarchy 4 upgrade retires iwd in the step that enables NetworkManager" pass "Omarchy 4 upgrade switches from iwd to NetworkManager atomically" # set -e aborts silently, so only an explicit banner distinguishes a # half-upgraded system from a finished one. grep -Fx 'trap cleanup_on_exit EXIT' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently" cleanup_body=$(function_body cleanup_on_exit) grep -F 'upgrade_started && ! upgrade_completed' <<<"$cleanup_body" >/dev/null || fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently" grep -F 'Upgrade incomplete - do NOT reboot.' <<<"$cleanup_body" >/dev/null || fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently" grep -F 'exit "$exit_status"' <<<"$cleanup_body" >/dev/null || fail "Omarchy 4 upgrade preserves the failing exit status" grep -F '>&2' <<<"$cleanup_body" >/dev/null || fail "Omarchy 4 upgrade reports an aborted run on stderr" started_line=$(grep -n '^upgrade_started=1$' "$upgrade_to_quattro" | cut -d: -f1) completed_line=$(grep -n '^upgrade_completed=1$' "$upgrade_to_quattro" | cut -d: -f1) suppress_line=$(grep -n '^suppress_hyprland_config_reload$' "$upgrade_to_quattro" | cut -d: -f1) # The reboot is the cutover, so the last mutating step hands the live session # back rather than swapping the shell out underneath it. last_step_line=$(grep -n '^restore_hyprland_config_reload$' "$upgrade_to_quattro" | cut -d: -f1) [[ -n $started_line && -n $completed_line && -n $suppress_line && -n $last_step_line ]] || fail "upgrade progress markers and the mutating step range exist" (( started_line < suppress_line )) || fail "the upgrade is marked started before the first mutation" (( completed_line > last_step_line )) || fail "the upgrade is marked complete only after the last step" pass "Omarchy 4 upgrade reports an aborted run instead of exiting silently" # Ordering alone would still pass if either retired entry point came back, so # name them: the reboot is the cutover, and nothing may swap the shell out from # under the session being replaced. ! grep -q 'start_omarchy_shell_session' "$upgrade_to_quattro" || fail "Omarchy 4 upgrade does not start the shell in the session it is replacing" ! grep -q 'stop_retired_session_processes' "$upgrade_to_quattro" || fail "Omarchy 4 upgrade leaves the retired session processes running until reboot" pass "Omarchy 4 upgrade leaves the Omarchy 3 session alone until the reboot" grep -F 'omarchy-bar defaults' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade restores service-aware bar defaults" grep -F 'install_hardware_transition_packages' "$upgrade_to_quattro" >/dev/null grep -F 'sof-firmware' "$upgrade_to_quattro" >/dev/null grep -F 'vulkan-intel' "$upgrade_to_quattro" >/dev/null grep -F 'apply_user_hardware_transition' "$upgrade_to_quattro" >/dev/null grep -F 'DX13260' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade backfills hardware support from the legacy release" grep -F 'omarchy-refresh-applications' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade refreshes application launchers" grep -F '/etc/systemd/system.conf.d/99-omarchy-nofile.conf' "$upgrade_to_quattro" >/dev/null grep -F '/etc/systemd/user.conf.d/99-omarchy-nofile.conf' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade removes stale nofile drop-ins" cmdline_line=$(grep -n '^preserve_kernel_cmdline_root$' "$upgrade_to_quattro" | cut -d: -f1) packages_line=$(grep -n '^install_omarchy_quattro_packages$' "$upgrade_to_quattro" | cut -d: -f1) verify_line=$(grep -n '^verify_kernel_cmdline_root$' "$upgrade_to_quattro" | cut -d: -f1) [[ -n $cmdline_line && -n $packages_line && -n $verify_line ]] || fail "kernel cmdline preservation, verification and package install calls exist" # The package transaction installs the += drop-in that drops root=, so the pin # has to be on disk before it runs or the UKI it bakes is unbootable. (( cmdline_line < packages_line )) || fail "kernel cmdline is pinned before the packages that can drop root=" # The UKIs are rebuilt by the transaction, so they can only be checked after it. (( verify_line > packages_line )) || fail "kernel cmdline is verified after the packages are installed" grep -F '/etc/default/limine' "$upgrade_to_quattro" >/dev/null grep -F 'KERNEL_CMDLINE[default]+=" ${boot_params[*]}"' "$upgrade_to_quattro" >/dev/null grep -F 'cat /proc/cmdline' "$upgrade_to_quattro" >/dev/null grep -F 'findmnt -no UUID /' "$upgrade_to_quattro" >/dev/null grep -F 'rootflags=subvol=' "$upgrade_to_quattro" >/dev/null grep -F 'cryptdevice' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade preserves the kernel cmdline root parameters" # The tool's effective cmdline still resolves root= from /proc/cmdline until the # first += drop-in lands, so it reads healthy on exactly the machines about to # break. Ask the config layers whether root= is stated instead, for the default # key alone so a fallback or kernel-specific pin cannot cover for it. grep -F 'kernel_cmdline_root_pinned' "$upgrade_to_quattro" >/dev/null grep -F 'KERNEL_CMDLINE\[default\]' "$upgrade_to_quattro" >/dev/null ! grep -F 'limine-entry-tool --get-cmdline' "$upgrade_to_quattro" >/dev/null || fail "the pin check does not depend on the fallback it is about to lose" pass "Omarchy 4 upgrade checks whether root= is pinned in the limine config" # Run the pin check against fixture config layers. A false positive skips the # pin and the next boot has no root=; a false negative appends a second pin. eval "$(sed -n '/^kernel_cmdline_root_pinned() {$/,/^}$/p' "$upgrade_to_quattro")" pin_root=$(mktemp -d) trap 'rm -rf "$pin_root"' EXIT as_root() { local script=${3//\/etc\//$pin_root/etc/} bash -c "${script//\/usr\/share\//$pin_root/usr/share/}" } # Takes pairs of a config layer and a line to append to it. root_pinned() { rm -rf "${pin_root:?}"/{etc,usr} mkdir -p "$pin_root/etc/default" "$pin_root/etc/limine-entry-tool.d" "$pin_root/usr/share/limine-entry-tool.d" while (($#)); do printf '%s\n' "$2" >>"$pin_root/$1" shift 2 done kernel_cmdline_root_pinned } root_pinned etc/default/limine 'KERNEL_CMDLINE[default]+=" root=UUID=abc rw"' || fail "a quoted root= pin is recognised" root_pinned etc/default/limine 'KERNEL_CMDLINE[default]=root=UUID=abc' || fail "an unquoted root= pin is recognised" root_pinned etc/default/limine 'KERNEL_CMDLINE[default]+=" dm-mod.create="foo" root=UUID=abc rw"' || fail "a pin with a quoted parameter before root= is recognised" root_pinned usr/share/limine-entry-tool.d/root.conf 'KERNEL_CMDLINE[default]+="root=UUID=abc"' \ etc/default/limine 'KERNEL_CMDLINE[default]+=" rw"' || fail "an appending layer keeps an earlier pin" root_pinned etc/default/limine 'KERNEL_CMDLINE[default] += " root=UUID=abc rw"' || fail "a pin spaced around += is recognised" ! root_pinned etc/default/limine "KERNEL_CMDLINE[default]+='root=UUID=abc rw'" || fail "single quotes are kept, as limine-entry-tool keeps them" ! root_pinned etc/default/limine 'OLD_KERNEL_CMDLINE[default]+=" root=UUID=abc rw"' || fail "a renamed key is not a pin" ! root_pinned etc/default/limine '# KERNEL_CMDLINE[default]+=" root=UUID=abc rw"' || fail "a commented-out pin is not a pin" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[fallback]+=" root=UUID=abc rw"' || fail "a fallback-only pin does not cover default" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[default]+=" rootflags=subvol=@ rw"' || fail "rootflags= is not root=" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[default]+=" systemd.setenv="root=UUID=abc" rw"' || fail "root= inside another parameter's value is not a pin" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[default]+=" systemd.setenv="NOTE=x root=UUID=abc" rw"' || fail "root= after a space inside a quoted value is not a pin" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[default]=systemd.setenv="NOTE=x root=UUID=abc rw' || fail "root= after an unmatched quote is not a pin" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[default]=ro"x"ot=UUID=abc rw' || fail "a quoted segment inside a parameter name does not make it root=" ! root_pinned etc/limine-entry-tool.conf 'KERNEL_CMDLINE[default]=root=UUID=abc rw' \ etc/default/limine 'KERNEL_CMDLINE[default]=quiet' || fail "a pin replaced by a later layer is not a pin" ! root_pinned etc/default/limine 'KERNEL_CMDLINE[default]+=" root=UUID=abc rw"' \ etc/default/limine 'KERNEL_CMDLINE[default]="quiet"' || fail "a pin replaced later in the same layer is not a pin" unset -f as_root pass "Omarchy 4 upgrade recognises exactly the root= pins that hold" # Installing the limine packages can deploy limine on a machine that had no # limine.conf when the pin ran, so verification pins whatever the first call skipped. ( eval "$(sed -n '/^kernel_cmdline_root_checked=/p;/^preserve_kernel_cmdline_root() {$/,/^}$/p;/^verify_kernel_cmdline_root() {$/,/^}$/p' "$upgrade_to_quattro")" limine_conf=0 pin_checks=0 as_root() { if [[ $1 == "test" ]]; then ((limine_conf)) fi } kernel_cmdline_root_pinned() { ((++pin_checks)); } limine-mkinitcpio() { :; } preserve_kernel_cmdline_root ((pin_checks == 0)) || fail "the pin waits for a limine.conf" limine_conf=1 verify_kernel_cmdline_root ((pin_checks == 1)) || fail "kernel cmdline verification pins a machine the transaction put on limine" verify_kernel_cmdline_root ((pin_checks == 1)) || fail "kernel cmdline verification pins a machine only once" ) pass "Omarchy 4 upgrade pins root= on machines the transaction moves to limine" # The crypt layer hides in the parents on LVM-on-LUKS, and a partial cmdline # for an encrypted root must not be written at all. grep -F 'findmnt -no SOURCE --nofsroot /' "$upgrade_to_quattro" >/dev/null grep -F 'lsblk -nso TYPE "$root_source"' "$upgrade_to_quattro" >/dev/null grep -F 'grep -qx crypt' "$upgrade_to_quattro" >/dev/null grep -F '((have_mount_mode)) || boot_params+=(rw)' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade repair path refuses a partial dm-crypt cmdline" # The cmdline that boots is the one embedded in the UKIs, and an unverified # root= must block the reboot rather than just warn. grep -F -- '--only-section=.cmdline' "$upgrade_to_quattro" >/dev/null grep -F "as_root find /boot/EFI/Linux -maxdepth 1 -name 'omarchy_linux*.efi'" "$upgrade_to_quattro" >/dev/null grep -F 'boot_cmdline_unsafe=1' "$upgrade_to_quattro" >/dev/null unsafe_line=$(grep -n 'if (( boot_cmdline_unsafe )); then' "$upgrade_to_quattro" | cut -d: -f1) reboot_line=$(grep -n 'Rebooting because --reboot was passed' "$upgrade_to_quattro" | cut -d: -f1) [[ -n $unsafe_line && -n $reboot_line ]] || fail "reboot gate and reboot branch exist" (( unsafe_line < reboot_line )) || fail "an unverified kernel cmdline blocks the reboot" pass "Omarchy 4 upgrade verifies the UKIs and refuses to reboot unverified" # Lazydocker is optional on fresh installs, but a pre-quattro install keeps it. lazydocker_body=$(function_body migrate_lazydocker_package) [[ -n $lazydocker_body ]] || fail "upgrade has a Lazydocker replacement step" eval "migrate_lazydocker_package() { $lazydocker_body; }" lazydocker_calls="" package_installed_exact() { [[ $1 == "lazydocker-bin" && $legacy_lazydocker == "yes" ]]; } log() { :; } warn() { :; } as_root() { lazydocker_calls+="swap:$*"$'\n' return "$lazydocker_swap_status" } mark_packages_explicit() { lazydocker_calls+="explicit:$*"$'\n'; } legacy_lazydocker=no lazydocker_swap_status=0 migrate_lazydocker_package [[ -z $lazydocker_calls ]] || fail "upgrade does not install Lazydocker for users without the legacy package" pass "upgrade does not install Lazydocker for users without the legacy package" legacy_lazydocker=yes migrate_lazydocker_package grep -Fxq 'swap:pacman -S --needed --noconfirm --ask 4 lazydocker' <<<"$lazydocker_calls" || fail "upgrade replaces existing lazydocker-bin with lazydocker" grep -Fxq 'explicit:lazydocker' <<<"$lazydocker_calls" || fail "upgrade retains Lazydocker as an explicit package" pass "upgrade replaces existing lazydocker-bin with lazydocker and keeps it explicit" lazydocker_calls="" lazydocker_swap_status=1 migrate_lazydocker_package if grep -Fq 'explicit:' <<<"$lazydocker_calls"; then fail "failed Lazydocker replacement does not mark an absent package explicit" fi for cleanup in remove_conflicting_legacy_packages remove_retired_default_packages; do if grep -qw lazydocker-bin <<<"$(function_body "$cleanup")"; then fail "upgrade cleanup must not remove existing Lazydocker when replacement fails" fi done pass "upgrade cleanup preserves existing Lazydocker when replacement fails" grep -Fxq migrate_lazydocker_package "$upgrade_to_quattro" || fail "upgrade invokes the Lazydocker replacement step" pass "upgrade invokes the Lazydocker replacement step"