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:
teknium1
2026-09-12 06:46:47 -07:00
committed by Teknium
parent 199544e054
commit 2ef9c55a92
2 changed files with 84 additions and 0 deletions

View File

@@ -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")

View 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`.")]