Files
hermes-agent/tests/hermes_cli/test_single_query_plugin_cli_ref.py
T
unsupportedpastels 4b6a8edd6a fix(plugins): fire pre_command in the gateway for every local command
After the cutover no local surface fired pre_command where the command runs.
slash.exec and command.dispatch (classic CLI forwards, Ink TUI, Desktop) never
fired it, and the classic CLI fired it from its own second copy of the plugin.

- The gateway fires pre_command for every recognized command a local session
  runs, under the session's profile: before the handler for commands it runs
  itself (slash.exec, command.dispatch; an accepted TUI read fires once inside
  the read path), and for commands the classic CLI runs in its own process
  through the new command.observe (plain data: the command as typed and its
  arguments). command.observe replies first, so the CLI never waits on a
  plugin. The hook arguments keep main's values: surface and platform "cli"
  on every local surface, session_key the session id.
- It fires only for commands that run. A busy rejection or a named refusal
  fires nothing; during a turn it fires when the busy dispatcher accepts the
  command, so a refused /goal, /loop or empty /queue or /steer fires nothing
  either. Every command that does run mid-turn fires, as main's CLI did,
  except messaging's escape hatches (/stop and the approval controls).
- Each delivery waits out its callback on its own thread, never the shared
  executor, with at most one delivery in flight per session: one session's
  deliveries fire in order and a slow callback never holds up another
  session's. The hook dispatcher's existing bounds apply
  (plugins.hook_callback_timeout and its abandoned-worker cap), and with
  plugins.isolation: host the hook runs in the plugin host. pre_command takes
  that bounded path everywhere, so off the gateway its callbacks also run on a
  worker thread.
- On a gateway session the classic CLI loads no plugin code. It reads skills,
  plugin commands and plugin toolsets from the gateway (commands.catalog,
  scoped to the session's own folder, and plugins.list) for /help, skill
  dispatch and completion, /tools and the status and banner fallbacks. -s and
  skills.auto_load are rendered in the gateway, and /reload-skills and
  /skills install rescan there and drop the CLI's cached catalog. These reads
  are cached per gateway life and never served once the connection has hung
  up. The "Unknown toolsets" warning comes from the gateway (a new
  unknown_toolsets field when the session opens). An older gateway without
  command.observe gets one named notice, and nothing fires locally.
- The canonical gateway connection serves plugins.list for its profile:
  loaded plugins with tool/hook/command counts, load error, toolsets and
  portable MCP server names (optional fields added to the contract; TS and
  OpenRPC regenerated). On a gateway session the CLI's /plugins reads it
  instead of this process's plugin manager. Off the gateway the in-process
  paths are unchanged.

Key-less inject_message (B2-11), which the classic CLI's own plugin copy
used to serve, is handled in the next commit.

Refs #134889 (PLG-7)
Refs #139523
2026-10-16 19:39:48 -04:00

50 lines
1.9 KiB
Python

"""``hermes chat -q``/``-Q`` runs leave this process's plugin manager alone.
On main they set ``PluginManager._cli_ref`` so a plugin tool dispatched from a one-shot turn got a
``parent_agent`` (#67597). The turn and its plugin tools now run in the gateway, and the CLI loads
no plugins (PLG-7), so there is nothing here to bind.
"""
from __future__ import annotations
from types import SimpleNamespace
import pytest
import cli
from hermes_cli.plugins import get_plugin_manager
@pytest.fixture(autouse=True)
def _one_shot_seams(monkeypatch):
monkeypatch.delenv("HERMES_KANBAN_GOAL_MODE", raising=False)
monkeypatch.setattr(cli, "_should_seed_interactive", lambda *a, **k: False)
monkeypatch.setattr(cli, "_collect_query_images", lambda q, i: (q, []))
monkeypatch.setattr(cli, "_collect_kanban_task_images", lambda imgs: [])
monkeypatch.setattr(cli, "_finalize_single_query", lambda c: None)
monkeypatch.setattr(get_plugin_manager(), "_cli_ref", None)
def _stub(**extra):
return SimpleNamespace(
_single_query_mode=False, _claim_active_session=lambda *a, **k: True,
console=SimpleNamespace(print=lambda *a, **k: None), _show_security_advisories=lambda: None,
chat=lambda *a, **k: "response", _print_exit_summary=lambda **k: None,
_last_turn_result={"failed": False}, **extra,
)
def test_one_shot_turn_binds_nothing_on_the_local_plugin_manager():
stub = _stub()
with pytest.raises(SystemExit):
cli._run_single_query_mode(stub, "do the thing", None, False, True)
assert get_plugin_manager()._cli_ref is None
def test_quiet_turn_binds_nothing_on_the_local_plugin_manager():
stub = _stub(_ensure_runtime_credentials=lambda: False, _credentials_rate_limited=False,
session_id="s1", model="m")
with pytest.raises(SystemExit):
cli._run_single_query_mode(stub, "do the thing", None, True, True)
assert get_plugin_manager()._cli_ref is None