From 7c96a0777da643746bd72c481b0dfa4c0be6dce5 Mon Sep 17 00:00:00 2001 From: Drew Cain Date: Tue, 10 Mar 2026 23:13:54 -0500 Subject: [PATCH] fix(cli): handle brew outdated exit code 1 as outdated, not error Signed-off-by: Drew Cain --- CHANGELOG.md | 7 +++++++ src/basic_memory/cli/auto_update.py | 12 +++++------- tests/cli/test_auto_update.py | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be40fa6..6242aa67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## v0.20.2 (2026-03-10) + +### Bug Fixes + +- Fix auto-update Homebrew detection: `brew outdated` exits 1 when a formula is outdated, not on error + - Previously treated exit code 1 as a failure, causing "Automatic update check failed" instead of detecting the available update + ## v0.20.1 (2026-03-10) ### Bug Fixes diff --git a/src/basic_memory/cli/auto_update.py b/src/basic_memory/cli/auto_update.py index 586308e4..5a0fce3c 100644 --- a/src/basic_memory/cli/auto_update.py +++ b/src/basic_memory/cli/auto_update.py @@ -134,13 +134,11 @@ def _check_homebrew_update_available(silent: bool) -> tuple[bool, str | None]: silent=silent, capture_output=True, ) - if result.returncode != 0: - stderr = (result.stderr or "").strip() - stdout = (result.stdout or "").strip() - detail = stderr or stdout or "brew outdated failed" - raise RuntimeError(detail) - - is_outdated = bool((result.stdout or "").strip()) + # Trigger: brew outdated exits 1 when the formula IS outdated (with name on stdout). + # Why: non-zero exit here means "outdated", not "error". + # Outcome: check stdout for the package name to determine outdated status. + stdout = (result.stdout or "").strip() + is_outdated = PACKAGE_NAME in stdout return is_outdated, None diff --git a/tests/cli/test_auto_update.py b/tests/cli/test_auto_update.py index 25009d78..0e404492 100644 --- a/tests/cli/test_auto_update.py +++ b/tests/cli/test_auto_update.py @@ -12,6 +12,7 @@ from basic_memory.cli.auto_update import ( AutoUpdateResult, AutoUpdateStatus, InstallSource, + _check_homebrew_update_available, _is_interactive_session, detect_install_source, maybe_run_periodic_auto_update, @@ -129,6 +130,30 @@ def test_force_bypasses_auto_update_disabled(monkeypatch, tmp_path): assert manager.save_calls == 1 +def test_check_homebrew_update_available_exit_code_1_means_outdated(monkeypatch): + """brew outdated exits 1 when the formula is outdated, not on error.""" + + def _fake_run(command, **kwargs): + return subprocess.CompletedProcess( + command, 1, stdout="basicmachines-co/basic-memory/basic-memory\n", stderr="" + ) + + monkeypatch.setattr("basic_memory.cli.auto_update._run_subprocess", _fake_run) + is_outdated, _ = _check_homebrew_update_available(silent=False) + assert is_outdated is True + + +def test_check_homebrew_update_available_exit_code_0_means_up_to_date(monkeypatch): + """brew outdated exits 0 when the formula is up to date.""" + + def _fake_run(command, **kwargs): + return subprocess.CompletedProcess(command, 0, stdout="", stderr="") + + monkeypatch.setattr("basic_memory.cli.auto_update._run_subprocess", _fake_run) + is_outdated, _ = _check_homebrew_update_available(silent=False) + assert is_outdated is False + + def test_homebrew_outdated_triggers_upgrade(monkeypatch, tmp_path): config = _base_config(tmp_path) manager = StubConfigManager(config)