diff --git a/hermes_cli/update_cmd.py b/hermes_cli/update_cmd.py index 092504d75b399..b87e99ef73174 100644 --- a/hermes_cli/update_cmd.py +++ b/hermes_cli/update_cmd.py @@ -200,6 +200,16 @@ def _validate_critical_modules_import(root) -> tuple[bool, str | None, str | Non "for name in %r:\n" " try:\n" " importlib.import_module(name)\n" + " except ModuleNotFoundError as exc:\n" + # A missing *third-party* module means dependencies aren't installed + # yet (this guard can run before the dependency sync on the git path), + # not a skewed checkout. Only treat it as breakage when the missing + # module is one of ours. + " missing = (getattr(exc, 'name', '') or '').split('.')[0]\n" + " if missing in ('tools', 'agent', 'gateway', 'plugins', 'providers') \\\n" + " or missing.startswith('hermes'):\n" + " sys.stdout.write(name + '\\n' + str(exc))\n" + " raise SystemExit(3)\n" " except ImportError as exc:\n" " sys.stdout.write(name + '\\n' + str(exc))\n" " raise SystemExit(3)\n" diff --git a/tests/hermes_cli/test_update_import_guard.py b/tests/hermes_cli/test_update_import_guard.py index 61827fa4694f8..a8cee3515c3a4 100644 --- a/tests/hermes_cli/test_update_import_guard.py +++ b/tests/hermes_cli/test_update_import_guard.py @@ -147,3 +147,27 @@ def test_import_guard_prefers_the_project_venv_interpreter(monkeypatch, tmp_path update_cmd._validate_critical_modules_import(tmp_path) assert seen["interpreter"] == str(venv_python) + + +def test_import_guard_ignores_missing_third_party_dependency(monkeypatch, tmp_path): + """A new third-party requirement is not a partially-updated tree. + + On the git path this guard runs BEFORE the dependency sync, so a release + that adds a dependency would otherwise look like breakage and trigger a + spurious `git reset --hard` rollback of a perfectly good update. + """ + (tmp_path / "consumer.py").write_text("import totally_not_installed_pkg\n") + monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + + assert update_cmd._validate_critical_modules_import(tmp_path) == (True, None, None) + + +def test_import_guard_flags_missing_first_party_module(monkeypatch, tmp_path): + """A missing *first-party* module IS skew — the update dropped a file.""" + (tmp_path / "consumer.py").write_text("import tools.nonexistent_module\n") + monkeypatch.setattr(update_cmd, "_UPDATE_CRITICAL_MODULES", ("consumer",)) + + ok, module, error = update_cmd._validate_critical_modules_import(tmp_path) + assert ok is False + assert module == "consumer" + assert error is not None and "tools.nonexistent_module" in error