diff --git a/lib/clean/project.sh b/lib/clean/project.sh index 209c02d..85a5c37 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -29,6 +29,7 @@ readonly PURGE_TARGETS=( ".nuxt" # Nuxt.js ".output" # Nuxt.js "vendor" # PHP Composer + "bin" # .NET build output (guarded; see is_protected_purge_artifact) "obj" # C# / Unity ".turbo" # Turborepo cache ".parcel-cache" # Parcel bundler @@ -246,6 +247,22 @@ is_php_project_root() { [[ -f "$dir/composer.json" ]] } +# Decide whether a "bin" directory is a .NET directory +is_dotnet_bin_dir() { + local path="$1" + [[ "$(basename "$path")" == "bin" ]] || return 1 + + # Check if parent directory has a .csproj/.fsproj/.vbproj file + local parent_dir + parent_dir="$(dirname "$path")" + find "$parent_dir" -maxdepth 1 \( -name "*.csproj" -o -name "*.fsproj" -o -name "*.vbproj" \) 2> /dev/null | grep -q . || return 1 + + # Check if bin directory contains Debug/ or Release/ subdirectories + [[ -d "$path/Debug" || -d "$path/Release" ]] || return 1 + + return 0 +} + # Check if a vendor directory should be protected from purge # Expects path to be a vendor directory (basename == vendor) # Strategy: Only clean PHP Composer vendor, protect all others @@ -284,6 +301,13 @@ is_protected_purge_artifact() { base=$(basename "$path") case "$base" in + bin) + # Only allow purging bin/ when we can detect .NET context. + if is_dotnet_bin_dir "$path"; then + return 1 + fi + return 0 + ;; vendor) is_protected_vendor_dir "$path" return $? diff --git a/tests/purge.bats b/tests/purge.bats index a11a0c5..5ccab1f 100644 --- a/tests/purge.bats +++ b/tests/purge.bats @@ -547,3 +547,97 @@ EOF [ -d "$HOME/.cache/mole" ] || [ -d "${XDG_CACHE_HOME:-$HOME/.cache}/mole" ] } + +# .NET bin directory detection tests +@test "is_dotnet_bin_dir: finds .NET context in parent directory with Debug dir" { + mkdir -p "$HOME/www/dotnet-app/bin/Debug" + touch "$HOME/www/dotnet-app/MyProject.csproj" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + if is_dotnet_bin_dir '$HOME/www/dotnet-app/bin'; then + echo 'FOUND' + else + echo 'NOT_FOUND' + fi + ") + + [[ "$result" == "FOUND" ]] +} + +@test "is_dotnet_bin_dir: requires .csproj AND Debug/Release" { + mkdir -p "$HOME/www/dotnet-app/bin" + touch "$HOME/www/dotnet-app/MyProject.csproj" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + if is_dotnet_bin_dir '$HOME/www/dotnet-app/bin'; then + echo 'FOUND' + else + echo 'NOT_FOUND' + fi + ") + + # Should not find it because Debug/Release directories don't exist + [[ "$result" == "NOT_FOUND" ]] +} + +@test "is_dotnet_bin_dir: rejects non-bin directories" { + mkdir -p "$HOME/www/dotnet-app/obj" + touch "$HOME/www/dotnet-app/MyProject.csproj" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + if is_dotnet_bin_dir '$HOME/www/dotnet-app/obj'; then + echo 'FOUND' + else + echo 'NOT_FOUND' + fi + ") + [[ "$result" == "NOT_FOUND" ]] +} + + +# Integration test for bin scanning +@test "scan_purge_targets: includes .NET bin directories with Debug/Release" { + mkdir -p "$HOME/www/dotnet-app/bin/Debug" + touch "$HOME/www/dotnet-app/MyProject.csproj" + + local scan_output + scan_output="$(mktemp)" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + scan_purge_targets '$HOME/www' '$scan_output' + if grep -q '$HOME/www/dotnet-app/bin' '$scan_output'; then + echo 'FOUND' + else + echo 'MISSING' + fi + ") + + rm -f "$scan_output" + + [[ "$result" == "FOUND" ]] +} + +@test "scan_purge_targets: skips generic bin directories (non-.NET)" { + mkdir -p "$HOME/www/ruby-app/bin" + touch "$HOME/www/ruby-app/Gemfile" + + local scan_output + scan_output="$(mktemp)" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + scan_purge_targets '$HOME/www' '$scan_output' + if grep -q '$HOME/www/ruby-app/bin' '$scan_output'; then + echo 'FOUND' + else + echo 'SKIPPED' + fi + ") + + rm -f "$scan_output" + [[ "$result" == "SKIPPED" ]] +}