diff --git a/src/basic_memory/cli/auto_update.py b/src/basic_memory/cli/auto_update.py index 862602ec..48456807 100644 --- a/src/basic_memory/cli/auto_update.py +++ b/src/basic_memory/cli/auto_update.py @@ -168,6 +168,20 @@ def _manual_update_hint(source: InstallSource) -> str: ) +def _preload_lazy_console_modules() -> None: + """Import modules the post-upgrade output path defers until print time. + + Trigger: an in-place upgrade is about to replace this installation on disk. + Why: rich and typer defer some imports until print/excepthook time; once + `brew upgrade` / `uv tool upgrade` removes the running version's files, + those imports raise ModuleNotFoundError and the final status message + crashes the exiting process. + Outcome: import the deferred modules now, while the files still exist. + """ + import rich._emoji_codes # noqa: F401 + import typer.rich_utils # noqa: F401 + + def _save_last_checked_timestamp(config_manager: ConfigManager, checked_at: datetime) -> None: """Persist the timestamp for the most recent attempted update check.""" config = config_manager.load_config() @@ -288,6 +302,7 @@ def run_auto_update( else BREW_UPGRADE_TIMEOUT_SECONDS ) + _preload_lazy_console_modules() install_result = _run_subprocess( command, timeout_seconds=timeout, diff --git a/tests/cli/test_auto_update.py b/tests/cli/test_auto_update.py index 662da549..3751d0c3 100644 --- a/tests/cli/test_auto_update.py +++ b/tests/cli/test_auto_update.py @@ -3,6 +3,7 @@ from __future__ import annotations import subprocess +import sys from datetime import datetime, timedelta, timezone from io import StringIO from typing import Any, cast @@ -15,6 +16,7 @@ InstallSource, _check_homebrew_update_available, _is_interactive_session, + _preload_lazy_console_modules, detect_install_source, maybe_run_periodic_auto_update, run_auto_update, @@ -159,6 +161,19 @@ def _fake_run(command, **kwargs): assert is_outdated is False +def test_preload_lazy_console_modules_imports_deferred_modules(monkeypatch): + # Regression: the in-place upgrade deletes the running install's files, so + # any module rich/typer defers until print time must already be loaded or + # the final status message crashes with ModuleNotFoundError. + monkeypatch.delitem(sys.modules, "rich._emoji_codes", raising=False) + monkeypatch.delitem(sys.modules, "typer.rich_utils", raising=False) + + _preload_lazy_console_modules() + + assert "rich._emoji_codes" in sys.modules + assert "typer.rich_utils" in sys.modules + + def test_homebrew_outdated_triggers_upgrade(monkeypatch, tmp_path): config = _base_config(tmp_path) manager = StubConfigManager(config)