8000 Mole/tests/purge.bats at main · dominic-sylvester/Mole · GitHub
[go: up one dir, main page]

Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Latest commit

 

History

History
2915 lines (2453 loc) · 89.8 KB

purge.bats

File metadata and controls

2915 lines (2453 loc) · 89.8 KB
#!/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-purge-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
}
teardown_file() {
if [[ "$HOME" == "${BATS_TEST_DIRNAME}/tmp-"* ]]; then
rm -rf "$HOME"
fi
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
# Safety: refuse to operate on a real home directory.
if [[ "$HOME" != "${BATS_TEST_DIRNAME}/tmp-"* ]]; then
printf 'FATAL: HOME is not a test temp dir: %s\n' "$HOME" >&2
return 1
fi
mkdir -p "$HOME/www"
mkdir -p "$HOME/dev"
mkdir -p "$HOME/.cache/mole"
rm -rf "${HOME:?}/www"/* "${HOME:?}/dev"/*
rm -rf "${HOME:?}/Library/CloudStorage" "${HOME:?}/Library/Mobile Documents"
}
@test "mole_purge_is_cloud_synced_path matches only exact cloud roots and descendants" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
physical_home="$HOME/cloud-home-physical"
logical_home="$HOME/cloud-home-link"
mkdir -p "$physical_home"
ln -s "$physical_home" "$logical_home"
HOME="$logical_home"
source "$PROJECT_ROOT/lib/clean/purge_shared.sh"
mole_purge_is_cloud_synced_path "$HOME/Library/CloudStorage"
mole_purge_is_cloud_synced_path "$HOME/Library/CloudStorage/Provider/project/target"
mole_purge_is_cloud_synced_path "$HOME/Library/Mobile Documents"
mole_purge_is_cloud_synced_path "$HOME/Library/Mobile Documents/com~apple~CloudDocs/project/node_modules"
mole_purge_is_cloud_synced_path "$physical_home/Library/CloudStorage/Provider/project/target"
mole_purge_is_cloud_synced_path "$physical_home/Library/Mobile Documents/com~apple~CloudDocs/project/node_modules"
if mole_purge_is_cloud_synced_path "$HOME/Library/CloudStorageBackup/project/target"; then
exit 1
fi
if mole_purge_is_cloud_synced_path "$HOME/Library/Mobile Documents-old/project/target"; then
exit 1
fi
if mole_purge_is_cloud_synced_path "$physical_home/Library/CloudStorageBackup/project/target"; then
exit 1
fi
if mole_purge_is_cloud_synced_path "$physical_home/Library/Mobile Documents-old/project/target"; then
exit 1
fi
EOF
[ "$status" -eq 0 ] || return 1
}
@test "is_safe_project_artifact: rejects shallow paths (protection against accidents)" {
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_safe_project_artifact '$HOME/www/node_modules' '$HOME/www'; then
echo 'UNSAFE'
else
echo 'SAFE'
fi
")
[[ "$result" == "SAFE" ]]
}
@test "is_safe_project_artifact: allows proper project artifacts" {
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_safe_project_artifact '$HOME/www/myproject/node_modules' '$HOME/www'; then
echo 'ALLOWED'
else
echo 'BLOCKED'
fi
")
[[ "$result" == "ALLOWED" ]]
}
@test "is_safe_project_artifact: rejects non-absolute paths" {
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_safe_project_artifact 'relative/path/node_modules' '$HOME/www'; then
echo 'UNSAFE'
else
echo 'SAFE'
fi
")
[[ "$result" == "SAFE" ]]
}
@test "is_safe_project_artifact: validates depth calculation" {
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_safe_project_artifact '$HOME/www/project/subdir/node_modules' '$HOME/www'; then
echo 'ALLOWED'
else
echo 'BLOCKED'
fi
")
[[ "$result" == "ALLOWED" ]]
}
@test "is_safe_project_artifact: allows direct child when search path is project root" {
mkdir -p "$HOME/single-project/node_modules"
touch "$HOME/single-project/package.json"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_safe_project_artifact '$HOME/single-project/node_modules' '$HOME/single-project'; then
echo 'ALLOWED'
else
echo 'BLOCKED'
fi
")
[[ "$result" == "ALLOWED" ]]
}
@test "is_safe_project_artifact: accepts physical path under symlinked search root" {
mkdir -p "$HOME/www/real/proj/node_modules"
touch "$HOME/www/real/proj/package.json"
ln -s "$HOME/www/real" "$HOME/www/link"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_safe_project_artifact '$HOME/www/real/proj/node_modules' '$HOME/www/link/proj'; then
echo 'ALLOWED'
else
echo 'BLOCKED'
fi
")
[[ "$result" == "ALLOWED" ]]
}
@test "is_safe_configured_purge_artifact rejects paths outside configured roots" {
mkdir -p "$HOME/www/project/node_modules" "$HOME/dev/other/node_modules"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
PURGE_SEARCH_PATHS=('$HOME/www')
if is_safe_configured_purge_artifact '$HOME/dev/other/node_modules'; then
echo 'UNSAFE'
else
echo 'BLOCKED'
fi
")
[[ "$result" == "BLOCKED" ]]
}
@test "is_safe_configured_purge_artifact checks a lexical owner before unrelated roots" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
PURGE_SEARCH_PATHS=("$HOME/unrelated" "$HOME/projects")
is_safe_project_artifact() {
printf '%s\n' "$2" >> "$HOME/safety-roots"
[[ "$2" == "$HOME/projects" ]]
}
is_safe_configured_purge_artifact "$HOME/projects/app/node_modules"
printf 'CALLS=%s ROOT=%s\n' \
"$(wc -l < "$HOME/safety-roots" | tr -d ' ')" \
"$(cat "$HOME/safety-roots")"
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == "CALLS=1 ROOT=$HOME/projects" ]] || return 1
}
@test "is_safe_configured_purge_artifact preserves physical alias fallback" {
mkdir -p "$HOME/www/real/proj/node_modules"
touch "$HOME/www/real/proj/package.json"
ln -s "$HOME/www/real" "$HOME/www/link"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
PURGE_SEARCH_PATHS=('$HOME/www/link/proj')
if is_safe_configured_purge_artifact '$HOME/www/real/proj/node_modules'; then
echo 'ALLOWED'
else
echo 'BLOCKED'
fi
")
[[ "$result" == "ALLOWED" ]]
}
@test "compact_purge_scan_path keeps the tail of long purge paths visible" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" MOLE_SKIP_MAIN=1 /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/bin/purge.sh"
compact_purge_scan_path "$HOME/projects/team/service/very/deep/component/node_modules" 32
EOF
[ "$status" -eq 0 ]
[[ "$output" == ".../deep/component/node_modules" ]]
}
@test "compact_purge_menu_path keeps the project tail visible" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
compact_purge_menu_path "$HOME/projects/team/service/very/deep/component/node_modules" 32
EOF
[ "$status" -eq 0 ]
[[ "$output" == ".../deep/component/node_modules" ]]
}
@test "format_purge_target_path rewrites home with tilde" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
format_purge_target_path "$HOME/www/app/node_modules"
EOF
[ "$status" -eq 0 ]
[[ "$output" == \~/www/app/node_modules ]]
}
@test "find_purge_project_root_for_artifact prefers the owning monorepo" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
artifact="$HOME/www/obelisk/apps/web/node_modules"
mkdir -p "$artifact"
touch "$HOME/www/obelisk/pnpm-workspace.yaml"
touch "$HOME/www/obelisk/apps/web/package.json"
find_purge_project_root_for_artifact "$artifact"
EOF
[ "$status" -eq 0 ] || return 1
[ "$output" = "$HOME/www/obelisk" ] || return 1
}
@test "find_purge_project_root_for_artifact prefers a worktree over its nested package" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
artifact="$HOME/.codex/worktrees/obelisk/apps/web/node_modules"
mkdir -p "$artifact"
touch "$HOME/.codex/worktrees/obelisk/.git"
touch "$HOME/.codex/worktrees/obelisk/apps/web/package.json"
find_purge_project_root_for_artifact "$artifact"
EOF
[ "$status" -eq 0 ] || return 1
[ "$output" = "$HOME/.codex/worktrees/obelisk" ] || return 1
}
@test "filter_nested_artifacts: removes nested node_modules" {
mkdir -p "$HOME/www/project/node_modules/package/node_modules"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
printf '%s\n' '$HOME/www/project/node_modules' '$HOME/www/project/node_modules/package/node_modules' | \
filter_nested_artifacts | wc -l | tr -d ' '
")
[[ "$result" == "1" ]]
}
@test "filter_nested_artifacts: keeps independent artifacts" {
mkdir -p "$HOME/www/project1/node_modules"
mkdir -p "$HOME/www/project2/target"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
printf '%s\n' '$HOME/www/project1/node_modules' '$HOME/www/project2/target' | \
filter_nested_artifacts | wc -l | tr -d ' '
")
[[ "$result" == "2" ]]
}
@test "filter_nested_artifacts: removes Xcode build subdirectories (Mac projects)" {
# Simulate Mac Xcode project with nested .build directories:
# ~/www/testapp/build
# ~/www/testapp/build/Framework.build
# ~/www/testapp/build/Package.build
mkdir -p "$HOME/www/testapp/build/Framework.build"
mkdir -p "$HOME/www/testapp/build/Package.build"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
printf '%s\n' \
'$HOME/www/testapp/build' \
'$HOME/www/testapp/build/Framework.build' \
'$HOME/www/testapp/build/Package.build' | \
filter_nested_artifacts | wc -l | tr -d ' '
")
# Should only keep the top-level 'build' directory, filtering out nested .build dirs
[[ "$result" == "1" ]]
}
# Vendor protection unit tests
@test "is_rails_project_root: detects valid Rails project" {
mkdir -p "$HOME/www/test-rails/config"
mkdir -p "$HOME/www/test-rails/bin"
touch "$HOME/www/test-rails/config/application.rb"
touch "$HOME/www/test-rails/Gemfile"
touch "$HOME/www/test-rails/bin/rails"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_rails_project_root '$HOME/www/test-rails'; then
echo 'YES'
else
echo 'NO'
fi
")
[[ "$result" == "YES" ]]
}
@test "is_rails_project_root: rejects non-Rails directory" {
mkdir -p "$HOME/www/not-rails"
touch "$HOME/www/not-rails/package.json"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_rails_project_root '$HOME/www/not-rails'; then
echo 'YES'
else
echo 'NO'
fi
")
[[ "$result" == "NO" ]]
}
@test "is_go_project_root: detects valid Go project" {
mkdir -p "$HOME/www/test-go"
touch "$HOME/www/test-go/go.mod"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_go_project_root '$HOME/www/test-go'; then
echo 'YES'
else
echo 'NO'
fi
")
[[ "$result" == "YES" ]]
}
@test "is_php_project_root: detects valid PHP Composer project" {
mkdir -p "$HOME/www/test-php"
touch "$HOME/www/test-php/composer.json"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_php_project_root '$HOME/www/test-php'; then
echo 'YES'
else
echo 'NO'
fi
")
[[ "$result" == "YES" ]]
}
@test "is_protected_vendor_dir: protects Rails vendor" {
mkdir -p "$HOME/www/rails-app/vendor"
mkdir -p "$HOME/www/rails-app/config"
touch "$HOME/www/rails-app/config/application.rb"
touch "$HOME/www/rails-app/Gemfile"
touch "$HOME/www/rails-app/config/environment.rb"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_protected_vendor_dir '$HOME/www/rails-app/vendor'; then
echo 'PROTECTED'
else
echo 'NOT_PROTECTED'
fi
")
[[ "$result" == "PROTECTED" ]]
}
@test "is_protected_vendor_dir: does not protect PHP vendor" {
mkdir -p "$HOME/www/php-app/vendor"
touch "$HOME/www/php-app/composer.json"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_protected_vendor_dir '$HOME/www/php-app/vendor'; then
echo 'PROTECTED'
else
echo 'NOT_PROTECTED'
fi
")
[[ "$result" == "NOT_PROTECTED" ]]
}
@test "is_project_container detects project indicators" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
mkdir -p "$HOME/Workspace2/project"
touch "$HOME/Workspace2/project/package.json"
if is_project_container "$HOME/Workspace2" 2; then
echo "yes"
fi
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"yes"* ]]
}
@test "is_project_container rejects purge targets so artifacts are never scanned into (#1459)" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
# Every npm package ships package.json, the first project indicator, so a
# stray ~/node_modules matches the container probe unless targets are rejected.
mkdir -p "$HOME/node_modules/markdown-it/dist"
printf '{"name":"markdown-it"}' > "$HOME/node_modules/markdown-it/package.json"
mkdir -p "$HOME/vendor/acme/lib"
printf '{"name":"acme/lib"}' > "$HOME/vendor/acme/composer.json"
mkdir -p "$HOME/Pods/SomePod"
printf 'x' > "$HOME/Pods/SomePod/Package.swift"
for artifact in node_modules vendor Pods; do
if is_project_container "$HOME/$artifact" 2; then
echo "FAIL: $artifact classified as container"
exit 1
fi
done
# A directory that is not a purge target still works.
mkdir -p "$HOME/Sources/app"
touch "$HOME/Sources/app/package.json"
is_project_container "$HOME/Sources" 2 || exit 1
echo ok
EOF
[ "$status" -eq 0 ] || {
echo "$output"
return 1
}
[[ "$output" == *"ok"* ]]
}
@test "purge emits no candidates inside a stray home-level node_modules (#1459)" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
mkdir -p "$HOME/node_modules/markdown-it/dist" "$HOME/node_modules/khroma/build"
printf '{"name":"markdown-it"}' > "$HOME/node_modules/markdown-it/package.json"
printf '{"name":"khroma"}' > "$HOME/node_modules/khroma/package.json"
# Deleting node_modules/<pkg>/dist leaves the package half-installed and needs
# a network restore, so the container must never be discovered as a scan root.
if discover_project_dirs | grep -q "^$HOME/node_modules$"; then
echo "FAIL: stray node_modules discovered as a purge search path"
exit 1
fi
echo ok
EOF
[ "$status" -eq 0 ] || {
echo "$output"
return 1
}
[[ "$output" == *"ok"* ]]
}
@test "discover_project_dirs includes detected containers" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
mkdir -p "$HOME/CustomProjects/app"
touch "$HOME/CustomProjects/app/go.mod"
discover_project_dirs | grep -q "$HOME/CustomProjects"
EOF
[ "$status" -eq 0 ]
}
@test "discover_project_dirs includes agent worktree containers" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
mkdir -p "$HOME/.codex/worktrees/checkout/node_modules"
discover_project_dirs | grep -q "^$HOME/.codex/worktrees$"
EOF
[ "$status" -eq 0 ]
}
@test "discover_project_dirs still ignores unlisted dot directories" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
mkdir -p "$HOME/.local/share/app"
touch "$HOME/.local/share/app/package.json"
if discover_project_dirs | grep -q "$HOME/.local"; then
echo "leaked"
fi
EOF
[ "$status" -eq 0 ]
[[ "$output" != *"leaked"* ]] || return 1
}
@test "agent worktree container does not allow direct-child artifact removal" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
mkdir -p "$HOME/.codex/worktrees/node_modules"
mkdir -p "$HOME/.codex/worktrees/checkout/node_modules"
if is_safe_project_artifact "$HOME/.codex/worktrees/node_modules" "$HOME/.codex/worktrees"; then
echo "direct-child-allowed"
fi
if is_safe_project_artifact "$HOME/.codex/worktrees/checkout/node_modules" "$HOME/.codex/worktrees"; then
echo "nested-allowed"
fi
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"nested-allowed"* ]] || return 1
[[ "$output" != *"direct-child-allowed"* ]] || return 1
}
@test "save_discovered_paths writes config with tilde" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
save_discovered_paths "$HOME/Projects"
grep -q "^~/" "$HOME/.config/mole/purge_paths"
EOF
[ "$status" -eq 0 ]
}
@test "select_purge_categories returns failure on empty input" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
if select_purge_categories; then
exit 1
fi
EOF
[ "$status" -eq 0 ]
}
@test "select_purge_categories restores caller EXIT/INT/TERM traps" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
trap 'echo parent-exit' EXIT
trap 'echo parent-int' INT
trap 'echo parent-term' TERM
before_exit=$(trap -p EXIT)
before_int=$(trap -p INT)
before_term=$(trap -p TERM)
PURGE_CATEGORY_SIZES="1"
PURGE_RECENT_CATEGORIES="false"
select_purge_categories "demo" <<< $'\n' > /dev/null 2>&1 || true
after_exit=$(trap -p EXIT)
after_int=$(trap -p INT)
after_term=$(trap -p TERM)
if [[ "$before_exit" == "$after_exit" && "$before_int" == "$after_int" && "$before_term" == "$after_term" ]]; then
echo "PASS"
else
echo "FAIL"
echo "before_exit=$before_exit"
echo "after_exit=$after_exit"
echo "before_int=$before_int"
echo "after_int=$after_int"
echo "before_term=$before_term"
echo "after_term=$after_term"
exit 1
fi
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"PASS"* ]]
}
@test "select_purge_categories names the destructive choice and keeps project skip visible when narrow" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
tput() {
[[ "${1:-}" == "cols" ]] && printf '32\n'
}
PURGE_CATEGORY_SIZES="1"
PURGE_RECENT_CATEGORIES="false"
if select_purge_categories "~/work/obelisk 4KB | node_modules" <<< $'q\n'; then
exit 1
fi
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"Select Artifacts to Purge"* ]] || return 1
[[ "$output" == *"X Skip"* ]] || return 1
[[ "$output" != *"Select Categories to Clean"* ]] || return 1
}
@test "select_purge_categories shows exact project boundaries and selection feedback" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
tput() {
case "${1:-}" in
cols) printf '100\n' ;;
lines) printf '24\n' ;;
esac
}
PURGE_CATEGORY_SIZES="100,50,25"
PURGE_CATEGORY_PROJECT_IDS_ARRAY=("project-a" "project-a" "project-b")
PURGE_CATEGORY_PROJECT_PATHS_ARRAY=("~/work/obelisk" "~/work/obelisk" "~/work/atlas")
PURGE_CATEGORY_SIZE_UNKNOWN_FLAGS_ARRAY=("false" "false" "false")
select_purge_categories "A node_modules" "A dist" "B target" <<< $'x\n'
printf 'RESULT=%s\n' "$PURGE_SELECTION_RESULT"
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"┌ A node_modules"* ]] || return 1
[[ "$output" == *"└ A dist"* ]] || return 1
[[ "$output" == *"─ B target"* ]] || return 1
[[ "$output" == *"~/work/obelisk · 154KB · 2/2 selected"* ]] || return 1
[[ "$output" == *"~/work/obelisk · 154KB · 0/2 selected"* ]] || return 1
[[ "$output" == *"RESULT=2"* ]] || return 1
}
@test "select_purge_categories skips every artifact with the current project ID" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
PURGE_CATEGORY_SIZES="1,2,3"
PURGE_CATEGORY_PROJECT_IDS_ARRAY=("project-a" "project-a" "project-b")
select_purge_categories "A node_modules" "A dist" "B target" <<< $'x\n' > /dev/null
printf 'RESULT=%s\n' "$PURGE_SELECTION_RESULT"
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"RESULT=2"* ]] || return 1
}
@test "select_purge_categories skips only the current row without a project ID" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
PURGE_CATEGORY_SIZES="1,2"
PURGE_CATEGORY_PROJECT_IDS_ARRAY=("" "project-a")
select_purge_categories "Unknown node_modules" "A dist" <<< $'x\n' > /dev/null
printf 'RESULT=%s\n' "$PURGE_SELECTION_RESULT"
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"RESULT=1"* ]] || return 1
}
@test "select_purge_categories does not group same-named projects" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
PURGE_CATEGORY_SIZES="1,2"
PURGE_CATEGORY_PROJECT_IDS_ARRAY=("inode:1:10" "inode:1:20")
select_purge_categories "~/client-a/obelisk node_modules" "~/client-b/obelisk dist" <<< $'x\n' > /dev/null
printf 'RESULT=%s\n' "$PURGE_SELECTION_RESULT"
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"RESULT=1"* ]] || return 1
}
@test "confirm_purge_cleanup accepts Enter" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
drain_pending_input() { :; }
confirm_purge_cleanup 2 1024 0 0 <<< ''
EOF
[ "$status" -eq 0 ]
}
@test "confirm_purge_cleanup shows selected paths" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
drain_pending_input() { :; }
confirm_purge_cleanup 2 1024 0 0 "~/www/app/node_modules" "~/www/app/dist" <<< ''
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"Selected paths:"* ]] || return 1
[[ "$output" == *"~/www/app/node_modules"* ]] || return 1
[[ "$output" == *"~/www/app/dist"* ]]
}
@test "confirm_purge_cleanup warns once before confirming cloud-synced artifacts" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
drain_pending_input() { :; }
confirm_purge_cleanup 2 1024 0 1 "[cloud] ~/Library/CloudStorage/Provider/app/target" "~/www/app/dist" <<< ''
EOF
[ "$status" -eq 0 ] || return 1
[[ "$output" == *"Cloud-synced artifacts may also be removed from other devices."* ]] || return 1
[[ "$output" == *"mo purge --paths"* ]] || return 1
local warning_count
warning_count=$(printf '%s\n' "$output" | grep -cF "Cloud-synced artifacts may also be removed from other devices.")
[ "$warning_count" -eq 1 ] || return 1
local warning_line prompt_line
warning_line=$(printf '%s\n' "$output" | grep -nF "Cloud-synced artifacts may also be removed from other devices." | cut -d: -f1)
prompt_line=$(printf '%s\n' "$output" | grep -nF "Remove 2 artifacts" | cut -d: -f1)
[ "$warning_line" -lt "$prompt_line" ] || return 1
}
@test "confirm_purge_cleanup cancels on ESC" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
drain_pending_input() { :; }
confirm_purge_cleanup 2 1024 0 0 <<< $'\033'
EOF
[ "$status" -eq 1 ]
}
@test "is_protected_vendor_dir: protects Go vendor" {
mkdir -p "$HOME/www/go-app/vendor"
touch "$HOME/www/go-app/go.mod"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_protected_vendor_dir '$HOME/www/go-app/vendor'; then
echo 'PROTECTED'
else
echo 'NOT_PROTECTED'
fi
")
[[ "$result" == "PROTECTED" ]]
}
@test "is_protected_vendor_dir: protects unknown vendor (conservative)" {
mkdir -p "$HOME/www/unknown-app/vendor"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_protected_vendor_dir '$HOME/www/unknown-app/vendor'; then
echo 'PROTECTED'
else
echo 'NOT_PROTECTED'
fi
")
[[ "$result" == "PROTECTED" ]]
}
@test "is_protected_purge_artifact: handles vendor directories correctly" {
mkdir -p "$HOME/www/php-app/vendor"
touch "$HOME/www/php-app/composer.json"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_protected_purge_artifact '$HOME/www/php-app/vendor'; then
echo 'PROTECTED'
else
echo 'NOT_PROTECTED'
fi
")
# PHP vendor should not be protected
[[ "$result" == "NOT_PROTECTED" ]]
}
@test "is_protected_purge_artifact: returns false for non-vendor artifacts" {
mkdir -p "$HOME/www/app/node_modules"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
if is_protected_purge_artifact '$HOME/www/app/node_modules'; then
echo 'PROTECTED'
else
echo 'NOT_PROTECTED'
fi
")
# node_modules is not in the protected list
[[ "$result" == "NOT_PROTECTED" ]]
}
@test "is_protected_purge_artifact avoids basename and dirname subprocesses" {
mkdir -p "$HOME/dotnet-app/bin/Debug"
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" /bin/bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/clean/project.sh"
basename() { return 99; }
dirname() { return 99; }
is_protected_purge_artifact "$HOME/dotnet-app/bin"
EOF
[ "$status" -eq 0 ]
}
# Integration tests
@test "scan_purge_targets: skips Rails vendor directory" {
mkdir -p "$HOME/www/rails-app/vendor/javascript"
mkdir -p "$HOME/www/rails-app/config"
touch "$HOME/www/rails-app/config/application.rb"
touch "$HOME/www/rails-app/Gemfile"
mkdir -p "$HOME/www/rails-app/bin"
touch "$HOME/www/rails-app/bin/rails"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
scan_purge_targets '$HOME/www' '$scan_output'
if grep -q '$HOME/www/rails-app/vendor' '$scan_output'; then
echo 'FOUND'
else
echo 'SKIPPED'
fi
")
rm -f "$scan_output"
[[ "$result" == "SKIPPED" ]]
}
@test "scan_purge_targets: cleans PHP Composer vendor directory" {
mkdir -p "$HOME/www/php-app/vendor"
touch "$HOME/www/php-app/composer.json"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
scan_purge_targets '$HOME/www' '$scan_output'
if grep -q '$HOME/www/php-app/vendor' '$scan_output'; then
echo 'FOUND'
else
echo 'MISSING'
fi
")
rm -f "$scan_output"
[[ "$result" == "FOUND" ]]
}
@test "scan_purge_targets: skips Go vendor directory" {
mkdir -p "$HOME/www/go-app/vendor"
touch "$HOME/www/go-app/go.mod"
touch "$HOME/www/go-app/go.sum"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
scan_purge_targets '$HOME/www' '$scan_output'
if grep -q '$HOME/www/go-app/vendor' '$scan_output'; then
echo 'FOUND'
else
echo 'SKIPPED'
fi
")
rm -f "$scan_output"
[[ "$result" == "SKIPPED" ]]
}
@test "scan_purge_targets: skips unknown vendor directory" {
# Create a vendor directory without any project file
mkdir -p "$HOME/www/unknown-app/vendor"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
scan_purge_targets '$HOME/www' '$scan_output'
if grep -q '$HOME/www/unknown-app/vendor' '$scan_output'; then
echo 'FOUND'
else
echo 'SKIPPED'
fi
")
rm -f "$scan_output"
# Unknown vendor should be protected (conservative approach)
[[ "$result" == "SKIPPED" ]]
}
@test "scan_purge_targets: finds direct-child artifacts in project root with find mode" {
mkdir -p "$HOME/single-project/node_modules"
touch "$HOME/single-project/package.json"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
MO_USE_FIND=1 scan_purge_targets '$HOME/single-project' '$scan_output'
if grep -q '$HOME/single-project/node_modules' '$scan_output'; then
echo 'FOUND'
else
echo 'MISSING'
fi
")
rm -f "$scan_output"
[[ "$result" == "FOUND" ]]
}
@test "scan_purge_targets: includes Terragrunt cache in project root with find mode" {
mkdir -p "$HOME/terragrunt-project/.terragrunt-cache"
touch "$HOME/terragrunt-project/terragrunt.hcl"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
MO_USE_FIND=1 scan_purge_targets '$HOME/terragrunt-project' '$scan_output'
if grep -q '$HOME/terragrunt-project/.terragrunt-cache' '$scan_output'; then
echo 'FOUND'
else
echo 'MISSING'
fi
")
rm -f "$scan_output"
[[ "$result" == "FOUND" ]]
}
@test "scan_purge_targets: supports trailing slash search path in find mode" {
mkdir -p "$HOME/single-project/node_modules"
touch "$HOME/single-project/package.json"
local scan_output
scan_output="$(mktemp)"
result=$(/bin/bash -c "
source '$PROJECT_ROOT/lib/clean/project.sh'
MO_USE_FIND=1 scan_purge_targets '$HOME/single-project/' '$scan_output'
if grep -q '$HOME/single-project/node_modules' '$scan_output'; then
echo 'FOUND'
else
echo 'MISSING'
fi
")
rm -f "$scan_output"
0