fix(update): name configured platforms whose extras failed to install
When `.[all]` fails and the per-extra fallback also fails for e.g. `feishu`, the update printed only "Skipped optional extras that still failed" and finished green. The running gateway kept its already-imported modules, so the loss surfaced hours later as "No adapter available for feishu" on the next restart (#10651). After the fallback, check every enabled+configured platform through its registry `check_fn` (and MCP when `mcp_servers` is set) and print which configured feature will fail to load, with its install hint. Unconfigured extras stay a quiet skipped line. Reworks PR #10733 (LeonSGP43) against the registry instead of a hand-written platform->module table so plugin platforms are covered. Fixes #10651 Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
This commit is contained in:
@@ -984,12 +984,50 @@ def _install_python_dependencies_with_optional_fallback(
|
||||
print(f" ✓ Reinstalled optional extras individually: {', '.join(installed_extras)}")
|
||||
if failed_extras:
|
||||
print(f" ⚠ Skipped optional extras that still failed: {', '.join(failed_extras)}")
|
||||
_warn_configured_features_missing_deps(install_cmd_prefix)
|
||||
# uv's incremental resolver has left newly added base deps silently missing on a half-stale
|
||||
# venv, surfacing hours later as a downstream ModuleNotFoundError. Verify here instead.
|
||||
_verify_core_dependencies_installed(install_cmd_prefix, env=env, group=group)
|
||||
_verify_console_scripts_installed(install_cmd_prefix, env=env)
|
||||
|
||||
|
||||
def _configured_features_missing_deps() -> list[tuple[str, str]]:
|
||||
"""``(feature, hint)`` for every configured platform / MCP whose optional deps are not importable
|
||||
in this interpreter. A running gateway masks the gap until its next restart (#10651), so the
|
||||
update has to say which configured feature will fail to load."""
|
||||
missing: list[tuple[str, str]] = []
|
||||
try:
|
||||
from gateway.config import load_gateway_config
|
||||
from gateway.platform_registry import platform_registry
|
||||
config = load_gateway_config()
|
||||
for platform in config.get_connected_platforms():
|
||||
entry = platform_registry.get(platform.value)
|
||||
if entry is None or entry.check_fn():
|
||||
continue
|
||||
missing.append((entry.label, entry.install_hint or f"reinstall the '{platform.value}' extra"))
|
||||
except Exception as exc: # the update must finish even when the gateway config is unreadable
|
||||
logger.debug("configured-platform dependency check skipped: %s", exc)
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
import importlib.util
|
||||
if (load_config().get("mcp_servers") or {}) and importlib.util.find_spec("mcp") is None:
|
||||
missing.append(("MCP servers", "install the 'mcp' extra"))
|
||||
except Exception as exc:
|
||||
logger.debug("configured-MCP dependency check skipped: %s", exc)
|
||||
return missing
|
||||
|
||||
|
||||
def _warn_configured_features_missing_deps(install_cmd_prefix: list[str]) -> None:
|
||||
missing = _configured_features_missing_deps()
|
||||
if not missing:
|
||||
return
|
||||
prefix = " ".join(shlex.quote(part) for part in install_cmd_prefix)
|
||||
print(" ⚠ Configured features whose dependencies are still missing — the gateway will fail to load them on restart:")
|
||||
for feature, hint in missing:
|
||||
print(f" - {feature}: {hint}")
|
||||
print(f" Retry with: {prefix} install -e '.[all]'")
|
||||
|
||||
|
||||
def _load_console_script_names() -> list[str]:
|
||||
"""Return ``[project.scripts]`` entry-point names from pyproject.toml."""
|
||||
project = _pyproject_project("console script verification: failed to read pyproject.toml: %s")
|
||||
|
||||
46
tests/hermes_cli/test_update_missing_configured_deps.py
Normal file
46
tests/hermes_cli/test_update_missing_configured_deps.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Update fallback must name configured features whose optional deps stayed missing (#10651)."""
|
||||
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
from hermes_cli import main_install_repair
|
||||
|
||||
|
||||
def _run_fallback_with_failed_extra(monkeypatch, capsys, *, extra_fails: str, missing_features):
|
||||
def fake_install(cmd, **kwargs):
|
||||
target = cmd[-1]
|
||||
if target in (".[all]", f".[{extra_fails}]"):
|
||||
raise subprocess.CalledProcessError(1, cmd)
|
||||
|
||||
monkeypatch.setattr(main_install_repair, "_run_quarantined_install", fake_install)
|
||||
monkeypatch.setattr(main_install_repair, "_verify_console_scripts_installed", lambda *a, **k: None)
|
||||
monkeypatch.setattr(main_install_repair, "_verify_core_dependencies_installed", lambda *a, **k: None)
|
||||
monkeypatch.setattr(main_install_repair, "_venv_scripts_dir", lambda: None)
|
||||
monkeypatch.setattr(main_install_repair, "_is_windows", lambda: False)
|
||||
monkeypatch.setattr(main_install_repair, "_load_installable_optional_extras", lambda group="all": [extra_fails, "mcp"])
|
||||
monkeypatch.setattr(main_install_repair, "_configured_features_missing_deps", lambda: missing_features)
|
||||
main_install_repair._install_python_dependencies_with_optional_fallback(["uv", "pip"])
|
||||
return capsys.readouterr().out
|
||||
|
||||
|
||||
def test_fallback_names_configured_platform_whose_extra_failed(monkeypatch, capsys):
|
||||
out = _run_fallback_with_failed_extra(
|
||||
monkeypatch, capsys, extra_fails="feishu",
|
||||
missing_features=[("Feishu / Lark", "Run `hermes setup` to install Feishu support.")])
|
||||
assert "fail to load them on restart" in out
|
||||
assert "Feishu / Lark" in out and "hermes setup" in out
|
||||
# Unconfigured features that failed stay a plain "skipped" line, no scary warning.
|
||||
quiet = _run_fallback_with_failed_extra(monkeypatch, capsys, extra_fails="feishu", missing_features=[])
|
||||
assert "Skipped optional extras that still failed: feishu" in quiet
|
||||
assert "fail to load them on restart" not in quiet
|
||||
|
||||
|
||||
def test_configured_features_reports_platform_with_missing_deps(monkeypatch):
|
||||
entry = SimpleNamespace(label="Feishu / Lark", check_fn=lambda: False, install_hint="Run `hermes setup`.")
|
||||
fake_registry = SimpleNamespace(get=lambda name: entry if name == "feishu" else None)
|
||||
fake_config = SimpleNamespace(get_connected_platforms=lambda: [SimpleNamespace(value="feishu")])
|
||||
with mock.patch("gateway.config.load_gateway_config", return_value=fake_config), \
|
||||
mock.patch("gateway.platform_registry.platform_registry", fake_registry), \
|
||||
mock.patch("hermes_cli.config.load_config", return_value={}):
|
||||
assert main_install_repair._configured_features_missing_deps() == [("Feishu / Lark", "Run `hermes setup`.")]
|
||||
Reference in New Issue
Block a user