1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 13:16:47 +00:00

feat: Create utility to find stale app installers

This commit is contained in:
Jack Phallen
2026-01-02 21:39:16 -08:00
parent 5e7f276722
commit a2f071fd48
5 changed files with 1016 additions and 0 deletions

View File

@@ -49,6 +49,7 @@ mo optimize # Refresh caches & services
mo analyze # Visual disk explorer mo analyze # Visual disk explorer
mo status # Live system health dashboard mo status # Live system health dashboard
mo purge # Clean project build artifacts mo purge # Clean project build artifacts
mo installers # Find and remove installer files
mo touchid # Configure Touch ID for sudo mo touchid # Configure Touch ID for sudo
mo completion # Setup shell tab completion mo completion # Setup shell tab completion

341
bin/installers.sh Executable file
View File

@@ -0,0 +1,341 @@
#!/bin/bash
# Mole - Installers command
# Highlights and helps remove installer files (.dmg, .pkg, .mpkg, .iso, .xip, .zip)
set -euo pipefail
# shellcheck disable=SC2154
# External variables set by menu_paginated.sh and environment
declare MOLE_SELECTION_RESULT
declare MOLE_INSTALLER_SCAN_MAX_DEPTH
export LC_ALL=C
export LANG=C
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/core/common.sh"
source "$SCRIPT_DIR/../lib/ui/menu_paginated.sh"
cleanup() {
show_cursor
cleanup_temp_files
}
trap cleanup EXIT
trap 'trap - EXIT; cleanup; exit 130' INT TERM
# Scan configuration
readonly INSTALLER_SCAN_MAX_DEPTH_DEFAULT=2
readonly INSTALLER_SCAN_PATHS=(
"$HOME/Downloads"
"$HOME/Desktop"
"$HOME/Documents"
"$HOME/Public"
"$HOME/Library/Downloads"
"/Users/Shared"
"/Users/Shared/Downloads" # Search one level deeper
)
readonly MAX_ZIP_ENTRIES=5
# Check for installer payloads inside ZIP (single pass, fused size and pattern check)
is_installer_zip() {
local zip="$1"
local cap="$MAX_ZIP_ENTRIES"
zipinfo -1 "$zip" >/dev/null 2>&1 || return 1
zipinfo -1 "$zip" 2>/dev/null \
| head -n $((cap + 1)) \
| awk -v cap="$cap" '
/\.(app|pkg|dmg|xip)(\/|$)/ { found=1 }
END {
if (NR > cap) exit 1
exit found ? 0 : 1
}
'
}
scan_installers_in_path() {
local path="$1"
local max_depth="${MOLE_INSTALLER_SCAN_MAX_DEPTH:-$INSTALLER_SCAN_MAX_DEPTH_DEFAULT}"
[[ -d "$path" ]] || return 0
local file
if command -v fd > /dev/null 2>&1; then
while IFS= read -r file; do
[[ -L "$file" ]] && continue # Skip symlinks explicitly
case "$file" in
*.dmg|*.pkg|*.mpkg|*.iso|*.xip)
echo "$file"
;;
*.zip)
[[ -r "$file" ]] || continue
if is_installer_zip "$file" 2>/dev/null; then
echo "$file"
fi
;;
esac
done < <(
fd --no-ignore --hidden --type f --max-depth "$max_depth" \
-e dmg -e pkg -e mpkg -e iso -e xip -e zip \
. "$path" 2>/dev/null || true
)
else
while IFS= read -r file; do
[[ -L "$file" ]] && continue # Skip symlinks explicitly
case "$file" in
*.dmg|*.pkg|*.mpkg|*.iso|*.xip)
echo "$file"
;;
*.zip)
[[ -r "$file" ]] || continue
if is_installer_zip "$file" 2>/dev/null; then
echo "$file"
fi
;;
esac
done < <(
find "$path" -maxdepth "$max_depth" -type f \
\( -name '*.dmg' -o -name '*.pkg' -o -name '*.mpkg' \
-o -name '*.iso' -o -name '*.xip' -o -name '*.zip' \) \
2>/dev/null || true
)
fi
}
scan_all_installers() {
for path in "${INSTALLER_SCAN_PATHS[@]}"; do
scan_installers_in_path "$path"
done
}
# Initialize stats
declare -i total_deleted=0
declare -i total_size_freed_kb=0
# Global arrays for installer data
declare -a INSTALLER_PATHS=()
declare -a INSTALLER_SIZES=()
declare -a DISPLAY_NAMES=()
# Collect all installers with their metadata
collect_installers() {
printf '\n'
echo -e "${BLUE}━━━ Scanning for installers ━━━${NC}"
# Clear previous results
INSTALLER_PATHS=()
INSTALLER_SIZES=()
DISPLAY_NAMES=()
# Scan all paths, deduplicate, and sort results
local -a all_files=()
local sorted_paths
sorted_paths=$(scan_all_installers | sort -u)
if [[ -z "$sorted_paths" ]]; then
echo -e " ${YELLOW}No installer files found${NC}"
return 1
fi
# Read sorted results into array
while IFS= read -r file; do
[[ -z "$file" ]] && continue
all_files+=("$file")
done <<< "$sorted_paths"
# Process each installer
for file in "${all_files[@]}"; do
# Calculate file size
local file_size=0
if [[ -f "$file" ]]; then
file_size=$(get_file_size "$file")
fi
# Store installer path and size in parallel arrays
INSTALLER_PATHS+=("$file")
INSTALLER_SIZES+=("$file_size")
DISPLAY_NAMES+=("$(basename "$file")")
done
echo -e " ${GREEN}Found ${#INSTALLER_PATHS[@]} installer(s)${NC}"
return 0
}
# Show menu for user selection
show_installer_menu() {
if [[ ${#DISPLAY_NAMES[@]} -eq 0 ]]; then
return 1
fi
echo ""
local title="Select installers to remove"
MOLE_SELECTION_RESULT=""
paginated_multi_select "$title" "${DISPLAY_NAMES[@]}"
local selection_exit=$?
if [[ $selection_exit -ne 0 ]]; then
echo ""
echo -e "${YELLOW}Cancelled${NC}"
return 1
fi
return 0
}
# Delete selected installers
delete_selected_installers() {
# Parse selection indices
local -a selected_indices=()
[[ -n "$MOLE_SELECTION_RESULT" ]] && IFS=',' read -ra selected_indices <<<"$MOLE_SELECTION_RESULT"
if [[ ${#selected_indices[@]} -eq 0 ]]; then
return 1
fi
printf '\n'
echo -e "${BLUE}━━━ Removing installers ━━━${NC}"
# Delete each selected installer
total_deleted=0
total_size_freed_kb=0
for idx in "${selected_indices[@]}"; do
if [[ ! "$idx" =~ ^[0-9]+$ ]] || [[ $idx -ge ${#INSTALLER_PATHS[@]} ]]; then
continue
fi
local file_path="${INSTALLER_PATHS[$idx]}"
local file_size="${INSTALLER_SIZES[$idx]}"
# Validate path before deletion
if ! validate_path_for_deletion "$file_path"; then
echo -e " ${RED}${ICON_FAILED}${NC} Cannot delete (invalid path): $(basename "$file_path")"
continue
fi
# Delete the file
if safe_remove "$file_path" true; then
local human_size
human_size=$(bytes_to_human "$file_size")
total_size_freed_kb=$((total_size_freed_kb + ((file_size + 1023) / 1024)))
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Deleted: $(basename "$file_path") ${GRAY}($human_size)${NC}"
total_deleted=$((total_deleted + 1))
else
echo -e " ${RED}${ICON_FAILED}${NC} Failed to delete: $(basename "$file_path")"
fi
done
return 0
}
# Perform the installers cleanup
perform_installers() {
# Collect installers
if ! collect_installers; then
return 2 # Nothing to clean
fi
# Show menu
if ! show_installer_menu; then
return 1 # User cancelled
fi
# Delete selected
delete_selected_installers
return 0
}
show_summary() {
echo ""
local summary_heading="Cleanup complete"
local -a summary_details=()
if [[ $total_deleted -gt 0 ]]; then
local freed_mb
freed_mb=$(echo "$total_size_freed_kb" | awk '{printf "%.2f", $1/1024}')
summary_details+=("Installers removed: $total_deleted")
summary_details+=("Space freed: ${GREEN}${freed_mb}MB${NC}")
summary_details+=("Free space now: $(get_free_space)")
else
summary_details+=("No installers were removed")
summary_details+=("Free space now: $(get_free_space)")
fi
print_summary_block "$summary_heading" "${summary_details[@]}"
printf '\n'
}
show_help() {
echo -e "${PURPLE_BOLD}Mole Installers${NC} - Find and remove installer files"
echo ""
echo -e "${YELLOW}Usage:${NC} mo installers [options]"
echo ""
echo -e "${YELLOW}Options:${NC}"
echo " --debug Enable debug logging"
echo " --help Show this help message"
echo ""
echo -e "${YELLOW}Default Paths${NC}"
echo " - ${HOME}/Downloads"
echo " - ${HOME}/Desktop"
echo " - ${HOME}/Documents"
echo " - ${HOME}/Public"
echo " - ${HOME}/Library/Downloads"
echo " - /Users/Shared"
}
main() {
for arg in "$@"; do
case "$arg" in
"--help")
show_help
exit 0
;;
"--debug")
export MO_DEBUG=1
;;
*)
echo "Unknown option: $arg"
echo "Use 'mo installers --help' for usage information"
exit 1
;;
esac
done
# Clear screen for better UX
if [[ -t 1 ]]; then
printf '\033[2J\033[H'
fi
printf '\n'
echo -e "${PURPLE_BOLD}Find & Remove Installers${NC}"
hide_cursor
perform_installers
local exit_code=$?
show_cursor
case $exit_code in
0)
show_summary
;;
1)
printf '\n'
;;
2)
printf '\n'
echo -e "${YELLOW}No installer files found in default locations${NC}"
printf '\n'
;;
esac
return 0
}
# Only run main if not in test mode
if [[ "${MOLE_TEST_MODE:-0}" != "1" ]]; then
main "$@"
fi

View File

@@ -8,6 +8,7 @@ MOLE_COMMANDS=(
"analyze:Explore disk usage" "analyze:Explore disk usage"
"status:Monitor system health" "status:Monitor system health"
"purge:Remove old project artifacts" "purge:Remove old project artifacts"
"installers:Find and remove installer files"
"touchid:Configure Touch ID for sudo" "touchid:Configure Touch ID for sudo"
"completion:Setup shell tab completion" "completion:Setup shell tab completion"
"update:Update to latest version" "update:Update to latest version"

4
mole
View File

@@ -254,6 +254,7 @@ show_help() {
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --dry-run" "$NC" "Preview optimization" printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --dry-run" "$NC" "Preview optimization"
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --whitelist" "$NC" "Manage protected items" 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" printf " %s%-28s%s %s\n" "$GREEN" "mo purge --paths" "$NC" "Configure scan directories"
printf " %s%-28s%s %s\n" "$GREEN" "mo installers --debug" "$NC" "Find installer files"
echo echo
printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC" printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC"
printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs" printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs"
@@ -780,6 +781,9 @@ main() {
"purge") "purge")
exec "$SCRIPT_DIR/bin/purge.sh" "${args[@]:1}" exec "$SCRIPT_DIR/bin/purge.sh" "${args[@]:1}"
;; ;;
"installers")
exec "$SCRIPT_DIR/bin/installers.sh" "${args[@]:1}"
;;
"touchid") "touchid")
exec "$SCRIPT_DIR/bin/touchid.sh" "${args[@]:1}" exec "$SCRIPT_DIR/bin/touchid.sh" "${args[@]:1}"
;; ;;

669
tests/installers.bats Normal file
View File

@@ -0,0 +1,669 @@
#!/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-installers-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
export TERM="xterm-256color"
export MO_DEBUG=0
# Create standard scan directories
mkdir -p "$HOME/Downloads"
mkdir -p "$HOME/Desktop"
mkdir -p "$HOME/Documents"
mkdir -p "$HOME/Public"
mkdir -p "$HOME/Library/Downloads"
# Clear previous test files
rm -rf "${HOME:?}/Downloads"/*
rm -rf "${HOME:?}/Desktop"/*
rm -rf "${HOME:?}/Documents"/*
}
# Test help and arguments
@test "installers.sh --help shows usage information" {
run "$PROJECT_ROOT/bin/installers.sh" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Mole Installers"* ]]
[[ "$output" == *"Usage:"* ]]
[[ "$output" == *"mo installers"* ]]
}
@test "installers.sh --help lists options and paths" {
run "$PROJECT_ROOT/bin/installers.sh" --help
[ "$status" -eq 0 ]
[[ "$output" == *"--debug"* ]]
[[ "$output" == *"--help"* ]]
[[ "$output" == *"mo installers"* ]]
}
@test "installers.sh --help shows scan scope" {
run "$PROJECT_ROOT/bin/installers.sh" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Downloads"* ]]
[[ "$output" == *"Desktop"* ]]
[[ "$output" == *"Documents"* ]]
}
@test "installers.sh rejects unknown options" {
run "$PROJECT_ROOT/bin/installers.sh" --unknown-option
[ "$status" -eq 1 ]
[[ "$output" == *"Unknown option"* ]]
[[ "$output" == *"--help"* ]]
}
# Test scan_installers_in_path function directly
# Tests are duplicated to cover both fd and find code paths
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Tests using fd (when available)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@test "scan_installers_in_path (fd): finds .dmg files" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
touch "$HOME/Downloads/Chrome.dmg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"Chrome.dmg"* ]]
}
@test "scan_installers_in_path (fd): finds multiple installer types" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
touch "$HOME/Downloads/App1.dmg"
touch "$HOME/Downloads/App2.pkg"
touch "$HOME/Downloads/App3.iso"
touch "$HOME/Downloads/App.mpkg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"App1.dmg"* ]]
[[ "$output" == *"App2.pkg"* ]]
[[ "$output" == *"App3.iso"* ]]
[[ "$output" == *"App.mpkg"* ]]
}
@test "scan_installers_in_path (fd): respects max depth" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
mkdir -p "$HOME/Downloads/level1/level2/level3"
touch "$HOME/Downloads/shallow.dmg"
touch "$HOME/Downloads/level1/mid.dmg"
touch "$HOME/Downloads/level1/level2/deep.dmg"
touch "$HOME/Downloads/level1/level2/level3/too-deep.dmg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
# Default max depth is 2
[[ "$output" == *"shallow.dmg"* ]]
[[ "$output" == *"mid.dmg"* ]]
[[ "$output" == *"deep.dmg"* ]]
[[ "$output" != *"too-deep.dmg"* ]]
}
@test "scan_installers_in_path (fd): honors MOLE_INSTALLER_SCAN_MAX_DEPTH" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
mkdir -p "$HOME/Downloads/level1"
touch "$HOME/Downloads/top.dmg"
touch "$HOME/Downloads/level1/nested.dmg"
run env MOLE_INSTALLER_SCAN_MAX_DEPTH=1 bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"top.dmg"* ]]
[[ "$output" != *"nested.dmg"* ]]
}
@test "scan_installers_in_path (fd): handles non-existent directory" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/NonExistent"
[ "$status" -eq 0 ]
[[ -z "$output" ]]
}
@test "scan_installers_in_path (fd): ignores non-installer files" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
touch "$HOME/Downloads/document.pdf"
touch "$HOME/Downloads/image.jpg"
touch "$HOME/Downloads/archive.tar.gz"
touch "$HOME/Downloads/Installer.dmg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" != *"document.pdf"* ]]
[[ "$output" != *"image.jpg"* ]]
[[ "$output" != *"archive.tar.gz"* ]]
[[ "$output" == *"Installer.dmg"* ]]
}
@test "scan_installers_in_path (fd): handles filenames with spaces" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
touch "$HOME/Downloads/My App Installer.dmg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"My App Installer.dmg"* ]]
}
@test "scan_installers_in_path (fd): handles filenames with special characters" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
touch "$HOME/Downloads/App-v1.2.3_beta.pkg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"App-v1.2.3_beta.pkg"* ]]
}
@test "scan_installers_in_path (fd): returns empty for directory with no installers" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
# Create some non-installer files
touch "$HOME/Downloads/document.pdf"
touch "$HOME/Downloads/image.png"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ -z "$output" ]]
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Tests using find (forced fallback by hiding fd)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@test "scan_installers_in_path (fallback find): finds .dmg files" {
touch "$HOME/Downloads/Chrome.dmg"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"Chrome.dmg"* ]]
}
@test "scan_installers_in_path (fallback find): finds multiple installer types" {
touch "$HOME/Downloads/App1.dmg"
touch "$HOME/Downloads/App2.pkg"
touch "$HOME/Downloads/App3.iso"
touch "$HOME/Downloads/App.mpkg"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"App1.dmg"* ]]
[[ "$output" == *"App2.pkg"* ]]
[[ "$output" == *"App3.iso"* ]]
[[ "$output" == *"App.mpkg"* ]]
}
@test "scan_installers_in_path (fallback find): respects max depth" {
mkdir -p "$HOME/Downloads/level1/level2/level3"
touch "$HOME/Downloads/shallow.dmg"
touch "$HOME/Downloads/level1/mid.dmg"
touch "$HOME/Downloads/level1/level2/deep.dmg"
touch "$HOME/Downloads/level1/level2/level3/too-deep.dmg"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
# Default max depth is 2
[[ "$output" == *"shallow.dmg"* ]]
[[ "$output" == *"mid.dmg"* ]]
[[ "$output" == *"deep.dmg"* ]]
[[ "$output" != *"too-deep.dmg"* ]]
}
@test "scan_installers_in_path (fallback find): honors MOLE_INSTALLER_SCAN_MAX_DEPTH" {
mkdir -p "$HOME/Downloads/level1"
touch "$HOME/Downloads/top.dmg"
touch "$HOME/Downloads/level1/nested.dmg"
run env PATH="/usr/bin:/bin" MOLE_INSTALLER_SCAN_MAX_DEPTH=1 bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"top.dmg"* ]]
[[ "$output" != *"nested.dmg"* ]]
}
@test "scan_installers_in_path (fallback find): handles non-existent directory" {
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/NonExistent"
[ "$status" -eq 0 ]
[[ -z "$output" ]]
}
@test "scan_installers_in_path (fallback find): ignores non-installer files" {
touch "$HOME/Downloads/document.pdf"
touch "$HOME/Downloads/image.jpg"
touch "$HOME/Downloads/archive.tar.gz"
touch "$HOME/Downloads/Installer.dmg"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" != *"document.pdf"* ]]
[[ "$output" != *"image.jpg"* ]]
[[ "$output" != *"archive.tar.gz"* ]]
[[ "$output" == *"Installer.dmg"* ]]
}
# Test ZIP installer detection
@test "is_installer_zip: rejects ZIP with installer content but too many entries" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
# Create a ZIP with too many files (exceeds MAX_ZIP_ENTRIES=5)
# Include a .app file to have installer content
mkdir -p "$HOME/Downloads/large-app"
touch "$HOME/Downloads/large-app/MyApp.app"
for i in {1..9}; do
touch "$HOME/Downloads/large-app/file$i.txt"
done
(cd "$HOME/Downloads" && zip -q -r large-installer.zip large-app)
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
if is_installer_zip "'"$HOME/Downloads/large-installer.zip"'"; then
echo "INSTALLER"
else
echo "NOT_INSTALLER"
fi
' bash "$PROJECT_ROOT/bin/installers.sh"
[ "$status" -eq 0 ]
[[ "$output" == "NOT_INSTALLER" ]]
}
@test "is_installer_zip: detects ZIP with app content" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
mkdir -p "$HOME/Downloads/app-content"
touch "$HOME/Downloads/app-content/MyApp.app"
(cd "$HOME/Downloads" && zip -q -r app.zip app-content)
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
if is_installer_zip "'"$HOME/Downloads/app.zip"'"; then
echo "INSTALLER"
else
echo "NOT_INSTALLER"
fi
' bash "$PROJECT_ROOT/bin/installers.sh"
[ "$status" -eq 0 ]
[[ "$output" == "INSTALLER" ]]
}
@test "is_installer_zip: rejects ZIP with only regular files" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
mkdir -p "$HOME/Downloads/data"
touch "$HOME/Downloads/data/file1.txt"
touch "$HOME/Downloads/data/file2.pdf"
(cd "$HOME/Downloads" && zip -q -r data.zip data)
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
if is_installer_zip "'"$HOME/Downloads/data.zip"'"; then
echo "INSTALLER"
else
echo "NOT_INSTALLER"
fi
' bash "$PROJECT_ROOT/bin/installers.sh"
[ "$status" -eq 0 ]
[[ "$output" == "NOT_INSTALLER" ]]
}
# Integration tests: ZIP scanning inside scan_all_installers
@test "scan_all_installers: finds installer ZIP in Downloads" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
# Create a valid installer ZIP (contains .app)
mkdir -p "$HOME/Downloads/app-content"
touch "$HOME/Downloads/app-content/MyApp.app"
(cd "$HOME/Downloads" && zip -q -r installer.zip app-content)
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_all_installers
' bash "$PROJECT_ROOT/bin/installers.sh"
[ "$status" -eq 0 ]
[[ "$output" == *"installer.zip"* ]]
}
@test "scan_all_installers: ignores non-installer ZIP in Downloads" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
# Create a non-installer ZIP (only regular files)
mkdir -p "$HOME/Downloads/data"
touch "$HOME/Downloads/data/file1.txt"
touch "$HOME/Downloads/data/file2.pdf"
(cd "$HOME/Downloads" && zip -q -r data.zip data)
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_all_installers
' bash "$PROJECT_ROOT/bin/installers.sh"
[ "$status" -eq 0 ]
[[ "$output" != *"data.zip"* ]]
}
@test "scan_all_installers: handles missing paths gracefully" {
# Don't create all scan directories, some may not exist
# Only create Downloads, delete others if they exist
rm -rf "$HOME/Desktop"
rm -rf "$HOME/Documents"
rm -rf "$HOME/Public"
rm -rf "$HOME/Public/Downloads"
rm -rf "$HOME/Library/Downloads"
mkdir -p "$HOME/Downloads"
# Add an installer to the one directory that exists
touch "$HOME/Downloads/test.dmg"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_all_installers
' bash "$PROJECT_ROOT/bin/installers.sh"
# Should succeed even with missing paths
[ "$status" -eq 0 ]
# Should still find the installer in the existing directory
[[ "$output" == *"test.dmg"* ]]
}
# Test edge cases
@test "scan_installers_in_path (fallback find): handles filenames with spaces" {
touch "$HOME/Downloads/My App Installer.dmg"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"My App Installer.dmg"* ]]
}
@test "scan_installers_in_path (fallback find): handles filenames with special characters" {
touch "$HOME/Downloads/App-v1.2.3_beta.pkg"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"App-v1.2.3_beta.pkg"* ]]
}
@test "scan_installers_in_path (fallback find): returns empty for directory with no installers" {
# Create some non-installer files
touch "$HOME/Downloads/document.pdf"
touch "$HOME/Downloads/image.png"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ -z "$output" ]]
}
# Failure path tests for scan_installers_in_path
@test "scan_installers_in_path: skips corrupt ZIP files" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
# Create a corrupt ZIP file by just writing garbage data
echo "This is not a valid ZIP file" > "$HOME/Downloads/corrupt.zip"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
# Should succeed (return 0) and silently skip the corrupt ZIP
[ "$status" -eq 0 ]
# Output should be empty since corrupt.zip is not a valid installer
[[ -z "$output" ]]
}
@test "scan_installers_in_path: handles permission-denied files" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
# Create a valid installer ZIP
mkdir -p "$HOME/Downloads/app-content"
touch "$HOME/Downloads/app-content/MyApp.app"
(cd "$HOME/Downloads" && zip -q -r readable.zip app-content)
# Create a readable installer ZIP alongside a permission-denied file
mkdir -p "$HOME/Downloads/restricted-app"
touch "$HOME/Downloads/restricted-app/App.app"
(cd "$HOME/Downloads" && zip -q -r restricted.zip restricted-app)
# Remove read permissions from restricted.zip
chmod 000 "$HOME/Downloads/restricted.zip"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
# Should succeed and find the readable.zip but skip restricted.zip
[ "$status" -eq 0 ]
[[ "$output" == *"readable.zip"* ]]
[[ "$output" != *"restricted.zip"* ]]
# Cleanup: restore permissions for teardown
chmod 644 "$HOME/Downloads/restricted.zip"
}
@test "scan_installers_in_path: finds installer ZIP alongside corrupt ZIPs" {
if ! command -v zipinfo > /dev/null 2>&1; then
skip "zipinfo not available"
fi
# Create a valid installer ZIP
mkdir -p "$HOME/Downloads/app-content"
touch "$HOME/Downloads/app-content/MyApp.app"
(cd "$HOME/Downloads" && zip -q -r valid-installer.zip app-content)
# Create a corrupt ZIP
echo "garbage data" > "$HOME/Downloads/corrupt.zip"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
# Should find the valid ZIP and silently skip the corrupt one
[ "$status" -eq 0 ]
[[ "$output" == *"valid-installer.zip"* ]]
[[ "$output" != *"corrupt.zip"* ]]
}
# Symlink handling tests
@test "scan_installers_in_path (fd): skips symlinks to regular files" {
if ! command -v fd > /dev/null 2>&1; then
skip "fd not available on this system"
fi
touch "$HOME/Downloads/real.dmg"
ln -s "$HOME/Downloads/real.dmg" "$HOME/Downloads/symlink.dmg"
ln -s /nonexistent "$HOME/Downloads/dangling.lnk"
run bash -euo pipefail -c '
export MOLE_TEST_MODE=1
source "$1"
scan_installers_in_path "$2"
' bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"real.dmg"* ]]
[[ "$output" != *"symlink.dmg"* ]]
[[ "$output" != *"dangling.lnk"* ]]
}
@test "scan_installers_in_path (fallback find): skips symlinks to regular files" {
touch "$HOME/Downloads/real.dmg"
ln -s "$HOME/Downloads/real.dmg" "$HOME/Downloads/symlink.dmg"
ln -s /nonexistent "$HOME/Downloads/dangling.lnk"
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
export MOLE_TEST_MODE=1
source \"\$1\"
scan_installers_in_path \"\$2\"
" bash "$PROJECT_ROOT/bin/installers.sh" "$HOME/Downloads"
[ "$status" -eq 0 ]
[[ "$output" == *"real.dmg"* ]]
[[ "$output" != *"symlink.dmg"* ]]
[[ "$output" != *"dangling.lnk"* ]]
}