From a38b24f740ec60c3219b7a49f0baa685602d8953 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 24 Mar 2026 10:29:52 +0800 Subject: [PATCH] fix(uninstall): pre-auth brew cask removals --- lib/uninstall/batch.sh | 28 ++++++++++----- tests/brew_uninstall.bats | 71 +++++++++++++++++++++++++-------------- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 464fedf..ad84857 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -285,6 +285,7 @@ batch_uninstall_applications() { # Pre-scan: running apps, sudo needs, size. local -a running_apps=() local -a sudo_apps=() + local -a brew_cask_apps=() local total_estimated_size=0 local -a app_details=() @@ -322,6 +323,10 @@ batch_uninstall_applications() { fi fi + if [[ "$is_brew_cask" == "true" ]]; then + brew_cask_apps+=("$app_name") + fi + # Check if sudo is needed local needs_sudo=false local app_owner=$(get_file_owner "$app_path") @@ -383,10 +388,7 @@ batch_uninstall_applications() { # Warn if brew cask apps are present. local has_brew_cask=false - for detail in "${app_details[@]}"; do - IFS='|' read -r _ _ _ _ _ _ _ _ is_brew_cask_flag _ <<< "$detail" - [[ "$is_brew_cask_flag" == "true" ]] && has_brew_cask=true - done + [[ ${#brew_cask_apps[@]} -gt 0 ]] && has_brew_cask=true if [[ "$has_brew_cask" == "true" ]]; then echo -e "${GRAY}${ICON_WARNING} Homebrew apps will be fully cleaned, --zap removes configs and data${NC}" @@ -467,11 +469,19 @@ batch_uninstall_applications() { # that user explicitly chose to uninstall. System-critical components remain protected. export MOLE_UNINSTALL_MODE=1 - # Request sudo if needed for non-Homebrew removal operations. - # Note: Homebrew resets sudo timestamp at process startup, so pre-auth would - # cause duplicate password prompts in cask-only flows. - if [[ ${#sudo_apps[@]} -gt 0 && "${MOLE_DRY_RUN:-0}" != "1" ]]; then - if ! ensure_sudo_session "Admin required for system apps: ${sudo_apps[*]}"; then + # Establish sudo once before uninstalling apps that need admin access. + # Homebrew cask removal can prompt via sudo during uninstall hooks, which + # does not work reliably under Mole's timed non-interactive execution path. + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]] && + { [[ ${#sudo_apps[@]} -gt 0 ]] || [[ ${#brew_cask_apps[@]} -gt 0 ]]; }; then + local admin_prompt="Admin required to uninstall selected apps" + if [[ ${#sudo_apps[@]} -gt 0 && ${#brew_cask_apps[@]} -eq 0 ]]; then + admin_prompt="Admin required for system apps: ${sudo_apps[*]}" + elif [[ ${#brew_cask_apps[@]} -gt 0 && ${#sudo_apps[@]} -eq 0 ]]; then + admin_prompt="Admin required for Homebrew casks: ${brew_cask_apps[*]}" + fi + + if ! ensure_sudo_session "$admin_prompt"; then echo "" log_error "Admin access denied" _restore_uninstall_traps diff --git a/tests/brew_uninstall.bats b/tests/brew_uninstall.bats index f10d3b1..5cc3b7c 100644 --- a/tests/brew_uninstall.bats +++ b/tests/brew_uninstall.bats @@ -101,6 +101,10 @@ remove_apps_from_dock() { :; } force_kill_app() { return 0; } run_with_timeout() { shift; "$@"; } export -f run_with_timeout +ensure_sudo_session() { + echo "ENSURE_SUDO:$*" >> "$HOME/brew_calls.log" + return 0 +} # Mock brew to track calls brew() { @@ -121,13 +125,14 @@ total_size_cleaned=0 # Simulate 'Enter' for confirmation printf '\n' | batch_uninstall_applications > /dev/null 2>&1 +grep -q "ENSURE_SUDO:Admin required for Homebrew casks: BrewApp" "$HOME/brew_calls.log" grep -q "uninstall --cask --zap brew-app-cask" "$HOME/brew_calls.log" EOF [ "$status" -eq 0 ] } -@test "batch_uninstall_applications does not pre-auth sudo for brew-only casks" { +@test "batch_uninstall_applications pre-auths sudo for brew-only casks" { local app_bundle="$HOME/Applications/BrewPreAuth.app" mkdir -p "$app_bundle" @@ -149,8 +154,8 @@ run_with_timeout() { shift; "$@"; } export -f run_with_timeout ensure_sudo_session() { - echo "UNEXPECTED_ENSURE_SUDO:$*" >> "$HOME/order.log" - return 1 + echo "ENSURE_SUDO:$*" >> "$HOME/order.log" + return 0 } brew() { @@ -169,8 +174,9 @@ total_size_cleaned=0 printf '\n' | batch_uninstall_applications > /dev/null 2>&1 +grep -q "ENSURE_SUDO:Admin required for Homebrew casks: BrewPreAuth" "$HOME/order.log" grep -q "BREW_CALL:uninstall --cask --zap brew-preauth-cask" "$HOME/order.log" -! grep -q "UNEXPECTED_ENSURE_SUDO:" "$HOME/order.log" +[[ "$(sed -n '1p' "$HOME/order.log")" == "ENSURE_SUDO:Admin required for Homebrew casks: BrewPreAuth" ]] EOF [ "$status" -eq 0 ] @@ -196,6 +202,7 @@ print_summary_block() { :; } force_kill_app() { return 0; } remove_apps_from_dock() { :; } refresh_launch_services_after_uninstall() { echo "LS_REFRESH"; } +ensure_sudo_session() { return 0; } get_brew_cask_name() { echo "brew-timeout-cask"; return 0; } brew_uninstall_cask() { return 0; } @@ -257,6 +264,7 @@ has_sensitive_data() { return 1; } decode_file_list() { return 0; } remove_file_list() { :; } run_with_timeout() { shift; "$@"; } +ensure_sudo_session() { return 0; } safe_remove() { echo "SAFE_REMOVE:$1" >> "$HOME/remove.log" @@ -315,6 +323,7 @@ has_sensitive_data() { return 1; } decode_file_list() { return 0; } remove_file_list() { :; } run_with_timeout() { shift; "$@"; } +ensure_sudo_session() { return 0; } safe_remove() { echo "SAFE_REMOVE:$1" >> "$HOME/remove.log" @@ -344,37 +353,47 @@ EOF [ "$status" -eq 0 ] } -@test "brew_uninstall_cask does not trigger extra sudo pre-auth" { +@test "batch_uninstall_applications skips brew sudo pre-auth in dry-run mode" { run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/core/common.sh" -source "$PROJECT_ROOT/lib/uninstall/brew.sh" - -debug_log() { :; } -get_path_size_kb() { echo "0"; } -run_with_timeout() { local _timeout="$1"; shift; "$@"; } - -sudo() { - echo "UNEXPECTED_SUDO_CALL:$*" - return 1 -} +source "$PROJECT_ROOT/lib/uninstall/batch.sh" brew() { - if [[ "${1:-}" == "uninstall" ]]; then + echo "BREW_CALL:$*" >> "$HOME/dry_run.log" return 0 - fi - if [[ "${1:-}" == "list" && "${2:-}" == "--cask" ]]; then - return 0 - fi - return 0 } -export -f sudo brew +export -f brew -brew_uninstall_cask "mock-cask" -echo "DONE" +start_inline_spinner() { :; } +stop_inline_spinner() { :; } +get_file_owner() { whoami; } +get_path_size_kb() { echo "100"; } +bytes_to_human() { echo "$1"; } +drain_pending_input() { :; } +print_summary_block() { :; } +remove_apps_from_dock() { :; } +force_kill_app() { return 0; } +ensure_sudo_session() { + echo "UNEXPECTED_ENSURE_SUDO:$*" >> "$HOME/dry_run.log" + return 1 +} +run_with_timeout() { shift; "$@"; } +export -f run_with_timeout + +get_brew_cask_name() { echo "brew-dry-run-cask"; return 0; } + +export MOLE_DRY_RUN=1 +selected_apps=("0|$HOME/Applications/BrewDryRun.app|BrewDryRun|com.example.brewdryrun|0|Never") +mkdir -p "$HOME/Applications/BrewDryRun.app" +files_cleaned=0 +total_items=0 +total_size_cleaned=0 + +printf '\n' | batch_uninstall_applications > /dev/null 2>&1 + +! grep -q "UNEXPECTED_ENSURE_SUDO:" "$HOME/dry_run.log" 2> /dev/null EOF [ "$status" -eq 0 ] - [[ "$output" == *"DONE"* ]] - [[ "$output" != *"UNEXPECTED_SUDO_CALL:"* ]] }