diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index d9840a7e72..e6c3956b87 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -1307,6 +1307,20 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin, PluginLedgerMixin): if manifests: logger.info("Plugin discovery complete: %d found, %d enabled", len(self._plugins), sum(1 for p in self._plugins.values() if p.enabled)) + self._refresh_plugin_compat_report(list(to_load.values())) + + def _refresh_plugin_compat_report(self, manifests: List[PluginManifest]) -> None: + """Refresh HERMES_HOME/.plugin-compat-report.json from this discovery pass (hermes_cli.plugin_compat). + + The Desktop boot modal has no Python runtime of its own and reads that file after the ``serve`` + backend is up, so the scan must run wherever plugins are discovered — not only under the CLI + banner / doctor / update, which never run inside the Desktop's backend. Fail-open: never raises. + """ + try: + from hermes_cli.plugin_compat import compat_report + compat_report(manifests, force=True) + except Exception as exc: + logger.debug("plugin compat report refresh skipped: %s", exc) def _gate_manifest( self, manifest: PluginManifest, disabled: Set[str], enabled: Optional[Set[str]], diff --git a/tests/test_plugin_compat_notice.py b/tests/test_plugin_compat_notice.py index e5395de5f3..fb5ef58138 100644 --- a/tests/test_plugin_compat_notice.py +++ b/tests/test_plugin_compat_notice.py @@ -115,3 +115,24 @@ def test_loader_skips_hitting_plugin_after_date(tmp_path, monkeypatch): mgr._load_plugin(real) loaded = next(lp for lp in mgr._plugins.values() if lp.manifest.name == "oldpaths") assert not loaded.enabled and loaded.error and pc.COMPAT_REMOVAL in loaded.error + + +def test_discovery_refreshes_report_file(tmp_path, monkeypatch): + """The Desktop modal reads the report the `serve` backend's discovery wrote — discovery itself must + write it (not only the CLI banner / doctor / update paths), and clear it once the plugin is fixed.""" + from hermes_cli.plugins import PluginManager + monkeypatch.setattr(pc, "load_manifest", lambda: MANIFEST) + monkeypatch.setattr(pc, "removal_in_effect", lambda today=None: False) + monkeypatch.setattr(pc, "report_file_path", lambda: tmp_path / "r.json") + plugin = tmp_path / "plugins" / "oldpaths"; plugin.mkdir(parents=True) + (plugin / "plugin.yaml").write_text("name: oldpaths\nversion: 0.1\ndescription: t\n") + (plugin / "__init__.py").write_text("from tools.web_tools import prefers_gateway\ndef register(ctx):\n pass\n") + from hermes_cli.plugins_manifest import PluginManifest + real = PluginManifest(name="oldpaths", version="0.1", description="t", source="user", path=str(plugin)) + mgr = PluginManager(scope_key=str(tmp_path)) + mgr._refresh_plugin_compat_report([real]) + data = json.loads((tmp_path / "r.json").read_text()) + assert list(data["plugins"]) == ["oldpaths"] and data["in_effect"] is False + (plugin / "__init__.py").write_text("from tools.tool_backend_helpers import prefers_gateway\ndef register(ctx):\n pass\n") + mgr._refresh_plugin_compat_report([real]) + assert not (tmp_path / "r.json").exists()