@@ -639,6 +639,38 @@ def test_wildcard_question_mark_in_fqn_prefix_rejected(kuzu_graph) -> None:
639639 assert "fqn_prefix" in out .message
640640
641641
642+ def test_search_wildcard_in_fqn_prefix_rejected_without_run_search (monkeypatch , kuzu_graph ) -> None :
643+ calls : list [int ] = []
644+
645+ def boom (* _a , ** _k ):
646+ calls .append (1 )
647+ return _fake_search_rows ()
648+
649+ monkeypatch .setattr ("mcp_v2.run_search" , boom )
650+ out = search_v2 ("anything" , filter = {"fqn_prefix" : "com.*" }, graph = kuzu_graph )
651+ assert out .success is False
652+ assert out .message
653+ assert "fqn_prefix" in out .message
654+ assert calls == []
655+
656+
657+ def test_neighbors_wildcard_in_filter_rejected_before_graph_query (kuzu_graph ) -> None :
658+ class ExplodeGraph :
659+ def _rows (self , * _a , ** _k ) -> list :
660+ raise AssertionError ("graph must not be queried when wildcard rejects filter" )
661+
662+ out = neighbors_v2 (
663+ "sym:unused" ,
664+ direction = "out" ,
665+ edge_types = ["CALLS" ],
666+ filter = {"fqn_prefix" : "com.*" },
667+ graph = ExplodeGraph (), # type: ignore[arg-type]
668+ )
669+ assert out .success is False
670+ assert out .message
671+ assert "fqn_prefix" in out .message
672+
673+
642674def test_describe_by_fqn_returns_symbol (kuzu_graph ) -> None :
643675 symbol = kuzu_graph .list_by_role ("SERVICE" , limit = 1 )[0 ]
644676 out = describe_v2 (fqn = symbol .fqn , graph = kuzu_graph )
@@ -665,6 +697,54 @@ def test_describe_by_fqn_id_takes_precedence(kuzu_graph) -> None:
665697 assert str (out .record .data .get ("role" ) or "" ) == "SERVICE"
666698
667699
700+ def test_describe_by_fqn_duplicate_returns_first_with_disambiguation_hint () -> None :
701+ class DupFqnGraph :
702+ def _rows (self , query : str , params : dict | None = None ) -> list :
703+ p = params or {}
704+ if "WHERE s.fqn = $fqn" in query :
705+ if p .get ("fqn" ) == "com.fixture.DupeName" :
706+ return [{"id" : "sym:dupe-a" }, {"id" : "sym:dupe-b" }]
707+ if "MATCH (n:Symbol)" in query and "WHERE n.id = $id" in query :
708+ if p .get ("id" ) == "sym:dupe-a" :
709+ return [
710+ {
711+ "id" : "sym:dupe-a" ,
712+ "kind" : "file" ,
713+ "name" : "DupeName" ,
714+ "fqn" : "com.fixture.DupeName" ,
715+ "package" : "com.fixture" ,
716+ "module" : "fixture" ,
717+ "microservice" : "svc-a" ,
718+ "filename" : "DupeName.java" ,
719+ "start_line" : 1 ,
720+ "end_line" : 1 ,
721+ "start_byte" : 0 ,
722+ "end_byte" : 0 ,
723+ "modifiers" : [],
724+ "annotations" : [],
725+ "capabilities" : [],
726+ "role" : "" ,
727+ "signature" : "" ,
728+ "parent_id" : "" ,
729+ "resolved" : True ,
730+ }
731+ ]
732+ return []
733+
734+ def edge_counts_for (self , node_id : str ) -> dict [str , dict [str , int ]]:
735+ return {}
736+
737+ out = describe_v2 (fqn = "com.fixture.DupeName" , graph = DupFqnGraph ()) # type: ignore[arg-type]
738+ assert out .success is True
739+ assert out .record is not None
740+ assert out .record .id == "sym:dupe-a"
741+ assert out .message
742+ assert "multiple symbols share this FQN" in out .message
743+ assert "find(kind='symbol'" in out .message
744+ assert "describe(id=..." in out .message
745+ assert "search(query=..." in out .message
746+
747+
668748def test_describe_by_fqn_requires_id_or_fqn (kuzu_graph ) -> None :
669749 out = describe_v2 (graph = kuzu_graph )
670750 assert out .success is False
0 commit comments