@@ -86,6 +86,15 @@ class _TestCoreAgent:
8686 "openai.types.chat.chat_completion_message_param" : MagicMock (),
8787 # Mock exa_py to avoid importing the real package when sdk.nexent.core.tools imports it
8888 "exa_py" : MagicMock (Exa = MagicMock ()),
89+ # Mock paramiko and cryptography to avoid PyO3 import issues in tests
90+ "paramiko" : MagicMock (),
91+ "cryptography" : MagicMock (),
92+ "cryptography.hazmat" : MagicMock (),
93+ "cryptography.hazmat.primitives" : MagicMock (),
94+ "cryptography.hazmat.primitives.ciphers" : MagicMock (),
95+ "cryptography.hazmat.primitives.ciphers.base" : MagicMock (),
96+ "cryptography.hazmat.bindings" : MagicMock (),
97+ "cryptography.hazmat.bindings._rust" : MagicMock (),
8998 # Mock the OpenAIModel import
9099 "sdk.nexent.core.models.openai_llm" : MagicMock (OpenAIModel = mock_openai_model_class ),
91100 # Mock CoreAgent import
@@ -100,6 +109,7 @@ class _TestCoreAgent:
100109# ---------------------------------------------------------------------------
101110with patch .dict ("sys.modules" , module_mocks ):
102111 from sdk .nexent .core .utils .observer import MessageObserver , ProcessType
112+ from sdk .nexent .core .agents import nexent_agent
103113 from sdk .nexent .core .agents .nexent_agent import NexentAgent , ActionStep , TaskStep
104114 from sdk .nexent .core .agents .agent_model import ToolConfig , ModelConfig , AgentConfig
105115
@@ -447,6 +457,236 @@ def test_create_tool_with_local_source(nexent_agent_instance):
447457 assert result == "local_tool"
448458
449459
460+ def test_create_local_tool_success (nexent_agent_instance ):
461+ """Test successful creation of a local tool."""
462+ mock_tool_class = MagicMock ()
463+ mock_tool_instance = MagicMock ()
464+ mock_tool_class .return_value = mock_tool_instance
465+
466+ tool_config = ToolConfig (
467+ class_name = "DummyTool" ,
468+ name = "dummy" ,
469+ description = "desc" ,
470+ inputs = "{}" ,
471+ output_type = "string" ,
472+ params = {"param1" : "value1" , "param2" : 42 },
473+ source = "local" ,
474+ metadata = {},
475+ )
476+
477+ # Patch the module's globals to include our mock tool class
478+ original_value = nexent_agent .__dict__ .get ("DummyTool" )
479+ nexent_agent .__dict__ ["DummyTool" ] = mock_tool_class
480+
481+ try :
482+ result = nexent_agent_instance .create_local_tool (tool_config )
483+ finally :
484+ # Restore original value
485+ if original_value is not None :
486+ nexent_agent .__dict__ ["DummyTool" ] = original_value
487+ elif "DummyTool" in nexent_agent .__dict__ :
488+ del nexent_agent .__dict__ ["DummyTool" ]
489+
490+ mock_tool_class .assert_called_once_with (param1 = "value1" , param2 = 42 )
491+ assert result == mock_tool_instance
492+
493+
494+ def test_create_local_tool_class_not_found (nexent_agent_instance ):
495+ """Test create_local_tool raises ValueError when class is not found."""
496+ tool_config = ToolConfig (
497+ class_name = "NonExistentTool" ,
498+ name = "dummy" ,
499+ description = "desc" ,
500+ inputs = "{}" ,
501+ output_type = "string" ,
502+ params = {},
503+ source = "local" ,
504+ metadata = {},
505+ )
506+
507+ with pytest .raises (ValueError , match = "NonExistentTool not found in local" ):
508+ nexent_agent_instance .create_local_tool (tool_config )
509+
510+
511+ def test_create_local_tool_knowledge_base_search_tool_success (nexent_agent_instance ):
512+ """Test successful creation of KnowledgeBaseSearchTool with metadata."""
513+ mock_kb_tool_class = MagicMock ()
514+ mock_kb_tool_instance = MagicMock ()
515+ mock_kb_tool_class .return_value = mock_kb_tool_instance
516+
517+ mock_vdb_core = MagicMock ()
518+ mock_embedding_model = MagicMock ()
519+
520+ tool_config = ToolConfig (
521+ class_name = "KnowledgeBaseSearchTool" ,
522+ name = "knowledge_base_search" ,
523+ description = "desc" ,
524+ inputs = "{}" ,
525+ output_type = "string" ,
526+ params = {"top_k" : 10 },
527+ source = "local" ,
528+ metadata = {
529+ "index_names" : ["index1" , "index2" ],
530+ "vdb_core" : mock_vdb_core ,
531+ "embedding_model" : mock_embedding_model ,
532+ },
533+ )
534+
535+ original_value = nexent_agent .__dict__ .get ("KnowledgeBaseSearchTool" )
536+ nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ] = mock_kb_tool_class
537+
538+ try :
539+ result = nexent_agent_instance .create_local_tool (tool_config )
540+ finally :
541+ # Restore original value
542+ if original_value is not None :
543+ nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ] = original_value
544+ elif "KnowledgeBaseSearchTool" in nexent_agent .__dict__ :
545+ del nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ]
546+
547+ # Verify only non-excluded params are passed to __init__
548+ mock_kb_tool_class .assert_called_once_with (
549+ top_k = 10 , # Only non-excluded params passed to __init__
550+ )
551+ # Verify excluded parameters were set directly as attributes after instantiation
552+ assert result == mock_kb_tool_instance
553+ assert mock_kb_tool_instance .observer == nexent_agent_instance .observer
554+ assert mock_kb_tool_instance .index_names == ["index1" , "index2" ]
555+ assert mock_kb_tool_instance .vdb_core == mock_vdb_core
556+ assert mock_kb_tool_instance .embedding_model == mock_embedding_model
557+
558+
559+ def test_create_local_tool_knowledge_base_search_tool_with_conflicting_params (nexent_agent_instance ):
560+ """Test KnowledgeBaseSearchTool creation filters out conflicting params from params dict."""
561+ mock_kb_tool_class = MagicMock ()
562+ mock_kb_tool_instance = MagicMock ()
563+ mock_kb_tool_class .return_value = mock_kb_tool_instance
564+
565+ mock_vdb_core = MagicMock ()
566+ mock_embedding_model = MagicMock ()
567+
568+ tool_config = ToolConfig (
569+ class_name = "KnowledgeBaseSearchTool" ,
570+ name = "knowledge_base_search" ,
571+ description = "desc" ,
572+ inputs = "{}" ,
573+ output_type = "string" ,
574+ params = {
575+ "top_k" : 10 ,
576+ "index_names" : ["conflicting_index" ], # This should be filtered out
577+ "vdb_core" : "conflicting_vdb" , # This should be filtered out
578+ "embedding_model" : "conflicting_model" , # This should be filtered out
579+ "observer" : "conflicting_observer" , # This should be filtered out
580+ },
581+ source = "local" ,
582+ metadata = {
583+ "index_names" : ["index1" , "index2" ], # These should be used instead
584+ "vdb_core" : mock_vdb_core ,
585+ "embedding_model" : mock_embedding_model ,
586+ },
587+ )
588+
589+ original_value = nexent_agent .__dict__ .get ("KnowledgeBaseSearchTool" )
590+ nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ] = mock_kb_tool_class
591+
592+ try :
593+ result = nexent_agent_instance .create_local_tool (tool_config )
594+ finally :
595+ # Restore original value
596+ if original_value is not None :
597+ nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ] = original_value
598+ elif "KnowledgeBaseSearchTool" in nexent_agent .__dict__ :
599+ del nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ]
600+
601+ # Verify conflicting params were filtered out from __init__ call
602+ # Only non-excluded params should be passed to __init__ due to smolagents wrapper restrictions
603+ mock_kb_tool_class .assert_called_once_with (
604+ top_k = 10 , # From filtered_params (not in conflict list)
605+ )
606+ # Verify excluded parameters were set directly as attributes after instantiation
607+ assert result == mock_kb_tool_instance
608+ assert mock_kb_tool_instance .observer == nexent_agent_instance .observer
609+ assert mock_kb_tool_instance .index_names == ["index1" , "index2" ] # From metadata, not params
610+ assert mock_kb_tool_instance .vdb_core == mock_vdb_core # From metadata, not params
611+ assert mock_kb_tool_instance .embedding_model == mock_embedding_model # From metadata, not params
612+
613+
614+ def test_create_local_tool_knowledge_base_search_tool_with_none_defaults (nexent_agent_instance ):
615+ """Test KnowledgeBaseSearchTool creation with None defaults when metadata is missing."""
616+ mock_kb_tool_class = MagicMock ()
617+ mock_kb_tool_instance = MagicMock ()
618+ mock_kb_tool_class .return_value = mock_kb_tool_instance
619+
620+ tool_config = ToolConfig (
621+ class_name = "KnowledgeBaseSearchTool" ,
622+ name = "knowledge_base_search" ,
623+ description = "desc" ,
624+ inputs = "{}" ,
625+ output_type = "string" ,
626+ params = {"top_k" : 5 },
627+ source = "local" ,
628+ metadata = {}, # No metadata provided
629+ )
630+
631+ original_value = nexent_agent .__dict__ .get ("KnowledgeBaseSearchTool" )
632+ nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ] = mock_kb_tool_class
633+
634+ try :
635+ result = nexent_agent_instance .create_local_tool (tool_config )
636+ finally :
637+ # Restore original value
638+ if original_value is not None :
639+ nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ] = original_value
640+ elif "KnowledgeBaseSearchTool" in nexent_agent .__dict__ :
641+ del nexent_agent .__dict__ ["KnowledgeBaseSearchTool" ]
642+
643+ # Verify only non-excluded params are passed to __init__
644+ mock_kb_tool_class .assert_called_once_with (
645+ top_k = 5 ,
646+ )
647+ # Verify excluded parameters were set directly as attributes with None defaults when metadata is missing
648+ assert result == mock_kb_tool_instance
649+ assert mock_kb_tool_instance .observer == nexent_agent_instance .observer
650+ assert mock_kb_tool_instance .index_names == [] # Empty list when None
651+ assert mock_kb_tool_instance .vdb_core is None
652+ assert mock_kb_tool_instance .embedding_model is None
653+ assert result == mock_kb_tool_instance
654+
655+
656+ def test_create_local_tool_with_observer_attribute (nexent_agent_instance ):
657+ """Test create_local_tool sets observer attribute on tool if it exists."""
658+ mock_tool_class = MagicMock ()
659+ mock_tool_instance = MagicMock ()
660+ mock_tool_instance .observer = None # Initially no observer
661+ mock_tool_class .return_value = mock_tool_instance
662+
663+ tool_config = ToolConfig (
664+ class_name = "ToolWithObserver" ,
665+ name = "tool" ,
666+ description = "desc" ,
667+ inputs = "{}" ,
668+ output_type = "string" ,
669+ params = {},
670+ source = "local" ,
671+ metadata = {},
672+ )
673+
674+ original_value = nexent_agent .__dict__ .get ("ToolWithObserver" )
675+ nexent_agent .__dict__ ["ToolWithObserver" ] = mock_tool_class
676+
677+ try :
678+ result = nexent_agent_instance .create_local_tool (tool_config )
679+ finally :
680+ # Restore original value
681+ if original_value is not None :
682+ nexent_agent .__dict__ ["ToolWithObserver" ] = original_value
683+ elif "ToolWithObserver" in nexent_agent .__dict__ :
684+ del nexent_agent .__dict__ ["ToolWithObserver" ]
685+
686+ # Verify observer was set on the tool instance
687+ assert result .observer == nexent_agent_instance .observer
688+
689+
450690def test_create_tool_with_mcp_source (nexent_agent_instance ):
451691 """Ensure create_tool dispatches to create_mcp_tool for mcp source."""
452692 tool_config = ToolConfig (
0 commit comments