Navegador

navegador / tests / test_mcp_server.py
Source Blame History 441 lines
2e96458… lmata 1 """Tests for navegador.mcp.server — create_mcp_server and all tool handlers."""
b663b12… lmata 2
2e96458… lmata 3 import json
b663b12… lmata 4 from unittest.mock import MagicMock, patch
b663b12… lmata 5
b663b12… lmata 6 import pytest
b663b12… lmata 7
b663b12… lmata 8 from navegador.context.loader import ContextBundle, ContextNode
b663b12… lmata 9
2e96458… lmata 10 # ── Helpers ───────────────────────────────────────────────────────────────────
b663b12… lmata 11
b663b12… lmata 12 def _node(name="foo", type_="Function", file_path="app.py"):
b663b12… lmata 13 return ContextNode(name=name, type=type_, file_path=file_path)
b663b12… lmata 14
b663b12… lmata 15
b663b12… lmata 16 def _bundle(name="target"):
b663b12… lmata 17 return ContextBundle(target=_node(name), nodes=[])
b663b12… lmata 18
b663b12… lmata 19
b663b12… lmata 20 def _mock_store():
b663b12… lmata 21 store = MagicMock()
b663b12… lmata 22 store.query.return_value = MagicMock(result_set=[])
b663b12… lmata 23 store.node_count.return_value = 5
b663b12… lmata 24 store.edge_count.return_value = 3
b663b12… lmata 25 return store
b663b12… lmata 26
b663b12… lmata 27
2e96458… lmata 28 class _ServerFixture:
2e96458… lmata 29 """
2e96458… lmata 30 Builds a navegador MCP server with mocked mcp module and captures the
2e96458… lmata 31 list_tools and call_tool async handlers so they can be invoked directly.
2e96458… lmata 32 """
2e96458… lmata 33
2e96458… lmata 34 def __init__(self, loader=None):
2e96458… lmata 35 self.store = _mock_store()
2e96458… lmata 36 self.loader = loader or self._default_loader()
2e96458… lmata 37 self.list_tools_fn = None
2e96458… lmata 38 self.call_tool_fn = None
2e96458… lmata 39 self._build()
2e96458… lmata 40
2e96458… lmata 41 def _default_loader(self):
2e96458… lmata 42 from navegador.context import ContextLoader
2e96458… lmata 43 loader = MagicMock(spec=ContextLoader)
2e96458… lmata 44 loader.store = self.store
2e96458… lmata 45 loader.load_file.return_value = _bundle("file_target")
2e96458… lmata 46 loader.load_function.return_value = _bundle("fn_target")
2e96458… lmata 47 loader.load_class.return_value = _bundle("cls_target")
ece88ca… lmata 48 loader.load_decision.return_value = _bundle("decision_target")
2e96458… lmata 49 loader.search.return_value = []
ece88ca… lmata 50 loader.find_owners.return_value = []
ece88ca… lmata 51 loader.search_knowledge.return_value = []
2e96458… lmata 52 return loader
2e96458… lmata 53
2e96458… lmata 54 def _build(self):
2e96458… lmata 55 list_holder = {}
2e96458… lmata 56 call_holder = {}
b663b12… lmata 57
b663b12… lmata 58 def list_tools_decorator():
b663b12… lmata 59 def decorator(fn):
2e96458… lmata 60 list_holder["fn"] = fn
b663b12… lmata 61 return fn
b663b12… lmata 62 return decorator
b663b12… lmata 63
b663b12… lmata 64 def call_tool_decorator():
b663b12… lmata 65 def decorator(fn):
2e96458… lmata 66 call_holder["fn"] = fn
2e96458… lmata 67 return fn
2e96458… lmata 68 return decorator
2e96458… lmata 69
2e96458… lmata 70 mock_server = MagicMock()
2e96458… lmata 71 mock_server.list_tools = list_tools_decorator
2e96458… lmata 72 mock_server.call_tool = call_tool_decorator
2e96458… lmata 73
2e96458… lmata 74 mock_mcp_server = MagicMock()
2e96458… lmata 75 mock_mcp_server.Server.return_value = mock_server
2e96458… lmata 76
2e96458… lmata 77 mock_mcp_types = MagicMock()
2e96458… lmata 78 mock_mcp_types.Tool = dict # Tool(...) → dict so we can inspect fields
2e96458… lmata 79 mock_mcp_types.TextContent = dict # TextContent(type=..., text=...) → dict
2e96458… lmata 80
2e96458… lmata 81 with patch.dict("sys.modules", {
2e96458… lmata 82 "mcp": MagicMock(),
2e96458… lmata 83 "mcp.server": mock_mcp_server,
2e96458… lmata 84 "mcp.types": mock_mcp_types,
2e96458… lmata 85 }), patch("navegador.context.ContextLoader", return_value=self.loader):
2e96458… lmata 86 from importlib import reload
2e96458… lmata 87
2e96458… lmata 88 import navegador.mcp.server as srv
2e96458… lmata 89 reload(srv)
2e96458… lmata 90 self.server = srv.create_mcp_server(lambda: self.store)
2e96458… lmata 91
2e96458… lmata 92 self.list_tools_fn = list_holder["fn"]
2e96458… lmata 93 self.call_tool_fn = call_holder["fn"]
2e96458… lmata 94
2e96458… lmata 95
2e96458… lmata 96 # ── Import guard ──────────────────────────────────────────────────────────────
2e96458… lmata 97
2e96458… lmata 98 class TestCreateMcpServerImport:
2e96458… lmata 99 def test_raises_import_error_if_mcp_not_installed(self):
2e96458… lmata 100 with patch.dict("sys.modules", {"mcp": None, "mcp.server": None, "mcp.types": None}):
2e96458… lmata 101 from importlib import reload
2e96458… lmata 102
2e96458… lmata 103 import navegador.mcp.server as srv
2e96458… lmata 104 reload(srv)
2e96458… lmata 105 with pytest.raises(ImportError, match="mcp"):
2e96458… lmata 106 srv.create_mcp_server(lambda: _mock_store())
2e96458… lmata 107
2e96458… lmata 108
2e96458… lmata 109 # ── list_tools ────────────────────────────────────────────────────────────────
2e96458… lmata 110
2e96458… lmata 111 class TestListTools:
2e96458… lmata 112 def setup_method(self):
2e96458… lmata 113 self.fx = _ServerFixture()
2e96458… lmata 114
2e96458… lmata 115 @pytest.mark.asyncio
1ceb8b0… lmata 116 async def test_returns_eleven_tools(self):
2e96458… lmata 117 tools = await self.fx.list_tools_fn()
1ceb8b0… lmata 118 assert len(tools) == 11
2e96458… lmata 119
2e96458… lmata 120 @pytest.mark.asyncio
2e96458… lmata 121 async def test_tool_names(self):
2e96458… lmata 122 tools = await self.fx.list_tools_fn()
2e96458… lmata 123 names = {t["name"] for t in tools}
2e96458… lmata 124 assert names == {
2e96458… lmata 125 "ingest_repo",
2e96458… lmata 126 "load_file_context",
2e96458… lmata 127 "load_function_context",
2e96458… lmata 128 "load_class_context",
2e96458… lmata 129 "search_symbols",
2e96458… lmata 130 "query_graph",
2e96458… lmata 131 "graph_stats",
ece88ca… lmata 132 "get_rationale",
ece88ca… lmata 133 "find_owners",
ece88ca… lmata 134 "search_knowledge",
1ceb8b0… lmata 135 "blast_radius",
2e96458… lmata 136 }
2e96458… lmata 137
2e96458… lmata 138 @pytest.mark.asyncio
2e96458… lmata 139 async def test_ingest_repo_requires_path(self):
2e96458… lmata 140 tools = await self.fx.list_tools_fn()
2e96458… lmata 141 t = next(t for t in tools if t["name"] == "ingest_repo")
2e96458… lmata 142 assert "path" in t["inputSchema"]["required"]
2e96458… lmata 143
2e96458… lmata 144 @pytest.mark.asyncio
2e96458… lmata 145 async def test_load_function_context_requires_name_and_file_path(self):
2e96458… lmata 146 tools = await self.fx.list_tools_fn()
2e96458… lmata 147 t = next(t for t in tools if t["name"] == "load_function_context")
2e96458… lmata 148 assert "name" in t["inputSchema"]["required"]
2e96458… lmata 149 assert "file_path" in t["inputSchema"]["required"]
2e96458… lmata 150
2e96458… lmata 151
2e96458… lmata 152 # ── call_tool — ingest_repo ───────────────────────────────────────────────────
2e96458… lmata 153
2e96458… lmata 154 class TestCallToolIngestRepo:
2e96458… lmata 155 def setup_method(self):
2e96458… lmata 156 self.fx = _ServerFixture()
2e96458… lmata 157
2e96458… lmata 158 @pytest.mark.asyncio
2e96458… lmata 159 async def test_calls_ingester_and_returns_json(self):
2e96458… lmata 160 mock_ingester = MagicMock()
2e96458… lmata 161 mock_ingester.ingest.return_value = {"files": 3, "functions": 10, "classes": 2, "edges": 15}
2e96458… lmata 162
2e96458… lmata 163 with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
2e96458… lmata 164 result = await self.fx.call_tool_fn("ingest_repo", {"path": "/some/repo"})
2e96458… lmata 165
2e96458… lmata 166 assert len(result) == 1
2e96458… lmata 167 data = json.loads(result[0]["text"])
2e96458… lmata 168 assert data["files"] == 3
2e96458… lmata 169 assert data["functions"] == 10
2e96458… lmata 170
2e96458… lmata 171 @pytest.mark.asyncio
2e96458… lmata 172 async def test_passes_clear_flag(self):
2e96458… lmata 173 mock_ingester = MagicMock()
2e96458… lmata 174 mock_ingester.ingest.return_value = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
2e96458… lmata 175
2e96458… lmata 176 with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
2e96458… lmata 177 await self.fx.call_tool_fn("ingest_repo", {"path": "/repo", "clear": True})
2e96458… lmata 178
2e96458… lmata 179 mock_ingester.ingest.assert_called_once_with("/repo", clear=True)
2e96458… lmata 180
2e96458… lmata 181 @pytest.mark.asyncio
2e96458… lmata 182 async def test_clear_defaults_to_false(self):
2e96458… lmata 183 mock_ingester = MagicMock()
2e96458… lmata 184 mock_ingester.ingest.return_value = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
2e96458… lmata 185
2e96458… lmata 186 with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
2e96458… lmata 187 await self.fx.call_tool_fn("ingest_repo", {"path": "/repo"})
2e96458… lmata 188
2e96458… lmata 189 mock_ingester.ingest.assert_called_once_with("/repo", clear=False)
2e96458… lmata 190
2e96458… lmata 191
2e96458… lmata 192 # ── call_tool — load_file_context ─────────────────────────────────────────────
2e96458… lmata 193
2e96458… lmata 194 class TestCallToolLoadFileContext:
2e96458… lmata 195 def setup_method(self):
2e96458… lmata 196 self.fx = _ServerFixture()
2e96458… lmata 197
2e96458… lmata 198 @pytest.mark.asyncio
2e96458… lmata 199 async def test_returns_markdown_by_default(self):
2e96458… lmata 200 result = await self.fx.call_tool_fn("load_file_context", {"file_path": "src/main.py"})
2e96458… lmata 201 self.fx.loader.load_file.assert_called_once_with("src/main.py")
2e96458… lmata 202 assert "file_target" in result[0]["text"]
2e96458… lmata 203
2e96458… lmata 204 @pytest.mark.asyncio
2e96458… lmata 205 async def test_returns_json_when_requested(self):
2e96458… lmata 206 result = await self.fx.call_tool_fn(
2e96458… lmata 207 "load_file_context", {"file_path": "src/main.py", "format": "json"}
2e96458… lmata 208 )
2e96458… lmata 209 data = json.loads(result[0]["text"])
2e96458… lmata 210 assert data["target"]["name"] == "file_target"
2e96458… lmata 211
2e96458… lmata 212 @pytest.mark.asyncio
2e96458… lmata 213 async def test_markdown_format_explicit(self):
2e96458… lmata 214 result = await self.fx.call_tool_fn(
2e96458… lmata 215 "load_file_context", {"file_path": "src/main.py", "format": "markdown"}
2e96458… lmata 216 )
2e96458… lmata 217 assert "file_target" in result[0]["text"]
2e96458… lmata 218
2e96458… lmata 219
2e96458… lmata 220 # ── call_tool — load_function_context ────────────────────────────────────────
2e96458… lmata 221
2e96458… lmata 222 class TestCallToolLoadFunctionContext:
2e96458… lmata 223 def setup_method(self):
2e96458… lmata 224 self.fx = _ServerFixture()
2e96458… lmata 225
2e96458… lmata 226 @pytest.mark.asyncio
2e96458… lmata 227 async def test_calls_loader_with_depth(self):
2e96458… lmata 228 await self.fx.call_tool_fn(
2e96458… lmata 229 "load_function_context",
2e96458… lmata 230 {"name": "parse", "file_path": "parser.py", "depth": 3},
2e96458… lmata 231 )
2e96458… lmata 232 self.fx.loader.load_function.assert_called_once_with("parse", "parser.py", depth=3)
2e96458… lmata 233
2e96458… lmata 234 @pytest.mark.asyncio
2e96458… lmata 235 async def test_depth_defaults_to_two(self):
2e96458… lmata 236 await self.fx.call_tool_fn(
2e96458… lmata 237 "load_function_context", {"name": "parse", "file_path": "parser.py"}
2e96458… lmata 238 )
2e96458… lmata 239 self.fx.loader.load_function.assert_called_once_with("parse", "parser.py", depth=2)
2e96458… lmata 240
2e96458… lmata 241 @pytest.mark.asyncio
2e96458… lmata 242 async def test_returns_json_when_requested(self):
2e96458… lmata 243 result = await self.fx.call_tool_fn(
2e96458… lmata 244 "load_function_context",
2e96458… lmata 245 {"name": "parse", "file_path": "parser.py", "format": "json"},
2e96458… lmata 246 )
2e96458… lmata 247 data = json.loads(result[0]["text"])
2e96458… lmata 248 assert data["target"]["name"] == "fn_target"
2e96458… lmata 249
2e96458… lmata 250
2e96458… lmata 251 # ── call_tool — load_class_context ───────────────────────────────────────────
2e96458… lmata 252
2e96458… lmata 253 class TestCallToolLoadClassContext:
2e96458… lmata 254 def setup_method(self):
2e96458… lmata 255 self.fx = _ServerFixture()
2e96458… lmata 256
2e96458… lmata 257 @pytest.mark.asyncio
2e96458… lmata 258 async def test_calls_loader_with_name_and_file(self):
2e96458… lmata 259 await self.fx.call_tool_fn(
2e96458… lmata 260 "load_class_context", {"name": "AuthService", "file_path": "auth.py"}
2e96458… lmata 261 )
2e96458… lmata 262 self.fx.loader.load_class.assert_called_once_with("AuthService", "auth.py")
2e96458… lmata 263
2e96458… lmata 264 @pytest.mark.asyncio
2e96458… lmata 265 async def test_returns_markdown_by_default(self):
2e96458… lmata 266 result = await self.fx.call_tool_fn(
2e96458… lmata 267 "load_class_context", {"name": "AuthService", "file_path": "auth.py"}
2e96458… lmata 268 )
2e96458… lmata 269 assert "cls_target" in result[0]["text"]
2e96458… lmata 270
2e96458… lmata 271 @pytest.mark.asyncio
2e96458… lmata 272 async def test_returns_json_when_requested(self):
2e96458… lmata 273 result = await self.fx.call_tool_fn(
2e96458… lmata 274 "load_class_context",
2e96458… lmata 275 {"name": "AuthService", "file_path": "auth.py", "format": "json"},
2e96458… lmata 276 )
2e96458… lmata 277 data = json.loads(result[0]["text"])
2e96458… lmata 278 assert data["target"]["name"] == "cls_target"
2e96458… lmata 279
2e96458… lmata 280
2e96458… lmata 281 # ── call_tool — search_symbols ───────────────────────────────────────────────
2e96458… lmata 282
2e96458… lmata 283 class TestCallToolSearchSymbols:
2e96458… lmata 284 def setup_method(self):
2e96458… lmata 285 self.fx = _ServerFixture()
2e96458… lmata 286
2e96458… lmata 287 @pytest.mark.asyncio
2e96458… lmata 288 async def test_returns_no_results_message_on_empty(self):
2e96458… lmata 289 result = await self.fx.call_tool_fn("search_symbols", {"query": "xyz"})
2e96458… lmata 290 assert result[0]["text"] == "No results."
2e96458… lmata 291
2e96458… lmata 292 @pytest.mark.asyncio
2e96458… lmata 293 async def test_formats_results_as_bullet_list(self):
2e96458… lmata 294 hit = ContextNode(name="do_thing", type="Function", file_path="utils.py", line_start=10)
2e96458… lmata 295 self.fx.loader.search.return_value = [hit]
2e96458… lmata 296 result = await self.fx.call_tool_fn("search_symbols", {"query": "do"})
2e96458… lmata 297 text = result[0]["text"]
2e96458… lmata 298 assert "do_thing" in text
2e96458… lmata 299 assert "utils.py" in text
2e96458… lmata 300 assert "Function" in text
2e96458… lmata 301
2e96458… lmata 302 @pytest.mark.asyncio
2e96458… lmata 303 async def test_passes_limit(self):
2e96458… lmata 304 await self.fx.call_tool_fn("search_symbols", {"query": "foo", "limit": 5})
2e96458… lmata 305 self.fx.loader.search.assert_called_once_with("foo", limit=5)
2e96458… lmata 306
2e96458… lmata 307 @pytest.mark.asyncio
2e96458… lmata 308 async def test_limit_defaults_to_twenty(self):
2e96458… lmata 309 await self.fx.call_tool_fn("search_symbols", {"query": "foo"})
2e96458… lmata 310 self.fx.loader.search.assert_called_once_with("foo", limit=20)
2e96458… lmata 311
2e96458… lmata 312
2e96458… lmata 313 # ── call_tool — query_graph ───────────────────────────────────────────────────
2e96458… lmata 314
2e96458… lmata 315 class TestCallToolQueryGraph:
2e96458… lmata 316 def setup_method(self):
2e96458… lmata 317 self.fx = _ServerFixture()
2e96458… lmata 318
2e96458… lmata 319 @pytest.mark.asyncio
2e96458… lmata 320 async def test_executes_cypher_and_returns_json(self):
2e96458… lmata 321 self.fx.store.query.return_value = MagicMock(result_set=[["node_a"], ["node_b"]])
b3b8664… lmata 322 result = await self.fx.call_tool_fn("query_graph", {"cypher": "MATCH (n) RETURN n LIMIT 10"})
b3b8664… lmata 323 self.fx.store.query.assert_called_once_with("MATCH (n) RETURN n LIMIT 10")
2e96458… lmata 324 data = json.loads(result[0]["text"])
2e96458… lmata 325 assert len(data) == 2
2e96458… lmata 326
2e96458… lmata 327 @pytest.mark.asyncio
2e96458… lmata 328 async def test_empty_result_set(self):
2e96458… lmata 329 self.fx.store.query.return_value = MagicMock(result_set=[])
b3b8664… lmata 330 result = await self.fx.call_tool_fn("query_graph", {"cypher": "MATCH (n) RETURN n LIMIT 10"})
2e96458… lmata 331 data = json.loads(result[0]["text"])
2e96458… lmata 332 assert data == []
2e96458… lmata 333
2e96458… lmata 334
2e96458… lmata 335 # ── call_tool — graph_stats ───────────────────────────────────────────────────
2e96458… lmata 336
2e96458… lmata 337 class TestCallToolGraphStats:
2e96458… lmata 338 def setup_method(self):
2e96458… lmata 339 self.fx = _ServerFixture()
2e96458… lmata 340
2e96458… lmata 341 @pytest.mark.asyncio
2e96458… lmata 342 async def test_returns_node_and_edge_counts(self):
2e96458… lmata 343 self.fx.store.node_count.return_value = 42
2e96458… lmata 344 self.fx.store.edge_count.return_value = 17
2e96458… lmata 345 result = await self.fx.call_tool_fn("graph_stats", {})
2e96458… lmata 346 data = json.loads(result[0]["text"])
2e96458… lmata 347 assert data["nodes"] == 42
2e96458… lmata 348 assert data["edges"] == 17
ece88ca… lmata 349
ece88ca… lmata 350
ece88ca… lmata 351 # ── call_tool — unknown tool ──────────────────────────────────────────────────
ece88ca… lmata 352
ece88ca… lmata 353 # ── call_tool — get_rationale ────────────────────────────────────────────────
ece88ca… lmata 354
ece88ca… lmata 355 class TestCallToolGetRationale:
ece88ca… lmata 356 def setup_method(self):
ece88ca… lmata 357 self.fx = _ServerFixture()
ece88ca… lmata 358
ece88ca… lmata 359 @pytest.mark.asyncio
ece88ca… lmata 360 async def test_returns_markdown_by_default(self):
ece88ca… lmata 361 result = await self.fx.call_tool_fn("get_rationale", {"name": "Use FalkorDB"})
ece88ca… lmata 362 self.fx.loader.load_decision.assert_called_once_with("Use FalkorDB")
ece88ca… lmata 363 assert "decision_target" in result[0]["text"]
ece88ca… lmata 364
ece88ca… lmata 365 @pytest.mark.asyncio
ece88ca… lmata 366 async def test_returns_json_when_requested(self):
ece88ca… lmata 367 result = await self.fx.call_tool_fn(
ece88ca… lmata 368 "get_rationale", {"name": "Use FalkorDB", "format": "json"}
ece88ca… lmata 369 )
ece88ca… lmata 370 data = json.loads(result[0]["text"])
ece88ca… lmata 371 assert data["target"]["name"] == "decision_target"
ece88ca… lmata 372
ece88ca… lmata 373
ece88ca… lmata 374 # ── call_tool — find_owners ──────────────────────────────────────────────────
ece88ca… lmata 375
ece88ca… lmata 376 class TestCallToolFindOwners:
ece88ca… lmata 377 def setup_method(self):
ece88ca… lmata 378 self.fx = _ServerFixture()
ece88ca… lmata 379
ece88ca… lmata 380 @pytest.mark.asyncio
ece88ca… lmata 381 async def test_returns_no_owners_message(self):
ece88ca… lmata 382 result = await self.fx.call_tool_fn("find_owners", {"name": "AuthService"})
ece88ca… lmata 383 assert result[0]["text"] == "No owners found."
ece88ca… lmata 384
ece88ca… lmata 385 @pytest.mark.asyncio
ece88ca… lmata 386 async def test_formats_owners(self):
ece88ca… lmata 387 owner = ContextNode(name="Alice", type="Person", description="role=lead, team=auth")
ece88ca… lmata 388 self.fx.loader.find_owners.return_value = [owner]
ece88ca… lmata 389 result = await self.fx.call_tool_fn("find_owners", {"name": "AuthService"})
ece88ca… lmata 390 assert "Alice" in result[0]["text"]
ece88ca… lmata 391 assert "role=lead" in result[0]["text"]
ece88ca… lmata 392
ece88ca… lmata 393 @pytest.mark.asyncio
ece88ca… lmata 394 async def test_passes_file_path(self):
ece88ca… lmata 395 await self.fx.call_tool_fn(
ece88ca… lmata 396 "find_owners", {"name": "AuthService", "file_path": "auth.py"}
ece88ca… lmata 397 )
ece88ca… lmata 398 self.fx.loader.find_owners.assert_called_once_with("AuthService", file_path="auth.py")
ece88ca… lmata 399
ece88ca… lmata 400
ece88ca… lmata 401 # ── call_tool — search_knowledge ─────────────────────────────────────────────
ece88ca… lmata 402
ece88ca… lmata 403 class TestCallToolSearchKnowledge:
ece88ca… lmata 404 def setup_method(self):
ece88ca… lmata 405 self.fx = _ServerFixture()
ece88ca… lmata 406
ece88ca… lmata 407 @pytest.mark.asyncio
ece88ca… lmata 408 async def test_returns_no_results_message(self):
ece88ca… lmata 409 result = await self.fx.call_tool_fn("search_knowledge", {"query": "xyz"})
ece88ca… lmata 410 assert result[0]["text"] == "No results."
ece88ca… lmata 411
ece88ca… lmata 412 @pytest.mark.asyncio
ece88ca… lmata 413 async def test_formats_results(self):
ece88ca… lmata 414 hit = ContextNode(name="JWT", type="Concept", description="Stateless auth token")
ece88ca… lmata 415 self.fx.loader.search_knowledge.return_value = [hit]
ece88ca… lmata 416 result = await self.fx.call_tool_fn("search_knowledge", {"query": "JWT"})
ece88ca… lmata 417 assert "JWT" in result[0]["text"]
ece88ca… lmata 418 assert "Concept" in result[0]["text"]
ece88ca… lmata 419
ece88ca… lmata 420 @pytest.mark.asyncio
ece88ca… lmata 421 async def test_passes_limit(self):
ece88ca… lmata 422 await self.fx.call_tool_fn("search_knowledge", {"query": "auth", "limit": 5})
ece88ca… lmata 423 self.fx.loader.search_knowledge.assert_called_once_with("auth", limit=5)
ece88ca… lmata 424
ece88ca… lmata 425 @pytest.mark.asyncio
ece88ca… lmata 426 async def test_limit_defaults_to_twenty(self):
ece88ca… lmata 427 await self.fx.call_tool_fn("search_knowledge", {"query": "auth"})
ece88ca… lmata 428 self.fx.loader.search_knowledge.assert_called_once_with("auth", limit=20)
2e96458… lmata 429
2e96458… lmata 430
2e96458… lmata 431 # ── call_tool — unknown tool ──────────────────────────────────────────────────
2e96458… lmata 432
2e96458… lmata 433 class TestCallToolUnknown:
2e96458… lmata 434 def setup_method(self):
2e96458… lmata 435 self.fx = _ServerFixture()
2e96458… lmata 436
2e96458… lmata 437 @pytest.mark.asyncio
2e96458… lmata 438 async def test_returns_unknown_tool_message(self):
2e96458… lmata 439 result = await self.fx.call_tool_fn("nonexistent_tool", {})
2e96458… lmata 440 assert "Unknown tool" in result[0]["text"]
2e96458… lmata 441 assert "nonexistent_tool" in result[0]["text"]

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button