From e6ee9ec490c1f05db11af1482de80796627148ea Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Mar 2026 03:15:39 +0100 Subject: [PATCH 1/2] fix macOS update false positives --- lib/check/all.sh | 25 ++++++++++++++++++ tests/clean_system_maintenance.bats | 40 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/lib/check/all.sh b/lib/check/all.sh index f38021b..6d2eabd 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -386,9 +386,34 @@ check_macos_update() { # Prefer avoiding false negatives: if the system indicates updates are pending, # only clear the flag when softwareupdate returns a list without any update entries. + # However, macOS doesn't distinguish between system and App Store updates in the + # LastRecommendedUpdatesAvailable counter, so we additionally require that at least + # one listed update is a macOS system update before showing a macOS update warning. if [[ $sw_status -eq 0 && -n "$sw_output" ]]; then if ! echo "$sw_output" | grep -qE '^[[:space:]]*\*'; then + # No update entries at all updates_available="false" + else + # softwareupdate output may include both macOS and App Store updates. + # Treat only entries whose Label contains "macOS" as system updates. + local has_macos_update="false" + while IFS= read -r line; do + [[ "$line" =~ ^[[:space:]]*\* ]] || continue + local label="$line" + label="${label#*Label: }" + label="${label%%,*}" + local lower_label + lower_label=$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]') + if [[ "$lower_label" == macos* || "$lower_label" == *"macos "* || "$lower_label" == *" macos"* ]]; then + has_macos_update="true" + break + fi + done <<< "$sw_output" + + if [[ "$has_macos_update" != "true" ]]; then + # Only App Store updates are pending – don't flag macOS as outdated + updates_available="false" + fi fi fi fi diff --git a/tests/clean_system_maintenance.bats b/tests/clean_system_maintenance.bats index 0626f2d..5bc0348 100644 --- a/tests/clean_system_maintenance.bats +++ b/tests/clean_system_maintenance.bats @@ -669,6 +669,46 @@ EOF [[ "$output" != *"SHOULD_NOT_CALL_SOFTWAREUPDATE"* ]] } +@test "check_macos_update does not flag macOS when only App Store updates are pending" { + run bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/check/all.sh" + +defaults() { echo "1"; } + +run_with_timeout() { + local timeout="${1:-}" + shift + if [[ "$timeout" != "10" ]]; then + echo "BAD_TIMEOUT:$timeout" + return 124 + fi + if [[ "${1:-}" == "softwareupdate" && "${2:-}" == "-l" && "${3:-}" == "--no-scan" ]]; then + cat <<'OUT' +Software Update Tool + +Software Update found the following new or updated software: + * Label: Xcode-15.3 +OUT + return 0 + fi + return 124 +} + +start_inline_spinner(){ :; } +stop_inline_spinner(){ :; } + +check_macos_update +echo "MACOS_UPDATE_AVAILABLE=$MACOS_UPDATE_AVAILABLE" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"System up to date"* ]] + [[ "$output" == *"MACOS_UPDATE_AVAILABLE=false"* ]] + [[ "$output" != *"BAD_TIMEOUT:"* ]] +} + @test "check_macos_update outputs debug info when MO_DEBUG set" { run bash --noprofile --norc << 'EOF' set -euo pipefail From 2d8c93833334a0c6565747e23ef8ab16824e5823 Mon Sep 17 00:00:00 2001 From: corevibe555 Date: Wed, 18 Mar 2026 04:59:33 +0100 Subject: [PATCH 2/2] trust softwareupdate for macOS status and harden tests --- lib/check/all.sh | 58 ++++++++++++++--------------- scripts/test.sh | 55 ++++++++++++++------------- tests/clean_system_maintenance.bats | 8 ++-- 3 files changed, 63 insertions(+), 58 deletions(-) diff --git a/lib/check/all.sh b/lib/check/all.sh index 6d2eabd..4c226b9 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -351,24 +351,29 @@ check_macos_update() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi - # Fast check using system preferences - local updates_available="false" - if [[ $(get_software_updates) == "Updates Available" ]]; then - updates_available="true" + # Fast check using system preferences to avoid unnecessary scans. + # We only surface a macOS update when softwareupdate itself lists a macOS + # system update. If softwareupdate fails, times out, or does not list any + # macOS-labelled entries, we treat the system as up to date to avoid + # false-positive warnings. + local updates_available + updates_available="false" + + if [[ $(get_software_updates) == "Updates Available" ]]; then + local sw_output + sw_output="" + local sw_status + sw_status=0 + local spinner_started + spinner_started="false" - # Verify with softwareupdate using --no-scan to avoid triggering a fresh scan - # which can timeout. We prioritize avoiding false negatives (missing actual updates) - # over false positives, so we only clear the update flag when softwareupdate - # explicitly reports "No new software available" - local sw_output="" - local sw_status=0 - local spinner_started=false if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking macOS updates..." - spinner_started=true + spinner_started="true" fi - local softwareupdate_timeout=10 + local softwareupdate_timeout + softwareupdate_timeout=10 if sw_output=$(run_with_timeout "$softwareupdate_timeout" softwareupdate -l --no-scan 2> /dev/null); then : else @@ -384,22 +389,18 @@ check_macos_update() { echo "[DEBUG] softwareupdate exit status: $sw_status, output lines: $(echo "$sw_output" | wc -l | tr -d ' ')" >&2 fi - # Prefer avoiding false negatives: if the system indicates updates are pending, - # only clear the flag when softwareupdate returns a list without any update entries. - # However, macOS doesn't distinguish between system and App Store updates in the - # LastRecommendedUpdatesAvailable counter, so we additionally require that at least - # one listed update is a macOS system update before showing a macOS update warning. + # Only trust softwareupdate as the source of truth. We surface a macOS + # update *only* when softwareupdate successfully returns at least one + # macOS-labelled entry; otherwise we prefer a false negative over a + # false positive. if [[ $sw_status -eq 0 && -n "$sw_output" ]]; then - if ! echo "$sw_output" | grep -qE '^[[:space:]]*\*'; then - # No update entries at all - updates_available="false" - else - # softwareupdate output may include both macOS and App Store updates. - # Treat only entries whose Label contains "macOS" as system updates. - local has_macos_update="false" + if echo "$sw_output" | grep -qE '^[[:space:]]*\*'; then + local has_macos_update + has_macos_update="false" while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*\* ]] || continue - local label="$line" + local label + label="$line" label="${label#*Label: }" label="${label%%,*}" local lower_label @@ -410,9 +411,8 @@ check_macos_update() { fi done <<< "$sw_output" - if [[ "$has_macos_update" != "true" ]]; then - # Only App Store updates are pending – don't flag macOS as outdated - updates_available="false" + if [[ "$has_macos_update" == "true" ]]; then + updates_available="true" fi fi fi diff --git a/scripts/test.sh b/scripts/test.sh index 7da5bfb..1a912fa 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -227,37 +227,42 @@ fi echo "" echo "6. Testing installation..." -# Skip if Homebrew mole is installed (install.sh will refuse to overwrite) -install_test_home="" -if command -v brew > /dev/null 2>&1 && brew list mole &> /dev/null; then - printf "${GREEN}${ICON_SUCCESS} Installation test skipped, Homebrew${NC}\n" +# Installation script is macOS-specific; skip this test on non-macOS platforms +if [[ "$(uname -s)" != "Darwin" ]]; then + printf "${YELLOW}${ICON_WARNING} Installation test skipped (non-macOS)${NC}\n" else - install_test_home="$(mktemp -d /tmp/mole-test-home.XXXXXX 2> /dev/null || true)" - if [[ -z "$install_test_home" ]]; then - install_test_home="/tmp/mole-test-home" - mkdir -p "$install_test_home" + # Skip if Homebrew mole is installed (install.sh will refuse to overwrite) + install_test_home="" + if command -v brew > /dev/null 2>&1 && brew list mole &> /dev/null; then + printf "${GREEN}${ICON_SUCCESS} Installation test skipped, Homebrew${NC}\n" + else + install_test_home="$(mktemp -d /tmp/mole-test-home.XXXXXX 2> /dev/null || true)" + if [[ -z "$install_test_home" ]]; then + install_test_home="/tmp/mole-test-home" + mkdir -p "$install_test_home" + fi fi -fi -if [[ -z "$install_test_home" ]]; then - : -elif HOME="$install_test_home" \ - XDG_CONFIG_HOME="$install_test_home/.config" \ - XDG_CACHE_HOME="$install_test_home/.cache" \ - MO_NO_OPLOG=1 \ - ./install.sh --prefix /tmp/mole-test > /dev/null 2>&1; then - if [ -f /tmp/mole-test/mole ]; then - printf "${GREEN}${ICON_SUCCESS} Installation test passed${NC}\n" + if [[ -z "$install_test_home" ]]; then + : + elif HOME="$install_test_home" \ + XDG_CONFIG_HOME="$install_test_home/.config" \ + XDG_CACHE_HOME="$install_test_home/.cache" \ + MO_NO_OPLOG=1 \ + ./install.sh --prefix /tmp/mole-test > /dev/null 2>&1; then + if [[ -f "/tmp/mole-test/mole" ]]; then + printf "${GREEN}${ICON_SUCCESS} Installation test passed${NC}\n" + else + printf "${RED}${ICON_ERROR} Installation test failed${NC}\n" + ((FAILED++)) + fi else printf "${RED}${ICON_ERROR} Installation test failed${NC}\n" ((FAILED++)) fi -else - printf "${RED}${ICON_ERROR} Installation test failed${NC}\n" - ((FAILED++)) -fi -MO_NO_OPLOG=1 safe_remove "/tmp/mole-test" true || true -if [[ -n "$install_test_home" ]]; then - MO_NO_OPLOG=1 safe_remove "$install_test_home" true || true + MO_NO_OPLOG=1 safe_remove "/tmp/mole-test" true || true + if [[ -n "$install_test_home" ]]; then + MO_NO_OPLOG=1 safe_remove "$install_test_home" true || true + fi fi echo "" diff --git a/tests/clean_system_maintenance.bats b/tests/clean_system_maintenance.bats index 5bc0348..4d9c547 100644 --- a/tests/clean_system_maintenance.bats +++ b/tests/clean_system_maintenance.bats @@ -607,8 +607,8 @@ echo "MACOS_UPDATE_AVAILABLE=$MACOS_UPDATE_AVAILABLE" EOF [ "$status" -eq 0 ] - [[ "$output" == *"Update available"* ]] - [[ "$output" == *"MACOS_UPDATE_AVAILABLE=true"* ]] + [[ "$output" == *"System up to date"* ]] + [[ "$output" == *"MACOS_UPDATE_AVAILABLE=false"* ]] [[ "$output" != *"BAD_TIMEOUT:"* ]] } @@ -641,8 +641,8 @@ echo "MACOS_UPDATE_AVAILABLE=$MACOS_UPDATE_AVAILABLE" EOF [ "$status" -eq 0 ] - [[ "$output" == *"Update available"* ]] - [[ "$output" == *"MACOS_UPDATE_AVAILABLE=true"* ]] + [[ "$output" == *"System up to date"* ]] + [[ "$output" == *"MACOS_UPDATE_AVAILABLE=false"* ]] [[ "$output" != *"BAD_TIMEOUT:"* ]] }