Skip to content

Commit 3e718cb

Browse files
committed
test: add unit tests for 9 MCP Design Pattern tools
1 parent 66c62a0 commit 3e718cb

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

tests/test_handlers_unit.py

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@
2121
handle_graph_statistics,
2222
handle_bulk_insert,
2323
handle_bulk_update,
24+
# MCP Design Pattern handlers
25+
handle_search_tools,
26+
handle_list_tools_by_category,
27+
handle_switch_context,
28+
handle_get_active_context,
29+
handle_list_contexts,
30+
handle_advance_workflow_stage,
31+
handle_get_tool_usage_stats,
32+
handle_unload_tools,
33+
TOOL_CATEGORIES,
34+
WORKFLOW_CONTEXTS,
35+
WORKFLOW_STAGES,
2436
)
2537

2638

@@ -609,3 +621,306 @@ def test_handle_graph_statistics_minimal_args(self, mock_calculate_graph_statist
609621
False, # aggregate_collections default
610622
False # per_collection_stats default
611623
)
624+
625+
626+
class TestMCPDesignPatternHandlers:
627+
"""Test MCP Design Pattern tool handlers."""
628+
629+
def setup_method(self):
630+
"""Set up test fixtures."""
631+
self.mock_db = Mock()
632+
633+
# ========================================================================
634+
# Pattern 1: Progressive Tool Discovery
635+
# ========================================================================
636+
637+
def test_handle_search_tools_by_keywords(self):
638+
"""Test searching tools by keywords returns matching tools."""
639+
args = {
640+
"keywords": ["query", "graph"],
641+
"categories": None,
642+
"detail_level": "summary"
643+
}
644+
645+
result = handle_search_tools(self.mock_db, args)
646+
647+
assert "matches" in result
648+
assert "total_matches" in result
649+
assert "keywords" in result
650+
assert result["keywords"] == ["query", "graph"]
651+
assert result["detail_level"] == "summary"
652+
assert isinstance(result["matches"], list)
653+
# Should find tools with "query" or "graph" in name/description
654+
assert result["total_matches"] > 0
655+
656+
def test_handle_search_tools_with_category_filter(self):
657+
"""Test searching tools with category filter."""
658+
args = {
659+
"keywords": ["insert"],
660+
"categories": ["core_data"],
661+
"detail_level": "name"
662+
}
663+
664+
result = handle_search_tools(self.mock_db, args)
665+
666+
assert result["categories_searched"] == ["core_data"]
667+
assert result["detail_level"] == "name"
668+
# All matches should only have "name" field
669+
for match in result["matches"]:
670+
assert "name" in match
671+
assert "description" not in match or len(match) == 1
672+
673+
def test_handle_search_tools_full_detail(self):
674+
"""Test searching tools with full detail level includes schemas."""
675+
args = {
676+
"keywords": ["query"],
677+
"categories": None,
678+
"detail_level": "full"
679+
}
680+
681+
result = handle_search_tools(self.mock_db, args)
682+
683+
assert result["detail_level"] == "full"
684+
# At least one match should have inputSchema
685+
if result["total_matches"] > 0:
686+
first_match = result["matches"][0]
687+
assert "name" in first_match
688+
assert "description" in first_match
689+
assert "inputSchema" in first_match
690+
691+
def test_handle_list_tools_by_category_all(self):
692+
"""Test listing all tool categories."""
693+
args = {"category": None}
694+
695+
result = handle_list_tools_by_category(self.mock_db, args)
696+
697+
assert "categories" in result
698+
assert "total_tools" in result
699+
assert len(result["categories"]) == 9 # 9 categories defined
700+
assert "core_data" in result["categories"]
701+
assert "graph_basic" in result["categories"]
702+
assert result["total_tools"] > 0
703+
704+
def test_handle_list_tools_by_category_single(self):
705+
"""Test listing tools for a single category."""
706+
args = {"category": "core_data"}
707+
708+
result = handle_list_tools_by_category(self.mock_db, args)
709+
710+
assert result["category"] == "core_data"
711+
assert "tools" in result
712+
assert "tool_count" in result
713+
assert isinstance(result["tools"], list)
714+
assert result["tool_count"] == len(result["tools"])
715+
# Verify core_data tools are present
716+
assert result["tool_count"] > 0
717+
718+
def test_handle_list_tools_by_category_invalid(self):
719+
"""Test listing tools with invalid category returns error."""
720+
args = {"category": "invalid_category"}
721+
722+
result = handle_list_tools_by_category(self.mock_db, args)
723+
724+
assert "error" in result
725+
assert "available_categories" in result
726+
assert "invalid_category" in result["error"]
727+
728+
# ========================================================================
729+
# Pattern 2: Context Switching
730+
# ========================================================================
731+
732+
def test_handle_switch_context_valid(self):
733+
"""Test switching to a valid workflow context."""
734+
args = {"context": "graph_modeling"}
735+
736+
result = handle_switch_context(self.mock_db, args)
737+
738+
assert "from_context" in result
739+
assert "to_context" in result
740+
assert result["to_context"] == "graph_modeling"
741+
assert "description" in result
742+
assert "tools_added" in result
743+
assert "tools_removed" in result
744+
assert "total_tools" in result
745+
assert "active_tools" in result
746+
assert isinstance(result["active_tools"], list)
747+
748+
def test_handle_switch_context_invalid(self):
749+
"""Test switching to invalid context returns error."""
750+
args = {"context": "invalid_context"}
751+
752+
result = handle_switch_context(self.mock_db, args)
753+
754+
assert "error" in result
755+
assert "available_contexts" in result
756+
assert "invalid_context" in result["error"]
757+
758+
def test_handle_switch_context_tracks_changes(self):
759+
"""Test context switching tracks tool additions and removals."""
760+
# First switch to data_analysis
761+
args1 = {"context": "data_analysis"}
762+
result1 = handle_switch_context(self.mock_db, args1)
763+
764+
# Then switch to graph_modeling
765+
args2 = {"context": "graph_modeling"}
766+
result2 = handle_switch_context(self.mock_db, args2)
767+
768+
assert result2["from_context"] == "data_analysis"
769+
assert result2["to_context"] == "graph_modeling"
770+
# Should have tools added and removed
771+
assert isinstance(result2["tools_added"], list)
772+
assert isinstance(result2["tools_removed"], list)
773+
774+
def test_handle_get_active_context(self):
775+
"""Test getting the currently active context."""
776+
# Switch to a known context first
777+
switch_args = {"context": "bulk_operations"}
778+
handle_switch_context(self.mock_db, switch_args)
779+
780+
# Now get active context
781+
result = handle_get_active_context(self.mock_db, None)
782+
783+
assert "active_context" in result
784+
assert result["active_context"] == "bulk_operations"
785+
assert "description" in result
786+
assert "tools" in result
787+
assert "tool_count" in result
788+
assert isinstance(result["tools"], list)
789+
assert result["tool_count"] == len(result["tools"])
790+
791+
def test_handle_list_contexts_without_tools(self):
792+
"""Test listing all contexts without tool details."""
793+
args = {"include_tools": False}
794+
795+
result = handle_list_contexts(self.mock_db, args)
796+
797+
assert "contexts" in result
798+
assert "total_contexts" in result
799+
assert "active_context" in result
800+
assert len(result["contexts"]) == 6 # 6 workflow contexts
801+
802+
# Verify each context has description and tool_count but not tools
803+
for context_name, context_info in result["contexts"].items():
804+
assert "description" in context_info
805+
assert "tool_count" in context_info
806+
assert "tools" not in context_info
807+
808+
def test_handle_list_contexts_with_tools(self):
809+
"""Test listing all contexts with tool details."""
810+
args = {"include_tools": True}
811+
812+
result = handle_list_contexts(self.mock_db, args)
813+
814+
assert "contexts" in result
815+
assert len(result["contexts"]) == 6
816+
817+
# Verify each context includes tools list
818+
for context_name, context_info in result["contexts"].items():
819+
assert "description" in context_info
820+
assert "tool_count" in context_info
821+
assert "tools" in context_info
822+
assert isinstance(context_info["tools"], list)
823+
824+
# ========================================================================
825+
# Pattern 3: Tool Unloading
826+
# ========================================================================
827+
828+
def test_handle_advance_workflow_stage_valid(self):
829+
"""Test advancing to a valid workflow stage."""
830+
args = {"stage": "data_loading"}
831+
832+
result = handle_advance_workflow_stage(self.mock_db, args)
833+
834+
assert "from_stage" in result
835+
assert "to_stage" in result
836+
assert result["to_stage"] == "data_loading"
837+
assert "description" in result
838+
assert "tools_unloaded" in result
839+
assert "tools_loaded" in result
840+
assert "active_tools" in result
841+
assert "total_active_tools" in result
842+
assert isinstance(result["active_tools"], list)
843+
assert result["total_active_tools"] == len(result["active_tools"])
844+
845+
def test_handle_advance_workflow_stage_invalid(self):
846+
"""Test advancing to invalid stage returns error."""
847+
args = {"stage": "invalid_stage"}
848+
849+
result = handle_advance_workflow_stage(self.mock_db, args)
850+
851+
assert "error" in result
852+
assert "available_stages" in result
853+
assert "invalid_stage" in result["error"]
854+
855+
def test_handle_advance_workflow_stage_progression(self):
856+
"""Test stage progression tracks tool lifecycle."""
857+
# First, reset to setup stage
858+
reset_args = {"stage": "setup"}
859+
handle_advance_workflow_stage(self.mock_db, reset_args)
860+
861+
# Advance from setup to data_loading
862+
args1 = {"stage": "data_loading"}
863+
result1 = handle_advance_workflow_stage(self.mock_db, args1)
864+
865+
assert result1["from_stage"] == "setup"
866+
assert result1["to_stage"] == "data_loading"
867+
868+
# Advance to analysis
869+
args2 = {"stage": "analysis"}
870+
result2 = handle_advance_workflow_stage(self.mock_db, args2)
871+
872+
assert result2["from_stage"] == "data_loading"
873+
assert result2["to_stage"] == "analysis"
874+
assert isinstance(result2["tools_unloaded"], list)
875+
assert isinstance(result2["tools_loaded"], list)
876+
877+
def test_handle_get_tool_usage_stats(self):
878+
"""Test getting tool usage statistics."""
879+
result = handle_get_tool_usage_stats(self.mock_db, None)
880+
881+
assert "current_stage" in result
882+
assert "tool_usage" in result
883+
assert "total_tools_used" in result
884+
assert "active_stage_tools" in result
885+
assert isinstance(result["tool_usage"], dict)
886+
assert isinstance(result["active_stage_tools"], list)
887+
888+
def test_handle_unload_tools_valid(self):
889+
"""Test manually unloading valid tools."""
890+
from mcp_arangodb_async.tools import ARANGO_QUERY, ARANGO_INSERT
891+
892+
args = {"tool_names": [ARANGO_QUERY, ARANGO_INSERT]}
893+
894+
result = handle_unload_tools(self.mock_db, args)
895+
896+
assert "unloaded" in result
897+
assert "not_found" in result
898+
assert "total_unloaded" in result
899+
assert ARANGO_QUERY in result["unloaded"]
900+
assert ARANGO_INSERT in result["unloaded"]
901+
assert result["total_unloaded"] == 2
902+
903+
def test_handle_unload_tools_invalid(self):
904+
"""Test unloading invalid tools tracks not found."""
905+
args = {"tool_names": ["invalid_tool_1", "invalid_tool_2"]}
906+
907+
result = handle_unload_tools(self.mock_db, args)
908+
909+
assert "unloaded" in result
910+
assert "not_found" in result
911+
assert len(result["not_found"]) == 2
912+
assert "invalid_tool_1" in result["not_found"]
913+
assert "invalid_tool_2" in result["not_found"]
914+
assert result["total_unloaded"] == 0
915+
916+
def test_handle_unload_tools_mixed(self):
917+
"""Test unloading mix of valid and invalid tools."""
918+
from mcp_arangodb_async.tools import ARANGO_QUERY
919+
920+
args = {"tool_names": [ARANGO_QUERY, "invalid_tool"]}
921+
922+
result = handle_unload_tools(self.mock_db, args)
923+
924+
assert ARANGO_QUERY in result["unloaded"]
925+
assert "invalid_tool" in result["not_found"]
926+
assert result["total_unloaded"] == 1

0 commit comments

Comments
 (0)