diff --git a/tests/tools/test_toolsets.py b/tests/tools/test_toolsets.py index d716c696d6dc1..2535099e14499 100644 --- a/tests/tools/test_toolsets.py +++ b/tests/tools/test_toolsets.py @@ -158,6 +158,33 @@ def test_mcp_alias_uses_live_registry(self, monkeypatch): assert validate_toolset("mcp-dynserver") is True assert "mcp__dynserver__ping" in resolve_toolset("dynserver") + def test_plugin_toolset_valid_before_discovery_finishes(self, monkeypatch): + """A configured plugin toolset must validate even while discovery is still in flight. + + The live registry is empty until plugin discovery completes, so validating against it alone + reported a perfectly valid `-t ` as unknown on every fast startup and printed + "Warning: Unknown toolsets: ". + """ + import hermes_cli.plugins as plugins + + monkeypatch.setattr("tools.registry.registry", ToolRegistry()) + monkeypatch.setattr( + plugins, "get_plugin_toolset_keys_nowait", lambda: {"dsh", "spotify"}, raising=False) + + assert validate_toolset("dsh") is True + assert validate_toolset("spotify") is True + + def test_declared_plugin_key_still_resolves_to_no_tools(self, monkeypatch): + """Name validation must not invent tools: an unloaded plugin toolset resolves empty.""" + import hermes_cli.plugins as plugins + + monkeypatch.setattr("tools.registry.registry", ToolRegistry()) + monkeypatch.setattr( + plugins, "get_plugin_toolset_keys_nowait", lambda: {"dsh"}, raising=False) + + assert validate_toolset("dsh") is True + assert resolve_toolset("dsh") == [] + class TestGetToolsetInfo: def test_leaf(self): diff --git a/toolsets.py b/toolsets.py index 1b2da7f579f92..49b065035193e 100644 --- a/toolsets.py +++ b/toolsets.py @@ -402,6 +402,23 @@ def _get_plugin_toolset_names() -> Set[str]: return {n for n in _registry_call("get_registered_toolset_names", ()) if n not in TOOLSETS} +def _declared_plugin_toolset_names() -> Set[str]: + """Plugin toolset keys that a CONFIG may legitimately name, without joining discovery. + + The live registry is empty until plugin discovery finishes, so validating a configured name + against it alone reports a valid plugin toolset (``-t dsh``) as unknown on every fast startup. + ``get_plugin_toolset_keys_nowait`` serves last launch's persisted set while a scan is in flight. + Only name validation uses this: a key with no live tools yet must still resolve to NO tools, so + ``get_toolset`` keeps reading the registry alone. + """ + try: + from hermes_cli.plugins import get_plugin_toolset_keys_nowait + + return set(get_plugin_toolset_keys_nowait()) + except Exception: + return set() + + def _get_registry_toolset_aliases() -> Dict[str, str]: return _registry_call("get_registered_toolset_aliases", {}) @@ -438,7 +455,8 @@ def get_toolset_names() -> List[str]: def validate_toolset(name: str) -> bool: return (name in {"all", "*"} or name in TOOLSETS - or name in _get_plugin_toolset_names() or name in _get_registry_toolset_aliases()) + or name in _get_plugin_toolset_names() or name in _get_registry_toolset_aliases() + or name in _declared_plugin_toolset_names()) def create_custom_toolset(name: str, description: str, tools: List[str] = None, includes: List[str] = None) -> None: