@@ -764,3 +764,135 @@ def test_multiple_plugins_register_different_commands(self):
764764 assert "cmd-b" in mgr ._plugin_commands
765765 assert mgr ._plugin_commands ["cmd-a" ]["plugin" ] == "plugin-a"
766766 assert mgr ._plugin_commands ["cmd-b" ]["plugin" ] == "plugin-b"
767+
768+
769+ # ── TestPluginDispatchTool ────────────────────────────────────────────────
770+
771+
772+ class TestPluginDispatchTool :
773+ """Tests for PluginContext.dispatch_tool() — tool dispatch with agent context."""
774+
775+ def test_dispatch_tool_calls_registry (self ):
776+ """dispatch_tool() delegates to registry.dispatch()."""
777+ mgr = PluginManager ()
778+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
779+ ctx = PluginContext (manifest , mgr )
780+
781+ mock_registry = MagicMock ()
782+ mock_registry .dispatch .return_value = '{"result": "ok"}'
783+
784+ with patch ("hermes_cli.plugins.PluginContext.dispatch_tool.__module__" , "hermes_cli.plugins" ):
785+ with patch .dict ("sys.modules" , {}):
786+ with patch ("tools.registry.registry" , mock_registry ):
787+ result = ctx .dispatch_tool ("web_search" , {"query" : "test" })
788+
789+ assert result == '{"result": "ok"}'
790+
791+ def test_dispatch_tool_injects_parent_agent_from_cli_ref (self ):
792+ """When _cli_ref has an agent, it's passed as parent_agent."""
793+ mgr = PluginManager ()
794+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
795+ ctx = PluginContext (manifest , mgr )
796+
797+ mock_agent = MagicMock ()
798+ mock_cli = MagicMock ()
799+ mock_cli .agent = mock_agent
800+ mgr ._cli_ref = mock_cli
801+
802+ mock_registry = MagicMock ()
803+ mock_registry .dispatch .return_value = '{"ok": true}'
804+
805+ with patch ("tools.registry.registry" , mock_registry ):
806+ ctx .dispatch_tool ("delegate_task" , {"goal" : "test" })
807+
808+ mock_registry .dispatch .assert_called_once ()
809+ call_kwargs = mock_registry .dispatch .call_args
810+ assert call_kwargs [1 ].get ("parent_agent" ) is mock_agent
811+
812+ def test_dispatch_tool_no_parent_agent_when_no_cli_ref (self ):
813+ """When _cli_ref is None (gateway mode), no parent_agent is injected."""
814+ mgr = PluginManager ()
815+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
816+ ctx = PluginContext (manifest , mgr )
817+ mgr ._cli_ref = None
818+
819+ mock_registry = MagicMock ()
820+ mock_registry .dispatch .return_value = '{"ok": true}'
821+
822+ with patch ("tools.registry.registry" , mock_registry ):
823+ ctx .dispatch_tool ("delegate_task" , {"goal" : "test" })
824+
825+ call_kwargs = mock_registry .dispatch .call_args
826+ assert "parent_agent" not in call_kwargs [1 ]
827+
828+ def test_dispatch_tool_no_parent_agent_when_agent_is_none (self ):
829+ """When cli_ref exists but agent is None (not yet initialized), skip parent_agent."""
830+ mgr = PluginManager ()
831+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
832+ ctx = PluginContext (manifest , mgr )
833+
834+ mock_cli = MagicMock ()
835+ mock_cli .agent = None
836+ mgr ._cli_ref = mock_cli
837+
838+ mock_registry = MagicMock ()
839+ mock_registry .dispatch .return_value = '{"ok": true}'
840+
841+ with patch ("tools.registry.registry" , mock_registry ):
842+ ctx .dispatch_tool ("delegate_task" , {"goal" : "test" })
843+
844+ call_kwargs = mock_registry .dispatch .call_args
845+ assert "parent_agent" not in call_kwargs [1 ]
846+
847+ def test_dispatch_tool_respects_explicit_parent_agent (self ):
848+ """Explicit parent_agent kwarg is not overwritten by _cli_ref.agent."""
849+ mgr = PluginManager ()
850+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
851+ ctx = PluginContext (manifest , mgr )
852+
853+ cli_agent = MagicMock (name = "cli_agent" )
854+ mock_cli = MagicMock ()
855+ mock_cli .agent = cli_agent
856+ mgr ._cli_ref = mock_cli
857+
858+ explicit_agent = MagicMock (name = "explicit_agent" )
859+
860+ mock_registry = MagicMock ()
861+ mock_registry .dispatch .return_value = '{"ok": true}'
862+
863+ with patch ("tools.registry.registry" , mock_registry ):
864+ ctx .dispatch_tool ("delegate_task" , {"goal" : "test" }, parent_agent = explicit_agent )
865+
866+ call_kwargs = mock_registry .dispatch .call_args
867+ assert call_kwargs [1 ]["parent_agent" ] is explicit_agent
868+
869+ def test_dispatch_tool_forwards_extra_kwargs (self ):
870+ """Extra kwargs are forwarded to registry.dispatch()."""
871+ mgr = PluginManager ()
872+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
873+ ctx = PluginContext (manifest , mgr )
874+ mgr ._cli_ref = None
875+
876+ mock_registry = MagicMock ()
877+ mock_registry .dispatch .return_value = '{"ok": true}'
878+
879+ with patch ("tools.registry.registry" , mock_registry ):
880+ ctx .dispatch_tool ("some_tool" , {"x" : 1 }, task_id = "test-123" )
881+
882+ call_kwargs = mock_registry .dispatch .call_args
883+ assert call_kwargs [1 ]["task_id" ] == "test-123"
884+
885+ def test_dispatch_tool_returns_json_string (self ):
886+ """dispatch_tool() returns the raw JSON string from the registry."""
887+ mgr = PluginManager ()
888+ manifest = PluginManifest (name = "test-plugin" , source = "user" )
889+ ctx = PluginContext (manifest , mgr )
890+ mgr ._cli_ref = None
891+
892+ mock_registry = MagicMock ()
893+ mock_registry .dispatch .return_value = '{"error": "Unknown tool: fake"}'
894+
895+ with patch ("tools.registry.registry" , mock_registry ):
896+ result = ctx .dispatch_tool ("fake" , {})
897+
898+ assert '"error"' in result
0 commit comments