The bare `hermes plugins` picker preselected only rows listed in plugins.enabled. Bundled platforms, backends and model providers are active without a list entry, so they opened unticked, and the save on exit wrote every unticked row into plugins.disabled: opening the picker and leaving without a change disabled every messaging adapter on the next gateway restart. Rows now open ticked by the load-time rule (_plugin_status), and only rows the user flipped are written. Ported onto the plugins_cmd_toggle sibling and the admission-authority save (the original targeted the pre-split plugins_cmd.py). The nested canonical-key composite test now ticks its row explicitly: it handed the menu a checkbox state that contradicted the config and relied on the old rebuild-everything save. The original helper-level tests are rewritten against real admission in a follow-up commit. (cherry picked from commit 3a0d5f1b8bce3b23b115ba4995b8c23dfbba9ad0)
119 lines
5.7 KiB
Python
119 lines
5.7 KiB
Python
"""Publication failures through the command and real worker, not a fake sync."""
|
|
import shutil
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from tests.hermes_cli.plugin_worker_support import (
|
|
plugin_world as plugin_world,
|
|
worker_command,
|
|
isolated_python as isolated_python,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("failure", ["config", "facts"])
|
|
def test_failed_publication_preserves_selection_and_imports(plugin_world, monkeypatch, failure):
|
|
from hermes_cli.plugins_admission import AdmissionRefused
|
|
from pm import client, paths, receipt
|
|
|
|
world = plugin_world
|
|
origin, sha = world.origin()
|
|
world.command("install", identifier=origin.as_uri(), ref=sha, no_enable=True, allow_removed=True)
|
|
world.command("enable", name="plugin-worker-proof", no_allow_tool_override=True)
|
|
world.imports()
|
|
selected = world.selected()
|
|
origin, sha = world.origin(name="publication-candidate")
|
|
world.command("install", identifier=origin.as_uri(), ref=sha, no_enable=True, allow_removed=True)
|
|
config = world.home / "config.yaml"
|
|
watched = [config, paths.runtime_facts_path()]
|
|
before = {path: path.read_bytes() for path in watched}
|
|
if failure == "config":
|
|
prelude = (
|
|
"from pm import publication\n"
|
|
"atomic_bytes = publication.durable_write_bytes\n"
|
|
"def fail(path, data):\n"
|
|
f" if path == Path({str(config)!r}) and b'publication-candidate' in data:\n"
|
|
" raise OSError('fixture config disk full')\n"
|
|
" return atomic_bytes(path, data)\n"
|
|
"publication.durable_write_bytes = fail\n"
|
|
)
|
|
else:
|
|
# Config and facts now publish in the worker; the failed command must
|
|
# return only after that same worker has restored the previous bytes.
|
|
prelude = (
|
|
"from pm.lock import Facts\n"
|
|
"def fail(self, *args, **kwargs):\n"
|
|
f" assert 'publication-candidate' in Path({str(config)!r}).read_text()\n"
|
|
" raise OSError('fixture facts disk full')\n"
|
|
"Facts.record_state = fail\n"
|
|
)
|
|
uv = shutil.which("uv")
|
|
assert uv
|
|
monkeypatch.setattr(client, "runtime_command", lambda path, **kw:
|
|
worker_command(path, uv, sys.executable, prelude=prelude,
|
|
runtime_python=world.runtime_python))
|
|
with pytest.raises(AdmissionRefused, match=f"fixture {failure} disk full"):
|
|
world.command("enable", name="publication-candidate", no_allow_tool_override=True)
|
|
assert {path: path.read_bytes() for path in watched} == before
|
|
assert world.selected() == selected
|
|
world.imports()
|
|
assert receipt.latest()["outcome"] == "failed"
|
|
|
|
|
|
def test_historical_writer_hands_off_without_mutating_config(plugin_world, monkeypatch):
|
|
from hermes_cli.plugins_cmd import _save_enabled_set
|
|
|
|
config = plugin_world.home / "config.yaml"
|
|
before = config.read_bytes()
|
|
def takeover():
|
|
raise SystemExit(73)
|
|
monkeypatch.setattr("hermes_cli._old_updater.stop_for_relaunch", takeover)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_save_enabled_set({"must-not-be-saved"})
|
|
assert exc.value.code == 73
|
|
assert config.read_bytes() == before
|
|
|
|
|
|
@pytest.mark.parametrize("surface", ["dashboard", "composite"])
|
|
def test_ui_conflict_is_reported_without_changing_selection(plugin_world, surface):
|
|
from hermes_cli import plugins_cmd
|
|
from hermes_cli.plugins_admission import AdmissionRefused
|
|
from pm import paths
|
|
|
|
world = plugin_world
|
|
origin, sha = world.origin()
|
|
world.command("install", identifier=origin.as_uri(), ref=sha, no_enable=True, allow_removed=True)
|
|
world.command("enable", name="plugin-worker-proof", no_allow_tool_override=True)
|
|
origin, sha = world.origin(name="conflicting-ui-plugin", pin="2.0")
|
|
world.command("install", identifier=origin.as_uri(), ref=sha, no_enable=True, allow_removed=True)
|
|
watched = [world.home / "config.yaml", paths.runtime_facts_path()]
|
|
before = {path: path.read_bytes() for path in watched}
|
|
if surface == "dashboard":
|
|
result = plugins_cmd.dashboard_set_agent_plugin_enabled("conflicting-ui-plugin", enabled=True)
|
|
assert not result["ok"] and "plugin-proof-dep" in result["error"]
|
|
else:
|
|
with pytest.raises(AdmissionRefused, match="plugin-proof-dep"):
|
|
# plugin-worker-proof is ticked on open; ticking the conflicting plugin is the refused flip.
|
|
plugins_cmd._persist_plugin_selection(["plugin-worker-proof", "conflicting-ui-plugin"], {0, 1}, set(), {0})
|
|
assert {path: path.read_bytes() for path in watched} == before
|
|
world.imports()
|
|
|
|
def test_core_conflict_names_the_plugin_and_keeps_selection(plugin_world, capsys):
|
|
from hermes_cli.plugins_admission import AdmissionRefused
|
|
from pm import paths
|
|
|
|
world = plugin_world
|
|
# Core pins plugin-core-dep==1.0; this plugin demands 2.0, so no union can resolve.
|
|
origin, sha = world.origin(name="core-conflict-plugin", dependency="plugin-core-dep", pin="2.0")
|
|
world.command("install", identifier=origin.as_uri(), ref=sha, no_enable=True, allow_removed=True)
|
|
watched = [world.home / "config.yaml", paths.runtime_facts_path()]
|
|
before = {path: path.read_bytes() for path in watched if path.exists()}
|
|
capsys.readouterr()
|
|
with pytest.raises(AdmissionRefused, match="Plugin 'core-conflict-plugin' conflicts with") as refused:
|
|
world.command("enable", name="core-conflict-plugin", no_allow_tool_override=True)
|
|
assert "plugin-core-dep" in str(refused.value) # the resolver's own cause stays visible
|
|
printed = " ".join(capsys.readouterr().out.split())
|
|
assert "Plugin 'core-conflict-plugin' conflicts with" in printed and "plugin-core-dep" in printed
|
|
assert {path: path.read_bytes() for path in watched if path.exists()} == before
|
|
assert world.enabled() == []
|