diff --git a/README.md b/README.md index 27c58cb..3984183 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ mo status # Live system health dashboard mo purge # Clean project build artifacts mo touchid # Configure Touch ID for sudo +mo completion # Setup shell tab completion mo update # Update Mole mo remove # Remove Mole from system mo --help # Show help @@ -70,6 +71,7 @@ mo purge --paths # Configure project scan directories - **Safety**: Built with strict protections. See our [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`. - **Whitelist**: Manage protected paths with `mo clean --whitelist`. - **Touch ID**: Enable Touch ID for sudo commands by running `mo touchid`. +- **Shell Completion**: Enable tab completion by running `mo completion` (auto-detect and install). - **Navigation**: Supports standard arrow keys and Vim bindings (`h/j/k/l`). - **Debug**: View detailed logs by appending the `--debug` flag (e.g., `mo clean --debug`). diff --git a/bin/completion.sh b/bin/completion.sh index 6757755..33a620c 100755 --- a/bin/completion.sh +++ b/bin/completion.sh @@ -1,20 +1,178 @@ #!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +source "$ROOT_DIR/lib/core/common.sh" +source "$ROOT_DIR/lib/core/commands.sh" + +command_names=() +for entry in "${MOLE_COMMANDS[@]}"; do + command_names+=("${entry%%:*}") +done +command_words="${command_names[*]}" + +emit_zsh_subcommands() { + for entry in "${MOLE_COMMANDS[@]}"; do + printf " '%s:%s'\n" "${entry%%:*}" "${entry#*:}" + done +} + +emit_fish_completions() { + local cmd="$1" + for entry in "${MOLE_COMMANDS[@]}"; do + local name="${entry%%:*}" + local desc="${entry#*:}" + printf 'complete -c %s -n "__fish_mole_no_subcommand" -a %s -d "%s"\n' "$cmd" "$name" "$desc" + done + + printf '\n' + printf 'complete -c %s -n "not __fish_mole_no_subcommand" -a bash -d "generate bash completion" -n "__fish_see_subcommand_path completion"\n' "$cmd" + printf 'complete -c %s -n "not __fish_mole_no_subcommand" -a zsh -d "generate zsh completion" -n "__fish_see_subcommand_path completion"\n' "$cmd" + printf 'complete -c %s -n "not __fish_mole_no_subcommand" -a fish -d "generate fish completion" -n "__fish_see_subcommand_path completion"\n' "$cmd" +} + +# Auto-install mode when run without arguments +if [[ $# -eq 0 ]]; then + # Detect current shell + current_shell="${SHELL##*/}" + if [[ -z "$current_shell" ]]; then + current_shell="$(ps -p "$PPID" -o comm= 2>/dev/null | awk '{print $1}')" + fi + + completion_name="" + if command -v mole > /dev/null 2>&1; then + completion_name="mole" + elif command -v mo > /dev/null 2>&1; then + completion_name="mo" + fi + + case "$current_shell" in + bash) + config_file="${HOME}/.bashrc" + [[ -f "${HOME}/.bash_profile" ]] && config_file="${HOME}/.bash_profile" + completion_line='if output="$('"$completion_name"' completion bash 2>/dev/null)"; then eval "$output"; fi' + ;; + zsh) + config_file="${HOME}/.zshrc" + completion_line='if output="$('"$completion_name"' completion zsh 2>/dev/null)"; then eval "$output"; fi' + ;; + fish) + config_file="${HOME}/.config/fish/config.fish" + completion_line='set -l output ('"$completion_name"' completion fish 2>/dev/null); and echo "$output" | source' + ;; + *) + log_error "Unsupported shell: $current_shell" + echo " mole completion " + exit 1 + ;; + esac + + if [[ -z "$completion_name" ]]; then + if [[ -f "$config_file" ]] && grep -Eq "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" 2>/dev/null; then + original_mode="" + original_mode="$(stat -f '%Mp%Lp' "$config_file" 2>/dev/null || true)" + temp_file="$(mktemp)" + grep -Ev "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" > "$temp_file" || true + mv "$temp_file" "$config_file" + if [[ -n "$original_mode" ]]; then + chmod "$original_mode" "$config_file" 2> /dev/null || true + fi + echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed stale completion entries from $config_file" + echo "" + fi + log_error "mole not found in PATH - install Mole before enabling completion" + exit 1 + fi + + # Check if already installed and normalize to latest line + if [[ -f "$config_file" ]] && grep -Eq "(mole|mo)[[:space:]]+completion" "$config_file" 2>/dev/null; then + original_mode="" + original_mode="$(stat -f '%Mp%Lp' "$config_file" 2>/dev/null || true)" + temp_file="$(mktemp)" + grep -Ev "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" > "$temp_file" || true + mv "$temp_file" "$config_file" + if [[ -n "$original_mode" ]]; then + chmod "$original_mode" "$config_file" 2> /dev/null || true + fi + echo "" >> "$config_file" + echo "# Mole shell completion" >> "$config_file" + echo "$completion_line" >> "$config_file" + echo "" + echo -e "${GREEN}${ICON_SUCCESS}${NC} Shell completion updated in $config_file" + echo "" + exit 0 + fi + + # Prompt user for installation + echo "" + echo -e "${GRAY}Will add to ${config_file}:${NC}" + echo " $completion_line" + echo "" + echo -ne "${PURPLE}${ICON_ARROW}${NC} Enable completion for ${GREEN}${current_shell}${NC}? ${GRAY}Enter confirm / Q cancel${NC}: " + IFS= read -r -s -n1 key || key="" + drain_pending_input + echo "" + + case "$key" in + $'\e' | [Qq] | [Nn]) + echo -e "${YELLOW}Cancelled${NC}" + exit 0 + ;; + "" | $'\n' | $'\r' | [Yy]) + ;; + *) + log_error "Invalid key" + exit 1 + ;; + esac + + # Create config file if it doesn't exist + if [[ ! -f "$config_file" ]]; then + mkdir -p "$(dirname "$config_file")" + touch "$config_file" + fi + + # Remove previous Mole completion lines to avoid duplicates + if [[ -f "$config_file" ]]; then + original_mode="" + original_mode="$(stat -f '%Mp%Lp' "$config_file" 2>/dev/null || true)" + temp_file="$(mktemp)" + grep -Ev "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" > "$temp_file" || true + mv "$temp_file" "$config_file" + if [[ -n "$original_mode" ]]; then + chmod "$original_mode" "$config_file" 2> /dev/null || true + fi + fi + + # Add completion line + echo "" >> "$config_file" + echo "# Mole shell completion" >> "$config_file" + echo "$completion_line" >> "$config_file" + + echo -e "${GREEN}${ICON_SUCCESS}${NC} Completion added to $config_file" + echo "" + echo "" + echo -e "${GRAY}To activate now:${NC}" + echo -e " ${GREEN}source $config_file${NC}" + exit 0 +fi + case "$1" in bash) - cat << 'EOF' + cat << EOF _mole_completions() { local cur_word prev_word - cur_word="${COMP_WORDS[COMP_CWORD]}" - prev_word="${COMP_WORDS[COMP_CWORD-1]}" + cur_word="\${COMP_WORDS[\$COMP_CWORD]}" + prev_word="\${COMP_WORDS[\$COMP_CWORD-1]}" - if [ "$COMP_CWORD" -eq 1 ]; then - COMPREPLY=( $(compgen -W "optimize clean uninstall analyze status purge touchid update remove help version completion" -- "$cur_word") ) + if [ "\$COMP_CWORD" -eq 1 ]; then + COMPREPLY=( \$(compgen -W "$command_words" -- "\$cur_word") ) else - case "$prev_word" in + case "\$prev_word" in completion) - COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur_word") ) + COMPREPLY=( \$(compgen -W "bash zsh fish" -- "\$cur_word") ) ;; *) COMPREPLY=() @@ -23,70 +181,63 @@ _mole_completions() fi } -complete -F _mole_completions mole +complete -F _mole_completions mole mo EOF ;; zsh) - cat << 'EOF' -#compdef mole - -_mole() { - local -a subcommands - subcommands=( - 'optimize:Free up disk space' - 'clean:Remove apps completely' - 'uninstall:Check and maintain system' - 'analyze:Explore disk usage' - 'status:Monitor system health' - 'purge:Remove old project artifacts' - 'touchid:Configure Touch ID for sudo' - 'update:Update to latest version' - 'remove:Remove Mole from system' - 'help:Show help' - 'version:Show version' - 'completion:Generate shell completions' - ) - _describe 'subcommand' subcommands -} - -_mole -EOF + printf '#compdef mole mo\n\n' + printf '_mole() {\n' + printf ' local -a subcommands\n' + printf ' subcommands=(\n' + emit_zsh_subcommands + printf ' )\n' + printf " _describe 'subcommand' subcommands\n" + printf '}\n\n' ;; fish) - cat << 'EOF' -complete -c mole -n "__fish_mole_no_subcommand" -a optimize -d "Free up disk space" -complete -c mole -n "__fish_mole_no_subcommand" -a clean -d "Remove apps completely" -complete -c mole -n "__fish_mole_no_subcommand" -a uninstall -d "Check and maintain system" -complete -c mole -n "__fish_mole_no_subcommand" -a analyze -d "Explore disk usage" -complete -c mole -n "__fish_mole_no_subcommand" -a status -d "Monitor system health" -complete -c mole -n "__fish_mole_no_subcommand" -a purge -d "Remove old project artifacts" -complete -c mole -n "__fish_mole_no_subcommand" -a touchid -d "Configure Touch ID for sudo" -complete -c mole -n "__fish_mole_no_subcommand" -a update -d "Update to latest version" -complete -c mole -n "__fish_mole_no_subcommand" -a remove -d "Remove Mole from system" -complete -c mole -n "__fish_mole_no_subcommand" -a help -d "Show help" -complete -c mole -n "__fish_mole_no_subcommand" -a version -d "Show version" -complete -c mole -n "__fish_mole_no_subcommand" -a completion -d "Generate shell completions" - -complete -c mole -n "not __fish_mole_no_subcommand" -a bash -d "generate bash completion" -n "__fish_see_subcommand_path completion" -complete -c mole -n "not __fish_mole_no_subcommand" -a zsh -d "generate zsh completion" -n "__fish_see_subcommand_path completion" -complete -c mole -n "not __fish_mole_no_subcommand" -a fish -d "generate fish completion" -n "__fish_see_subcommand_path completion" - -function __fish_mole_no_subcommand - for i in (commandline -opc) - if contains -- $i optimize clean uninstall analyze status purge touchid update remove help version completion - return 1 - end - end - return 0 -end - -function __fish_see_subcommand_path - string match -q -- "completion" (commandline -opc)[1] -end -EOF + printf '# Completions for mole\n' + emit_fish_completions mole + printf '\n# Completions for mo (alias)\n' + emit_fish_completions mo + printf '\nfunction __fish_mole_no_subcommand\n' + printf ' for i in (commandline -opc)\n' + printf ' if contains -- $i %s\n' "$command_words" + printf ' return 1\n' + printf ' end\n' + printf ' end\n' + printf ' return 0\n' + printf 'end\n\n' + printf 'function __fish_see_subcommand_path\n' + printf ' string match -q -- "completion" (commandline -opc)[1]\n' + printf 'end\n' ;; *) - echo "Usage: mole completion [bash|zsh|fish]" + cat << 'EOF' +Usage: mole completion [bash|zsh|fish] + +Setup shell tab completion for mole and mo commands. + +Auto-install: + mole completion # Auto-detect shell and install + +Manual install: + mole completion bash # Generate bash completion script + mole completion zsh # Generate zsh completion script + mole completion fish # Generate fish completion script + +Examples: + # Auto-install (recommended) + mole completion + + # Manual install - Bash + eval "$(mole completion bash)" + + # Manual install - Zsh + eval "$(mole completion zsh)" + + # Manual install - Fish + mole completion fish | source +EOF exit 1 ;; esac diff --git a/lib/core/commands.sh b/lib/core/commands.sh new file mode 100644 index 0000000..2a8aec0 --- /dev/null +++ b/lib/core/commands.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Shared command list for help text and completions. +MOLE_COMMANDS=( + "clean:Free up disk space" + "uninstall:Remove apps completely" + "optimize:Check and maintain system" + "analyze:Explore disk usage" + "status:Monitor system health" + "purge:Remove old project artifacts" + "touchid:Configure Touch ID for sudo" + "completion:Setup shell tab completion" + "update:Update to latest version" + "remove:Remove Mole from system" + "help:Show help" + "version:Show version" +) diff --git a/mole b/mole index 83c0ee0..ed0814a 100755 --- a/mole +++ b/mole @@ -8,6 +8,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" +source "$SCRIPT_DIR/lib/core/commands.sh" trap cleanup_temp_files EXIT INT TERM @@ -238,17 +239,14 @@ show_help() { echo printf "%s%s%s\n" "$BLUE" "COMMANDS" "$NC" printf " %s%-28s%s %s\n" "$GREEN" "mo" "$NC" "Main menu" - printf " %s%-28s%s %s\n" "$GREEN" "mo clean" "$NC" "Free up disk space" - printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall" "$NC" "Remove apps completely" - printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "Check and maintain system" - printf " %s%-28s%s %s\n" "$GREEN" "mo analyze" "$NC" "Explore disk usage" - printf " %s%-28s%s %s\n" "$GREEN" "mo status" "$NC" "Monitor system health" - printf " %s%-28s%s %s\n" "$GREEN" "mo purge" "$NC" "Remove old project artifacts" - printf " %s%-28s%s %s\n" "$GREEN" "mo touchid" "$NC" "Configure Touch ID for sudo" - printf " %s%-28s%s %s\n" "$GREEN" "mo update" "$NC" "Update to latest version" - printf " %s%-28s%s %s\n" "$GREEN" "mo remove" "$NC" "Remove Mole from system" - printf " %s%-28s%s %s\n" "$GREEN" "mo --help" "$NC" "Show help" - printf " %s%-28s%s %s\n" "$GREEN" "mo --version" "$NC" "Show version" + for entry in "${MOLE_COMMANDS[@]}"; do + local name="${entry%%:*}" + local desc="${entry#*:}" + local display="mo $name" + [[ "$name" == "help" ]] && display="mo --help" + [[ "$name" == "version" ]] && display="mo --version" + printf " %s%-28s%s %s\n" "$GREEN" "$display" "$NC" "$desc" + done echo printf " %s%-28s%s %s\n" "$GREEN" "mo clean --dry-run" "$NC" "Preview cleanup" printf " %s%-28s%s %s\n" "$GREEN" "mo clean --whitelist" "$NC" "Manage protected caches" @@ -257,8 +255,6 @@ show_help() { printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --whitelist" "$NC" "Manage protected items" printf " %s%-28s%s %s\n" "$GREEN" "mo purge --paths" "$NC" "Configure scan directories" echo - printf " %s%-28s%s %s\n" "$GREEN" "mo completion" "$NC" "Configure shell completion" - echo printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC" printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs" echo @@ -787,6 +783,9 @@ main() { "touchid") exec "$SCRIPT_DIR/bin/touchid.sh" "${args[@]:1}" ;; + "completion") + exec "$SCRIPT_DIR/bin/completion.sh" "${args[@]:1}" + ;; "update") update_mole exit 0 diff --git a/tests/browser_version_cleanup.bats b/tests/browser_version_cleanup.bats new file mode 100644 index 0000000..1e06b0a --- /dev/null +++ b/tests/browser_version_cleanup.bats @@ -0,0 +1,289 @@ +#!/usr/bin/env bats + +setup_file() { + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT + + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME + + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-browser-cleanup.XXXXXX")" + export HOME + + mkdir -p "$HOME" +} + +teardown_file() { + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi +} + +@test "clean_chrome_old_versions skips when Chrome is running" { + 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/clean/user.sh" + +# Mock pgrep to simulate Chrome running +pgrep() { return 0; } +export -f pgrep + +clean_chrome_old_versions +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Google Chrome running"* ]] + [[ "$output" == *"old versions cleanup skipped"* ]] +} + +@test "clean_chrome_old_versions removes old versions but keeps current" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +# Mock pgrep to simulate Chrome not running +pgrep() { return 1; } +export -f pgrep + +# Create mock Chrome directory structure +CHROME_APP="$HOME/Applications/Google Chrome.app" +VERSIONS_DIR="$CHROME_APP/Contents/Frameworks/Google Chrome Framework.framework/Versions" +mkdir -p "$VERSIONS_DIR"/{128.0.0.0,129.0.0.0,130.0.0.0} + +# Create Current symlink pointing to 130.0.0.0 +ln -s "130.0.0.0" "$VERSIONS_DIR/Current" + +# Mock functions +is_path_whitelisted() { return 1; } +get_path_size_kb() { echo "10240"; } +bytes_to_human() { echo "10M"; } +note_activity() { :; } +export -f is_path_whitelisted get_path_size_kb bytes_to_human note_activity + +# Initialize counters +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +clean_chrome_old_versions + +# Verify output mentions old versions cleanup +echo "Cleaned: $files_cleaned items" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Chrome old versions"* ]] + [[ "$output" == *"dry"* ]] + [[ "$output" == *"Cleaned: 2 items"* ]] +} + +@test "clean_chrome_old_versions respects whitelist" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +# Mock pgrep to simulate Chrome not running +pgrep() { return 1; } +export -f pgrep + +# Create mock Chrome directory structure +CHROME_APP="$HOME/Applications/Google Chrome.app" +VERSIONS_DIR="$CHROME_APP/Contents/Frameworks/Google Chrome Framework.framework/Versions" +mkdir -p "$VERSIONS_DIR"/{128.0.0.0,129.0.0.0,130.0.0.0} + +# Create Current symlink pointing to 130.0.0.0 +ln -s "130.0.0.0" "$VERSIONS_DIR/Current" + +# Mock is_path_whitelisted to protect version 128.0.0.0 +is_path_whitelisted() { + [[ "$1" == *"128.0.0.0"* ]] && return 0 + return 1 +} +get_path_size_kb() { echo "10240"; } +bytes_to_human() { echo "10M"; } +note_activity() { :; } +export -f is_path_whitelisted get_path_size_kb bytes_to_human note_activity + +# Initialize counters +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +clean_chrome_old_versions + +# Should only clean 129.0.0.0 (not 128.0.0.0 which is whitelisted) +echo "Cleaned: $files_cleaned items" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Cleaned: 1 items"* ]] +} + +@test "clean_chrome_old_versions DRY_RUN mode does not delete files" { + # Create test directory + CHROME_APP="$HOME/Applications/Google Chrome.app" + VERSIONS_DIR="$CHROME_APP/Contents/Frameworks/Google Chrome Framework.framework/Versions" + mkdir -p "$VERSIONS_DIR"/{128.0.0.0,130.0.0.0} + + # Remove Current if it exists as a directory, then create symlink + rm -rf "$VERSIONS_DIR/Current" + ln -s "130.0.0.0" "$VERSIONS_DIR/Current" + + # Create a marker file in old version + touch "$VERSIONS_DIR/128.0.0.0/marker.txt" + + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +pgrep() { return 1; } +is_path_whitelisted() { return 1; } +get_path_size_kb() { echo "10240"; } +bytes_to_human() { echo "10M"; } +note_activity() { :; } +export -f pgrep is_path_whitelisted get_path_size_kb bytes_to_human note_activity + +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +clean_chrome_old_versions +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"dry"* ]] + # Verify marker file still exists (not deleted in dry run) + [ -f "$VERSIONS_DIR/128.0.0.0/marker.txt" ] +} + +@test "clean_chrome_old_versions handles missing Current symlink gracefully" { + # Use a fresh temp directory for this test + TEST_HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-test5.XXXXXX")" + + run env HOME="$TEST_HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +pgrep() { return 1; } +is_path_whitelisted() { return 1; } +get_path_size_kb() { echo "10240"; } +bytes_to_human() { echo "10M"; } +note_activity() { :; } +export -f pgrep is_path_whitelisted get_path_size_kb bytes_to_human note_activity + +# Initialize counters to prevent unbound variable errors +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +# Create Chrome app without Current symlink +CHROME_APP="$HOME/Applications/Google Chrome.app" +VERSIONS_DIR="$CHROME_APP/Contents/Frameworks/Google Chrome Framework.framework/Versions" +mkdir -p "$VERSIONS_DIR"/{128.0.0.0,129.0.0.0} +# No Current symlink created + +clean_chrome_old_versions +EOF + + rm -rf "$TEST_HOME" + [ "$status" -eq 0 ] + # Should exit gracefully with no output +} + +@test "clean_edge_old_versions skips when Edge is running" { + 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/clean/user.sh" + +# Mock pgrep to simulate Edge running +pgrep() { return 0; } +export -f pgrep + +clean_edge_old_versions +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Microsoft Edge running"* ]] + [[ "$output" == *"old versions cleanup skipped"* ]] +} + +@test "clean_edge_old_versions removes old versions but keeps current" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +pgrep() { return 1; } +export -f pgrep + +# Create mock Edge directory structure +EDGE_APP="$HOME/Applications/Microsoft Edge.app" +VERSIONS_DIR="$EDGE_APP/Contents/Frameworks/Microsoft Edge Framework.framework/Versions" +mkdir -p "$VERSIONS_DIR"/{120.0.0.0,121.0.0.0,122.0.0.0} + +# Create Current symlink pointing to 122.0.0.0 +ln -s "122.0.0.0" "$VERSIONS_DIR/Current" + +is_path_whitelisted() { return 1; } +get_path_size_kb() { echo "10240"; } +bytes_to_human() { echo "10M"; } +note_activity() { :; } +export -f is_path_whitelisted get_path_size_kb bytes_to_human note_activity + +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +clean_edge_old_versions + +echo "Cleaned: $files_cleaned items" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Edge old versions"* ]] + [[ "$output" == *"dry"* ]] + [[ "$output" == *"Cleaned: 2 items"* ]] +} + +@test "clean_edge_old_versions handles no old versions gracefully" { + # Use a fresh temp directory for this test + TEST_HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-test8.XXXXXX")" + + run env HOME="$TEST_HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +pgrep() { return 1; } +is_path_whitelisted() { return 1; } +get_path_size_kb() { echo "10240"; } +bytes_to_human() { echo "10M"; } +note_activity() { :; } +export -f pgrep is_path_whitelisted get_path_size_kb bytes_to_human note_activity + +# Initialize counters +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +# Create Edge with only current version +EDGE_APP="$HOME/Applications/Microsoft Edge.app" +VERSIONS_DIR="$EDGE_APP/Contents/Frameworks/Microsoft Edge Framework.framework/Versions" +mkdir -p "$VERSIONS_DIR/122.0.0.0" +ln -s "122.0.0.0" "$VERSIONS_DIR/Current" + +clean_edge_old_versions +EOF + + rm -rf "$TEST_HOME" + [ "$status" -eq 0 ] + # Should exit gracefully with no cleanup output + [[ "$output" != *"Edge old versions"* ]] +} diff --git a/tests/completion.bats b/tests/completion.bats new file mode 100755 index 0000000..5f131de --- /dev/null +++ b/tests/completion.bats @@ -0,0 +1,157 @@ +#!/usr/bin/env bats + +setup_file() { + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT + + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME + + ORIGINAL_PATH="${PATH:-}" + export ORIGINAL_PATH + + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-completion-home.XXXXXX")" + export HOME + + mkdir -p "$HOME" + + PATH="$PROJECT_ROOT:$PATH" + export PATH +} + +teardown_file() { + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi + if [[ -n "${ORIGINAL_PATH:-}" ]]; then + export PATH="$ORIGINAL_PATH" + fi +} + +setup() { + rm -rf "$HOME/.config" + rm -rf "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" + mkdir -p "$HOME" +} + +@test "completion script exists and is executable" { + [ -f "$PROJECT_ROOT/bin/completion.sh" ] + [ -x "$PROJECT_ROOT/bin/completion.sh" ] +} + +@test "completion script has valid bash syntax" { + run bash -n "$PROJECT_ROOT/bin/completion.sh" + [ "$status" -eq 0 ] +} + +@test "completion --help shows usage" { + run "$PROJECT_ROOT/bin/completion.sh" --help + [ "$status" -ne 0 ] + [[ "$output" == *"Usage: mole completion"* ]] + [[ "$output" == *"Auto-install"* ]] +} + +@test "completion bash generates valid bash script" { + run "$PROJECT_ROOT/bin/completion.sh" bash + [ "$status" -eq 0 ] + [[ "$output" == *"_mole_completions"* ]] + [[ "$output" == *"complete -F _mole_completions mole mo"* ]] +} + +@test "completion bash script includes all commands" { + run "$PROJECT_ROOT/bin/completion.sh" bash + [ "$status" -eq 0 ] + [[ "$output" == *"optimize"* ]] + [[ "$output" == *"clean"* ]] + [[ "$output" == *"uninstall"* ]] + [[ "$output" == *"analyze"* ]] + [[ "$output" == *"status"* ]] + [[ "$output" == *"purge"* ]] + [[ "$output" == *"touchid"* ]] + [[ "$output" == *"completion"* ]] +} + +@test "completion bash script supports mo command" { + run "$PROJECT_ROOT/bin/completion.sh" bash + [ "$status" -eq 0 ] + [[ "$output" == *"complete -F _mole_completions mole mo"* ]] +} + +@test "completion bash can be loaded in bash" { + run bash -c "eval \"\$(\"$PROJECT_ROOT/bin/completion.sh\" bash)\" && complete -p mole" + [ "$status" -eq 0 ] + [[ "$output" == *"_mole_completions"* ]] +} + +@test "completion zsh generates valid zsh script" { + run "$PROJECT_ROOT/bin/completion.sh" zsh + [ "$status" -eq 0 ] + [[ "$output" == *"#compdef mole mo"* ]] + [[ "$output" == *"_mole()"* ]] +} + +@test "completion zsh includes command descriptions" { + run "$PROJECT_ROOT/bin/completion.sh" zsh + [ "$status" -eq 0 ] + [[ "$output" == *"optimize:Free up disk space"* ]] + [[ "$output" == *"clean:Remove apps completely"* ]] +} + +@test "completion fish generates valid fish script" { + run "$PROJECT_ROOT/bin/completion.sh" fish + [ "$status" -eq 0 ] + [[ "$output" == *"complete -c mole"* ]] + [[ "$output" == *"complete -c mo"* ]] +} + +@test "completion fish includes both mole and mo commands" { + output="$("$PROJECT_ROOT/bin/completion.sh" fish)" + mole_count=$(echo "$output" | grep -c "complete -c mole") + mo_count=$(echo "$output" | grep -c "complete -c mo") + + [ "$mole_count" -gt 0 ] + [ "$mo_count" -gt 0 ] +} + +@test "completion auto-install detects zsh" { + export SHELL=/bin/zsh + + # Simulate auto-install (no interaction) + run bash -c "echo 'y' | \"$PROJECT_ROOT/bin/completion.sh\"" + + if [[ "$output" == *"Already configured"* ]]; then + skip "Already configured from previous test" + fi + + [ -f "$HOME/.zshrc" ] || skip "Auto-install didn't create .zshrc" + + run grep -E "mole[[:space:]]+completion" "$HOME/.zshrc" + [ "$status" -eq 0 ] +} + +@test "completion auto-install detects already installed" { + export SHELL=/bin/zsh + mkdir -p "$HOME" + echo 'eval "$(mole completion zsh)"' > "$HOME/.zshrc" + + run "$PROJECT_ROOT/bin/completion.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"updated"* ]] +} + +@test "completion script handles invalid shell argument" { + run "$PROJECT_ROOT/bin/completion.sh" invalid-shell + [ "$status" -ne 0 ] +} + +@test "completion subcommand supports bash/zsh/fish" { + run "$PROJECT_ROOT/bin/completion.sh" bash + [ "$status" -eq 0 ] + + run "$PROJECT_ROOT/bin/completion.sh" zsh + [ "$status" -eq 0 ] + + run "$PROJECT_ROOT/bin/completion.sh" fish + [ "$status" -eq 0 ] +}