Navegador

test: strengthen MCP, parser call extraction, and ingestion tests MCP server tests rewritten to actually invoke all 7 async tool handlers and assert on output content (not just that the function exists). server.py coverage: 33% → 100%. Parser _extract_calls tests fixed: byte offsets now match the source buffer so callee name is verified, not just edge count. Applies to Go, Rust, Java, TypeScript. TestGetParser now tests the actual lazy-import path for all 5 languages (Python, TypeScript, Go, Rust, Java) and asserts the correct class is instantiated. TestLanguageMap extended with Go/Rust/Java extension assertions.

lmata 2026-03-22 23:56 trunk
Commit 2e964587f650db828e2396e409c3d868c56b9c6ccb1dfe552c93ef039aeec30f
--- tests/test_go_parser.py
+++ tests/test_go_parser.py
@@ -178,11 +178,11 @@
178178
179179
class TestGoExtractCalls:
180180
def test_extracts_call(self):
181181
parser = _make_parser()
182182
store = _make_store()
183
- source = b"func foo() { bar() }"
183
+ source = b"bar"
184184
callee = _text_node(b"bar")
185185
call_node = MockNode("call_expression")
186186
call_node.set_field("function", callee)
187187
body = MockNode("block", children=[call_node])
188188
fn_node = MockNode("function_declaration")
@@ -189,11 +189,12 @@
189189
fn_node.set_field("body", body)
190190
stats = {"functions": 0, "classes": 0, "edges": 0}
191191
parser._extract_calls(fn_node, source, "main.go", "foo",
192192
NodeLabel.Function, store, stats)
193193
assert stats["edges"] == 1
194
- store.create_edge.assert_called_once()
194
+ edge_call = store.create_edge.call_args[0]
195
+ assert edge_call[4]["name"] == "bar"
195196
196197
def test_no_calls_in_empty_body(self):
197198
parser = _make_parser()
198199
store = _make_store()
199200
fn_node = MockNode("function_declaration")
200201
--- tests/test_go_parser.py
+++ tests/test_go_parser.py
@@ -178,11 +178,11 @@
178
179 class TestGoExtractCalls:
180 def test_extracts_call(self):
181 parser = _make_parser()
182 store = _make_store()
183 source = b"func foo() { bar() }"
184 callee = _text_node(b"bar")
185 call_node = MockNode("call_expression")
186 call_node.set_field("function", callee)
187 body = MockNode("block", children=[call_node])
188 fn_node = MockNode("function_declaration")
@@ -189,11 +189,12 @@
189 fn_node.set_field("body", body)
190 stats = {"functions": 0, "classes": 0, "edges": 0}
191 parser._extract_calls(fn_node, source, "main.go", "foo",
192 NodeLabel.Function, store, stats)
193 assert stats["edges"] == 1
194 store.create_edge.assert_called_once()
 
195
196 def test_no_calls_in_empty_body(self):
197 parser = _make_parser()
198 store = _make_store()
199 fn_node = MockNode("function_declaration")
200
--- tests/test_go_parser.py
+++ tests/test_go_parser.py
@@ -178,11 +178,11 @@
178
179 class TestGoExtractCalls:
180 def test_extracts_call(self):
181 parser = _make_parser()
182 store = _make_store()
183 source = b"bar"
184 callee = _text_node(b"bar")
185 call_node = MockNode("call_expression")
186 call_node.set_field("function", callee)
187 body = MockNode("block", children=[call_node])
188 fn_node = MockNode("function_declaration")
@@ -189,11 +189,12 @@
189 fn_node.set_field("body", body)
190 stats = {"functions": 0, "classes": 0, "edges": 0}
191 parser._extract_calls(fn_node, source, "main.go", "foo",
192 NodeLabel.Function, store, stats)
193 assert stats["edges"] == 1
194 edge_call = store.create_edge.call_args[0]
195 assert edge_call[4]["name"] == "bar"
196
197 def test_no_calls_in_empty_body(self):
198 parser = _make_parser()
199 store = _make_store()
200 fn_node = MockNode("function_declaration")
201
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -28,10 +28,15 @@
2828
2929
def test_javascript_extensions(self):
3030
assert LANGUAGE_MAP[".js"] == "javascript"
3131
assert LANGUAGE_MAP[".jsx"] == "javascript"
3232
33
+ def test_go_rust_java_extensions(self):
34
+ assert LANGUAGE_MAP[".go"] == "go"
35
+ assert LANGUAGE_MAP[".rs"] == "rust"
36
+ assert LANGUAGE_MAP[".java"] == "java"
37
+
3338
def test_no_entry_for_unknown(self):
3439
assert ".rb" not in LANGUAGE_MAP
3540
assert ".php" not in LANGUAGE_MAP
3641
3742
@@ -256,27 +261,64 @@
256261
store = _make_store()
257262
ingester = RepoIngester(store)
258263
with pytest.raises(ValueError, match="Unsupported language"):
259264
ingester._get_parser("ruby")
260265
261
- def test_creates_python_parser(self):
266
+ def test_creates_python_parser_via_lazy_import(self):
262267
store = _make_store()
263268
ingester = RepoIngester(store)
264269
mock_py_parser = MagicMock()
265270
mock_py_class = MagicMock(return_value=mock_py_parser)
266
-
267
- with patch("navegador.ingestion.parser.PythonParser", mock_py_class, create=True):
268
- with patch.dict("sys.modules", {
269
- "navegador.ingestion.python": MagicMock(PythonParser=mock_py_class)
270
- }):
271
- # Just verify caching works by pre-populating
272
- ingester._parsers["python"] = mock_py_parser
273
- result = ingester._get_parser("python")
274
- assert result is mock_py_parser
275
-
276
- def test_creates_typescript_parser(self):
271
+ with patch.dict("sys.modules", {
272
+ "navegador.ingestion.python": MagicMock(PythonParser=mock_py_class)
273
+ }):
274
+ result = ingester._get_parser("python")
275
+ assert result is mock_py_parser
276
+ mock_py_class.assert_called_once_with()
277
+
278
+ def test_creates_typescript_parser_via_lazy_import(self):
277279
store = _make_store()
278280
ingester = RepoIngester(store)
279281
mock_ts_parser = MagicMock()
280
- ingester._parsers["typescript"] = mock_ts_parser
281
- result = ingester._get_parser("typescript")
282
+ mock_ts_class = MagicMock(return_value=mock_ts_parser)
283
+ with patch.dict("sys.modules", {
284
+ "navegador.ingestion.typescript": MagicMock(TypeScriptParser=mock_ts_class)
285
+ }):
286
+ result = ingester._get_parser("typescript")
282287
assert result is mock_ts_parser
288
+ mock_ts_class.assert_called_once_with("typescript")
289
+
290
+ def test_creates_go_parser_via_lazy_import(self):
291
+ store = _make_store()
292
+ ingester = RepoIngester(store)
293
+ mock_go_parser = MagicMock()
294
+ mock_go_class = MagicMock(return_value=mock_go_parser)
295
+ with patch.dict("sys.modules", {
296
+ "navegador.ingestion.go": MagicMock(GoParser=mock_go_class)
297
+ }):
298
+ result = ingester._get_parser("go")
299
+ assert result is mock_go_parser
300
+ mock_go_class.assert_called_once_with()
301
+
302
+ def test_creates_rust_parser_via_lazy_import(self):
303
+ store = _make_store()
304
+ ingester = RepoIngester(store)
305
+ mock_rust_parser = MagicMock()
306
+ mock_rust_class = MagicMock(return_value=mock_rust_parser)
307
+ with patch.dict("sys.modules", {
308
+ "navegador.ingestion.rust": MagicMock(RustParser=mock_rust_class)
309
+ }):
310
+ result = ingester._get_parser("rust")
311
+ assert result is mock_rust_parser
312
+ mock_rust_class.assert_called_once_with()
313
+
314
+ def test_creates_java_parser_via_lazy_import(self):
315
+ store = _make_store()
316
+ ingester = RepoIngester(store)
317
+ mock_java_parser = MagicMock()
318
+ mock_java_class = MagicMock(return_value=mock_java_parser)
319
+ with patch.dict("sys.modules", {
320
+ "navegador.ingestion.java": MagicMock(JavaParser=mock_java_class)
321
+ }):
322
+ result = ingester._get_parser("java")
323
+ assert result is mock_java_parser
324
+ mock_java_class.assert_called_once_with()
283325
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -28,10 +28,15 @@
28
29 def test_javascript_extensions(self):
30 assert LANGUAGE_MAP[".js"] == "javascript"
31 assert LANGUAGE_MAP[".jsx"] == "javascript"
32
 
 
 
 
 
33 def test_no_entry_for_unknown(self):
34 assert ".rb" not in LANGUAGE_MAP
35 assert ".php" not in LANGUAGE_MAP
36
37
@@ -256,27 +261,64 @@
256 store = _make_store()
257 ingester = RepoIngester(store)
258 with pytest.raises(ValueError, match="Unsupported language"):
259 ingester._get_parser("ruby")
260
261 def test_creates_python_parser(self):
262 store = _make_store()
263 ingester = RepoIngester(store)
264 mock_py_parser = MagicMock()
265 mock_py_class = MagicMock(return_value=mock_py_parser)
266
267 with patch("navegador.ingestion.parser.PythonParser", mock_py_class, create=True):
268 with patch.dict("sys.modules", {
269 "navegador.ingestion.python": MagicMock(PythonParser=mock_py_class)
270 }):
271 # Just verify caching works by pre-populating
272 ingester._parsers["python"] = mock_py_parser
273 result = ingester._get_parser("python")
274 assert result is mock_py_parser
275
276 def test_creates_typescript_parser(self):
277 store = _make_store()
278 ingester = RepoIngester(store)
279 mock_ts_parser = MagicMock()
280 ingester._parsers["typescript"] = mock_ts_parser
281 result = ingester._get_parser("typescript")
 
 
 
282 assert result is mock_ts_parser
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -28,10 +28,15 @@
28
29 def test_javascript_extensions(self):
30 assert LANGUAGE_MAP[".js"] == "javascript"
31 assert LANGUAGE_MAP[".jsx"] == "javascript"
32
33 def test_go_rust_java_extensions(self):
34 assert LANGUAGE_MAP[".go"] == "go"
35 assert LANGUAGE_MAP[".rs"] == "rust"
36 assert LANGUAGE_MAP[".java"] == "java"
37
38 def test_no_entry_for_unknown(self):
39 assert ".rb" not in LANGUAGE_MAP
40 assert ".php" not in LANGUAGE_MAP
41
42
@@ -256,27 +261,64 @@
261 store = _make_store()
262 ingester = RepoIngester(store)
263 with pytest.raises(ValueError, match="Unsupported language"):
264 ingester._get_parser("ruby")
265
266 def test_creates_python_parser_via_lazy_import(self):
267 store = _make_store()
268 ingester = RepoIngester(store)
269 mock_py_parser = MagicMock()
270 mock_py_class = MagicMock(return_value=mock_py_parser)
271 with patch.dict("sys.modules", {
272 "navegador.ingestion.python": MagicMock(PythonParser=mock_py_class)
273 }):
274 result = ingester._get_parser("python")
275 assert result is mock_py_parser
276 mock_py_class.assert_called_once_with()
277
278 def test_creates_typescript_parser_via_lazy_import(self):
 
 
 
279 store = _make_store()
280 ingester = RepoIngester(store)
281 mock_ts_parser = MagicMock()
282 mock_ts_class = MagicMock(return_value=mock_ts_parser)
283 with patch.dict("sys.modules", {
284 "navegador.ingestion.typescript": MagicMock(TypeScriptParser=mock_ts_class)
285 }):
286 result = ingester._get_parser("typescript")
287 assert result is mock_ts_parser
288 mock_ts_class.assert_called_once_with("typescript")
289
290 def test_creates_go_parser_via_lazy_import(self):
291 store = _make_store()
292 ingester = RepoIngester(store)
293 mock_go_parser = MagicMock()
294 mock_go_class = MagicMock(return_value=mock_go_parser)
295 with patch.dict("sys.modules", {
296 "navegador.ingestion.go": MagicMock(GoParser=mock_go_class)
297 }):
298 result = ingester._get_parser("go")
299 assert result is mock_go_parser
300 mock_go_class.assert_called_once_with()
301
302 def test_creates_rust_parser_via_lazy_import(self):
303 store = _make_store()
304 ingester = RepoIngester(store)
305 mock_rust_parser = MagicMock()
306 mock_rust_class = MagicMock(return_value=mock_rust_parser)
307 with patch.dict("sys.modules", {
308 "navegador.ingestion.rust": MagicMock(RustParser=mock_rust_class)
309 }):
310 result = ingester._get_parser("rust")
311 assert result is mock_rust_parser
312 mock_rust_class.assert_called_once_with()
313
314 def test_creates_java_parser_via_lazy_import(self):
315 store = _make_store()
316 ingester = RepoIngester(store)
317 mock_java_parser = MagicMock()
318 mock_java_class = MagicMock(return_value=mock_java_parser)
319 with patch.dict("sys.modules", {
320 "navegador.ingestion.java": MagicMock(JavaParser=mock_java_class)
321 }):
322 result = ingester._get_parser("java")
323 assert result is mock_java_parser
324 mock_java_class.assert_called_once_with()
325
--- tests/test_java_parser.py
+++ tests/test_java_parser.py
@@ -230,21 +230,22 @@
230230
231231
class TestJavaExtractCalls:
232232
def test_extracts_method_invocation(self):
233233
parser = _make_parser()
234234
store = _make_store()
235
- source = b"void foo() { bar(); }"
235
+ source = b"bar"
236236
callee = _text_node(b"bar")
237237
invocation = MockNode("method_invocation")
238238
invocation.set_field("name", callee)
239239
body = MockNode("block", children=[invocation])
240240
node = MockNode("method_declaration")
241241
node.set_field("body", body)
242242
stats = {"functions": 0, "classes": 0, "edges": 0}
243243
parser._extract_calls(node, source, "Foo.java", "foo", store, stats)
244244
assert stats["edges"] == 1
245
- store.create_edge.assert_called_once()
245
+ edge_call = store.create_edge.call_args[0]
246
+ assert edge_call[4]["name"] == "bar"
246247
247248
def test_no_calls_in_empty_body(self):
248249
parser = _make_parser()
249250
store = _make_store()
250251
node = MockNode("method_declaration")
251252
--- tests/test_java_parser.py
+++ tests/test_java_parser.py
@@ -230,21 +230,22 @@
230
231 class TestJavaExtractCalls:
232 def test_extracts_method_invocation(self):
233 parser = _make_parser()
234 store = _make_store()
235 source = b"void foo() { bar(); }"
236 callee = _text_node(b"bar")
237 invocation = MockNode("method_invocation")
238 invocation.set_field("name", callee)
239 body = MockNode("block", children=[invocation])
240 node = MockNode("method_declaration")
241 node.set_field("body", body)
242 stats = {"functions": 0, "classes": 0, "edges": 0}
243 parser._extract_calls(node, source, "Foo.java", "foo", store, stats)
244 assert stats["edges"] == 1
245 store.create_edge.assert_called_once()
 
246
247 def test_no_calls_in_empty_body(self):
248 parser = _make_parser()
249 store = _make_store()
250 node = MockNode("method_declaration")
251
--- tests/test_java_parser.py
+++ tests/test_java_parser.py
@@ -230,21 +230,22 @@
230
231 class TestJavaExtractCalls:
232 def test_extracts_method_invocation(self):
233 parser = _make_parser()
234 store = _make_store()
235 source = b"bar"
236 callee = _text_node(b"bar")
237 invocation = MockNode("method_invocation")
238 invocation.set_field("name", callee)
239 body = MockNode("block", children=[invocation])
240 node = MockNode("method_declaration")
241 node.set_field("body", body)
242 stats = {"functions": 0, "classes": 0, "edges": 0}
243 parser._extract_calls(node, source, "Foo.java", "foo", store, stats)
244 assert stats["edges"] == 1
245 edge_call = store.create_edge.call_args[0]
246 assert edge_call[4]["name"] == "bar"
247
248 def test_no_calls_in_empty_body(self):
249 parser = _make_parser()
250 store = _make_store()
251 node = MockNode("method_declaration")
252
--- tests/test_mcp_server.py
+++ tests/test_mcp_server.py
@@ -1,13 +1,15 @@
1
-"""Tests for navegador.mcp.server — create_mcp_server and tool handlers."""
1
+"""Tests for navegador.mcp.server — create_mcp_server and all tool handlers."""
22
3
+import json
34
from unittest.mock import MagicMock, patch
45
56
import pytest
67
78
from navegador.context.loader import ContextBundle, ContextNode
89
10
+# ── Helpers ───────────────────────────────────────────────────────────────────
911
1012
def _node(name="foo", type_="Function", file_path="app.py"):
1113
return ContextNode(name=name, type=type_, file_path=file_path)
1214
1315
@@ -21,104 +23,332 @@
2123
store.node_count.return_value = 5
2224
store.edge_count.return_value = 3
2325
return store
2426
2527
26
-def _mock_loader(store=None):
27
- from navegador.context import ContextLoader
28
- loader = MagicMock(spec=ContextLoader)
29
- loader.store = store or _mock_store()
30
- loader.load_file.return_value = _bundle()
31
- loader.load_function.return_value = _bundle()
32
- loader.load_class.return_value = _bundle()
33
- loader.search.return_value = []
34
- return loader
35
-
36
-
37
-# ── create_mcp_server — import error ─────────────────────────────────────────
38
-
39
-class TestCreateMcpServerImport:
40
- def test_raises_import_error_if_mcp_not_installed(self):
41
- with patch.dict("sys.modules", {"mcp": None, "mcp.server": None, "mcp.types": None}):
42
- from navegador.mcp.server import create_mcp_server
43
- with pytest.raises(ImportError, match="mcp"):
44
- create_mcp_server(lambda: _mock_store())
45
-
46
-
47
-# ── create_mcp_server — happy path ────────────────────────────────────────────
48
-
49
-class TestCreateMcpServer:
50
- def _make_server(self, loader=None):
51
- """Build a server with mocked mcp module."""
52
- mock_loader = loader or _mock_loader()
53
- mock_server = MagicMock()
54
-
55
- # Capture the decorated functions
56
- list_tools_fn = None
57
- call_tool_fn = None
28
+class _ServerFixture:
29
+ """
30
+ Builds a navegador MCP server with mocked mcp module and captures the
31
+ list_tools and call_tool async handlers so they can be invoked directly.
32
+ """
33
+
34
+ def __init__(self, loader=None):
35
+ self.store = _mock_store()
36
+ self.loader = loader or self._default_loader()
37
+ self.list_tools_fn = None
38
+ self.call_tool_fn = None
39
+ self._build()
40
+
41
+ def _default_loader(self):
42
+ from navegador.context import ContextLoader
43
+ loader = MagicMock(spec=ContextLoader)
44
+ loader.store = self.store
45
+ loader.load_file.return_value = _bundle("file_target")
46
+ loader.load_function.return_value = _bundle("fn_target")
47
+ loader.load_class.return_value = _bundle("cls_target")
48
+ loader.search.return_value = []
49
+ return loader
50
+
51
+ def _build(self):
52
+ list_holder = {}
53
+ call_holder = {}
5854
5955
def list_tools_decorator():
6056
def decorator(fn):
61
- nonlocal list_tools_fn
62
- list_tools_fn = fn
57
+ list_holder["fn"] = fn
6358
return fn
6459
return decorator
6560
6661
def call_tool_decorator():
6762
def decorator(fn):
68
- nonlocal call_tool_fn
69
- call_tool_fn = fn
70
- return fn
71
- return decorator
72
-
73
- mock_server.list_tools = list_tools_decorator
74
- mock_server.call_tool = call_tool_decorator
75
-
76
- mock_mcp_server_module = MagicMock()
77
- mock_mcp_server_module.Server.return_value = mock_server
78
- mock_mcp_types_module = MagicMock()
79
- mock_mcp_types_module.Tool = MagicMock
80
- mock_mcp_types_module.TextContent = MagicMock
81
-
82
- with patch.dict("sys.modules", {
83
- "mcp": MagicMock(),
84
- "mcp.server": mock_mcp_server_module,
85
- "mcp.types": mock_mcp_types_module,
86
- }), patch("navegador.context.ContextLoader", return_value=mock_loader):
87
- from importlib import reload
88
-
89
- import navegador.mcp.server as srv
90
- reload(srv)
91
- srv.create_mcp_server(lambda: mock_loader.store)
92
-
93
- return list_tools_fn, call_tool_fn, mock_loader
94
-
95
- def test_returns_server(self):
96
- mock_server = MagicMock()
97
- mock_server.list_tools = lambda: lambda f: f
98
- mock_server.call_tool = lambda: lambda f: f
99
-
100
- mock_mcp_server_module = MagicMock()
101
- mock_mcp_server_module.Server.return_value = mock_server
102
-
103
- with patch.dict("sys.modules", {
104
- "mcp": MagicMock(),
105
- "mcp.server": mock_mcp_server_module,
106
- "mcp.types": MagicMock(),
107
- }):
108
- from importlib import reload
109
-
110
- import navegador.mcp.server as srv
111
- reload(srv)
112
- result = srv.create_mcp_server(lambda: _mock_store())
113
- assert result is mock_server
114
-
115
- def test_raises_if_mcp_not_available(self):
116
- with patch.dict("sys.modules", {
117
- "mcp": None, "mcp.server": None, "mcp.types": None,
118
- }):
119
- from importlib import reload
120
-
121
- import navegador.mcp.server as srv
122
- reload(srv)
123
- with pytest.raises(ImportError):
124
- srv.create_mcp_server(lambda: _mock_store())
63
+ call_holder["fn"] = fn
64
+ return fn
65
+ return decorator
66
+
67
+ mock_server = MagicMock()
68
+ mock_server.list_tools = list_tools_decorator
69
+ mock_server.call_tool = call_tool_decorator
70
+
71
+ mock_mcp_server = MagicMock()
72
+ mock_mcp_server.Server.return_value = mock_server
73
+
74
+ mock_mcp_types = MagicMock()
75
+ mock_mcp_types.Tool = dict # Tool(...) → dict so we can inspect fields
76
+ mock_mcp_types.TextContent = dict # TextContent(type=..., text=...) → dict
77
+
78
+ with patch.dict("sys.modules", {
79
+ "mcp": MagicMock(),
80
+ "mcp.server": mock_mcp_server,
81
+ "mcp.types": mock_mcp_types,
82
+ }), patch("navegador.context.ContextLoader", return_value=self.loader):
83
+ from importlib import reload
84
+
85
+ import navegador.mcp.server as srv
86
+ reload(srv)
87
+ self.server = srv.create_mcp_server(lambda: self.store)
88
+
89
+ self.list_tools_fn = list_holder["fn"]
90
+ self.call_tool_fn = call_holder["fn"]
91
+
92
+
93
+# ── Import guard ──────────────────────────────────────────────────────────────
94
+
95
+class TestCreateMcpServerImport:
96
+ def test_raises_import_error_if_mcp_not_installed(self):
97
+ with patch.dict("sys.modules", {"mcp": None, "mcp.server": None, "mcp.types": None}):
98
+ from importlib import reload
99
+
100
+ import navegador.mcp.server as srv
101
+ reload(srv)
102
+ with pytest.raises(ImportError, match="mcp"):
103
+ srv.create_mcp_server(lambda: _mock_store())
104
+
105
+
106
+# ── list_tools ────────────────────────────────────────────────────────────────
107
+
108
+class TestListTools:
109
+ def setup_method(self):
110
+ self.fx = _ServerFixture()
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_returns_seven_tools(self):
114
+ tools = await self.fx.list_tools_fn()
115
+ assert len(tools) == 7
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_tool_names(self):
119
+ tools = await self.fx.list_tools_fn()
120
+ names = {t["name"] for t in tools}
121
+ assert names == {
122
+ "ingest_repo",
123
+ "load_file_context",
124
+ "load_function_context",
125
+ "load_class_context",
126
+ "search_symbols",
127
+ "query_graph",
128
+ "graph_stats",
129
+ }
130
+
131
+ @pytest.mark.asyncio
132
+ async def test_ingest_repo_requires_path(self):
133
+ tools = await self.fx.list_tools_fn()
134
+ t = next(t for t in tools if t["name"] == "ingest_repo")
135
+ assert "path" in t["inputSchema"]["required"]
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_load_function_context_requires_name_and_file_path(self):
139
+ tools = await self.fx.list_tools_fn()
140
+ t = next(t for t in tools if t["name"] == "load_function_context")
141
+ assert "name" in t["inputSchema"]["required"]
142
+ assert "file_path" in t["inputSchema"]["required"]
143
+
144
+
145
+# ── call_tool — ingest_repo ───────────────────────────────────────────────────
146
+
147
+class TestCallToolIngestRepo:
148
+ def setup_method(self):
149
+ self.fx = _ServerFixture()
150
+
151
+ @pytest.mark.asyncio
152
+ async def test_calls_ingester_and_returns_json(self):
153
+ mock_ingester = MagicMock()
154
+ mock_ingester.ingest.return_value = {"files": 3, "functions": 10, "classes": 2, "edges": 15}
155
+
156
+ with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
157
+ result = await self.fx.call_tool_fn("ingest_repo", {"path": "/some/repo"})
158
+
159
+ assert len(result) == 1
160
+ data = json.loads(result[0]["text"])
161
+ assert data["files"] == 3
162
+ assert data["functions"] == 10
163
+
164
+ @pytest.mark.asyncio
165
+ async def test_passes_clear_flag(self):
166
+ mock_ingester = MagicMock()
167
+ mock_ingester.ingest.return_value = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
168
+
169
+ with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
170
+ await self.fx.call_tool_fn("ingest_repo", {"path": "/repo", "clear": True})
171
+
172
+ mock_ingester.ingest.assert_called_once_with("/repo", clear=True)
173
+
174
+ @pytest.mark.asyncio
175
+ async def test_clear_defaults_to_false(self):
176
+ mock_ingester = MagicMock()
177
+ mock_ingester.ingest.return_value = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
178
+
179
+ with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
180
+ await self.fx.call_tool_fn("ingest_repo", {"path": "/repo"})
181
+
182
+ mock_ingester.ingest.assert_called_once_with("/repo", clear=False)
183
+
184
+
185
+# ── call_tool — load_file_context ─────────────────────────────────────────────
186
+
187
+class TestCallToolLoadFileContext:
188
+ def setup_method(self):
189
+ self.fx = _ServerFixture()
190
+
191
+ @pytest.mark.asyncio
192
+ async def test_returns_markdown_by_default(self):
193
+ result = await self.fx.call_tool_fn("load_file_context", {"file_path": "src/main.py"})
194
+ self.fx.loader.load_file.assert_called_once_with("src/main.py")
195
+ assert "file_target" in result[0]["text"]
196
+
197
+ @pytest.mark.asyncio
198
+ async def test_returns_json_when_requested(self):
199
+ result = await self.fx.call_tool_fn(
200
+ "load_file_context", {"file_path": "src/main.py", "format": "json"}
201
+ )
202
+ data = json.loads(result[0]["text"])
203
+ assert data["target"]["name"] == "file_target"
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_markdown_format_explicit(self):
207
+ result = await self.fx.call_tool_fn(
208
+ "load_file_context", {"file_path": "src/main.py", "format": "markdown"}
209
+ )
210
+ assert "file_target" in result[0]["text"]
211
+
212
+
213
+# ── call_tool — load_function_context ────────────────────────────────────────
214
+
215
+class TestCallToolLoadFunctionContext:
216
+ def setup_method(self):
217
+ self.fx = _ServerFixture()
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_calls_loader_with_depth(self):
221
+ await self.fx.call_tool_fn(
222
+ "load_function_context",
223
+ {"name": "parse", "file_path": "parser.py", "depth": 3},
224
+ )
225
+ self.fx.loader.load_function.assert_called_once_with("parse", "parser.py", depth=3)
226
+
227
+ @pytest.mark.asyncio
228
+ async def test_depth_defaults_to_two(self):
229
+ await self.fx.call_tool_fn(
230
+ "load_function_context", {"name": "parse", "file_path": "parser.py"}
231
+ )
232
+ self.fx.loader.load_function.assert_called_once_with("parse", "parser.py", depth=2)
233
+
234
+ @pytest.mark.asyncio
235
+ async def test_returns_json_when_requested(self):
236
+ result = await self.fx.call_tool_fn(
237
+ "load_function_context",
238
+ {"name": "parse", "file_path": "parser.py", "format": "json"},
239
+ )
240
+ data = json.loads(result[0]["text"])
241
+ assert data["target"]["name"] == "fn_target"
242
+
243
+
244
+# ── call_tool — load_class_context ───────────────────────────────────────────
245
+
246
+class TestCallToolLoadClassContext:
247
+ def setup_method(self):
248
+ self.fx = _ServerFixture()
249
+
250
+ @pytest.mark.asyncio
251
+ async def test_calls_loader_with_name_and_file(self):
252
+ await self.fx.call_tool_fn(
253
+ "load_class_context", {"name": "AuthService", "file_path": "auth.py"}
254
+ )
255
+ self.fx.loader.load_class.assert_called_once_with("AuthService", "auth.py")
256
+
257
+ @pytest.mark.asyncio
258
+ async def test_returns_markdown_by_default(self):
259
+ result = await self.fx.call_tool_fn(
260
+ "load_class_context", {"name": "AuthService", "file_path": "auth.py"}
261
+ )
262
+ assert "cls_target" in result[0]["text"]
263
+
264
+ @pytest.mark.asyncio
265
+ async def test_returns_json_when_requested(self):
266
+ result = await self.fx.call_tool_fn(
267
+ "load_class_context",
268
+ {"name": "AuthService", "file_path": "auth.py", "format": "json"},
269
+ )
270
+ data = json.loads(result[0]["text"])
271
+ assert data["target"]["name"] == "cls_target"
272
+
273
+
274
+# ── call_tool — search_symbols ───────────────────────────────────────────────
275
+
276
+class TestCallToolSearchSymbols:
277
+ def setup_method(self):
278
+ self.fx = _ServerFixture()
279
+
280
+ @pytest.mark.asyncio
281
+ async def test_returns_no_results_message_on_empty(self):
282
+ result = await self.fx.call_tool_fn("search_symbols", {"query": "xyz"})
283
+ assert result[0]["text"] == "No results."
284
+
285
+ @pytest.mark.asyncio
286
+ async def test_formats_results_as_bullet_list(self):
287
+ hit = ContextNode(name="do_thing", type="Function", file_path="utils.py", line_start=10)
288
+ self.fx.loader.search.return_value = [hit]
289
+ result = await self.fx.call_tool_fn("search_symbols", {"query": "do"})
290
+ text = result[0]["text"]
291
+ assert "do_thing" in text
292
+ assert "utils.py" in text
293
+ assert "Function" in text
294
+
295
+ @pytest.mark.asyncio
296
+ async def test_passes_limit(self):
297
+ await self.fx.call_tool_fn("search_symbols", {"query": "foo", "limit": 5})
298
+ self.fx.loader.search.assert_called_once_with("foo", limit=5)
299
+
300
+ @pytest.mark.asyncio
301
+ async def test_limit_defaults_to_twenty(self):
302
+ await self.fx.call_tool_fn("search_symbols", {"query": "foo"})
303
+ self.fx.loader.search.assert_called_once_with("foo", limit=20)
304
+
305
+
306
+# ── call_tool — query_graph ───────────────────────────────────────────────────
307
+
308
+class TestCallToolQueryGraph:
309
+ def setup_method(self):
310
+ self.fx = _ServerFixture()
311
+
312
+ @pytest.mark.asyncio
313
+ async def test_executes_cypher_and_returns_json(self):
314
+ self.fx.store.query.return_value = MagicMock(result_set=[["node_a"], ["node_b"]])
315
+ result = await self.fx.call_tool_fn("query_graph", {"cypher": "MATCH (n) RETURN n"})
316
+ self.fx.store.query.assert_called_once_with("MATCH (n) RETURN n")
317
+ data = json.loads(result[0]["text"])
318
+ assert len(data) == 2
319
+
320
+ @pytest.mark.asyncio
321
+ async def test_empty_result_set(self):
322
+ self.fx.store.query.return_value = MagicMock(result_set=[])
323
+ result = await self.fx.call_tool_fn("query_graph", {"cypher": "MATCH (n) RETURN n"})
324
+ data = json.loads(result[0]["text"])
325
+ assert data == []
326
+
327
+
328
+# ── call_tool — graph_stats ───────────────────────────────────────────────────
329
+
330
+class TestCallToolGraphStats:
331
+ def setup_method(self):
332
+ self.fx = _ServerFixture()
333
+
334
+ @pytest.mark.asyncio
335
+ async def test_returns_node_and_edge_counts(self):
336
+ self.fx.store.node_count.return_value = 42
337
+ self.fx.store.edge_count.return_value = 17
338
+ result = await self.fx.call_tool_fn("graph_stats", {})
339
+ data = json.loads(result[0]["text"])
340
+ assert data["nodes"] == 42
341
+ assert data["edges"] == 17
342
+
343
+
344
+# ── call_tool — unknown tool ──────────────────────────────────────────────────
345
+
346
+class TestCallToolUnknown:
347
+ def setup_method(self):
348
+ self.fx = _ServerFixture()
349
+
350
+ @pytest.mark.asyncio
351
+ async def test_returns_unknown_tool_message(self):
352
+ result = await self.fx.call_tool_fn("nonexistent_tool", {})
353
+ assert "Unknown tool" in result[0]["text"]
354
+ assert "nonexistent_tool" in result[0]["text"]
125355
--- tests/test_mcp_server.py
+++ tests/test_mcp_server.py
@@ -1,13 +1,15 @@
1 """Tests for navegador.mcp.server — create_mcp_server and tool handlers."""
2
 
3 from unittest.mock import MagicMock, patch
4
5 import pytest
6
7 from navegador.context.loader import ContextBundle, ContextNode
8
 
9
10 def _node(name="foo", type_="Function", file_path="app.py"):
11 return ContextNode(name=name, type=type_, file_path=file_path)
12
13
@@ -21,104 +23,332 @@
21 store.node_count.return_value = 5
22 store.edge_count.return_value = 3
23 return store
24
25
26 def _mock_loader(store=None):
27 from navegador.context import ContextLoader
28 loader = MagicMock(spec=ContextLoader)
29 loader.store = store or _mock_store()
30 loader.load_file.return_value = _bundle()
31 loader.load_function.return_value = _bundle()
32 loader.load_class.return_value = _bundle()
33 loader.search.return_value = []
34 return loader
35
36
37 # ── create_mcp_server — import error ─────────────────────────────────────────
38
39 class TestCreateMcpServerImport:
40 def test_raises_import_error_if_mcp_not_installed(self):
41 with patch.dict("sys.modules", {"mcp": None, "mcp.server": None, "mcp.types": None}):
42 from navegador.mcp.server import create_mcp_server
43 with pytest.raises(ImportError, match="mcp"):
44 create_mcp_server(lambda: _mock_store())
45
46
47 # ── create_mcp_server — happy path ────────────────────────────────────────────
48
49 class TestCreateMcpServer:
50 def _make_server(self, loader=None):
51 """Build a server with mocked mcp module."""
52 mock_loader = loader or _mock_loader()
53 mock_server = MagicMock()
54
55 # Capture the decorated functions
56 list_tools_fn = None
57 call_tool_fn = None
58
59 def list_tools_decorator():
60 def decorator(fn):
61 nonlocal list_tools_fn
62 list_tools_fn = fn
63 return fn
64 return decorator
65
66 def call_tool_decorator():
67 def decorator(fn):
68 nonlocal call_tool_fn
69 call_tool_fn = fn
70 return fn
71 return decorator
72
73 mock_server.list_tools = list_tools_decorator
74 mock_server.call_tool = call_tool_decorator
75
76 mock_mcp_server_module = MagicMock()
77 mock_mcp_server_module.Server.return_value = mock_server
78 mock_mcp_types_module = MagicMock()
79 mock_mcp_types_module.Tool = MagicMock
80 mock_mcp_types_module.TextContent = MagicMock
81
82 with patch.dict("sys.modules", {
83 "mcp": MagicMock(),
84 "mcp.server": mock_mcp_server_module,
85 "mcp.types": mock_mcp_types_module,
86 }), patch("navegador.context.ContextLoader", return_value=mock_loader):
87 from importlib import reload
88
89 import navegador.mcp.server as srv
90 reload(srv)
91 srv.create_mcp_server(lambda: mock_loader.store)
92
93 return list_tools_fn, call_tool_fn, mock_loader
94
95 def test_returns_server(self):
96 mock_server = MagicMock()
97 mock_server.list_tools = lambda: lambda f: f
98 mock_server.call_tool = lambda: lambda f: f
99
100 mock_mcp_server_module = MagicMock()
101 mock_mcp_server_module.Server.return_value = mock_server
102
103 with patch.dict("sys.modules", {
104 "mcp": MagicMock(),
105 "mcp.server": mock_mcp_server_module,
106 "mcp.types": MagicMock(),
107 }):
108 from importlib import reload
109
110 import navegador.mcp.server as srv
111 reload(srv)
112 result = srv.create_mcp_server(lambda: _mock_store())
113 assert result is mock_server
114
115 def test_raises_if_mcp_not_available(self):
116 with patch.dict("sys.modules", {
117 "mcp": None, "mcp.server": None, "mcp.types": None,
118 }):
119 from importlib import reload
120
121 import navegador.mcp.server as srv
122 reload(srv)
123 with pytest.raises(ImportError):
124 srv.create_mcp_server(lambda: _mock_store())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
--- tests/test_mcp_server.py
+++ tests/test_mcp_server.py
@@ -1,13 +1,15 @@
1 """Tests for navegador.mcp.server — create_mcp_server and all tool handlers."""
2
3 import json
4 from unittest.mock import MagicMock, patch
5
6 import pytest
7
8 from navegador.context.loader import ContextBundle, ContextNode
9
10 # ── Helpers ───────────────────────────────────────────────────────────────────
11
12 def _node(name="foo", type_="Function", file_path="app.py"):
13 return ContextNode(name=name, type=type_, file_path=file_path)
14
15
@@ -21,104 +23,332 @@
23 store.node_count.return_value = 5
24 store.edge_count.return_value = 3
25 return store
26
27
28 class _ServerFixture:
29 """
30 Builds a navegador MCP server with mocked mcp module and captures the
31 list_tools and call_tool async handlers so they can be invoked directly.
32 """
33
34 def __init__(self, loader=None):
35 self.store = _mock_store()
36 self.loader = loader or self._default_loader()
37 self.list_tools_fn = None
38 self.call_tool_fn = None
39 self._build()
40
41 def _default_loader(self):
42 from navegador.context import ContextLoader
43 loader = MagicMock(spec=ContextLoader)
44 loader.store = self.store
45 loader.load_file.return_value = _bundle("file_target")
46 loader.load_function.return_value = _bundle("fn_target")
47 loader.load_class.return_value = _bundle("cls_target")
48 loader.search.return_value = []
49 return loader
50
51 def _build(self):
52 list_holder = {}
53 call_holder = {}
 
 
 
 
 
 
54
55 def list_tools_decorator():
56 def decorator(fn):
57 list_holder["fn"] = fn
 
58 return fn
59 return decorator
60
61 def call_tool_decorator():
62 def decorator(fn):
63 call_holder["fn"] = fn
64 return fn
65 return decorator
66
67 mock_server = MagicMock()
68 mock_server.list_tools = list_tools_decorator
69 mock_server.call_tool = call_tool_decorator
70
71 mock_mcp_server = MagicMock()
72 mock_mcp_server.Server.return_value = mock_server
73
74 mock_mcp_types = MagicMock()
75 mock_mcp_types.Tool = dict # Tool(...) → dict so we can inspect fields
76 mock_mcp_types.TextContent = dict # TextContent(type=..., text=...) → dict
77
78 with patch.dict("sys.modules", {
79 "mcp": MagicMock(),
80 "mcp.server": mock_mcp_server,
81 "mcp.types": mock_mcp_types,
82 }), patch("navegador.context.ContextLoader", return_value=self.loader):
83 from importlib import reload
84
85 import navegador.mcp.server as srv
86 reload(srv)
87 self.server = srv.create_mcp_server(lambda: self.store)
88
89 self.list_tools_fn = list_holder["fn"]
90 self.call_tool_fn = call_holder["fn"]
91
92
93 # ── Import guard ──────────────────────────────────────────────────────────────
94
95 class TestCreateMcpServerImport:
96 def test_raises_import_error_if_mcp_not_installed(self):
97 with patch.dict("sys.modules", {"mcp": None, "mcp.server": None, "mcp.types": None}):
98 from importlib import reload
99
100 import navegador.mcp.server as srv
101 reload(srv)
102 with pytest.raises(ImportError, match="mcp"):
103 srv.create_mcp_server(lambda: _mock_store())
104
105
106 # ── list_tools ────────────────────────────────────────────────────────────────
107
108 class TestListTools:
109 def setup_method(self):
110 self.fx = _ServerFixture()
111
112 @pytest.mark.asyncio
113 async def test_returns_seven_tools(self):
114 tools = await self.fx.list_tools_fn()
115 assert len(tools) == 7
116
117 @pytest.mark.asyncio
118 async def test_tool_names(self):
119 tools = await self.fx.list_tools_fn()
120 names = {t["name"] for t in tools}
121 assert names == {
122 "ingest_repo",
123 "load_file_context",
124 "load_function_context",
125 "load_class_context",
126 "search_symbols",
127 "query_graph",
128 "graph_stats",
129 }
130
131 @pytest.mark.asyncio
132 async def test_ingest_repo_requires_path(self):
133 tools = await self.fx.list_tools_fn()
134 t = next(t for t in tools if t["name"] == "ingest_repo")
135 assert "path" in t["inputSchema"]["required"]
136
137 @pytest.mark.asyncio
138 async def test_load_function_context_requires_name_and_file_path(self):
139 tools = await self.fx.list_tools_fn()
140 t = next(t for t in tools if t["name"] == "load_function_context")
141 assert "name" in t["inputSchema"]["required"]
142 assert "file_path" in t["inputSchema"]["required"]
143
144
145 # ── call_tool — ingest_repo ───────────────────────────────────────────────────
146
147 class TestCallToolIngestRepo:
148 def setup_method(self):
149 self.fx = _ServerFixture()
150
151 @pytest.mark.asyncio
152 async def test_calls_ingester_and_returns_json(self):
153 mock_ingester = MagicMock()
154 mock_ingester.ingest.return_value = {"files": 3, "functions": 10, "classes": 2, "edges": 15}
155
156 with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
157 result = await self.fx.call_tool_fn("ingest_repo", {"path": "/some/repo"})
158
159 assert len(result) == 1
160 data = json.loads(result[0]["text"])
161 assert data["files"] == 3
162 assert data["functions"] == 10
163
164 @pytest.mark.asyncio
165 async def test_passes_clear_flag(self):
166 mock_ingester = MagicMock()
167 mock_ingester.ingest.return_value = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
168
169 with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
170 await self.fx.call_tool_fn("ingest_repo", {"path": "/repo", "clear": True})
171
172 mock_ingester.ingest.assert_called_once_with("/repo", clear=True)
173
174 @pytest.mark.asyncio
175 async def test_clear_defaults_to_false(self):
176 mock_ingester = MagicMock()
177 mock_ingester.ingest.return_value = {"files": 0, "functions": 0, "classes": 0, "edges": 0}
178
179 with patch("navegador.ingestion.RepoIngester", return_value=mock_ingester):
180 await self.fx.call_tool_fn("ingest_repo", {"path": "/repo"})
181
182 mock_ingester.ingest.assert_called_once_with("/repo", clear=False)
183
184
185 # ── call_tool — load_file_context ─────────────────────────────────────────────
186
187 class TestCallToolLoadFileContext:
188 def setup_method(self):
189 self.fx = _ServerFixture()
190
191 @pytest.mark.asyncio
192 async def test_returns_markdown_by_default(self):
193 result = await self.fx.call_tool_fn("load_file_context", {"file_path": "src/main.py"})
194 self.fx.loader.load_file.assert_called_once_with("src/main.py")
195 assert "file_target" in result[0]["text"]
196
197 @pytest.mark.asyncio
198 async def test_returns_json_when_requested(self):
199 result = await self.fx.call_tool_fn(
200 "load_file_context", {"file_path": "src/main.py", "format": "json"}
201 )
202 data = json.loads(result[0]["text"])
203 assert data["target"]["name"] == "file_target"
204
205 @pytest.mark.asyncio
206 async def test_markdown_format_explicit(self):
207 result = await self.fx.call_tool_fn(
208 "load_file_context", {"file_path": "src/main.py", "format": "markdown"}
209 )
210 assert "file_target" in result[0]["text"]
211
212
213 # ── call_tool — load_function_context ────────────────────────────────────────
214
215 class TestCallToolLoadFunctionContext:
216 def setup_method(self):
217 self.fx = _ServerFixture()
218
219 @pytest.mark.asyncio
220 async def test_calls_loader_with_depth(self):
221 await self.fx.call_tool_fn(
222 "load_function_context",
223 {"name": "parse", "file_path": "parser.py", "depth": 3},
224 )
225 self.fx.loader.load_function.assert_called_once_with("parse", "parser.py", depth=3)
226
227 @pytest.mark.asyncio
228 async def test_depth_defaults_to_two(self):
229 await self.fx.call_tool_fn(
230 "load_function_context", {"name": "parse", "file_path": "parser.py"}
231 )
232 self.fx.loader.load_function.assert_called_once_with("parse", "parser.py", depth=2)
233
234 @pytest.mark.asyncio
235 async def test_returns_json_when_requested(self):
236 result = await self.fx.call_tool_fn(
237 "load_function_context",
238 {"name": "parse", "file_path": "parser.py", "format": "json"},
239 )
240 data = json.loads(result[0]["text"])
241 assert data["target"]["name"] == "fn_target"
242
243
244 # ── call_tool — load_class_context ───────────────────────────────────────────
245
246 class TestCallToolLoadClassContext:
247 def setup_method(self):
248 self.fx = _ServerFixture()
249
250 @pytest.mark.asyncio
251 async def test_calls_loader_with_name_and_file(self):
252 await self.fx.call_tool_fn(
253 "load_class_context", {"name": "AuthService", "file_path": "auth.py"}
254 )
255 self.fx.loader.load_class.assert_called_once_with("AuthService", "auth.py")
256
257 @pytest.mark.asyncio
258 async def test_returns_markdown_by_default(self):
259 result = await self.fx.call_tool_fn(
260 "load_class_context", {"name": "AuthService", "file_path": "auth.py"}
261 )
262 assert "cls_target" in result[0]["text"]
263
264 @pytest.mark.asyncio
265 async def test_returns_json_when_requested(self):
266 result = await self.fx.call_tool_fn(
267 "load_class_context",
268 {"name": "AuthService", "file_path": "auth.py", "format": "json"},
269 )
270 data = json.loads(result[0]["text"])
271 assert data["target"]["name"] == "cls_target"
272
273
274 # ── call_tool — search_symbols ───────────────────────────────────────────────
275
276 class TestCallToolSearchSymbols:
277 def setup_method(self):
278 self.fx = _ServerFixture()
279
280 @pytest.mark.asyncio
281 async def test_returns_no_results_message_on_empty(self):
282 result = await self.fx.call_tool_fn("search_symbols", {"query": "xyz"})
283 assert result[0]["text"] == "No results."
284
285 @pytest.mark.asyncio
286 async def test_formats_results_as_bullet_list(self):
287 hit = ContextNode(name="do_thing", type="Function", file_path="utils.py", line_start=10)
288 self.fx.loader.search.return_value = [hit]
289 result = await self.fx.call_tool_fn("search_symbols", {"query": "do"})
290 text = result[0]["text"]
291 assert "do_thing" in text
292 assert "utils.py" in text
293 assert "Function" in text
294
295 @pytest.mark.asyncio
296 async def test_passes_limit(self):
297 await self.fx.call_tool_fn("search_symbols", {"query": "foo", "limit": 5})
298 self.fx.loader.search.assert_called_once_with("foo", limit=5)
299
300 @pytest.mark.asyncio
301 async def test_limit_defaults_to_twenty(self):
302 await self.fx.call_tool_fn("search_symbols", {"query": "foo"})
303 self.fx.loader.search.assert_called_once_with("foo", limit=20)
304
305
306 # ── call_tool — query_graph ───────────────────────────────────────────────────
307
308 class TestCallToolQueryGraph:
309 def setup_method(self):
310 self.fx = _ServerFixture()
311
312 @pytest.mark.asyncio
313 async def test_executes_cypher_and_returns_json(self):
314 self.fx.store.query.return_value = MagicMock(result_set=[["node_a"], ["node_b"]])
315 result = await self.fx.call_tool_fn("query_graph", {"cypher": "MATCH (n) RETURN n"})
316 self.fx.store.query.assert_called_once_with("MATCH (n) RETURN n")
317 data = json.loads(result[0]["text"])
318 assert len(data) == 2
319
320 @pytest.mark.asyncio
321 async def test_empty_result_set(self):
322 self.fx.store.query.return_value = MagicMock(result_set=[])
323 result = await self.fx.call_tool_fn("query_graph", {"cypher": "MATCH (n) RETURN n"})
324 data = json.loads(result[0]["text"])
325 assert data == []
326
327
328 # ── call_tool — graph_stats ───────────────────────────────────────────────────
329
330 class TestCallToolGraphStats:
331 def setup_method(self):
332 self.fx = _ServerFixture()
333
334 @pytest.mark.asyncio
335 async def test_returns_node_and_edge_counts(self):
336 self.fx.store.node_count.return_value = 42
337 self.fx.store.edge_count.return_value = 17
338 result = await self.fx.call_tool_fn("graph_stats", {})
339 data = json.loads(result[0]["text"])
340 assert data["nodes"] == 42
341 assert data["edges"] == 17
342
343
344 # ── call_tool — unknown tool ──────────────────────────────────────────────────
345
346 class TestCallToolUnknown:
347 def setup_method(self):
348 self.fx = _ServerFixture()
349
350 @pytest.mark.asyncio
351 async def test_returns_unknown_tool_message(self):
352 result = await self.fx.call_tool_fn("nonexistent_tool", {})
353 assert "Unknown tool" in result[0]["text"]
354 assert "nonexistent_tool" in result[0]["text"]
355
--- tests/test_rust_parser.py
+++ tests/test_rust_parser.py
@@ -223,11 +223,11 @@
223223
224224
class TestRustExtractCalls:
225225
def test_extracts_call(self):
226226
parser = _make_parser()
227227
store = _make_store()
228
- source = b"fn foo() { bar() }"
228
+ source = b"bar"
229229
callee = _text_node(b"bar")
230230
call_node = MockNode("call_expression")
231231
call_node.set_field("function", callee)
232232
body = MockNode("block", children=[call_node])
233233
fn_node = MockNode("function_item")
@@ -234,10 +234,12 @@
234234
fn_node.set_field("body", body)
235235
stats = {"functions": 0, "classes": 0, "edges": 0}
236236
parser._extract_calls(fn_node, source, "lib.rs", "foo",
237237
NodeLabel.Function, store, stats)
238238
assert stats["edges"] == 1
239
+ edge_call = store.create_edge.call_args[0]
240
+ assert edge_call[4]["name"] == "bar"
239241
240242
def test_handles_method_call_syntax(self):
241243
parser = _make_parser()
242244
store = _make_store()
243245
source = b"Repo::save"
244246
--- tests/test_rust_parser.py
+++ tests/test_rust_parser.py
@@ -223,11 +223,11 @@
223
224 class TestRustExtractCalls:
225 def test_extracts_call(self):
226 parser = _make_parser()
227 store = _make_store()
228 source = b"fn foo() { bar() }"
229 callee = _text_node(b"bar")
230 call_node = MockNode("call_expression")
231 call_node.set_field("function", callee)
232 body = MockNode("block", children=[call_node])
233 fn_node = MockNode("function_item")
@@ -234,10 +234,12 @@
234 fn_node.set_field("body", body)
235 stats = {"functions": 0, "classes": 0, "edges": 0}
236 parser._extract_calls(fn_node, source, "lib.rs", "foo",
237 NodeLabel.Function, store, stats)
238 assert stats["edges"] == 1
 
 
239
240 def test_handles_method_call_syntax(self):
241 parser = _make_parser()
242 store = _make_store()
243 source = b"Repo::save"
244
--- tests/test_rust_parser.py
+++ tests/test_rust_parser.py
@@ -223,11 +223,11 @@
223
224 class TestRustExtractCalls:
225 def test_extracts_call(self):
226 parser = _make_parser()
227 store = _make_store()
228 source = b"bar"
229 callee = _text_node(b"bar")
230 call_node = MockNode("call_expression")
231 call_node.set_field("function", callee)
232 body = MockNode("block", children=[call_node])
233 fn_node = MockNode("function_item")
@@ -234,10 +234,12 @@
234 fn_node.set_field("body", body)
235 stats = {"functions": 0, "classes": 0, "edges": 0}
236 parser._extract_calls(fn_node, source, "lib.rs", "foo",
237 NodeLabel.Function, store, stats)
238 assert stats["edges"] == 1
239 edge_call = store.create_edge.call_args[0]
240 assert edge_call[4]["name"] == "bar"
241
242 def test_handles_method_call_syntax(self):
243 parser = _make_parser()
244 store = _make_store()
245 source = b"Repo::save"
246
--- tests/test_typescript_parser.py
+++ tests/test_typescript_parser.py
@@ -289,20 +289,22 @@
289289
290290
class TestTsExtractCalls:
291291
def test_extracts_call(self):
292292
parser = _make_parser()
293293
store = _make_store()
294
- source = b"function foo() { bar() }"
294
+ source = b"bar"
295295
callee = _text_node(b"bar")
296296
call_node = MockNode("call_expression")
297297
call_node.set_field("function", callee)
298298
body = MockNode("statement_block", children=[call_node])
299299
fn_node = MockNode("function_declaration", children=[body])
300300
stats = {"functions": 0, "classes": 0, "edges": 0}
301301
parser._extract_calls(fn_node, source, "app.ts", "foo",
302302
NodeLabel.Function, store, stats)
303303
assert stats["edges"] == 1
304
+ edge_call = store.create_edge.call_args[0]
305
+ assert edge_call[4]["name"] == "bar"
304306
305307
def test_no_calls_in_empty_body(self):
306308
parser = _make_parser()
307309
store = _make_store()
308310
fn_node = MockNode("function_declaration",
309311
--- tests/test_typescript_parser.py
+++ tests/test_typescript_parser.py
@@ -289,20 +289,22 @@
289
290 class TestTsExtractCalls:
291 def test_extracts_call(self):
292 parser = _make_parser()
293 store = _make_store()
294 source = b"function foo() { bar() }"
295 callee = _text_node(b"bar")
296 call_node = MockNode("call_expression")
297 call_node.set_field("function", callee)
298 body = MockNode("statement_block", children=[call_node])
299 fn_node = MockNode("function_declaration", children=[body])
300 stats = {"functions": 0, "classes": 0, "edges": 0}
301 parser._extract_calls(fn_node, source, "app.ts", "foo",
302 NodeLabel.Function, store, stats)
303 assert stats["edges"] == 1
 
 
304
305 def test_no_calls_in_empty_body(self):
306 parser = _make_parser()
307 store = _make_store()
308 fn_node = MockNode("function_declaration",
309
--- tests/test_typescript_parser.py
+++ tests/test_typescript_parser.py
@@ -289,20 +289,22 @@
289
290 class TestTsExtractCalls:
291 def test_extracts_call(self):
292 parser = _make_parser()
293 store = _make_store()
294 source = b"bar"
295 callee = _text_node(b"bar")
296 call_node = MockNode("call_expression")
297 call_node.set_field("function", callee)
298 body = MockNode("statement_block", children=[call_node])
299 fn_node = MockNode("function_declaration", children=[body])
300 stats = {"functions": 0, "classes": 0, "edges": 0}
301 parser._extract_calls(fn_node, source, "app.ts", "foo",
302 NodeLabel.Function, store, stats)
303 assert stats["edges"] == 1
304 edge_call = store.create_edge.call_args[0]
305 assert edge_call[4]["name"] == "bar"
306
307 def test_no_calls_in_empty_body(self):
308 parser = _make_parser()
309 store = _make_store()
310 fn_node = MockNode("function_declaration",
311

Keyboard Shortcuts

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