compat(plugins): discovery itself refreshes the Desktop's report file

Live Desktop E2E (real Electron, worktree backend, demo plugin on old paths) found the modal never fired:
compat_report() was only called from the CLI banner / doctor / update / plugins-compat surfaces, none of
which run inside the Desktop's `serve` backend, so .plugin-compat-report.json was never written.
PluginManager.discover_and_load now refreshes the report from the manifests it just discovered (fail-open).

E2E after the fix, all five acceptance steps green on the real seat: report written and names the plugin;
native dialog 'Plugins need an update' with plugin, date, and `hermes plugins compat`; OK persists the
dismissal; relaunch with the same userData shows nothing and logs no 'compat notice shown'; fixing the
plugin's imports deletes the report and shows nothing.
This commit is contained in:
Teknium
2026-09-04 01:48:47 -07:00
parent 064dcda706
commit 78288b488b
2 changed files with 35 additions and 0 deletions
+14
View File
@@ -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]],
+21
View File
@@ -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()