Navegador

test: boost coverage from 91% to 96% with 232 targeted tests Covers gaps in monorepo, cluster, API schema, PM integration, completions, CLI commands, and new language parsers. Closes #63

lmata 2026-03-23 05:50 trunk
Commit c054b6b37525dad5107c138f9a2cf857d4f495faf4f1ef9fa6210b13485aed06
--- a/tests/test_coverage_boost.py
+++ b/tests/test_coverage_boost.py
@@ -0,0 +1,2526 @@
1
+"""
2
+Targeted tests to boost coverage from ~91% to 95%+.
3
+
4
+One test class per module. All external dependencies (Redis, tree-sitter,
5
+HTTP) are mocked; no real infrastructure is required.
6
+"""
7
+
8
+from __future__ import annotations
9
+
10
+import json
11
+import tempfile
12
+from pathlib import Path
13
+from unittest.mock import MagicMock, patch
14
+
15
+import pytest
16
+
17
+
18
+# ── Helpers ───────────────────────────────────────────────────────────────────
19
+
20
+
21
+def _make_store():
22
+ store = MagicMock()
23
+ store.query.return_value = MagicMock(result_set=[])
24
+ store.node_count.return_value = 0
25
+ store.edge_count.return_value = 0
26
+ return store
27
+
28
+
29
+def _write(path: Path, content: str) -> None:
30
+ path.parent.mkdir(parents=True, exist_ok=True)
31
+ path.write_text(content, encoding="utf-8")
32
+
33
+
34
+# ── MockNode for AST tests ────────────────────────────────────────────────────
35
+
36
+
37
+class MockNode:
38
+ def __init__(
39
+ self,
40
+ type_: str,
41
+ text: bytes = b"",
42
+ children: list = None,
43
+ start_byte: int = 0,
44
+ end_byte: int = 0,
45
+ start_point: tuple = (0, 0),
46
+ end_point: tuple = (5, 0),
47
+ ):
48
+ self.type = type_
49
+ self.children = children or []
50
+ self.start_byte = start_byte
51
+ self.end_byte = end_byte
52
+ self.start_point = start_point
53
+ self.end_point = end_point
54
+ self._fields: dict = {}
55
+
56
+ def child_by_field_name(self, name: str):
57
+ return self._fields.get(name)
58
+
59
+ def set_field(self, name: str, node: "MockNode") -> "MockNode":
60
+ self._fields[name] = node
61
+ return self
62
+
63
+
64
+def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
65
+ return MockNode(type_, text, start_byte=0, end_byte=len(text))
66
+
67
+
68
+def _make_mock_tree(root_node: MockNode):
69
+ tree = MagicMock()
70
+ tree.root_node = root_node
71
+ return tree
72
+
73
+
74
+def _mock_ts(lang_module_name: str):
75
+ mock_lang_module = MagicMock()
76
+ mock_ts = MagicMock()
77
+ return patch.dict("sys.modules", {lang_module_name: mock_lang_module, "tree_sitter": mock_ts})
78
+
79
+
80
+# ===========================================================================
81
+# navegador.api_schema (57% → target ~90%)
82
+# ===========================================================================
83
+
84
+
85
+class TestAPISchemaIngesterOpenAPI:
86
+ """Cover lines 72, 105, 213, 217-225, 234-241, 255-294, 299-319."""
87
+
88
+ def _make(self):
89
+ from navegador.api_schema import APISchemaIngester
90
+
91
+ return APISchemaIngester(_make_store())
92
+
93
+ def test_ingest_openapi_yaml_with_paths(self, tmp_path):
94
+ content = """
95
+openapi: 3.0.0
96
+info:
97
+ title: Test
98
+paths:
99
+ /users:
100
+ get:
101
+ operationId: listUsers
102
+ summary: List users
103
+ tags:
104
+ - users
105
+ post:
106
+ summary: Create user
107
+components:
108
+ schemas:
109
+ User:
110
+ type: object
111
+ description: A user object
112
+"""
113
+ f = tmp_path / "api.yaml"
114
+ f.write_text(content)
115
+ ingester = self._make()
116
+ stats = ingester.ingest_openapi(str(f))
117
+ assert stats["endpoints"] >= 2
118
+ assert stats["schemas"] >= 1
119
+
120
+ def test_ingest_openapi_json(self, tmp_path):
121
+ spec = {
122
+ "openapi": "3.0.0",
123
+ "paths": {
124
+ "/items": {
125
+ "get": {"operationId": "listItems", "summary": "List"},
126
+ "delete": {"summary": "Delete"},
127
+ }
128
+ },
129
+ "components": {"schemas": {"Item": {"description": "item"}}},
130
+ }
131
+ f = tmp_path / "api.json"
132
+ f.write_text(json.dumps(spec))
133
+ ingester = self._make()
134
+ stats = ingester.ingest_openapi(str(f))
135
+ assert stats["endpoints"] >= 2
136
+ assert stats["schemas"] >= 1
137
+
138
+ def test_ingest_openapi_missing_file(self, tmp_path):
139
+ ingester = self._make()
140
+ stats = ingester.ingest_openapi(str(tmp_path / "missing.yaml"))
141
+ assert stats == {"endpoints": 0, "schemas": 0}
142
+
143
+ def test_ingest_openapi_swagger2_definitions(self, tmp_path):
144
+ spec = {
145
+ "swagger": "2.0",
146
+ "paths": {
147
+ "/pets": {"get": {"summary": "List pets"}}
148
+ },
149
+ "definitions": {"Pet": {"description": "pet"}},
150
+ }
151
+ f = tmp_path / "swagger.json"
152
+ f.write_text(json.dumps(spec))
153
+ ingester = self._make()
154
+ stats = ingester.ingest_openapi(str(f))
155
+ assert stats["schemas"] >= 1
156
+
157
+ def test_ingest_openapi_operation_no_id(self, tmp_path):
158
+ # operationId absent → synthesised as "METHOD /path"
159
+ spec = {
160
+ "paths": {"/x": {"put": {"summary": "update"}}},
161
+ }
162
+ f = tmp_path / "api.json"
163
+ f.write_text(json.dumps(spec))
164
+ ingester = self._make()
165
+ stats = ingester.ingest_openapi(str(f))
166
+ assert stats["endpoints"] == 1
167
+
168
+ def test_ingest_openapi_invalid_json(self, tmp_path):
169
+ f = tmp_path / "bad.json"
170
+ f.write_text("{not valid json")
171
+ ingester = self._make()
172
+ stats = ingester.ingest_openapi(str(f))
173
+ assert stats == {"endpoints": 0, "schemas": 0}
174
+
175
+ def test_ingest_openapi_unknown_extension_tries_json(self, tmp_path):
176
+ spec = {"paths": {"/x": {"get": {"summary": "hi"}}}}
177
+ f = tmp_path / "api.txt"
178
+ f.write_text(json.dumps(spec))
179
+ ingester = self._make()
180
+ stats = ingester.ingest_openapi(str(f))
181
+ assert stats["endpoints"] == 1
182
+
183
+ def test_ingest_openapi_unknown_extension_falls_back_to_yaml(self, tmp_path):
184
+ # Not valid JSON → falls back to _parse_yaml
185
+ f = tmp_path / "api.txt"
186
+ f.write_text("paths:\n /x:\n get:\n summary: hi\n")
187
+ ingester = self._make()
188
+ # Should not raise
189
+ ingester.ingest_openapi(str(f))
190
+
191
+
192
+class TestAPISchemaIngesterGraphQL:
193
+ def _make(self):
194
+ from navegador.api_schema import APISchemaIngester
195
+
196
+ return APISchemaIngester(_make_store())
197
+
198
+ def test_ingest_graphql_types_and_fields(self, tmp_path):
199
+ sdl = """
200
+type Query {
201
+ user(id: ID!): User
202
+ users: [User]
203
+}
204
+type Mutation {
205
+ createUser(name: String!): User
206
+}
207
+type User {
208
+ id: ID
209
+ name: String
210
+}
211
+input CreateUserInput {
212
+ name: String!
213
+}
214
+"""
215
+ f = tmp_path / "schema.graphql"
216
+ f.write_text(sdl)
217
+ ingester = self._make()
218
+ stats = ingester.ingest_graphql(str(f))
219
+ assert stats["fields"] >= 2 # Query fields
220
+ assert stats["types"] >= 2 # User + CreateUserInput
221
+
222
+ def test_ingest_graphql_missing_file(self, tmp_path):
223
+ ingester = self._make()
224
+ stats = ingester.ingest_graphql(str(tmp_path / "missing.graphql"))
225
+ assert stats == {"types": 0, "fields": 0}
226
+
227
+ def test_ingest_graphql_empty_schema(self, tmp_path):
228
+ f = tmp_path / "empty.graphql"
229
+ f.write_text("# just a comment")
230
+ ingester = self._make()
231
+ stats = ingester.ingest_graphql(str(f))
232
+ assert stats["types"] == 0
233
+ assert stats["fields"] == 0
234
+
235
+
236
+class TestMinimalYamlLoad:
237
+ def test_simple_key_value(self):
238
+ from navegador.api_schema import _minimal_yaml_load
239
+
240
+ result = _minimal_yaml_load("title: My API\nversion: 3")
241
+ assert result.get("title") == "My API"
242
+
243
+ def test_boolean_scalars(self):
244
+ from navegador.api_schema import _yaml_scalar
245
+
246
+ assert _yaml_scalar("true") is True
247
+ assert _yaml_scalar("false") is False
248
+ assert _yaml_scalar("yes") is True
249
+ assert _yaml_scalar("no") is False
250
+
251
+ def test_null_scalars(self):
252
+ from navegador.api_schema import _yaml_scalar
253
+
254
+ assert _yaml_scalar("null") is None
255
+ assert _yaml_scalar("~") is None
256
+ assert _yaml_scalar("") is None
257
+
258
+ def test_quoted_strings(self):
259
+ from navegador.api_schema import _yaml_scalar
260
+
261
+ assert _yaml_scalar('"hello"') == "hello"
262
+ assert _yaml_scalar("'world'") == "world"
263
+
264
+ def test_int_float(self):
265
+ from navegador.api_schema import _yaml_scalar
266
+
267
+ assert _yaml_scalar("42") == 42
268
+ assert _yaml_scalar("3.14") == pytest.approx(3.14)
269
+
270
+ def test_bare_string(self):
271
+ from navegador.api_schema import _yaml_scalar
272
+
273
+ assert _yaml_scalar("application/json") == "application/json"
274
+
275
+ def test_list_items(self):
276
+ from navegador.api_schema import _minimal_yaml_load
277
+
278
+ text = "tags:\n - users\n - admin\n"
279
+ # Should not raise even if list parsing is minimal
280
+ result = _minimal_yaml_load(text)
281
+ assert isinstance(result, dict)
282
+
283
+ def test_comments_skipped(self):
284
+ from navegador.api_schema import _minimal_yaml_load
285
+
286
+ text = "# comment\ntitle: test\n"
287
+ result = _minimal_yaml_load(text)
288
+ assert result.get("title") == "test"
289
+
290
+ def test_block_scalar_placeholder(self):
291
+ from navegador.api_schema import _minimal_yaml_load
292
+
293
+ text = "description: |\n some text\ntitle: test\n"
294
+ result = _minimal_yaml_load(text)
295
+ # Should have a nested dict for the block scalar key
296
+ assert "description" in result
297
+
298
+ def test_parse_yaml_uses_pyyaml_if_available(self, tmp_path):
299
+ from navegador.api_schema import APISchemaIngester
300
+
301
+ ingester = APISchemaIngester(_make_store())
302
+ mock_yaml = MagicMock()
303
+ mock_yaml.safe_load.return_value = {"openapi": "3.0.0", "paths": {}}
304
+ with patch.dict("sys.modules", {"yaml": mock_yaml}):
305
+ result = ingester._parse_yaml("openapi: 3.0.0")
306
+ assert result == {"openapi": "3.0.0", "paths": {}}
307
+
308
+ def test_parse_yaml_falls_back_when_no_pyyaml(self):
309
+ from navegador.api_schema import APISchemaIngester
310
+
311
+ ingester = APISchemaIngester(_make_store())
312
+ import sys
313
+
314
+ original = sys.modules.pop("yaml", None)
315
+ try:
316
+ # Simulate no yaml installed
317
+ with patch.dict("sys.modules", {"yaml": None}):
318
+ result = ingester._parse_yaml("title: test\n")
319
+ assert isinstance(result, dict)
320
+ finally:
321
+ if original is not None:
322
+ sys.modules["yaml"] = original
323
+
324
+
325
+# ===========================================================================
326
+# navegador.cluster.core (50% → target ~85%)
327
+# ===========================================================================
328
+
329
+
330
+class TestClusterManagerLocalVersion:
331
+ """Cover _local_version, _set_local_version, snapshot_to_local, push_to_shared, sync."""
332
+
333
+ def _make(self, tmp_path):
334
+ from navegador.cluster.core import ClusterManager
335
+
336
+ r = MagicMock()
337
+ pipe = MagicMock()
338
+ pipe.execute.return_value = [True, True, True]
339
+ r.pipeline.return_value = pipe
340
+ r.get.return_value = b"3"
341
+ return ClusterManager(
342
+ "redis://localhost:6379",
343
+ local_db_path=str(tmp_path / "graph.db"),
344
+ redis_client=r,
345
+ ), r, pipe
346
+
347
+ def test_local_version_zero_when_no_meta(self, tmp_path):
348
+ mgr, _, _ = self._make(tmp_path)
349
+ assert mgr._local_version() == 0
350
+
351
+ def test_set_and_read_local_version(self, tmp_path):
352
+ mgr, _, _ = self._make(tmp_path)
353
+ mgr._set_local_version(7)
354
+ assert mgr._local_version() == 7
355
+
356
+ def test_set_local_version_merges_existing_keys(self, tmp_path):
357
+ mgr, _, _ = self._make(tmp_path)
358
+ mgr._set_local_version(1)
359
+ mgr._set_local_version(2)
360
+ assert mgr._local_version() == 2
361
+
362
+ def test_local_version_handles_corrupt_json(self, tmp_path):
363
+ mgr, _, _ = self._make(tmp_path)
364
+ meta = tmp_path / ".navegador" / "cluster_meta.json"
365
+ meta.parent.mkdir(parents=True, exist_ok=True)
366
+ meta.write_text("{bad json")
367
+ # Should fall back to 0
368
+ # local_db_path is tmp_path/graph.db, so meta is tmp_path/cluster_meta.json
369
+ mgr2, _, _ = self._make(tmp_path)
370
+ assert mgr2._local_version() == 0
371
+
372
+ def test_snapshot_to_local_no_snapshot(self, tmp_path):
373
+ mgr, r, _ = self._make(tmp_path)
374
+ r.get.return_value = None # no snapshot key
375
+ # Should log warning and return without error
376
+ with patch("navegador.cluster.core.logger") as mock_log:
377
+ mgr.snapshot_to_local()
378
+ mock_log.warning.assert_called_once()
379
+
380
+ def test_snapshot_to_local_imports_data(self, tmp_path):
381
+ mgr, r, _ = self._make(tmp_path)
382
+ snapshot_data = {"nodes": [], "edges": []}
383
+ # First call returns version key, subsequent get returns snapshot
384
+ r.get.side_effect = [json.dumps(snapshot_data).encode(), b"5"]
385
+
386
+ with patch.object(mgr, "_import_to_local_graph") as mock_import:
387
+ with patch.object(mgr, "_set_local_version") as mock_set:
388
+ with patch.object(mgr, "_redis_version", return_value=5):
389
+ r.get.side_effect = None
390
+ r.get.return_value = json.dumps(snapshot_data).encode()
391
+ mgr.snapshot_to_local()
392
+ mock_import.assert_called_once_with(snapshot_data)
393
+
394
+ def test_push_to_shared(self, tmp_path):
395
+ mgr, r, pipe = self._make(tmp_path)
396
+ r.get.return_value = b"2"
397
+
398
+ with patch.object(mgr, "_export_local_graph", return_value={"nodes": [], "edges": []}):
399
+ mgr.push_to_shared()
400
+
401
+ pipe.set.assert_called()
402
+ pipe.execute.assert_called_once()
403
+
404
+ def test_sync_pulls_when_shared_newer(self, tmp_path):
405
+ mgr, r, _ = self._make(tmp_path)
406
+ with patch.object(mgr, "_local_version", return_value=1):
407
+ with patch.object(mgr, "_redis_version", return_value=5):
408
+ with patch.object(mgr, "snapshot_to_local") as mock_pull:
409
+ mgr.sync()
410
+ mock_pull.assert_called_once()
411
+
412
+ def test_sync_pushes_when_local_current(self, tmp_path):
413
+ mgr, r, _ = self._make(tmp_path)
414
+ with patch.object(mgr, "_local_version", return_value=5):
415
+ with patch.object(mgr, "_redis_version", return_value=3):
416
+ with patch.object(mgr, "push_to_shared") as mock_push:
417
+ mgr.sync()
418
+ mock_push.assert_called_once()
419
+
420
+ def test_connect_redis_raises_on_missing_dep(self):
421
+ from navegador.cluster.core import ClusterManager
422
+
423
+ with patch.dict("sys.modules", {"redis": None}):
424
+ with pytest.raises(ImportError, match="redis"):
425
+ ClusterManager._connect_redis("redis://localhost")
426
+
427
+ def test_redis_version_returns_zero_when_none(self, tmp_path):
428
+ mgr, r, _ = self._make(tmp_path)
429
+ r.get.return_value = None
430
+ assert mgr._redis_version() == 0
431
+
432
+ def test_export_local_graph_calls_store(self, tmp_path):
433
+ mgr, r, _ = self._make(tmp_path)
434
+ mock_store = MagicMock()
435
+ nodes_result = MagicMock()
436
+ nodes_result.result_set = []
437
+ edges_result = MagicMock()
438
+ edges_result.result_set = []
439
+ mock_store.query.side_effect = [nodes_result, edges_result]
440
+
441
+ with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store):
442
+ data = mgr._export_local_graph()
443
+ assert data == {"nodes": [], "edges": []}
444
+
445
+ def test_import_to_local_graph_creates_nodes(self, tmp_path):
446
+ mgr, r, _ = self._make(tmp_path)
447
+ mock_store = MagicMock()
448
+
449
+ data = {
450
+ "nodes": [{"labels": ["Function"], "properties": {"name": "foo"}}],
451
+ "edges": [
452
+ {
453
+ "src_labels": ["Function"],
454
+ "src_props": {"name": "foo", "file_path": "f.py"},
455
+ "rel_type": "CALLS",
456
+ "dst_labels": ["Function"],
457
+ "dst_props": {"name": "bar", "file_path": "f.py"},
458
+ "rel_props": {},
459
+ }
460
+ ],
461
+ }
462
+ with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store):
463
+ mgr._import_to_local_graph(data)
464
+ mock_store.create_node.assert_called_once()
465
+ mock_store.create_edge.assert_called_once()
466
+
467
+ def test_import_to_local_graph_skips_edge_without_src_key(self, tmp_path):
468
+ mgr, r, _ = self._make(tmp_path)
469
+ mock_store = MagicMock()
470
+
471
+ data = {
472
+ "nodes": [],
473
+ "edges": [
474
+ {
475
+ "src_labels": ["Function"],
476
+ "src_props": {}, # no name/file_path → no key
477
+ "rel_type": "CALLS",
478
+ "dst_labels": ["Function"],
479
+ "dst_props": {"name": "bar", "file_path": "f.py"},
480
+ "rel_props": {},
481
+ }
482
+ ],
483
+ }
484
+ with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store):
485
+ mgr._import_to_local_graph(data)
486
+ mock_store.create_edge.assert_not_called()
487
+
488
+
489
+# ===========================================================================
490
+# navegador.monorepo (76% → target ~90%)
491
+# ===========================================================================
492
+
493
+
494
+class TestWorkspaceDetectorEdgeCases:
495
+ """Cover lines 89-90, 124-125, 128, 142-143, 165-166, 180-181, 193,
496
+ 223, 235-236, 253-255, 270-274, 288-289."""
497
+
498
+ def test_yarn_workspaces_berry_format(self, tmp_path):
499
+ from navegador.monorepo import WorkspaceDetector
500
+
501
+ pkg_json = {
502
+ "name": "root",
503
+ "workspaces": {"packages": ["packages/*"]},
504
+ }
505
+ _write(tmp_path / "package.json", json.dumps(pkg_json))
506
+ (tmp_path / "packages" / "app").mkdir(parents=True)
507
+ _write(tmp_path / "packages" / "app" / "package.json", '{"name":"app"}')
508
+ config = WorkspaceDetector().detect(tmp_path)
509
+ assert config is not None
510
+ assert config.type == "yarn"
511
+
512
+ def test_yarn_package_json_parse_error(self, tmp_path):
513
+ from navegador.monorepo import WorkspaceDetector
514
+
515
+ _write(tmp_path / "package.json", "{bad json")
516
+ # No workspaces key (parse failed) → no yarn config returned
517
+ config = WorkspaceDetector().detect(tmp_path)
518
+ assert config is None
519
+
520
+ def test_js_workspace_packages_berry_patterns(self, tmp_path):
521
+ from navegador.monorepo import WorkspaceDetector
522
+
523
+ pkg_json = {"workspaces": {"packages": ["packages/*"]}}
524
+ _write(tmp_path / "package.json", json.dumps(pkg_json))
525
+ (tmp_path / "packages" / "a").mkdir(parents=True)
526
+ det = WorkspaceDetector()
527
+ packages = det._js_workspace_packages(tmp_path)
528
+ assert any(p.name == "a" for p in packages)
529
+
530
+ def test_js_workspace_packages_no_package_json(self, tmp_path):
531
+ from navegador.monorepo import WorkspaceDetector
532
+
533
+ # fallback: scan for package.json one level down
534
+ (tmp_path / "pkg_a").mkdir()
535
+ _write(tmp_path / "pkg_a" / "package.json", '{"name":"pkg_a"}')
536
+ det = WorkspaceDetector()
537
+ packages = det._js_workspace_packages(tmp_path)
538
+ assert any(p.name == "pkg_a" for p in packages)
539
+
540
+ def test_js_workspace_packages_parse_error(self, tmp_path):
541
+ from navegador.monorepo import WorkspaceDetector
542
+
543
+ _write(tmp_path / "package.json", "{bad json")
544
+ # Falls back to _fallback_packages
545
+ det = WorkspaceDetector()
546
+ packages = det._js_workspace_packages(tmp_path)
547
+ assert isinstance(packages, list)
548
+
549
+ def test_nx_packages_from_subdirs(self, tmp_path):
550
+ from navegador.monorepo import WorkspaceDetector
551
+
552
+ _write(tmp_path / "nx.json", '{}')
553
+ (tmp_path / "apps" / "app1").mkdir(parents=True)
554
+ (tmp_path / "libs" / "lib1").mkdir(parents=True)
555
+ config = WorkspaceDetector().detect(tmp_path)
556
+ assert config is not None
557
+ assert config.type == "nx"
558
+ pkg_names = [p.name for p in config.packages]
559
+ assert "app1" in pkg_names
560
+ assert "lib1" in pkg_names
561
+
562
+ def test_nx_packages_fallback_to_js_workspaces(self, tmp_path):
563
+ from navegador.monorepo import WorkspaceDetector
564
+
565
+ # nx.json exists but no apps/libs/packages dirs
566
+ _write(tmp_path / "nx.json", '{}')
567
+ # fallback triggers _js_workspace_packages → _fallback_packages
568
+ config = WorkspaceDetector().detect(tmp_path)
569
+ assert config is not None
570
+ assert config.type == "nx"
571
+
572
+ def test_pnpm_workspace_parse(self, tmp_path):
573
+ from navegador.monorepo import WorkspaceDetector
574
+
575
+ _write(
576
+ tmp_path / "pnpm-workspace.yaml",
577
+ "packages:\n - 'packages/*'\n - 'apps/*'\n",
578
+ )
579
+ (tmp_path / "packages" / "core").mkdir(parents=True)
580
+ config = WorkspaceDetector().detect(tmp_path)
581
+ assert config is not None
582
+ assert config.type == "pnpm"
583
+
584
+ def test_pnpm_workspace_read_error(self, tmp_path):
585
+ from navegador.monorepo import WorkspaceDetector
586
+
587
+ # pnpm-workspace.yaml exists but cannot be read → IOError path
588
+ yaml_path = tmp_path / "pnpm-workspace.yaml"
589
+ yaml_path.touch()
590
+ det = WorkspaceDetector()
591
+ with patch.object(Path, "read_text", side_effect=OSError("perm")):
592
+ packages = det._pnpm_packages(tmp_path)
593
+ assert isinstance(packages, list)
594
+
595
+ def test_pnpm_no_patterns_fallback(self, tmp_path):
596
+ from navegador.monorepo import WorkspaceDetector
597
+
598
+ _write(tmp_path / "pnpm-workspace.yaml", "# empty\n")
599
+ (tmp_path / "sub").mkdir()
600
+ _write(tmp_path / "sub" / "package.json", '{"name":"sub"}')
601
+ det = WorkspaceDetector()
602
+ packages = det._pnpm_packages(tmp_path)
603
+ assert any(p.name == "sub" for p in packages)
604
+
605
+ def test_cargo_workspace_parse(self, tmp_path):
606
+ from navegador.monorepo import WorkspaceDetector
607
+
608
+ cargo_toml = """
609
+[workspace]
610
+members = [
611
+ "crates/core",
612
+ "crates/cli",
613
+]
614
+"""
615
+ _write(tmp_path / "Cargo.toml", cargo_toml)
616
+ (tmp_path / "crates" / "core").mkdir(parents=True)
617
+ (tmp_path / "crates" / "cli").mkdir(parents=True)
618
+ config = WorkspaceDetector().detect(tmp_path)
619
+ assert config is not None
620
+ assert config.type == "cargo"
621
+ assert len(config.packages) == 2
622
+
623
+ def test_cargo_workspace_not_workspace(self, tmp_path):
624
+ from navegador.monorepo import WorkspaceDetector
625
+
626
+ _write(tmp_path / "Cargo.toml", "[package]\nname = \"myapp\"\n")
627
+ config = WorkspaceDetector().detect(tmp_path)
628
+ assert config is None
629
+
630
+ def test_cargo_read_error(self, tmp_path):
631
+ from navegador.monorepo import WorkspaceDetector
632
+
633
+ cargo_toml = tmp_path / "Cargo.toml"
634
+ cargo_toml.touch()
635
+ det = WorkspaceDetector()
636
+ with patch.object(Path, "read_text", side_effect=OSError("perm")):
637
+ result = det._cargo_packages(tmp_path, cargo_toml)
638
+ assert result is None
639
+
640
+ def test_cargo_wildcard_members(self, tmp_path):
641
+ from navegador.monorepo import WorkspaceDetector
642
+
643
+ cargo_toml = "[workspace]\nmembers = [\"crates/*\"]\n"
644
+ _write(tmp_path / "Cargo.toml", cargo_toml)
645
+ (tmp_path / "crates" / "a").mkdir(parents=True)
646
+ (tmp_path / "crates" / "b").mkdir(parents=True)
647
+ det = WorkspaceDetector()
648
+ pkgs = det._cargo_packages(tmp_path, tmp_path / "Cargo.toml")
649
+ assert pkgs is not None
650
+ assert len(pkgs) == 2
651
+
652
+ def test_go_workspace_parse(self, tmp_path):
653
+ from navegador.monorepo import WorkspaceDetector
654
+
655
+ (tmp_path / "cmd").mkdir()
656
+ (tmp_path / "pkg").mkdir()
657
+ _write(tmp_path / "go.work", "go 1.21\nuse ./cmd\nuse ./pkg\n")
658
+ config = WorkspaceDetector().detect(tmp_path)
659
+ assert config is not None
660
+ assert config.type == "go"
661
+
662
+ def test_go_workspace_read_error(self, tmp_path):
663
+ from navegador.monorepo import WorkspaceDetector
664
+
665
+ go_work = tmp_path / "go.work"
666
+ go_work.touch()
667
+ det = WorkspaceDetector()
668
+ with patch.object(Path, "read_text", side_effect=OSError("perm")):
669
+ packages = det._go_packages(tmp_path)
670
+ assert isinstance(packages, list)
671
+
672
+ def test_glob_packages_negation_skipped(self, tmp_path):
673
+ from navegador.monorepo import WorkspaceDetector
674
+
675
+ (tmp_path / "packages" / "a").mkdir(parents=True)
676
+ det = WorkspaceDetector()
677
+ pkgs = det._glob_packages(tmp_path, ["!packages/*"])
678
+ assert pkgs == []
679
+
680
+ def test_glob_packages_literal_path(self, tmp_path):
681
+ from navegador.monorepo import WorkspaceDetector
682
+
683
+ (tmp_path / "myapp").mkdir()
684
+ det = WorkspaceDetector()
685
+ pkgs = det._glob_packages(tmp_path, ["myapp"])
686
+ assert any(p.name == "myapp" for p in pkgs)
687
+
688
+ def test_fallback_packages_skips_dotdirs(self, tmp_path):
689
+ from navegador.monorepo import WorkspaceDetector
690
+
691
+ (tmp_path / ".git").mkdir()
692
+ _write(tmp_path / ".git" / "package.json", '{}')
693
+ (tmp_path / "real").mkdir()
694
+ _write(tmp_path / "real" / "package.json", '{}')
695
+ det = WorkspaceDetector()
696
+ pkgs = det._fallback_packages(tmp_path)
697
+ names = [p.name for p in pkgs]
698
+ assert ".git" not in names
699
+ assert "real" in names
700
+
701
+
702
+class TestMonorepoIngesterEdgeCases:
703
+ """Cover lines 373, 404-405, 451-452, 466, 471, 474-475, 485-503, 509-531."""
704
+
705
+ def test_ingest_fallback_when_no_workspace(self, tmp_path):
706
+ from navegador.monorepo import MonorepoIngester
707
+
708
+ store = _make_store()
709
+ ingester = MonorepoIngester(store)
710
+ with patch("navegador.monorepo.RepoIngester") as MockRI:
711
+ instance = MockRI.return_value
712
+ instance.ingest.return_value = {
713
+ "files": 1, "functions": 2, "classes": 0, "edges": 0, "skipped": 0
714
+ }
715
+ stats = ingester.ingest(str(tmp_path))
716
+ assert stats["packages"] == 0
717
+ assert stats["workspace_type"] == "none"
718
+
719
+ def test_ingest_raises_on_missing_path(self, tmp_path):
720
+ from navegador.monorepo import MonorepoIngester
721
+
722
+ ingester = MonorepoIngester(_make_store())
723
+ with pytest.raises(FileNotFoundError):
724
+ ingester.ingest(str(tmp_path / "does_not_exist"))
725
+
726
+ def test_ingest_package_exception_logged(self, tmp_path):
727
+ from navegador.monorepo import MonorepoIngester, WorkspaceConfig
728
+
729
+ store = _make_store()
730
+ pkg_dir = tmp_path / "pkg"
731
+ pkg_dir.mkdir()
732
+ config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_dir])
733
+
734
+ with patch("navegador.monorepo.WorkspaceDetector") as MockDet:
735
+ MockDet.return_value.detect.return_value = config
736
+ with patch("navegador.monorepo.RepoIngester") as MockRI:
737
+ MockRI.return_value.ingest.side_effect = RuntimeError("parse fail")
738
+ stats = MonorepoIngester(store).ingest(str(tmp_path))
739
+ assert stats["packages"] == 0
740
+
741
+ def test_js_deps(self, tmp_path):
742
+ from navegador.monorepo import MonorepoIngester
743
+
744
+ ingester = MonorepoIngester(_make_store())
745
+ pkg_json = {
746
+ "dependencies": {"react": "^18"},
747
+ "devDependencies": {"jest": "^29"},
748
+ "peerDependencies": {"typescript": ">=5"},
749
+ }
750
+ _write(tmp_path / "package.json", json.dumps(pkg_json))
751
+ deps = ingester._js_deps(tmp_path)
752
+ assert "react" in deps
753
+ assert "jest" in deps
754
+ assert "typescript" in deps
755
+
756
+ def test_js_deps_no_file(self, tmp_path):
757
+ from navegador.monorepo import MonorepoIngester
758
+
759
+ ingester = MonorepoIngester(_make_store())
760
+ assert ingester._js_deps(tmp_path) == []
761
+
762
+ def test_js_deps_parse_error(self, tmp_path):
763
+ from navegador.monorepo import MonorepoIngester
764
+
765
+ ingester = MonorepoIngester(_make_store())
766
+ _write(tmp_path / "package.json", "{bad json")
767
+ assert ingester._js_deps(tmp_path) == []
768
+
769
+ def test_cargo_deps(self, tmp_path):
770
+ from navegador.monorepo import MonorepoIngester
771
+
772
+ ingester = MonorepoIngester(_make_store())
773
+ cargo = "[dependencies]\nserde = \"1.0\"\ntokio = { version = \"1\" }\n[dev-dependencies]\ntempfile = \"3\"\n"
774
+ _write(tmp_path / "Cargo.toml", cargo)
775
+ deps = ingester._cargo_deps(tmp_path)
776
+ assert "serde" in deps
777
+ assert "tokio" in deps
778
+ assert "tempfile" in deps
779
+
780
+ def test_cargo_deps_no_file(self, tmp_path):
781
+ from navegador.monorepo import MonorepoIngester
782
+
783
+ ingester = MonorepoIngester(_make_store())
784
+ assert ingester._cargo_deps(tmp_path) == []
785
+
786
+ def test_cargo_deps_read_error(self, tmp_path):
787
+ from navegador.monorepo import MonorepoIngester
788
+
789
+ ingester = MonorepoIngester(_make_store())
790
+ cargo = tmp_path / "Cargo.toml"
791
+ cargo.touch()
792
+ with patch.object(Path, "read_text", side_effect=OSError("perm")):
793
+ result = ingester._cargo_deps(tmp_path)
794
+ assert result == []
795
+
796
+ def test_go_deps(self, tmp_path):
797
+ from navegador.monorepo import MonorepoIngester
798
+
799
+ ingester = MonorepoIngester(_make_store())
800
+ go_mod = "module example.com/myapp\ngo 1.21\n\nrequire (\n github.com/pkg/errors v0.9.1\n golang.org/x/net v0.17.0\n)\n\nrequire github.com/single/dep v1.0.0\n"
801
+ _write(tmp_path / "go.mod", go_mod)
802
+ deps = ingester._go_deps(tmp_path)
803
+ assert "github.com/pkg/errors" in deps
804
+ assert "golang.org/x/net" in deps
805
+ assert "github.com/single/dep" in deps
806
+
807
+ def test_go_deps_no_file(self, tmp_path):
808
+ from navegador.monorepo import MonorepoIngester
809
+
810
+ ingester = MonorepoIngester(_make_store())
811
+ assert ingester._go_deps(tmp_path) == []
812
+
813
+ def test_go_deps_read_error(self, tmp_path):
814
+ from navegador.monorepo import MonorepoIngester
815
+
816
+ ingester = MonorepoIngester(_make_store())
817
+ go_mod = tmp_path / "go.mod"
818
+ go_mod.touch()
819
+ with patch.object(Path, "read_text", side_effect=OSError("perm")):
820
+ result = ingester._go_deps(tmp_path)
821
+ assert result == []
822
+
823
+ def test_read_package_deps_unknown_type(self, tmp_path):
824
+ from navegador.monorepo import MonorepoIngester
825
+
826
+ ingester = MonorepoIngester(_make_store())
827
+ assert ingester._read_package_deps("unknown", tmp_path) == []
828
+
829
+ def test_dependency_edges_scoped_package(self, tmp_path):
830
+ from navegador.monorepo import MonorepoIngester, WorkspaceConfig
831
+
832
+ store = _make_store()
833
+ ingester = MonorepoIngester(store)
834
+
835
+ pkg_a = tmp_path / "pkg_a"
836
+ pkg_b = tmp_path / "pkg_b"
837
+ pkg_a.mkdir()
838
+ pkg_b.mkdir()
839
+
840
+ _write(pkg_a / "package.json", json.dumps({
841
+ "dependencies": {"@scope/pkg_b": "^1.0"}
842
+ }))
843
+
844
+ config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_a, pkg_b])
845
+ packages = [("pkg_a", pkg_a), ("pkg_b", pkg_b)]
846
+
847
+ ingester._create_dependency_edges(config, packages)
848
+ # store.create_edge should have been called at least once for the dependency
849
+ # (pkg_b matches the bare name)
850
+ store.create_edge.assert_called()
851
+
852
+ def test_dependency_edges_exception_logged(self, tmp_path):
853
+ from navegador.monorepo import MonorepoIngester, WorkspaceConfig
854
+
855
+ store = _make_store()
856
+ store.create_edge.side_effect = Exception("DB error")
857
+ ingester = MonorepoIngester(store)
858
+
859
+ pkg_a = tmp_path / "pkg_a"
860
+ pkg_b = tmp_path / "pkg_b"
861
+ pkg_a.mkdir()
862
+ pkg_b.mkdir()
863
+ _write(pkg_a / "package.json", json.dumps({"dependencies": {"pkg_b": "^1"}}))
864
+
865
+ config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_a, pkg_b])
866
+ packages = [("pkg_a", pkg_a), ("pkg_b", pkg_b)]
867
+ # Should not raise
868
+ count = ingester._create_dependency_edges(config, packages)
869
+ assert count == 0
870
+
871
+
872
+# ===========================================================================
873
+# navegador.pm (79% → target ~90%)
874
+# ===========================================================================
875
+
876
+
877
+class TestTicketIngester:
878
+ """Cover lines 243-245, 261-287."""
879
+
880
+ def _make(self):
881
+ from navegador.pm import TicketIngester
882
+
883
+ return TicketIngester(_make_store())
884
+
885
+ def test_ingest_linear_raises(self):
886
+ ing = self._make()
887
+ with pytest.raises(NotImplementedError, match="Linear"):
888
+ ing.ingest_linear(api_key="lin_xxx")
889
+
890
+ def test_ingest_jira_raises(self):
891
+ ing = self._make()
892
+ with pytest.raises(NotImplementedError, match="Jira"):
893
+ ing.ingest_jira(url="https://co.atlassian.net", token="tok")
894
+
895
+ def test_github_severity_critical(self):
896
+ from navegador.pm import TicketIngester
897
+
898
+ assert TicketIngester._github_severity(["critical"]) == "critical"
899
+ assert TicketIngester._github_severity(["blocker"]) == "critical"
900
+ assert TicketIngester._github_severity(["p0", "other"]) == "critical"
901
+
902
+ def test_github_severity_warning(self):
903
+ from navegador.pm import TicketIngester
904
+
905
+ assert TicketIngester._github_severity(["bug"]) == "warning"
906
+ assert TicketIngester._github_severity(["high"]) == "warning"
907
+ assert TicketIngester._github_severity(["important"]) == "warning"
908
+
909
+ def test_github_severity_info(self):
910
+ from navegador.pm import TicketIngester
911
+
912
+ assert TicketIngester._github_severity([]) == "info"
913
+ assert TicketIngester._github_severity(["enhancement"]) == "info"
914
+
915
+ def test_link_to_code_returns_zero_on_empty_graph(self):
916
+ from navegador.pm import TicketIngester
917
+
918
+ store = _make_store()
919
+ store.query.return_value = MagicMock(result_set=[])
920
+ ing = TicketIngester(store)
921
+ result = ing._link_to_code("myrepo")
922
+ assert result == 0
923
+
924
+ def test_link_to_code_returns_zero_on_query_failure(self):
925
+ from navegador.pm import TicketIngester
926
+
927
+ store = _make_store()
928
+ store.query.side_effect = Exception("DB down")
929
+ ing = TicketIngester(store)
930
+ result = ing._link_to_code("myrepo")
931
+ assert result == 0
932
+
933
+ def test_link_to_code_matches_tokens(self):
934
+ from navegador.pm import TicketIngester
935
+
936
+ store = _make_store()
937
+ # First call: tickets
938
+ ticket_result = MagicMock()
939
+ ticket_result.result_set = [("#1: authenticate user", "fix auth flow")]
940
+ # Second call: code nodes
941
+ code_result = MagicMock()
942
+ code_result.result_set = [("Function", "authenticate"), ("Function", "unrelated")]
943
+ store.query.side_effect = [ticket_result, code_result, None]
944
+
945
+ ing = TicketIngester(store)
946
+ result = ing._link_to_code("myrepo")
947
+ assert result >= 1
948
+
949
+ def test_link_to_code_skips_short_tokens(self):
950
+ from navegador.pm import TicketIngester
951
+
952
+ store = _make_store()
953
+ ticket_result = MagicMock()
954
+ # Only short words (< 4 chars)
955
+ ticket_result.result_set = [("#1: fix", "x")]
956
+ code_result = MagicMock()
957
+ code_result.result_set = [("Function", "fix")]
958
+ store.query.side_effect = [ticket_result, code_result]
959
+
960
+ ing = TicketIngester(store)
961
+ # "fix" is exactly 3 chars → skipped as a token
962
+ result = ing._link_to_code("myrepo")
963
+ assert result == 0
964
+
965
+ def test_ingest_github_issues_http_error(self):
966
+ from navegador.pm import TicketIngester
967
+
968
+ store = _make_store()
969
+ ing = TicketIngester(store)
970
+ with patch("urllib.request.urlopen", side_effect=Exception("network err")):
971
+ with pytest.raises(RuntimeError, match="Failed to fetch"):
972
+ ing.ingest_github_issues("owner/repo", token="tok")
973
+
974
+ def test_ingest_github_issues_success(self):
975
+ from navegador.pm import TicketIngester
976
+
977
+ store = _make_store()
978
+ ing = TicketIngester(store)
979
+ issues = [
980
+ {"number": 1, "title": "Fix auth", "body": "desc", "html_url": "http://x",
981
+ "labels": [{"name": "bug"}], "assignees": [{"login": "alice"}]},
982
+ ]
983
+ mock_resp = MagicMock()
984
+ mock_resp.__enter__ = MagicMock(return_value=mock_resp)
985
+ mock_resp.__exit__ = MagicMock(return_value=False)
986
+ mock_resp.read.return_value = json.dumps(issues).encode()
987
+
988
+ with patch("urllib.request.urlopen", return_value=mock_resp):
989
+ stats = ing.ingest_github_issues("owner/repo")
990
+ assert stats["tickets"] == 1
991
+
992
+
993
+# ===========================================================================
994
+# navegador.completions — install path (lines 66-70)
995
+# ===========================================================================
996
+
997
+
998
+class TestGetInstallInstruction:
999
+ def test_bash(self):
1000
+ from navegador.completions import get_install_instruction
1001
+
1002
+ instruction = get_install_instruction("bash")
1003
+ assert "~/.bashrc" in instruction
1004
+ assert "bash_source" in instruction
1005
+
1006
+ def test_zsh(self):
1007
+ from navegador.completions import get_install_instruction
1008
+
1009
+ instruction = get_install_instruction("zsh")
1010
+ assert "~/.zshrc" in instruction
1011
+ assert "zsh_source" in instruction
1012
+
1013
+ def test_fish(self):
1014
+ from navegador.completions import get_install_instruction
1015
+
1016
+ instruction = get_install_instruction("fish")
1017
+ assert "config.fish" in instruction
1018
+ assert "fish_source" in instruction
1019
+
1020
+ def test_invalid_raises(self):
1021
+ from navegador.completions import get_install_instruction
1022
+
1023
+ with pytest.raises(ValueError, match="Unsupported"):
1024
+ get_install_instruction("pwsh")
1025
+
1026
+
1027
+# ===========================================================================
1028
+# navegador.cli.commands — watch callback (lines ~179-185)
1029
+# ===========================================================================
1030
+
1031
+
1032
+class TestIngestWatchCallback:
1033
+ """Exercise the _on_cycle callback inside the watch branch of ingest."""
1034
+
1035
+ def test_watch_callback_with_changed_files(self, tmp_path):
1036
+ from click.testing import CliRunner
1037
+
1038
+ from navegador.cli.commands import main
1039
+
1040
+ runner = CliRunner()
1041
+ repo = str(tmp_path)
1042
+
1043
+ cycle_calls = []
1044
+
1045
+ def fake_watch(path, interval=2.0, callback=None):
1046
+ if callback:
1047
+ # Simulate a cycle with changed files
1048
+ result = callback({"files": 3, "skipped": 10})
1049
+ cycle_calls.append(result)
1050
+ # Second call: simulate KeyboardInterrupt to exit
1051
+ raise KeyboardInterrupt
1052
+
1053
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1054
+ mock_gs.return_value = _make_store()
1055
+ with patch("navegador.ingestion.RepoIngester") as MockRI:
1056
+ MockRI.return_value.watch.side_effect = fake_watch
1057
+ result = runner.invoke(main, ["ingest", repo, "--watch", "--interval", "1"])
1058
+
1059
+ assert result.exit_code == 0
1060
+ assert cycle_calls == [True]
1061
+
1062
+ def test_watch_callback_no_changed_files(self, tmp_path):
1063
+ """Callback with 0 changed files should still return True."""
1064
+ from click.testing import CliRunner
1065
+
1066
+ from navegador.cli.commands import main
1067
+
1068
+ runner = CliRunner()
1069
+
1070
+ def fake_watch(path, interval=2.0, callback=None):
1071
+ if callback:
1072
+ result = callback({"files": 0, "skipped": 5})
1073
+ assert result is True
1074
+ raise KeyboardInterrupt
1075
+
1076
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1077
+ mock_gs.return_value = _make_store()
1078
+ with patch("navegador.ingestion.RepoIngester") as MockRI:
1079
+ MockRI.return_value.watch.side_effect = fake_watch
1080
+ result = runner.invoke(main, ["ingest", str(tmp_path), "--watch"])
1081
+
1082
+ assert result.exit_code == 0
1083
+
1084
+
1085
+# ===========================================================================
1086
+# navegador.cli.commands — additional uncovered CLI branches
1087
+# ===========================================================================
1088
+
1089
+
1090
+class TestCLIBranchesDeadcode:
1091
+ """Cover lines 1395-1407 (unreachable_classes and orphan_files branches)."""
1092
+
1093
+ def test_deadcode_shows_unreachable_classes(self, tmp_path):
1094
+ from click.testing import CliRunner
1095
+
1096
+ from navegador.cli.commands import main
1097
+ from navegador.analysis.deadcode import DeadCodeReport
1098
+
1099
+ runner = CliRunner()
1100
+ report = DeadCodeReport(
1101
+ unreachable_functions=[],
1102
+ unreachable_classes=[
1103
+ {"name": "OldClass", "file_path": "old.py", "line_start": 1, "type": "Class"}
1104
+ ],
1105
+ orphan_files=["orphan.py"],
1106
+ )
1107
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1108
+ mock_gs.return_value = _make_store()
1109
+ with patch("navegador.analysis.deadcode.DeadCodeDetector") as MockDC:
1110
+ MockDC.return_value.detect.return_value = report
1111
+ result = runner.invoke(main, ["deadcode"])
1112
+ assert result.exit_code == 0
1113
+ assert "OldClass" in result.output
1114
+ assert "orphan.py" in result.output
1115
+
1116
+ def test_deadcode_no_dead_code_message(self, tmp_path):
1117
+ from click.testing import CliRunner
1118
+
1119
+ from navegador.cli.commands import main
1120
+ from navegador.analysis.deadcode import DeadCodeReport
1121
+
1122
+ runner = CliRunner()
1123
+ report = DeadCodeReport(
1124
+ unreachable_functions=[], unreachable_classes=[], orphan_files=[]
1125
+ )
1126
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1127
+ mock_gs.return_value = _make_store()
1128
+ with patch("navegador.analysis.deadcode.DeadCodeDetector") as MockDC:
1129
+ MockDC.return_value.detect.return_value = report
1130
+ result = runner.invoke(main, ["deadcode"])
1131
+ assert result.exit_code == 0
1132
+ assert "No dead code" in result.output
1133
+
1134
+
1135
+class TestCLIBranchesTestmap:
1136
+ """Cover lines 1439-1452 (testmap table and unmatched branches)."""
1137
+
1138
+ def test_testmap_shows_table_and_unmatched(self):
1139
+ from click.testing import CliRunner
1140
+
1141
+ from navegador.cli.commands import main
1142
+ from navegador.analysis.testmap import TestMapResult, TestLink
1143
+
1144
+ runner = CliRunner()
1145
+ link = TestLink(
1146
+ test_name="test_foo",
1147
+ test_file="test_foo.py",
1148
+ prod_name="foo",
1149
+ prod_file="foo.py",
1150
+ prod_type="Function",
1151
+ source="name",
1152
+ )
1153
+ result_obj = TestMapResult(
1154
+ links=[link],
1155
+ unmatched_tests=[{"name": "test_orphan", "file_path": "test_x.py"}],
1156
+ edges_created=1,
1157
+ )
1158
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1159
+ mock_gs.return_value = _make_store()
1160
+ with patch("navegador.analysis.testmap.TestMapper") as MockTM:
1161
+ MockTM.return_value.map_tests.return_value = result_obj
1162
+ result = runner.invoke(main, ["testmap"])
1163
+ assert result.exit_code == 0
1164
+ assert "test_foo" in result.output
1165
+ assert "test_orphan" in result.output
1166
+
1167
+
1168
+class TestCLIBranchesRename:
1169
+ """Cover lines 1640-1650 (rename non-JSON output)."""
1170
+
1171
+ def test_rename_preview_output(self):
1172
+ from click.testing import CliRunner
1173
+
1174
+ from navegador.cli.commands import main
1175
+ from navegador.refactor import RenameResult
1176
+
1177
+ runner = CliRunner()
1178
+ rename_result = RenameResult(
1179
+ old_name="old_func",
1180
+ new_name="new_func",
1181
+ affected_nodes=[{"name": "old_func", "file_path": "f.py", "type": "Function", "line_start": 1}],
1182
+ affected_files=["f.py", "g.py"],
1183
+ edges_updated=3,
1184
+ )
1185
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1186
+ mock_gs.return_value = _make_store()
1187
+ with patch("navegador.refactor.SymbolRenamer") as MockSR:
1188
+ MockSR.return_value.preview_rename.return_value = rename_result
1189
+ result = runner.invoke(main, ["rename", "old_func", "new_func", "--preview"])
1190
+ assert result.exit_code == 0
1191
+ assert "old_func" in result.output
1192
+ assert "f.py" in result.output
1193
+
1194
+ def test_rename_apply_output(self):
1195
+ from click.testing import CliRunner
1196
+
1197
+ from navegador.cli.commands import main
1198
+ from navegador.refactor import RenameResult
1199
+
1200
+ runner = CliRunner()
1201
+ rename_result = RenameResult(
1202
+ old_name="old_func",
1203
+ new_name="new_func",
1204
+ affected_nodes=[],
1205
+ affected_files=[],
1206
+ edges_updated=0,
1207
+ )
1208
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1209
+ mock_gs.return_value = _make_store()
1210
+ with patch("navegador.refactor.SymbolRenamer") as MockSR:
1211
+ MockSR.return_value.apply_rename.return_value = rename_result
1212
+ result = runner.invoke(main, ["rename", "old_func", "new_func"])
1213
+ assert result.exit_code == 0
1214
+ assert "Renamed" in result.output
1215
+
1216
+
1217
+class TestCLIBranchesSemantic:
1218
+ """Cover lines 2068-2080 (semantic-search table output)."""
1219
+
1220
+ def test_semantic_search_table_output(self):
1221
+ from click.testing import CliRunner
1222
+
1223
+ from navegador.cli.commands import main
1224
+
1225
+ runner = CliRunner()
1226
+ search_results = [
1227
+ {"score": 0.95, "type": "Function", "name": "authenticate", "file_path": "auth.py"},
1228
+ ]
1229
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1230
+ mock_gs.return_value = _make_store()
1231
+ with patch("navegador.intelligence.search.SemanticSearch") as MockSS:
1232
+ MockSS.return_value.search.return_value = search_results
1233
+ with patch("navegador.llm.auto_provider") as mock_ap:
1234
+ mock_ap.return_value = MagicMock()
1235
+ result = runner.invoke(main, ["semantic-search", "auth tokens"])
1236
+ assert result.exit_code == 0
1237
+ assert "authenticate" in result.output
1238
+
1239
+ def test_semantic_search_no_results(self):
1240
+ from click.testing import CliRunner
1241
+
1242
+ from navegador.cli.commands import main
1243
+
1244
+ runner = CliRunner()
1245
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1246
+ mock_gs.return_value = _make_store()
1247
+ with patch("navegador.intelligence.search.SemanticSearch") as MockSS:
1248
+ MockSS.return_value.search.return_value = []
1249
+ with patch("navegador.llm.auto_provider") as mock_ap:
1250
+ mock_ap.return_value = MagicMock()
1251
+ result = runner.invoke(main, ["semantic-search", "nothing"])
1252
+ assert result.exit_code == 0
1253
+ assert "--index" in result.output
1254
+
1255
+ def test_semantic_search_with_index_flag(self):
1256
+ from click.testing import CliRunner
1257
+
1258
+ from navegador.cli.commands import main
1259
+
1260
+ runner = CliRunner()
1261
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1262
+ mock_gs.return_value = _make_store()
1263
+ with patch("navegador.intelligence.search.SemanticSearch") as MockSS:
1264
+ inst = MockSS.return_value
1265
+ inst.index.return_value = 42
1266
+ inst.search.return_value = []
1267
+ with patch("navegador.llm.auto_provider") as mock_ap:
1268
+ mock_ap.return_value = MagicMock()
1269
+ result = runner.invoke(main, ["semantic-search", "auth", "--index"])
1270
+ assert result.exit_code == 0
1271
+ assert "42" in result.output
1272
+
1273
+
1274
+class TestCLIBranchesRepoCommands:
1275
+ """Cover lines 1539-1572 (repo list/ingest-all table output)."""
1276
+
1277
+ def test_repo_list_table_output(self):
1278
+ from click.testing import CliRunner
1279
+
1280
+ from navegador.cli.commands import main
1281
+
1282
+ runner = CliRunner()
1283
+ repos = [{"name": "myrepo", "path": "/path/to/myrepo"}]
1284
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1285
+ mock_gs.return_value = _make_store()
1286
+ with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1287
+ MockMRM.return_value.list_repos.return_value = repos
1288
+ result = runner.invoke(main, ["repo", "list"])
1289
+ assert result.exit_code == 0
1290
+ assert "myrepo" in result.output
1291
+
1292
+ def test_repo_list_empty(self):
1293
+ from click.testing import CliRunner
1294
+
1295
+ from navegador.cli.commands import main
1296
+
1297
+ runner = CliRunner()
1298
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1299
+ mock_gs.return_value = _make_store()
1300
+ with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1301
+ MockMRM.return_value.list_repos.return_value = []
1302
+ result = runner.invoke(main, ["repo", "list"])
1303
+ assert result.exit_code == 0
1304
+ assert "No repositories" in result.output
1305
+
1306
+ def test_repo_ingest_all_table(self):
1307
+ from click.testing import CliRunner
1308
+
1309
+ from navegador.cli.commands import main
1310
+
1311
+ runner = CliRunner()
1312
+ summary = {"myrepo": {"files": 5, "functions": 10, "classes": 2}}
1313
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1314
+ mock_gs.return_value = _make_store()
1315
+ with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1316
+ MockMRM.return_value.ingest_all.return_value = summary
1317
+ result = runner.invoke(main, ["repo", "ingest-all"])
1318
+ assert result.exit_code == 0
1319
+ assert "myrepo" in result.output
1320
+
1321
+ def test_repo_search_table(self):
1322
+ from click.testing import CliRunner
1323
+
1324
+ from navegador.cli.commands import main
1325
+
1326
+ runner = CliRunner()
1327
+ results = [{"label": "Function", "name": "foo", "file_path": "foo.py"}]
1328
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1329
+ mock_gs.return_value = _make_store()
1330
+ with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1331
+ MockMRM.return_value.cross_repo_search.return_value = results
1332
+ result = runner.invoke(main, ["repo", "search", "foo"])
1333
+ assert result.exit_code == 0
1334
+ assert "foo" in result.output
1335
+
1336
+ def test_repo_search_empty(self):
1337
+ from click.testing import CliRunner
1338
+
1339
+ from navegador.cli.commands import main
1340
+
1341
+ runner = CliRunner()
1342
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1343
+ mock_gs.return_value = _make_store()
1344
+ with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1345
+ MockMRM.return_value.cross_repo_search.return_value = []
1346
+ result = runner.invoke(main, ["repo", "search", "nothing"])
1347
+ assert result.exit_code == 0
1348
+ assert "No results" in result.output
1349
+
1350
+
1351
+class TestCLIBranchesPM:
1352
+ """Cover lines 1793-1806 (pm ingest output)."""
1353
+
1354
+ def test_pm_ingest_no_github_raises(self):
1355
+ from click.testing import CliRunner
1356
+
1357
+ from navegador.cli.commands import main
1358
+
1359
+ runner = CliRunner()
1360
+ result = runner.invoke(main, ["pm", "ingest"])
1361
+ assert result.exit_code != 0
1362
+
1363
+ def test_pm_ingest_table_output(self):
1364
+ from click.testing import CliRunner
1365
+
1366
+ from navegador.cli.commands import main
1367
+
1368
+ runner = CliRunner()
1369
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1370
+ mock_gs.return_value = _make_store()
1371
+ with patch("navegador.pm.TicketIngester") as MockTI:
1372
+ MockTI.return_value.ingest_github_issues.return_value = {
1373
+ "tickets": 5, "linked": 2
1374
+ }
1375
+ result = runner.invoke(main, ["pm", "ingest", "--github", "owner/repo"])
1376
+ assert result.exit_code == 0
1377
+ assert "5" in result.output
1378
+
1379
+ def test_pm_ingest_json(self):
1380
+ from click.testing import CliRunner
1381
+
1382
+ from navegador.cli.commands import main
1383
+
1384
+ runner = CliRunner()
1385
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1386
+ mock_gs.return_value = _make_store()
1387
+ with patch("navegador.pm.TicketIngester") as MockTI:
1388
+ MockTI.return_value.ingest_github_issues.return_value = {
1389
+ "tickets": 3, "linked": 1
1390
+ }
1391
+ result = runner.invoke(
1392
+ main, ["pm", "ingest", "--github", "owner/repo", "--json"]
1393
+ )
1394
+ assert result.exit_code == 0
1395
+ data = json.loads(result.output)
1396
+ assert data["tickets"] == 3
1397
+
1398
+
1399
+class TestCLIBranchesIngest:
1400
+ """Cover lines 179-185 (ingest --monorepo table output)."""
1401
+
1402
+ def test_ingest_monorepo_table(self, tmp_path):
1403
+ from click.testing import CliRunner
1404
+
1405
+ from navegador.cli.commands import main
1406
+
1407
+ runner = CliRunner()
1408
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1409
+ mock_gs.return_value = _make_store()
1410
+ with patch("navegador.monorepo.MonorepoIngester") as MockMI:
1411
+ MockMI.return_value.ingest.return_value = {
1412
+ "files": 10, "functions": 20, "packages": 3, "workspace_type": "yarn"
1413
+ }
1414
+ result = runner.invoke(
1415
+ main, ["ingest", str(tmp_path), "--monorepo"]
1416
+ )
1417
+ assert result.exit_code == 0
1418
+
1419
+ def test_ingest_monorepo_json(self, tmp_path):
1420
+ from click.testing import CliRunner
1421
+
1422
+ from navegador.cli.commands import main
1423
+
1424
+ runner = CliRunner()
1425
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1426
+ mock_gs.return_value = _make_store()
1427
+ with patch("navegador.monorepo.MonorepoIngester") as MockMI:
1428
+ MockMI.return_value.ingest.return_value = {
1429
+ "files": 5, "packages": 2
1430
+ }
1431
+ result = runner.invoke(
1432
+ main, ["ingest", str(tmp_path), "--monorepo", "--json"]
1433
+ )
1434
+ assert result.exit_code == 0
1435
+ data = json.loads(result.output)
1436
+ assert data["files"] == 5
1437
+
1438
+
1439
+class TestCLIBranchesSubmodulesIngest:
1440
+ """Cover lines 1901-1916 (submodules ingest output)."""
1441
+
1442
+ def test_submodules_ingest_output(self, tmp_path):
1443
+ from click.testing import CliRunner
1444
+
1445
+ from navegador.cli.commands import main
1446
+
1447
+ runner = CliRunner()
1448
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1449
+ mock_gs.return_value = _make_store()
1450
+ with patch("navegador.submodules.SubmoduleIngester") as MockSI:
1451
+ MockSI.return_value.ingest_with_submodules.return_value = {
1452
+ "total_files": 10,
1453
+ "submodules": {"sub1": {}, "sub2": {}},
1454
+ }
1455
+ result = runner.invoke(main, ["submodules", "ingest", str(tmp_path)])
1456
+ assert result.exit_code == 0
1457
+ assert "sub1" in result.output
1458
+
1459
+ def test_submodules_ingest_json(self, tmp_path):
1460
+ from click.testing import CliRunner
1461
+
1462
+ from navegador.cli.commands import main
1463
+
1464
+ runner = CliRunner()
1465
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1466
+ mock_gs.return_value = _make_store()
1467
+ with patch("navegador.submodules.SubmoduleIngester") as MockSI:
1468
+ MockSI.return_value.ingest_with_submodules.return_value = {
1469
+ "total_files": 5,
1470
+ "submodules": {},
1471
+ }
1472
+ result = runner.invoke(
1473
+ main, ["submodules", "ingest", str(tmp_path), "--json"]
1474
+ )
1475
+ assert result.exit_code == 0
1476
+ data = json.loads(result.output)
1477
+ assert data["total_files"] == 5
1478
+
1479
+
1480
+class TestCLIBranchesCommunities:
1481
+ """Cover lines 2105-2141 (communities store-labels + table)."""
1482
+
1483
+ def test_communities_store_labels(self):
1484
+ from click.testing import CliRunner
1485
+
1486
+ from navegador.cli.commands import main
1487
+ from navegador.intelligence.community import Community
1488
+
1489
+ runner = CliRunner()
1490
+ comm = Community(name="c1", members=["a", "b", "c"], density=0.5)
1491
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1492
+ mock_gs.return_value = _make_store()
1493
+ with patch("navegador.intelligence.community.CommunityDetector") as MockCD:
1494
+ inst = MockCD.return_value
1495
+ inst.detect.return_value = [comm]
1496
+ inst.store_communities.return_value = 3
1497
+ result = runner.invoke(main, ["communities", "--store-labels"])
1498
+ assert result.exit_code == 0
1499
+ assert "3" in result.output
1500
+
1501
+ def test_communities_empty(self):
1502
+ from click.testing import CliRunner
1503
+
1504
+ from navegador.cli.commands import main
1505
+
1506
+ runner = CliRunner()
1507
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1508
+ mock_gs.return_value = _make_store()
1509
+ with patch("navegador.intelligence.community.CommunityDetector") as MockCD:
1510
+ MockCD.return_value.detect.return_value = []
1511
+ result = runner.invoke(main, ["communities"])
1512
+ assert result.exit_code == 0
1513
+ assert "No communities" in result.output
1514
+
1515
+ def test_communities_large_preview(self):
1516
+ from click.testing import CliRunner
1517
+
1518
+ from navegador.cli.commands import main
1519
+ from navegador.intelligence.community import Community
1520
+
1521
+ runner = CliRunner()
1522
+ comm = Community(name="big", members=list("abcdefgh"), density=0.8)
1523
+ with patch("navegador.cli.commands._get_store") as mock_gs:
1524
+ mock_gs.return_value = _make_store()
1525
+ with patch("navegador.intelligence.community.CommunityDetector") as MockCD:
1526
+ MockCD.return_value.detect.return_value = [comm]
1527
+ result = runner.invoke(main, ["communities"])
1528
+ assert result.exit_code == 0
1529
+ assert "+" in result.output # preview truncation
1530
+
1531
+
1532
+# ===========================================================================
1533
+# navegador.cluster.messaging (86% → target ~95%)
1534
+# ===========================================================================
1535
+
1536
+
1537
+class TestMessageBus:
1538
+ """Cover lines 74-78, 154, 178-182, 208."""
1539
+
1540
+ def _make(self):
1541
+ from navegador.cluster.messaging import MessageBus
1542
+
1543
+ r = MagicMock()
1544
+ r.smembers.return_value = set()
1545
+ r.lrange.return_value = []
1546
+ return MessageBus("redis://localhost:6379", _redis_client=r), r
1547
+
1548
+ def test_send_returns_message_id(self):
1549
+ bus, r = self._make()
1550
+ msg_id = bus.send("agent1", "agent2", "task", {"k": "v"})
1551
+ assert isinstance(msg_id, str) and len(msg_id) == 36
1552
+ r.rpush.assert_called_once()
1553
+
1554
+ def test_receive_returns_unacked_messages(self):
1555
+ import json as _json
1556
+ from navegador.cluster.messaging import Message
1557
+ import time
1558
+
1559
+ bus, r = self._make()
1560
+ msg = Message(
1561
+ id="abc123", from_agent="a1", to_agent="a2",
1562
+ type="task", payload={}, timestamp=time.time()
1563
+ )
1564
+ r.lrange.return_value = [_json.dumps(msg.to_dict()).encode()]
1565
+ r.smembers.return_value = set()
1566
+ messages = bus.receive("a2")
1567
+ assert len(messages) == 1
1568
+ assert messages[0].id == "abc123"
1569
+
1570
+ def test_receive_filters_acked(self):
1571
+ import json as _json
1572
+ from navegador.cluster.messaging import Message
1573
+ import time
1574
+
1575
+ bus, r = self._make()
1576
+ msg = Message(
1577
+ id="acked_id", from_agent="a1", to_agent="a2",
1578
+ type="task", payload={}, timestamp=time.time()
1579
+ )
1580
+ r.lrange.return_value = [_json.dumps(msg.to_dict()).encode()]
1581
+ r.smembers.return_value = {b"acked_id"}
1582
+ messages = bus.receive("a2")
1583
+ assert messages == []
1584
+
1585
+ def test_acknowledge_with_agent_id(self):
1586
+ bus, r = self._make()
1587
+ bus.acknowledge("msg123", agent_id="a2")
1588
+ r.sadd.assert_called()
1589
+
1590
+ def test_acknowledge_without_agent_id_broadcasts(self):
1591
+ bus, r = self._make()
1592
+ r.smembers.return_value = {b"agent1", b"agent2"}
1593
+ bus.acknowledge("msg123")
1594
+ assert r.sadd.call_count == 2
1595
+
1596
+ def test_broadcast_skips_sender(self):
1597
+ bus, r = self._make()
1598
+ r.smembers.return_value = {b"sender", b"agent2", b"agent3"}
1599
+ with patch.object(bus, "send", return_value="mid") as mock_send:
1600
+ ids = bus.broadcast("sender", "task", {})
1601
+ assert len(ids) == 2
1602
+ for call_args in mock_send.call_args_list:
1603
+ assert call_args[0][0] == "sender"
1604
+ assert call_args[0][1] != "sender"
1605
+
1606
+ def test_client_lazy_init_raises_on_missing_redis(self):
1607
+ from navegador.cluster.messaging import MessageBus
1608
+
1609
+ bus = MessageBus("redis://localhost:6379")
1610
+ with patch.dict("sys.modules", {"redis": None}):
1611
+ with pytest.raises(ImportError, match="redis"):
1612
+ bus._client()
1613
+
1614
+
1615
+# ===========================================================================
1616
+# navegador.cluster.locking (90% → target ~97%)
1617
+# ===========================================================================
1618
+
1619
+
1620
+class TestDistributedLock:
1621
+ """Cover lines 72-76, 120."""
1622
+
1623
+ def _make(self):
1624
+ from navegador.cluster.locking import DistributedLock
1625
+
1626
+ r = MagicMock()
1627
+ return DistributedLock("redis://localhost:6379", "test-lock", _redis_client=r), r
1628
+
1629
+ def test_acquire_success(self):
1630
+ lock, r = self._make()
1631
+ r.set.return_value = True
1632
+ assert lock.acquire() is True
1633
+ assert lock._token is not None
1634
+
1635
+ def test_acquire_failure(self):
1636
+ lock, r = self._make()
1637
+ r.set.return_value = None
1638
+ assert lock.acquire() is False
1639
+
1640
+ def test_release_when_token_matches(self):
1641
+ lock, r = self._make()
1642
+ lock._token = "mytoken"
1643
+ r.get.return_value = b"mytoken"
1644
+ lock.release()
1645
+ r.delete.assert_called_once()
1646
+ assert lock._token is None
1647
+
1648
+ def test_release_when_token_not_held(self):
1649
+ lock, r = self._make()
1650
+ lock._token = None
1651
+ lock.release()
1652
+ r.delete.assert_not_called()
1653
+
1654
+ def test_release_when_stored_token_differs(self):
1655
+ lock, r = self._make()
1656
+ lock._token = "mytoken"
1657
+ r.get.return_value = b"other_token"
1658
+ lock.release()
1659
+ r.delete.assert_not_called()
1660
+
1661
+ def test_context_manager_acquires_and_releases(self):
1662
+ lock, r = self._make()
1663
+ r.set.return_value = True
1664
+ r.get.return_value = None # simulate already released
1665
+ with lock:
1666
+ pass
1667
+ assert lock._token is None
1668
+
1669
+ def test_context_manager_raises_on_timeout(self):
1670
+ import time
1671
+ from navegador.cluster.locking import LockTimeout
1672
+
1673
+ lock, r = self._make()
1674
+ r.set.return_value = None # never acquired
1675
+ lock._timeout = 0
1676
+ lock._retry_interval = 0
1677
+
1678
+ with pytest.raises(LockTimeout):
1679
+ with lock:
1680
+ pass
1681
+
1682
+ def test_client_lazy_init_raises_on_missing_redis(self):
1683
+ from navegador.cluster.locking import DistributedLock
1684
+
1685
+ lock = DistributedLock("redis://localhost:6379", "x")
1686
+ with patch.dict("sys.modules", {"redis": None}):
1687
+ with pytest.raises(ImportError, match="redis"):
1688
+ lock._client()
1689
+
1690
+
1691
+# ===========================================================================
1692
+# navegador.cluster.observability (89% → target ~97%)
1693
+# ===========================================================================
1694
+
1695
+
1696
+class TestSwarmDashboard:
1697
+ """Cover lines 44-48, 93, 108, 160."""
1698
+
1699
+ def _make(self):
1700
+ from navegador.cluster.observability import SwarmDashboard
1701
+
1702
+ r = MagicMock()
1703
+ r.keys.return_value = []
1704
+ r.get.return_value = None
1705
+ return SwarmDashboard("redis://localhost:6379", _redis_client=r), r
1706
+
1707
+ def test_register_agent(self):
1708
+ dash, r = self._make()
1709
+ dash.register_agent("agent1", metadata={"role": "ingester"})
1710
+ r.setex.assert_called_once()
1711
+
1712
+ def test_register_agent_no_metadata(self):
1713
+ dash, r = self._make()
1714
+ dash.register_agent("agent1")
1715
+ r.setex.assert_called_once()
1716
+
1717
+ def test_agent_status_empty(self):
1718
+ dash, r = self._make()
1719
+ r.keys.return_value = []
1720
+ agents = dash.agent_status()
1721
+ assert agents == []
1722
+
1723
+ def test_agent_status_returns_active_agents(self):
1724
+ import json as _json
1725
+ dash, r = self._make()
1726
+ payload = {"agent_id": "a1", "last_seen": 12345, "state": "active"}
1727
+ r.keys.return_value = [b"navegador:obs:agent:a1"]
1728
+ r.get.return_value = _json.dumps(payload).encode()
1729
+ agents = dash.agent_status()
1730
+ assert len(agents) == 1
1731
+ assert agents[0]["agent_id"] == "a1"
1732
+
1733
+ def test_task_metrics_default(self):
1734
+ dash, r = self._make()
1735
+ r.get.return_value = None
1736
+ metrics = dash.task_metrics()
1737
+ assert metrics == {"pending": 0, "active": 0, "completed": 0, "failed": 0}
1738
+
1739
+ def test_task_metrics_from_redis(self):
1740
+ import json as _json
1741
+ dash, r = self._make()
1742
+ stored = {"pending": 3, "active": 1, "completed": 10, "failed": 0}
1743
+ r.get.return_value = _json.dumps(stored).encode()
1744
+ metrics = dash.task_metrics()
1745
+ assert metrics["pending"] == 3
1746
+
1747
+ def test_update_task_metrics(self):
1748
+ import json as _json
1749
+ dash, r = self._make()
1750
+ r.get.return_value = _json.dumps(
1751
+ {"pending": 0, "active": 0, "completed": 0, "failed": 0}
1752
+ ).encode()
1753
+ dash.update_task_metrics(pending=5, active=2)
1754
+ r.set.assert_called_once()
1755
+
1756
+ def test_graph_metrics(self):
1757
+ store = _make_store()
1758
+ store.node_count.return_value = 42
1759
+ store.edge_count.return_value = 100
1760
+ dash, r = self._make()
1761
+ result = dash.graph_metrics(store)
1762
+ assert result["node_count"] == 42
1763
+ assert result["edge_count"] == 100
1764
+
1765
+ def test_to_json_structure(self):
1766
+ import json as _json
1767
+ dash, r = self._make()
1768
+ r.keys.return_value = []
1769
+ r.get.side_effect = [None, None] # graph_meta + task_metrics
1770
+ snapshot = _json.loads(dash.to_json())
1771
+ assert "agents" in snapshot
1772
+ assert "task_metrics" in snapshot
1773
+
1774
+ def test_client_lazy_init_raises_on_missing_redis(self):
1775
+ from navegador.cluster.observability import SwarmDashboard
1776
+
1777
+ dash = SwarmDashboard("redis://localhost:6379")
1778
+ with patch.dict("sys.modules", {"redis": None}):
1779
+ with pytest.raises(ImportError, match="redis"):
1780
+ dash._client()
1781
+
1782
+
1783
+# ===========================================================================
1784
+# navegador.cluster.pubsub (86% → target ~97%)
1785
+# ===========================================================================
1786
+
1787
+
1788
+class TestGraphNotifier:
1789
+ """Cover lines 72-76, 159-162."""
1790
+
1791
+ def _make(self):
1792
+ from navegador.cluster.pubsub import GraphNotifier
1793
+
1794
+ r = MagicMock()
1795
+ pubsub_mock = MagicMock()
1796
+ pubsub_mock.listen.return_value = iter([])
1797
+ r.pubsub.return_value = pubsub_mock
1798
+ r.publish.return_value = 1
1799
+ return GraphNotifier("redis://localhost:6379", redis_client=r), r
1800
+
1801
+ def test_publish_returns_receiver_count(self):
1802
+ from navegador.cluster.pubsub import EventType
1803
+
1804
+ notifier, r = self._make()
1805
+ count = notifier.publish(EventType.NODE_CREATED, {"name": "foo"})
1806
+ assert count == 1
1807
+ r.publish.assert_called_once()
1808
+
1809
+ def test_publish_with_string_event_type(self):
1810
+ notifier, r = self._make()
1811
+ count = notifier.publish("custom_event", {"key": "val"})
1812
+ assert count == 1
1813
+
1814
+ def test_subscribe_run_in_thread(self):
1815
+ from navegador.cluster.pubsub import EventType
1816
+
1817
+ notifier, r = self._make()
1818
+ import json as _json
1819
+ import threading
1820
+
1821
+ # Return one message then stop
1822
+ msg = {
1823
+ "type": "message",
1824
+ "data": _json.dumps({"event_type": "node_created", "data": {"k": "v"}}).encode(),
1825
+ }
1826
+ r.pubsub.return_value.listen.return_value = iter([msg])
1827
+
1828
+ received = []
1829
+
1830
+ def callback(et, data):
1831
+ received.append((et, data))
1832
+
1833
+ t = notifier.subscribe([EventType.NODE_CREATED], callback, run_in_thread=True)
1834
+ assert isinstance(t, threading.Thread)
1835
+ t.join(timeout=2)
1836
+ assert received == [("node_created", {"k": "v"})]
1837
+
1838
+ def test_close(self):
1839
+ notifier, r = self._make()
1840
+ notifier.close()
1841
+ r.close.assert_called_once()
1842
+
1843
+ def test_close_ignores_exception(self):
1844
+ notifier, r = self._make()
1845
+ r.close.side_effect = Exception("closed")
1846
+ notifier.close() # should not raise
1847
+
1848
+ def test_connect_redis_raises_on_missing_dep(self):
1849
+ from navegador.cluster.pubsub import GraphNotifier
1850
+
1851
+ with patch.dict("sys.modules", {"redis": None}):
1852
+ with pytest.raises(ImportError, match="redis"):
1853
+ GraphNotifier._connect_redis("redis://localhost")
1854
+
1855
+
1856
+# ===========================================================================
1857
+# navegador.ingestion.ruby — extra branches (66% → target ~85%)
1858
+# ===========================================================================
1859
+
1860
+
1861
+class TestRubyParserBranches:
1862
+ """Exercise _handle_class superclass, _handle_module body, _maybe_handle_require,
1863
+ _extract_calls and fallback paths."""
1864
+
1865
+ def _make_parser(self):
1866
+ with _mock_ts("tree_sitter_ruby"):
1867
+ from navegador.ingestion.ruby import RubyParser
1868
+ return RubyParser()
1869
+
1870
+ def test_handle_class_with_superclass(self):
1871
+ with _mock_ts("tree_sitter_ruby"):
1872
+ from navegador.ingestion.ruby import RubyParser
1873
+
1874
+ parser = RubyParser()
1875
+ store = _make_store()
1876
+
1877
+ name_node = _text_node(b"MyClass")
1878
+ superclass_node = _text_node(b"< BaseClass", "constant")
1879
+ class_node = MockNode("class", children=[name_node, superclass_node])
1880
+ class_node.set_field("name", name_node)
1881
+ class_node.set_field("superclass", superclass_node)
1882
+
1883
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1884
+ parser._handle_class(class_node, b"class MyClass < BaseClass\nend", "f.rb", store, stats)
1885
+ assert stats["classes"] == 1
1886
+ assert stats["edges"] >= 2 # CONTAINS + INHERITS
1887
+
1888
+ def test_handle_class_no_name_node(self):
1889
+ with _mock_ts("tree_sitter_ruby"):
1890
+ from navegador.ingestion.ruby import RubyParser
1891
+
1892
+ parser = RubyParser()
1893
+ store = _make_store()
1894
+ anon_class = MockNode("class")
1895
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1896
+ parser._handle_class(anon_class, b"class; end", "f.rb", store, stats)
1897
+ assert stats["classes"] == 0
1898
+
1899
+ def test_handle_module_with_body(self):
1900
+ with _mock_ts("tree_sitter_ruby"):
1901
+ from navegador.ingestion.ruby import RubyParser
1902
+
1903
+ parser = RubyParser()
1904
+ store = _make_store()
1905
+
1906
+ name_node = _text_node(b"MyModule")
1907
+ method_name = _text_node(b"my_method")
1908
+ method_node = MockNode("method", children=[method_name])
1909
+ method_node.set_field("name", method_name)
1910
+ body_node = MockNode("body_statement", children=[method_node])
1911
+ mod_node = MockNode("module", children=[name_node, body_node])
1912
+ mod_node.set_field("name", name_node)
1913
+ # body found via body_statement child (no "body" field)
1914
+
1915
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1916
+ src = b"module MyModule\n def my_method; end\nend"
1917
+ parser._handle_module(mod_node, src, "f.rb", store, stats)
1918
+ assert stats["classes"] == 1
1919
+
1920
+ def test_handle_method_standalone_function(self):
1921
+ with _mock_ts("tree_sitter_ruby"):
1922
+ from navegador.ingestion.ruby import RubyParser
1923
+
1924
+ parser = RubyParser()
1925
+ store = _make_store()
1926
+
1927
+ name_node = _text_node(b"standalone_fn")
1928
+ fn_node = MockNode("method", children=[name_node])
1929
+ fn_node.set_field("name", name_node)
1930
+
1931
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1932
+ parser._handle_method(fn_node, b"def standalone_fn; end", "f.rb", store, stats, class_name=None)
1933
+ assert stats["functions"] == 1
1934
+ # No class → Function node
1935
+ create_call = store.create_node.call_args_list[-1]
1936
+ from navegador.graph.schema import NodeLabel
1937
+ assert create_call[0][0] == NodeLabel.Function
1938
+
1939
+ def test_handle_method_no_name(self):
1940
+ with _mock_ts("tree_sitter_ruby"):
1941
+ from navegador.ingestion.ruby import RubyParser
1942
+
1943
+ parser = RubyParser()
1944
+ store = _make_store()
1945
+ anon_method = MockNode("method")
1946
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1947
+ parser._handle_method(anon_method, b"", "f.rb", store, stats, class_name=None)
1948
+ assert stats["functions"] == 0
1949
+
1950
+ def test_maybe_handle_require(self):
1951
+ with _mock_ts("tree_sitter_ruby"):
1952
+ from navegador.ingestion.ruby import RubyParser
1953
+
1954
+ parser = RubyParser()
1955
+ store = _make_store()
1956
+
1957
+ method_node = _text_node(b"require", "identifier")
1958
+ string_node = _text_node(b"'json'", "string")
1959
+ args_node = MockNode("argument_list", children=[string_node])
1960
+ call_node = MockNode("call", children=[method_node, args_node])
1961
+ call_node.set_field("method", method_node)
1962
+ call_node.set_field("arguments", args_node)
1963
+
1964
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1965
+ src = b"require 'json'"
1966
+ parser._maybe_handle_require(call_node, src, "f.rb", store, stats)
1967
+ store.create_node.assert_called_once()
1968
+
1969
+ def test_maybe_handle_require_skips_non_require(self):
1970
+ with _mock_ts("tree_sitter_ruby"):
1971
+ from navegador.ingestion.ruby import RubyParser
1972
+
1973
+ parser = RubyParser()
1974
+ store = _make_store()
1975
+
1976
+ method_node = _text_node(b"puts", "identifier")
1977
+ call_node = MockNode("call", children=[method_node])
1978
+ call_node.set_field("method", method_node)
1979
+
1980
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1981
+ parser._maybe_handle_require(call_node, b"puts 'hi'", "f.rb", store, stats)
1982
+ store.create_node.assert_not_called()
1983
+
1984
+ def test_extract_calls(self):
1985
+ with _mock_ts("tree_sitter_ruby"):
1986
+ from navegador.ingestion.ruby import RubyParser
1987
+ from navegador.graph.schema import NodeLabel
1988
+
1989
+ parser = RubyParser()
1990
+ store = _make_store()
1991
+
1992
+ callee_node = _text_node(b"helper", "identifier")
1993
+ call_node = MockNode("call", children=[callee_node])
1994
+ call_node.set_field("method", callee_node)
1995
+ body_node = MockNode("body_statement", children=[call_node])
1996
+ fn_node = MockNode("method", children=[body_node])
1997
+
1998
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1999
+ parser._extract_calls(fn_node, b"def foo; helper; end", "f.rb", "foo", NodeLabel.Function, store, stats)
2000
+ store.create_edge.assert_called()
2001
+
2002
+
2003
+# ===========================================================================
2004
+# navegador.ingestion.cpp — extra branches (73% → target ~90%)
2005
+# ===========================================================================
2006
+
2007
+
2008
+class TestCppParserBranches:
2009
+ def _make_parser(self):
2010
+ with _mock_ts("tree_sitter_cpp"):
2011
+ from navegador.ingestion.cpp import CppParser
2012
+ return CppParser()
2013
+
2014
+ def test_handle_class_with_inheritance(self):
2015
+ with _mock_ts("tree_sitter_cpp"):
2016
+ from navegador.ingestion.cpp import CppParser
2017
+
2018
+ parser = CppParser()
2019
+ store = _make_store()
2020
+
2021
+ name_node = _text_node(b"MyClass", "type_identifier")
2022
+ parent_node = _text_node(b"BaseClass", "type_identifier")
2023
+ base_clause_node = MockNode("base_class_clause", children=[parent_node])
2024
+ class_node = MockNode("class_specifier", children=[name_node, base_clause_node])
2025
+ class_node.set_field("name", name_node)
2026
+ class_node.set_field("base_clause", base_clause_node)
2027
+
2028
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2029
+ parser._handle_class(class_node, b"class MyClass : public BaseClass {};", "f.cpp", store, stats)
2030
+ assert stats["classes"] == 1
2031
+ assert stats["edges"] >= 2
2032
+
2033
+ def test_handle_class_no_name(self):
2034
+ with _mock_ts("tree_sitter_cpp"):
2035
+ from navegador.ingestion.cpp import CppParser
2036
+
2037
+ parser = CppParser()
2038
+ store = _make_store()
2039
+ anon_class = MockNode("class_specifier")
2040
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2041
+ parser._handle_class(anon_class, b"struct {};", "f.cpp", store, stats)
2042
+ assert stats["classes"] == 0
2043
+
2044
+ def test_handle_function_with_class_name(self):
2045
+ with _mock_ts("tree_sitter_cpp"):
2046
+ from navegador.ingestion.cpp import CppParser
2047
+
2048
+ parser = CppParser()
2049
+ store = _make_store()
2050
+
2051
+ fn_name_node = _text_node(b"myMethod", "identifier")
2052
+ declarator = MockNode("function_declarator", children=[fn_name_node])
2053
+ declarator.set_field("declarator", fn_name_node)
2054
+ body = MockNode("compound_statement")
2055
+ fn_node = MockNode("function_definition", children=[declarator, body])
2056
+ fn_node.set_field("declarator", declarator)
2057
+ fn_node.set_field("body", body)
2058
+
2059
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2060
+ parser._handle_function(fn_node, b"void myMethod() {}", "f.cpp", store, stats, class_name="MyClass")
2061
+ assert stats["functions"] == 1
2062
+ from navegador.graph.schema import NodeLabel
2063
+ assert store.create_node.call_args[0][0] == NodeLabel.Method
2064
+
2065
+ def test_extract_function_name_qualified(self):
2066
+ with _mock_ts("tree_sitter_cpp"):
2067
+ from navegador.ingestion.cpp import CppParser
2068
+
2069
+ parser = CppParser()
2070
+ src = b"method"
2071
+ name_node = MockNode("identifier", start_byte=0, end_byte=len(src))
2072
+ qualified = MockNode("qualified_identifier", children=[name_node])
2073
+ qualified.set_field("name", name_node)
2074
+ result = parser._extract_function_name(qualified, src)
2075
+ assert result == "method"
2076
+
2077
+ def test_extract_function_name_qualified_fallback(self):
2078
+ with _mock_ts("tree_sitter_cpp"):
2079
+ from navegador.ingestion.cpp import CppParser
2080
+
2081
+ parser = CppParser()
2082
+ src = b"method"
2083
+ id_node = MockNode("identifier", start_byte=0, end_byte=len(src))
2084
+ qualified = MockNode("qualified_identifier", children=[id_node])
2085
+ # No name field → fallback to last identifier child
2086
+ result = parser._extract_function_name(qualified, src)
2087
+ assert result == "method"
2088
+
2089
+ def test_extract_function_name_pointer_declarator(self):
2090
+ with _mock_ts("tree_sitter_cpp"):
2091
+ from navegador.ingestion.cpp import CppParser
2092
+
2093
+ parser = CppParser()
2094
+ src = b"fp"
2095
+ inner = MockNode("identifier", start_byte=0, end_byte=len(src))
2096
+ ptr_decl = MockNode("pointer_declarator", children=[inner])
2097
+ ptr_decl.set_field("declarator", inner)
2098
+ result = parser._extract_function_name(ptr_decl, src)
2099
+ assert result == "fp"
2100
+
2101
+ def test_extract_function_name_fallback_child(self):
2102
+ with _mock_ts("tree_sitter_cpp"):
2103
+ from navegador.ingestion.cpp import CppParser
2104
+
2105
+ parser = CppParser()
2106
+ src = b"fallbackFn"
2107
+ id_node = MockNode("identifier", start_byte=0, end_byte=len(src))
2108
+ unknown_decl = MockNode("unknown_declarator", children=[id_node])
2109
+ result = parser._extract_function_name(unknown_decl, src)
2110
+ assert result == "fallbackFn"
2111
+
2112
+ def test_extract_function_name_none(self):
2113
+ with _mock_ts("tree_sitter_cpp"):
2114
+ from navegador.ingestion.cpp import CppParser
2115
+
2116
+ parser = CppParser()
2117
+ assert parser._extract_function_name(None, b"") is None
2118
+
2119
+ def test_handle_include(self):
2120
+ with _mock_ts("tree_sitter_cpp"):
2121
+ from navegador.ingestion.cpp import CppParser
2122
+
2123
+ parser = CppParser()
2124
+ store = _make_store()
2125
+
2126
+ path_node = _text_node(b'"vector"', "string_literal")
2127
+ include_node = MockNode("preproc_include", children=[path_node])
2128
+ include_node.set_field("path", path_node)
2129
+
2130
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2131
+ parser._handle_include(include_node, b'#include "vector"', "f.cpp", store, stats)
2132
+ store.create_node.assert_called_once()
2133
+
2134
+ def test_namespace_recurse(self):
2135
+ with _mock_ts("tree_sitter_cpp"):
2136
+ from navegador.ingestion.cpp import CppParser
2137
+
2138
+ parser = CppParser()
2139
+ store = _make_store()
2140
+
2141
+ # namespace body contains a function
2142
+ fn_name = _text_node(b"inner_fn", "identifier")
2143
+ fn_decl = MockNode("function_declarator", children=[fn_name])
2144
+ fn_decl.set_field("declarator", fn_name)
2145
+ fn_body = MockNode("compound_statement")
2146
+ fn_def = MockNode("function_definition", children=[fn_decl, fn_body])
2147
+ fn_def.set_field("declarator", fn_decl)
2148
+
2149
+ decl_list = MockNode("declaration_list", children=[fn_def])
2150
+ ns_node = MockNode("namespace_definition", children=[decl_list])
2151
+
2152
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2153
+ parser._walk(ns_node, b"namespace ns { void inner_fn(){} }", "f.cpp", store, stats, class_name=None)
2154
+ assert stats["functions"] == 1
2155
+
2156
+
2157
+# ===========================================================================
2158
+# navegador.ingestion.csharp — extra branches (79% → target ~90%)
2159
+# ===========================================================================
2160
+
2161
+
2162
+class TestCSharpParserBranches:
2163
+ def test_handle_class_with_bases(self):
2164
+ with _mock_ts("tree_sitter_c_sharp"):
2165
+ from navegador.ingestion.csharp import CSharpParser
2166
+
2167
+ parser = CSharpParser()
2168
+ store = _make_store()
2169
+
2170
+ name_node = _text_node(b"MyService", "identifier")
2171
+ base_id = _text_node(b"IService", "identifier")
2172
+ bases_node = MockNode("base_list", children=[base_id])
2173
+ class_node = MockNode("class_declaration", children=[name_node, bases_node])
2174
+ class_node.set_field("name", name_node)
2175
+ class_node.set_field("bases", bases_node)
2176
+
2177
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2178
+ parser._handle_class(class_node, b"class MyService : IService {}", "f.cs", store, stats)
2179
+ assert stats["classes"] == 1
2180
+ assert stats["edges"] >= 2
2181
+
2182
+ def test_handle_class_no_name(self):
2183
+ with _mock_ts("tree_sitter_c_sharp"):
2184
+ from navegador.ingestion.csharp import CSharpParser
2185
+
2186
+ parser = CSharpParser()
2187
+ store = _make_store()
2188
+ anon = MockNode("class_declaration")
2189
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2190
+ parser._handle_class(anon, b"", "f.cs", store, stats)
2191
+ assert stats["classes"] == 0
2192
+
2193
+ def test_handle_method_standalone(self):
2194
+ with _mock_ts("tree_sitter_c_sharp"):
2195
+ from navegador.ingestion.csharp import CSharpParser
2196
+
2197
+ parser = CSharpParser()
2198
+ store = _make_store()
2199
+
2200
+ name_node = _text_node(b"DoWork", "identifier")
2201
+ fn_node = MockNode("method_declaration", children=[name_node])
2202
+ fn_node.set_field("name", name_node)
2203
+
2204
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2205
+ parser._handle_method(fn_node, b"void DoWork() {}", "f.cs", store, stats, class_name=None)
2206
+ assert stats["functions"] == 1
2207
+
2208
+ def test_handle_method_no_name(self):
2209
+ with _mock_ts("tree_sitter_c_sharp"):
2210
+ from navegador.ingestion.csharp import CSharpParser
2211
+
2212
+ parser = CSharpParser()
2213
+ store = _make_store()
2214
+ anon = MockNode("method_declaration")
2215
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2216
+ parser._handle_method(anon, b"", "f.cs", store, stats, class_name=None)
2217
+ assert stats["functions"] == 0
2218
+
2219
+ def test_handle_using(self):
2220
+ with _mock_ts("tree_sitter_c_sharp"):
2221
+ from navegador.ingestion.csharp import CSharpParser
2222
+
2223
+ parser = CSharpParser()
2224
+ store = _make_store()
2225
+
2226
+ src = b"using System.Collections.Generic;"
2227
+ using_node = MockNode(
2228
+ "using_directive",
2229
+ start_byte=0,
2230
+ end_byte=len(src),
2231
+ start_point=(0, 0),
2232
+ end_point=(0, len(src)),
2233
+ )
2234
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2235
+ parser._handle_using(using_node, src, "f.cs", store, stats)
2236
+ store.create_node.assert_called_once()
2237
+
2238
+ def test_extract_calls(self):
2239
+ with _mock_ts("tree_sitter_c_sharp"):
2240
+ from navegador.ingestion.csharp import CSharpParser
2241
+ from navegador.graph.schema import NodeLabel
2242
+
2243
+ parser = CSharpParser()
2244
+ store = _make_store()
2245
+
2246
+ callee_node = _text_node(b"DoWork", "identifier")
2247
+ invoke_node = MockNode("invocation_expression", children=[callee_node])
2248
+ invoke_node.set_field("function", callee_node)
2249
+ block_node = MockNode("block", children=[invoke_node])
2250
+ fn_node = MockNode("method_declaration", children=[block_node])
2251
+ fn_node.set_field("body", block_node)
2252
+
2253
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2254
+ parser._extract_calls(fn_node, b"DoWork()", "f.cs", "Run", NodeLabel.Method, store, stats)
2255
+ store.create_edge.assert_called()
2256
+
2257
+
2258
+# ===========================================================================
2259
+# navegador.ingestion.kotlin — extra branches (79% → target ~90%)
2260
+# ===========================================================================
2261
+
2262
+
2263
+class TestKotlinParserBranches:
2264
+ def test_handle_class_no_name(self):
2265
+ with _mock_ts("tree_sitter_kotlin"):
2266
+ from navegador.ingestion.kotlin import KotlinParser
2267
+
2268
+ parser = KotlinParser()
2269
+ store = _make_store()
2270
+ anon = MockNode("class_declaration")
2271
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2272
+ parser._handle_class(anon, b"", "f.kt", store, stats)
2273
+ assert stats["classes"] == 0
2274
+
2275
+ def test_handle_class_with_body(self):
2276
+ with _mock_ts("tree_sitter_kotlin"):
2277
+ from navegador.ingestion.kotlin import KotlinParser
2278
+
2279
+ parser = KotlinParser()
2280
+ store = _make_store()
2281
+
2282
+ name_node = _text_node(b"MyClass", "simple_identifier")
2283
+ fn_name = _text_node(b"doSomething", "simple_identifier")
2284
+ fn_node = MockNode("function_declaration", children=[fn_name])
2285
+ fn_node.set_field("name", fn_name)
2286
+ body_node = MockNode("class_body", children=[fn_node])
2287
+ class_node = MockNode("class_declaration", children=[name_node, body_node])
2288
+ class_node.set_field("name", name_node)
2289
+ class_node.set_field("body", body_node)
2290
+
2291
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2292
+ src = b"class MyClass { fun doSomething() {} }"
2293
+ parser._handle_class(class_node, src, "f.kt", store, stats)
2294
+ assert stats["classes"] == 1
2295
+ assert stats["functions"] == 1
2296
+
2297
+ def test_handle_function_no_name(self):
2298
+ with _mock_ts("tree_sitter_kotlin"):
2299
+ from navegador.ingestion.kotlin import KotlinParser
2300
+
2301
+ parser = KotlinParser()
2302
+ store = _make_store()
2303
+ anon = MockNode("function_declaration")
2304
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2305
+ parser._handle_function(anon, b"", "f.kt", store, stats, class_name=None)
2306
+ assert stats["functions"] == 0
2307
+
2308
+ def test_handle_import(self):
2309
+ with _mock_ts("tree_sitter_kotlin"):
2310
+ from navegador.ingestion.kotlin import KotlinParser
2311
+
2312
+ parser = KotlinParser()
2313
+ store = _make_store()
2314
+
2315
+ src = b"import kotlin.collections.List"
2316
+ import_node = MockNode(
2317
+ "import_header",
2318
+ start_byte=0,
2319
+ end_byte=len(src),
2320
+ start_point=(0, 0),
2321
+ end_point=(0, len(src)),
2322
+ )
2323
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2324
+ parser._handle_import(import_node, src, "f.kt", store, stats)
2325
+ store.create_node.assert_called_once()
2326
+
2327
+
2328
+# ===========================================================================
2329
+# navegador.ingestion.php — extra branches (79% → target ~90%)
2330
+# ===========================================================================
2331
+
2332
+
2333
+class TestPHPParserBranches:
2334
+ def test_handle_class_no_name(self):
2335
+ with _mock_ts("tree_sitter_php"):
2336
+ from navegador.ingestion.php import PHPParser
2337
+
2338
+ parser = PHPParser()
2339
+ store = _make_store()
2340
+ anon = MockNode("class_declaration")
2341
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2342
+ parser._handle_class(anon, b"", "f.php", store, stats)
2343
+ assert stats["classes"] == 0
2344
+
2345
+ def test_handle_function_no_name(self):
2346
+ with _mock_ts("tree_sitter_php"):
2347
+ from navegador.ingestion.php import PHPParser
2348
+
2349
+ parser = PHPParser()
2350
+ store = _make_store()
2351
+ anon = MockNode("function_definition")
2352
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2353
+ parser._handle_function(anon, b"", "f.php", store, stats, class_name=None)
2354
+ assert stats["functions"] == 0
2355
+
2356
+ def test_handle_class_with_body_methods(self):
2357
+ with _mock_ts("tree_sitter_php"):
2358
+ from navegador.ingestion.php import PHPParser
2359
+
2360
+ parser = PHPParser()
2361
+ store = _make_store()
2362
+
2363
+ name_node = _text_node(b"MyController", "name")
2364
+ fn_name = _text_node(b"index", "name")
2365
+ method_node = MockNode("method_declaration", children=[fn_name])
2366
+ method_node.set_field("name", fn_name)
2367
+ body_node = MockNode("declaration_list", children=[method_node])
2368
+ class_node = MockNode("class_declaration", children=[name_node, body_node])
2369
+ class_node.set_field("name", name_node)
2370
+ class_node.set_field("body", body_node)
2371
+
2372
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2373
+ src = b"class MyController { public function index() {} }"
2374
+ parser._handle_class(class_node, src, "f.php", store, stats)
2375
+ assert stats["classes"] == 1
2376
+ assert stats["functions"] >= 1
2377
+
2378
+
2379
+# ===========================================================================
2380
+# navegador.ingestion.swift — extra branches (79% → target ~90%)
2381
+# ===========================================================================
2382
+
2383
+
2384
+class TestSwiftParserBranches:
2385
+ def test_handle_class_no_name(self):
2386
+ with _mock_ts("tree_sitter_swift"):
2387
+ from navegador.ingestion.swift import SwiftParser
2388
+
2389
+ parser = SwiftParser()
2390
+ store = _make_store()
2391
+ anon = MockNode("class_declaration")
2392
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2393
+ parser._handle_class(anon, b"", "f.swift", store, stats)
2394
+ assert stats["classes"] == 0
2395
+
2396
+ def test_handle_function_no_name(self):
2397
+ with _mock_ts("tree_sitter_swift"):
2398
+ from navegador.ingestion.swift import SwiftParser
2399
+
2400
+ parser = SwiftParser()
2401
+ store = _make_store()
2402
+ anon = MockNode("function_declaration")
2403
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2404
+ parser._handle_function(anon, b"", "f.swift", store, stats, class_name=None)
2405
+ assert stats["functions"] == 0
2406
+
2407
+ def test_handle_import(self):
2408
+ with _mock_ts("tree_sitter_swift"):
2409
+ from navegador.ingestion.swift import SwiftParser
2410
+
2411
+ parser = SwiftParser()
2412
+ store = _make_store()
2413
+
2414
+ src = b"import Foundation"
2415
+ import_node = MockNode(
2416
+ "import_declaration",
2417
+ start_byte=0,
2418
+ end_byte=len(src),
2419
+ start_point=(0, 0),
2420
+ end_point=(0, len(src)),
2421
+ )
2422
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2423
+ parser._handle_import(import_node, src, "f.swift", store, stats)
2424
+ store.create_node.assert_called_once()
2425
+
2426
+ def test_handle_class_with_body(self):
2427
+ with _mock_ts("tree_sitter_swift"):
2428
+ from navegador.ingestion.swift import SwiftParser
2429
+
2430
+ parser = SwiftParser()
2431
+ store = _make_store()
2432
+
2433
+ name_node = _text_node(b"MyView", "type_identifier")
2434
+ fn_name = _text_node(b"body", "simple_identifier")
2435
+ fn_node = MockNode("function_declaration", children=[fn_name])
2436
+ fn_node.set_field("name", fn_name)
2437
+ body_node = MockNode("class_body", children=[fn_node])
2438
+ class_node = MockNode("class_declaration", children=[name_node, body_node])
2439
+ class_node.set_field("name", name_node)
2440
+ class_node.set_field("body", body_node)
2441
+
2442
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2443
+ src = b"class MyView { func body() {} }"
2444
+ parser._handle_class(class_node, src, "f.swift", store, stats)
2445
+ assert stats["classes"] == 1
2446
+ assert stats["functions"] == 1
2447
+
2448
+
2449
+# ===========================================================================
2450
+# navegador.ingestion.c — extra branches (76% → target ~90%)
2451
+# ===========================================================================
2452
+
2453
+
2454
+class TestCParserBranches:
2455
+ def test_handle_function(self):
2456
+ with _mock_ts("tree_sitter_c"):
2457
+ from navegador.ingestion.c import CParser
2458
+
2459
+ parser = CParser()
2460
+ store = _make_store()
2461
+
2462
+ fn_name_node = _text_node(b"myFunc", "identifier")
2463
+ declarator = MockNode("function_declarator", children=[fn_name_node])
2464
+ declarator.set_field("declarator", fn_name_node)
2465
+ body = MockNode("compound_statement")
2466
+ fn_node = MockNode("function_definition", children=[declarator, body])
2467
+ fn_node.set_field("declarator", declarator)
2468
+ fn_node.set_field("body", body)
2469
+
2470
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2471
+ parser._handle_function(fn_node, b"void myFunc() {}", "f.c", store, stats)
2472
+ assert stats["functions"] == 1
2473
+
2474
+ def test_handle_struct(self):
2475
+ with _mock_ts("tree_sitter_c"):
2476
+ from navegador.ingestion.c import CParser
2477
+
2478
+ parser = CParser()
2479
+ store = _make_store()
2480
+
2481
+ name_node = _text_node(b"Point", "type_identifier")
2482
+ struct_node = MockNode("struct_specifier", children=[name_node])
2483
+ struct_node.set_field("name", name_node)
2484
+
2485
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2486
+ parser._handle_struct(struct_node, b"struct Point {};", "f.c", store, stats)
2487
+ assert stats["classes"] == 1
2488
+
2489
+ def test_handle_struct_no_name(self):
2490
+ with _mock_ts("tree_sitter_c"):
2491
+ from navegador.ingestion.c import CParser
2492
+
2493
+ parser = CParser()
2494
+ store = _make_store()
2495
+ anon = MockNode("struct_specifier")
2496
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2497
+ parser._handle_struct(anon, b"", "f.c", store, stats)
2498
+ assert stats["classes"] == 0
2499
+
2500
+ def test_handle_include(self):
2501
+ with _mock_ts("tree_sitter_c"):
2502
+ from navegador.ingestion.c import CParser
2503
+
2504
+ parser = CParser()
2505
+ store = _make_store()
2506
+
2507
+ path_node = _text_node(b"<stdio.h>", "system_lib_string")
2508
+ include_node = MockNode("preproc_include", children=[path_node])
2509
+ include_node.set_field("path", path_node)
2510
+
2511
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2512
+ parser._handle_include(include_node, b"#include <stdio.h>", "f.c", store, stats)
2513
+ store.create_node.assert_called_once()
2514
+
2515
+ def test_handle_function_no_name(self):
2516
+ with _mock_ts("tree_sitter_c"):
2517
+ from navegador.ingestion.c import CParser
2518
+
2519
+ parser = CParser()
2520
+ store = _make_store()
2521
+
2522
+ fn_node = MockNode("function_definition")
2523
+ # No declarator field → _extract_function_name returns None
2524
+ stats = {"functions": 0, "classes": 0, "edges": 0}
2525
+ parser._handle_function(fn_node, b"", "f.c", store, stats)
2526
+ assert stats["functions"] == 0
--- a/tests/test_coverage_boost.py
+++ b/tests/test_coverage_boost.py
@@ -0,0 +1,2526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_coverage_boost.py
+++ b/tests/test_coverage_boost.py
@@ -0,0 +1,2526 @@
1 """
2 Targeted tests to boost coverage from ~91% to 95%+.
3
4 One test class per module. All external dependencies (Redis, tree-sitter,
5 HTTP) are mocked; no real infrastructure is required.
6 """
7
8 from __future__ import annotations
9
10 import json
11 import tempfile
12 from pathlib import Path
13 from unittest.mock import MagicMock, patch
14
15 import pytest
16
17
18 # ── Helpers ───────────────────────────────────────────────────────────────────
19
20
21 def _make_store():
22 store = MagicMock()
23 store.query.return_value = MagicMock(result_set=[])
24 store.node_count.return_value = 0
25 store.edge_count.return_value = 0
26 return store
27
28
29 def _write(path: Path, content: str) -> None:
30 path.parent.mkdir(parents=True, exist_ok=True)
31 path.write_text(content, encoding="utf-8")
32
33
34 # ── MockNode for AST tests ────────────────────────────────────────────────────
35
36
37 class MockNode:
38 def __init__(
39 self,
40 type_: str,
41 text: bytes = b"",
42 children: list = None,
43 start_byte: int = 0,
44 end_byte: int = 0,
45 start_point: tuple = (0, 0),
46 end_point: tuple = (5, 0),
47 ):
48 self.type = type_
49 self.children = children or []
50 self.start_byte = start_byte
51 self.end_byte = end_byte
52 self.start_point = start_point
53 self.end_point = end_point
54 self._fields: dict = {}
55
56 def child_by_field_name(self, name: str):
57 return self._fields.get(name)
58
59 def set_field(self, name: str, node: "MockNode") -> "MockNode":
60 self._fields[name] = node
61 return self
62
63
64 def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
65 return MockNode(type_, text, start_byte=0, end_byte=len(text))
66
67
68 def _make_mock_tree(root_node: MockNode):
69 tree = MagicMock()
70 tree.root_node = root_node
71 return tree
72
73
74 def _mock_ts(lang_module_name: str):
75 mock_lang_module = MagicMock()
76 mock_ts = MagicMock()
77 return patch.dict("sys.modules", {lang_module_name: mock_lang_module, "tree_sitter": mock_ts})
78
79
80 # ===========================================================================
81 # navegador.api_schema (57% → target ~90%)
82 # ===========================================================================
83
84
85 class TestAPISchemaIngesterOpenAPI:
86 """Cover lines 72, 105, 213, 217-225, 234-241, 255-294, 299-319."""
87
88 def _make(self):
89 from navegador.api_schema import APISchemaIngester
90
91 return APISchemaIngester(_make_store())
92
93 def test_ingest_openapi_yaml_with_paths(self, tmp_path):
94 content = """
95 openapi: 3.0.0
96 info:
97 title: Test
98 paths:
99 /users:
100 get:
101 operationId: listUsers
102 summary: List users
103 tags:
104 - users
105 post:
106 summary: Create user
107 components:
108 schemas:
109 User:
110 type: object
111 description: A user object
112 """
113 f = tmp_path / "api.yaml"
114 f.write_text(content)
115 ingester = self._make()
116 stats = ingester.ingest_openapi(str(f))
117 assert stats["endpoints"] >= 2
118 assert stats["schemas"] >= 1
119
120 def test_ingest_openapi_json(self, tmp_path):
121 spec = {
122 "openapi": "3.0.0",
123 "paths": {
124 "/items": {
125 "get": {"operationId": "listItems", "summary": "List"},
126 "delete": {"summary": "Delete"},
127 }
128 },
129 "components": {"schemas": {"Item": {"description": "item"}}},
130 }
131 f = tmp_path / "api.json"
132 f.write_text(json.dumps(spec))
133 ingester = self._make()
134 stats = ingester.ingest_openapi(str(f))
135 assert stats["endpoints"] >= 2
136 assert stats["schemas"] >= 1
137
138 def test_ingest_openapi_missing_file(self, tmp_path):
139 ingester = self._make()
140 stats = ingester.ingest_openapi(str(tmp_path / "missing.yaml"))
141 assert stats == {"endpoints": 0, "schemas": 0}
142
143 def test_ingest_openapi_swagger2_definitions(self, tmp_path):
144 spec = {
145 "swagger": "2.0",
146 "paths": {
147 "/pets": {"get": {"summary": "List pets"}}
148 },
149 "definitions": {"Pet": {"description": "pet"}},
150 }
151 f = tmp_path / "swagger.json"
152 f.write_text(json.dumps(spec))
153 ingester = self._make()
154 stats = ingester.ingest_openapi(str(f))
155 assert stats["schemas"] >= 1
156
157 def test_ingest_openapi_operation_no_id(self, tmp_path):
158 # operationId absent → synthesised as "METHOD /path"
159 spec = {
160 "paths": {"/x": {"put": {"summary": "update"}}},
161 }
162 f = tmp_path / "api.json"
163 f.write_text(json.dumps(spec))
164 ingester = self._make()
165 stats = ingester.ingest_openapi(str(f))
166 assert stats["endpoints"] == 1
167
168 def test_ingest_openapi_invalid_json(self, tmp_path):
169 f = tmp_path / "bad.json"
170 f.write_text("{not valid json")
171 ingester = self._make()
172 stats = ingester.ingest_openapi(str(f))
173 assert stats == {"endpoints": 0, "schemas": 0}
174
175 def test_ingest_openapi_unknown_extension_tries_json(self, tmp_path):
176 spec = {"paths": {"/x": {"get": {"summary": "hi"}}}}
177 f = tmp_path / "api.txt"
178 f.write_text(json.dumps(spec))
179 ingester = self._make()
180 stats = ingester.ingest_openapi(str(f))
181 assert stats["endpoints"] == 1
182
183 def test_ingest_openapi_unknown_extension_falls_back_to_yaml(self, tmp_path):
184 # Not valid JSON → falls back to _parse_yaml
185 f = tmp_path / "api.txt"
186 f.write_text("paths:\n /x:\n get:\n summary: hi\n")
187 ingester = self._make()
188 # Should not raise
189 ingester.ingest_openapi(str(f))
190
191
192 class TestAPISchemaIngesterGraphQL:
193 def _make(self):
194 from navegador.api_schema import APISchemaIngester
195
196 return APISchemaIngester(_make_store())
197
198 def test_ingest_graphql_types_and_fields(self, tmp_path):
199 sdl = """
200 type Query {
201 user(id: ID!): User
202 users: [User]
203 }
204 type Mutation {
205 createUser(name: String!): User
206 }
207 type User {
208 id: ID
209 name: String
210 }
211 input CreateUserInput {
212 name: String!
213 }
214 """
215 f = tmp_path / "schema.graphql"
216 f.write_text(sdl)
217 ingester = self._make()
218 stats = ingester.ingest_graphql(str(f))
219 assert stats["fields"] >= 2 # Query fields
220 assert stats["types"] >= 2 # User + CreateUserInput
221
222 def test_ingest_graphql_missing_file(self, tmp_path):
223 ingester = self._make()
224 stats = ingester.ingest_graphql(str(tmp_path / "missing.graphql"))
225 assert stats == {"types": 0, "fields": 0}
226
227 def test_ingest_graphql_empty_schema(self, tmp_path):
228 f = tmp_path / "empty.graphql"
229 f.write_text("# just a comment")
230 ingester = self._make()
231 stats = ingester.ingest_graphql(str(f))
232 assert stats["types"] == 0
233 assert stats["fields"] == 0
234
235
236 class TestMinimalYamlLoad:
237 def test_simple_key_value(self):
238 from navegador.api_schema import _minimal_yaml_load
239
240 result = _minimal_yaml_load("title: My API\nversion: 3")
241 assert result.get("title") == "My API"
242
243 def test_boolean_scalars(self):
244 from navegador.api_schema import _yaml_scalar
245
246 assert _yaml_scalar("true") is True
247 assert _yaml_scalar("false") is False
248 assert _yaml_scalar("yes") is True
249 assert _yaml_scalar("no") is False
250
251 def test_null_scalars(self):
252 from navegador.api_schema import _yaml_scalar
253
254 assert _yaml_scalar("null") is None
255 assert _yaml_scalar("~") is None
256 assert _yaml_scalar("") is None
257
258 def test_quoted_strings(self):
259 from navegador.api_schema import _yaml_scalar
260
261 assert _yaml_scalar('"hello"') == "hello"
262 assert _yaml_scalar("'world'") == "world"
263
264 def test_int_float(self):
265 from navegador.api_schema import _yaml_scalar
266
267 assert _yaml_scalar("42") == 42
268 assert _yaml_scalar("3.14") == pytest.approx(3.14)
269
270 def test_bare_string(self):
271 from navegador.api_schema import _yaml_scalar
272
273 assert _yaml_scalar("application/json") == "application/json"
274
275 def test_list_items(self):
276 from navegador.api_schema import _minimal_yaml_load
277
278 text = "tags:\n - users\n - admin\n"
279 # Should not raise even if list parsing is minimal
280 result = _minimal_yaml_load(text)
281 assert isinstance(result, dict)
282
283 def test_comments_skipped(self):
284 from navegador.api_schema import _minimal_yaml_load
285
286 text = "# comment\ntitle: test\n"
287 result = _minimal_yaml_load(text)
288 assert result.get("title") == "test"
289
290 def test_block_scalar_placeholder(self):
291 from navegador.api_schema import _minimal_yaml_load
292
293 text = "description: |\n some text\ntitle: test\n"
294 result = _minimal_yaml_load(text)
295 # Should have a nested dict for the block scalar key
296 assert "description" in result
297
298 def test_parse_yaml_uses_pyyaml_if_available(self, tmp_path):
299 from navegador.api_schema import APISchemaIngester
300
301 ingester = APISchemaIngester(_make_store())
302 mock_yaml = MagicMock()
303 mock_yaml.safe_load.return_value = {"openapi": "3.0.0", "paths": {}}
304 with patch.dict("sys.modules", {"yaml": mock_yaml}):
305 result = ingester._parse_yaml("openapi: 3.0.0")
306 assert result == {"openapi": "3.0.0", "paths": {}}
307
308 def test_parse_yaml_falls_back_when_no_pyyaml(self):
309 from navegador.api_schema import APISchemaIngester
310
311 ingester = APISchemaIngester(_make_store())
312 import sys
313
314 original = sys.modules.pop("yaml", None)
315 try:
316 # Simulate no yaml installed
317 with patch.dict("sys.modules", {"yaml": None}):
318 result = ingester._parse_yaml("title: test\n")
319 assert isinstance(result, dict)
320 finally:
321 if original is not None:
322 sys.modules["yaml"] = original
323
324
325 # ===========================================================================
326 # navegador.cluster.core (50% → target ~85%)
327 # ===========================================================================
328
329
330 class TestClusterManagerLocalVersion:
331 """Cover _local_version, _set_local_version, snapshot_to_local, push_to_shared, sync."""
332
333 def _make(self, tmp_path):
334 from navegador.cluster.core import ClusterManager
335
336 r = MagicMock()
337 pipe = MagicMock()
338 pipe.execute.return_value = [True, True, True]
339 r.pipeline.return_value = pipe
340 r.get.return_value = b"3"
341 return ClusterManager(
342 "redis://localhost:6379",
343 local_db_path=str(tmp_path / "graph.db"),
344 redis_client=r,
345 ), r, pipe
346
347 def test_local_version_zero_when_no_meta(self, tmp_path):
348 mgr, _, _ = self._make(tmp_path)
349 assert mgr._local_version() == 0
350
351 def test_set_and_read_local_version(self, tmp_path):
352 mgr, _, _ = self._make(tmp_path)
353 mgr._set_local_version(7)
354 assert mgr._local_version() == 7
355
356 def test_set_local_version_merges_existing_keys(self, tmp_path):
357 mgr, _, _ = self._make(tmp_path)
358 mgr._set_local_version(1)
359 mgr._set_local_version(2)
360 assert mgr._local_version() == 2
361
362 def test_local_version_handles_corrupt_json(self, tmp_path):
363 mgr, _, _ = self._make(tmp_path)
364 meta = tmp_path / ".navegador" / "cluster_meta.json"
365 meta.parent.mkdir(parents=True, exist_ok=True)
366 meta.write_text("{bad json")
367 # Should fall back to 0
368 # local_db_path is tmp_path/graph.db, so meta is tmp_path/cluster_meta.json
369 mgr2, _, _ = self._make(tmp_path)
370 assert mgr2._local_version() == 0
371
372 def test_snapshot_to_local_no_snapshot(self, tmp_path):
373 mgr, r, _ = self._make(tmp_path)
374 r.get.return_value = None # no snapshot key
375 # Should log warning and return without error
376 with patch("navegador.cluster.core.logger") as mock_log:
377 mgr.snapshot_to_local()
378 mock_log.warning.assert_called_once()
379
380 def test_snapshot_to_local_imports_data(self, tmp_path):
381 mgr, r, _ = self._make(tmp_path)
382 snapshot_data = {"nodes": [], "edges": []}
383 # First call returns version key, subsequent get returns snapshot
384 r.get.side_effect = [json.dumps(snapshot_data).encode(), b"5"]
385
386 with patch.object(mgr, "_import_to_local_graph") as mock_import:
387 with patch.object(mgr, "_set_local_version") as mock_set:
388 with patch.object(mgr, "_redis_version", return_value=5):
389 r.get.side_effect = None
390 r.get.return_value = json.dumps(snapshot_data).encode()
391 mgr.snapshot_to_local()
392 mock_import.assert_called_once_with(snapshot_data)
393
394 def test_push_to_shared(self, tmp_path):
395 mgr, r, pipe = self._make(tmp_path)
396 r.get.return_value = b"2"
397
398 with patch.object(mgr, "_export_local_graph", return_value={"nodes": [], "edges": []}):
399 mgr.push_to_shared()
400
401 pipe.set.assert_called()
402 pipe.execute.assert_called_once()
403
404 def test_sync_pulls_when_shared_newer(self, tmp_path):
405 mgr, r, _ = self._make(tmp_path)
406 with patch.object(mgr, "_local_version", return_value=1):
407 with patch.object(mgr, "_redis_version", return_value=5):
408 with patch.object(mgr, "snapshot_to_local") as mock_pull:
409 mgr.sync()
410 mock_pull.assert_called_once()
411
412 def test_sync_pushes_when_local_current(self, tmp_path):
413 mgr, r, _ = self._make(tmp_path)
414 with patch.object(mgr, "_local_version", return_value=5):
415 with patch.object(mgr, "_redis_version", return_value=3):
416 with patch.object(mgr, "push_to_shared") as mock_push:
417 mgr.sync()
418 mock_push.assert_called_once()
419
420 def test_connect_redis_raises_on_missing_dep(self):
421 from navegador.cluster.core import ClusterManager
422
423 with patch.dict("sys.modules", {"redis": None}):
424 with pytest.raises(ImportError, match="redis"):
425 ClusterManager._connect_redis("redis://localhost")
426
427 def test_redis_version_returns_zero_when_none(self, tmp_path):
428 mgr, r, _ = self._make(tmp_path)
429 r.get.return_value = None
430 assert mgr._redis_version() == 0
431
432 def test_export_local_graph_calls_store(self, tmp_path):
433 mgr, r, _ = self._make(tmp_path)
434 mock_store = MagicMock()
435 nodes_result = MagicMock()
436 nodes_result.result_set = []
437 edges_result = MagicMock()
438 edges_result.result_set = []
439 mock_store.query.side_effect = [nodes_result, edges_result]
440
441 with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store):
442 data = mgr._export_local_graph()
443 assert data == {"nodes": [], "edges": []}
444
445 def test_import_to_local_graph_creates_nodes(self, tmp_path):
446 mgr, r, _ = self._make(tmp_path)
447 mock_store = MagicMock()
448
449 data = {
450 "nodes": [{"labels": ["Function"], "properties": {"name": "foo"}}],
451 "edges": [
452 {
453 "src_labels": ["Function"],
454 "src_props": {"name": "foo", "file_path": "f.py"},
455 "rel_type": "CALLS",
456 "dst_labels": ["Function"],
457 "dst_props": {"name": "bar", "file_path": "f.py"},
458 "rel_props": {},
459 }
460 ],
461 }
462 with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store):
463 mgr._import_to_local_graph(data)
464 mock_store.create_node.assert_called_once()
465 mock_store.create_edge.assert_called_once()
466
467 def test_import_to_local_graph_skips_edge_without_src_key(self, tmp_path):
468 mgr, r, _ = self._make(tmp_path)
469 mock_store = MagicMock()
470
471 data = {
472 "nodes": [],
473 "edges": [
474 {
475 "src_labels": ["Function"],
476 "src_props": {}, # no name/file_path → no key
477 "rel_type": "CALLS",
478 "dst_labels": ["Function"],
479 "dst_props": {"name": "bar", "file_path": "f.py"},
480 "rel_props": {},
481 }
482 ],
483 }
484 with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store):
485 mgr._import_to_local_graph(data)
486 mock_store.create_edge.assert_not_called()
487
488
489 # ===========================================================================
490 # navegador.monorepo (76% → target ~90%)
491 # ===========================================================================
492
493
494 class TestWorkspaceDetectorEdgeCases:
495 """Cover lines 89-90, 124-125, 128, 142-143, 165-166, 180-181, 193,
496 223, 235-236, 253-255, 270-274, 288-289."""
497
498 def test_yarn_workspaces_berry_format(self, tmp_path):
499 from navegador.monorepo import WorkspaceDetector
500
501 pkg_json = {
502 "name": "root",
503 "workspaces": {"packages": ["packages/*"]},
504 }
505 _write(tmp_path / "package.json", json.dumps(pkg_json))
506 (tmp_path / "packages" / "app").mkdir(parents=True)
507 _write(tmp_path / "packages" / "app" / "package.json", '{"name":"app"}')
508 config = WorkspaceDetector().detect(tmp_path)
509 assert config is not None
510 assert config.type == "yarn"
511
512 def test_yarn_package_json_parse_error(self, tmp_path):
513 from navegador.monorepo import WorkspaceDetector
514
515 _write(tmp_path / "package.json", "{bad json")
516 # No workspaces key (parse failed) → no yarn config returned
517 config = WorkspaceDetector().detect(tmp_path)
518 assert config is None
519
520 def test_js_workspace_packages_berry_patterns(self, tmp_path):
521 from navegador.monorepo import WorkspaceDetector
522
523 pkg_json = {"workspaces": {"packages": ["packages/*"]}}
524 _write(tmp_path / "package.json", json.dumps(pkg_json))
525 (tmp_path / "packages" / "a").mkdir(parents=True)
526 det = WorkspaceDetector()
527 packages = det._js_workspace_packages(tmp_path)
528 assert any(p.name == "a" for p in packages)
529
530 def test_js_workspace_packages_no_package_json(self, tmp_path):
531 from navegador.monorepo import WorkspaceDetector
532
533 # fallback: scan for package.json one level down
534 (tmp_path / "pkg_a").mkdir()
535 _write(tmp_path / "pkg_a" / "package.json", '{"name":"pkg_a"}')
536 det = WorkspaceDetector()
537 packages = det._js_workspace_packages(tmp_path)
538 assert any(p.name == "pkg_a" for p in packages)
539
540 def test_js_workspace_packages_parse_error(self, tmp_path):
541 from navegador.monorepo import WorkspaceDetector
542
543 _write(tmp_path / "package.json", "{bad json")
544 # Falls back to _fallback_packages
545 det = WorkspaceDetector()
546 packages = det._js_workspace_packages(tmp_path)
547 assert isinstance(packages, list)
548
549 def test_nx_packages_from_subdirs(self, tmp_path):
550 from navegador.monorepo import WorkspaceDetector
551
552 _write(tmp_path / "nx.json", '{}')
553 (tmp_path / "apps" / "app1").mkdir(parents=True)
554 (tmp_path / "libs" / "lib1").mkdir(parents=True)
555 config = WorkspaceDetector().detect(tmp_path)
556 assert config is not None
557 assert config.type == "nx"
558 pkg_names = [p.name for p in config.packages]
559 assert "app1" in pkg_names
560 assert "lib1" in pkg_names
561
562 def test_nx_packages_fallback_to_js_workspaces(self, tmp_path):
563 from navegador.monorepo import WorkspaceDetector
564
565 # nx.json exists but no apps/libs/packages dirs
566 _write(tmp_path / "nx.json", '{}')
567 # fallback triggers _js_workspace_packages → _fallback_packages
568 config = WorkspaceDetector().detect(tmp_path)
569 assert config is not None
570 assert config.type == "nx"
571
572 def test_pnpm_workspace_parse(self, tmp_path):
573 from navegador.monorepo import WorkspaceDetector
574
575 _write(
576 tmp_path / "pnpm-workspace.yaml",
577 "packages:\n - 'packages/*'\n - 'apps/*'\n",
578 )
579 (tmp_path / "packages" / "core").mkdir(parents=True)
580 config = WorkspaceDetector().detect(tmp_path)
581 assert config is not None
582 assert config.type == "pnpm"
583
584 def test_pnpm_workspace_read_error(self, tmp_path):
585 from navegador.monorepo import WorkspaceDetector
586
587 # pnpm-workspace.yaml exists but cannot be read → IOError path
588 yaml_path = tmp_path / "pnpm-workspace.yaml"
589 yaml_path.touch()
590 det = WorkspaceDetector()
591 with patch.object(Path, "read_text", side_effect=OSError("perm")):
592 packages = det._pnpm_packages(tmp_path)
593 assert isinstance(packages, list)
594
595 def test_pnpm_no_patterns_fallback(self, tmp_path):
596 from navegador.monorepo import WorkspaceDetector
597
598 _write(tmp_path / "pnpm-workspace.yaml", "# empty\n")
599 (tmp_path / "sub").mkdir()
600 _write(tmp_path / "sub" / "package.json", '{"name":"sub"}')
601 det = WorkspaceDetector()
602 packages = det._pnpm_packages(tmp_path)
603 assert any(p.name == "sub" for p in packages)
604
605 def test_cargo_workspace_parse(self, tmp_path):
606 from navegador.monorepo import WorkspaceDetector
607
608 cargo_toml = """
609 [workspace]
610 members = [
611 "crates/core",
612 "crates/cli",
613 ]
614 """
615 _write(tmp_path / "Cargo.toml", cargo_toml)
616 (tmp_path / "crates" / "core").mkdir(parents=True)
617 (tmp_path / "crates" / "cli").mkdir(parents=True)
618 config = WorkspaceDetector().detect(tmp_path)
619 assert config is not None
620 assert config.type == "cargo"
621 assert len(config.packages) == 2
622
623 def test_cargo_workspace_not_workspace(self, tmp_path):
624 from navegador.monorepo import WorkspaceDetector
625
626 _write(tmp_path / "Cargo.toml", "[package]\nname = \"myapp\"\n")
627 config = WorkspaceDetector().detect(tmp_path)
628 assert config is None
629
630 def test_cargo_read_error(self, tmp_path):
631 from navegador.monorepo import WorkspaceDetector
632
633 cargo_toml = tmp_path / "Cargo.toml"
634 cargo_toml.touch()
635 det = WorkspaceDetector()
636 with patch.object(Path, "read_text", side_effect=OSError("perm")):
637 result = det._cargo_packages(tmp_path, cargo_toml)
638 assert result is None
639
640 def test_cargo_wildcard_members(self, tmp_path):
641 from navegador.monorepo import WorkspaceDetector
642
643 cargo_toml = "[workspace]\nmembers = [\"crates/*\"]\n"
644 _write(tmp_path / "Cargo.toml", cargo_toml)
645 (tmp_path / "crates" / "a").mkdir(parents=True)
646 (tmp_path / "crates" / "b").mkdir(parents=True)
647 det = WorkspaceDetector()
648 pkgs = det._cargo_packages(tmp_path, tmp_path / "Cargo.toml")
649 assert pkgs is not None
650 assert len(pkgs) == 2
651
652 def test_go_workspace_parse(self, tmp_path):
653 from navegador.monorepo import WorkspaceDetector
654
655 (tmp_path / "cmd").mkdir()
656 (tmp_path / "pkg").mkdir()
657 _write(tmp_path / "go.work", "go 1.21\nuse ./cmd\nuse ./pkg\n")
658 config = WorkspaceDetector().detect(tmp_path)
659 assert config is not None
660 assert config.type == "go"
661
662 def test_go_workspace_read_error(self, tmp_path):
663 from navegador.monorepo import WorkspaceDetector
664
665 go_work = tmp_path / "go.work"
666 go_work.touch()
667 det = WorkspaceDetector()
668 with patch.object(Path, "read_text", side_effect=OSError("perm")):
669 packages = det._go_packages(tmp_path)
670 assert isinstance(packages, list)
671
672 def test_glob_packages_negation_skipped(self, tmp_path):
673 from navegador.monorepo import WorkspaceDetector
674
675 (tmp_path / "packages" / "a").mkdir(parents=True)
676 det = WorkspaceDetector()
677 pkgs = det._glob_packages(tmp_path, ["!packages/*"])
678 assert pkgs == []
679
680 def test_glob_packages_literal_path(self, tmp_path):
681 from navegador.monorepo import WorkspaceDetector
682
683 (tmp_path / "myapp").mkdir()
684 det = WorkspaceDetector()
685 pkgs = det._glob_packages(tmp_path, ["myapp"])
686 assert any(p.name == "myapp" for p in pkgs)
687
688 def test_fallback_packages_skips_dotdirs(self, tmp_path):
689 from navegador.monorepo import WorkspaceDetector
690
691 (tmp_path / ".git").mkdir()
692 _write(tmp_path / ".git" / "package.json", '{}')
693 (tmp_path / "real").mkdir()
694 _write(tmp_path / "real" / "package.json", '{}')
695 det = WorkspaceDetector()
696 pkgs = det._fallback_packages(tmp_path)
697 names = [p.name for p in pkgs]
698 assert ".git" not in names
699 assert "real" in names
700
701
702 class TestMonorepoIngesterEdgeCases:
703 """Cover lines 373, 404-405, 451-452, 466, 471, 474-475, 485-503, 509-531."""
704
705 def test_ingest_fallback_when_no_workspace(self, tmp_path):
706 from navegador.monorepo import MonorepoIngester
707
708 store = _make_store()
709 ingester = MonorepoIngester(store)
710 with patch("navegador.monorepo.RepoIngester") as MockRI:
711 instance = MockRI.return_value
712 instance.ingest.return_value = {
713 "files": 1, "functions": 2, "classes": 0, "edges": 0, "skipped": 0
714 }
715 stats = ingester.ingest(str(tmp_path))
716 assert stats["packages"] == 0
717 assert stats["workspace_type"] == "none"
718
719 def test_ingest_raises_on_missing_path(self, tmp_path):
720 from navegador.monorepo import MonorepoIngester
721
722 ingester = MonorepoIngester(_make_store())
723 with pytest.raises(FileNotFoundError):
724 ingester.ingest(str(tmp_path / "does_not_exist"))
725
726 def test_ingest_package_exception_logged(self, tmp_path):
727 from navegador.monorepo import MonorepoIngester, WorkspaceConfig
728
729 store = _make_store()
730 pkg_dir = tmp_path / "pkg"
731 pkg_dir.mkdir()
732 config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_dir])
733
734 with patch("navegador.monorepo.WorkspaceDetector") as MockDet:
735 MockDet.return_value.detect.return_value = config
736 with patch("navegador.monorepo.RepoIngester") as MockRI:
737 MockRI.return_value.ingest.side_effect = RuntimeError("parse fail")
738 stats = MonorepoIngester(store).ingest(str(tmp_path))
739 assert stats["packages"] == 0
740
741 def test_js_deps(self, tmp_path):
742 from navegador.monorepo import MonorepoIngester
743
744 ingester = MonorepoIngester(_make_store())
745 pkg_json = {
746 "dependencies": {"react": "^18"},
747 "devDependencies": {"jest": "^29"},
748 "peerDependencies": {"typescript": ">=5"},
749 }
750 _write(tmp_path / "package.json", json.dumps(pkg_json))
751 deps = ingester._js_deps(tmp_path)
752 assert "react" in deps
753 assert "jest" in deps
754 assert "typescript" in deps
755
756 def test_js_deps_no_file(self, tmp_path):
757 from navegador.monorepo import MonorepoIngester
758
759 ingester = MonorepoIngester(_make_store())
760 assert ingester._js_deps(tmp_path) == []
761
762 def test_js_deps_parse_error(self, tmp_path):
763 from navegador.monorepo import MonorepoIngester
764
765 ingester = MonorepoIngester(_make_store())
766 _write(tmp_path / "package.json", "{bad json")
767 assert ingester._js_deps(tmp_path) == []
768
769 def test_cargo_deps(self, tmp_path):
770 from navegador.monorepo import MonorepoIngester
771
772 ingester = MonorepoIngester(_make_store())
773 cargo = "[dependencies]\nserde = \"1.0\"\ntokio = { version = \"1\" }\n[dev-dependencies]\ntempfile = \"3\"\n"
774 _write(tmp_path / "Cargo.toml", cargo)
775 deps = ingester._cargo_deps(tmp_path)
776 assert "serde" in deps
777 assert "tokio" in deps
778 assert "tempfile" in deps
779
780 def test_cargo_deps_no_file(self, tmp_path):
781 from navegador.monorepo import MonorepoIngester
782
783 ingester = MonorepoIngester(_make_store())
784 assert ingester._cargo_deps(tmp_path) == []
785
786 def test_cargo_deps_read_error(self, tmp_path):
787 from navegador.monorepo import MonorepoIngester
788
789 ingester = MonorepoIngester(_make_store())
790 cargo = tmp_path / "Cargo.toml"
791 cargo.touch()
792 with patch.object(Path, "read_text", side_effect=OSError("perm")):
793 result = ingester._cargo_deps(tmp_path)
794 assert result == []
795
796 def test_go_deps(self, tmp_path):
797 from navegador.monorepo import MonorepoIngester
798
799 ingester = MonorepoIngester(_make_store())
800 go_mod = "module example.com/myapp\ngo 1.21\n\nrequire (\n github.com/pkg/errors v0.9.1\n golang.org/x/net v0.17.0\n)\n\nrequire github.com/single/dep v1.0.0\n"
801 _write(tmp_path / "go.mod", go_mod)
802 deps = ingester._go_deps(tmp_path)
803 assert "github.com/pkg/errors" in deps
804 assert "golang.org/x/net" in deps
805 assert "github.com/single/dep" in deps
806
807 def test_go_deps_no_file(self, tmp_path):
808 from navegador.monorepo import MonorepoIngester
809
810 ingester = MonorepoIngester(_make_store())
811 assert ingester._go_deps(tmp_path) == []
812
813 def test_go_deps_read_error(self, tmp_path):
814 from navegador.monorepo import MonorepoIngester
815
816 ingester = MonorepoIngester(_make_store())
817 go_mod = tmp_path / "go.mod"
818 go_mod.touch()
819 with patch.object(Path, "read_text", side_effect=OSError("perm")):
820 result = ingester._go_deps(tmp_path)
821 assert result == []
822
823 def test_read_package_deps_unknown_type(self, tmp_path):
824 from navegador.monorepo import MonorepoIngester
825
826 ingester = MonorepoIngester(_make_store())
827 assert ingester._read_package_deps("unknown", tmp_path) == []
828
829 def test_dependency_edges_scoped_package(self, tmp_path):
830 from navegador.monorepo import MonorepoIngester, WorkspaceConfig
831
832 store = _make_store()
833 ingester = MonorepoIngester(store)
834
835 pkg_a = tmp_path / "pkg_a"
836 pkg_b = tmp_path / "pkg_b"
837 pkg_a.mkdir()
838 pkg_b.mkdir()
839
840 _write(pkg_a / "package.json", json.dumps({
841 "dependencies": {"@scope/pkg_b": "^1.0"}
842 }))
843
844 config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_a, pkg_b])
845 packages = [("pkg_a", pkg_a), ("pkg_b", pkg_b)]
846
847 ingester._create_dependency_edges(config, packages)
848 # store.create_edge should have been called at least once for the dependency
849 # (pkg_b matches the bare name)
850 store.create_edge.assert_called()
851
852 def test_dependency_edges_exception_logged(self, tmp_path):
853 from navegador.monorepo import MonorepoIngester, WorkspaceConfig
854
855 store = _make_store()
856 store.create_edge.side_effect = Exception("DB error")
857 ingester = MonorepoIngester(store)
858
859 pkg_a = tmp_path / "pkg_a"
860 pkg_b = tmp_path / "pkg_b"
861 pkg_a.mkdir()
862 pkg_b.mkdir()
863 _write(pkg_a / "package.json", json.dumps({"dependencies": {"pkg_b": "^1"}}))
864
865 config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_a, pkg_b])
866 packages = [("pkg_a", pkg_a), ("pkg_b", pkg_b)]
867 # Should not raise
868 count = ingester._create_dependency_edges(config, packages)
869 assert count == 0
870
871
872 # ===========================================================================
873 # navegador.pm (79% → target ~90%)
874 # ===========================================================================
875
876
877 class TestTicketIngester:
878 """Cover lines 243-245, 261-287."""
879
880 def _make(self):
881 from navegador.pm import TicketIngester
882
883 return TicketIngester(_make_store())
884
885 def test_ingest_linear_raises(self):
886 ing = self._make()
887 with pytest.raises(NotImplementedError, match="Linear"):
888 ing.ingest_linear(api_key="lin_xxx")
889
890 def test_ingest_jira_raises(self):
891 ing = self._make()
892 with pytest.raises(NotImplementedError, match="Jira"):
893 ing.ingest_jira(url="https://co.atlassian.net", token="tok")
894
895 def test_github_severity_critical(self):
896 from navegador.pm import TicketIngester
897
898 assert TicketIngester._github_severity(["critical"]) == "critical"
899 assert TicketIngester._github_severity(["blocker"]) == "critical"
900 assert TicketIngester._github_severity(["p0", "other"]) == "critical"
901
902 def test_github_severity_warning(self):
903 from navegador.pm import TicketIngester
904
905 assert TicketIngester._github_severity(["bug"]) == "warning"
906 assert TicketIngester._github_severity(["high"]) == "warning"
907 assert TicketIngester._github_severity(["important"]) == "warning"
908
909 def test_github_severity_info(self):
910 from navegador.pm import TicketIngester
911
912 assert TicketIngester._github_severity([]) == "info"
913 assert TicketIngester._github_severity(["enhancement"]) == "info"
914
915 def test_link_to_code_returns_zero_on_empty_graph(self):
916 from navegador.pm import TicketIngester
917
918 store = _make_store()
919 store.query.return_value = MagicMock(result_set=[])
920 ing = TicketIngester(store)
921 result = ing._link_to_code("myrepo")
922 assert result == 0
923
924 def test_link_to_code_returns_zero_on_query_failure(self):
925 from navegador.pm import TicketIngester
926
927 store = _make_store()
928 store.query.side_effect = Exception("DB down")
929 ing = TicketIngester(store)
930 result = ing._link_to_code("myrepo")
931 assert result == 0
932
933 def test_link_to_code_matches_tokens(self):
934 from navegador.pm import TicketIngester
935
936 store = _make_store()
937 # First call: tickets
938 ticket_result = MagicMock()
939 ticket_result.result_set = [("#1: authenticate user", "fix auth flow")]
940 # Second call: code nodes
941 code_result = MagicMock()
942 code_result.result_set = [("Function", "authenticate"), ("Function", "unrelated")]
943 store.query.side_effect = [ticket_result, code_result, None]
944
945 ing = TicketIngester(store)
946 result = ing._link_to_code("myrepo")
947 assert result >= 1
948
949 def test_link_to_code_skips_short_tokens(self):
950 from navegador.pm import TicketIngester
951
952 store = _make_store()
953 ticket_result = MagicMock()
954 # Only short words (< 4 chars)
955 ticket_result.result_set = [("#1: fix", "x")]
956 code_result = MagicMock()
957 code_result.result_set = [("Function", "fix")]
958 store.query.side_effect = [ticket_result, code_result]
959
960 ing = TicketIngester(store)
961 # "fix" is exactly 3 chars → skipped as a token
962 result = ing._link_to_code("myrepo")
963 assert result == 0
964
965 def test_ingest_github_issues_http_error(self):
966 from navegador.pm import TicketIngester
967
968 store = _make_store()
969 ing = TicketIngester(store)
970 with patch("urllib.request.urlopen", side_effect=Exception("network err")):
971 with pytest.raises(RuntimeError, match="Failed to fetch"):
972 ing.ingest_github_issues("owner/repo", token="tok")
973
974 def test_ingest_github_issues_success(self):
975 from navegador.pm import TicketIngester
976
977 store = _make_store()
978 ing = TicketIngester(store)
979 issues = [
980 {"number": 1, "title": "Fix auth", "body": "desc", "html_url": "http://x",
981 "labels": [{"name": "bug"}], "assignees": [{"login": "alice"}]},
982 ]
983 mock_resp = MagicMock()
984 mock_resp.__enter__ = MagicMock(return_value=mock_resp)
985 mock_resp.__exit__ = MagicMock(return_value=False)
986 mock_resp.read.return_value = json.dumps(issues).encode()
987
988 with patch("urllib.request.urlopen", return_value=mock_resp):
989 stats = ing.ingest_github_issues("owner/repo")
990 assert stats["tickets"] == 1
991
992
993 # ===========================================================================
994 # navegador.completions — install path (lines 66-70)
995 # ===========================================================================
996
997
998 class TestGetInstallInstruction:
999 def test_bash(self):
1000 from navegador.completions import get_install_instruction
1001
1002 instruction = get_install_instruction("bash")
1003 assert "~/.bashrc" in instruction
1004 assert "bash_source" in instruction
1005
1006 def test_zsh(self):
1007 from navegador.completions import get_install_instruction
1008
1009 instruction = get_install_instruction("zsh")
1010 assert "~/.zshrc" in instruction
1011 assert "zsh_source" in instruction
1012
1013 def test_fish(self):
1014 from navegador.completions import get_install_instruction
1015
1016 instruction = get_install_instruction("fish")
1017 assert "config.fish" in instruction
1018 assert "fish_source" in instruction
1019
1020 def test_invalid_raises(self):
1021 from navegador.completions import get_install_instruction
1022
1023 with pytest.raises(ValueError, match="Unsupported"):
1024 get_install_instruction("pwsh")
1025
1026
1027 # ===========================================================================
1028 # navegador.cli.commands — watch callback (lines ~179-185)
1029 # ===========================================================================
1030
1031
1032 class TestIngestWatchCallback:
1033 """Exercise the _on_cycle callback inside the watch branch of ingest."""
1034
1035 def test_watch_callback_with_changed_files(self, tmp_path):
1036 from click.testing import CliRunner
1037
1038 from navegador.cli.commands import main
1039
1040 runner = CliRunner()
1041 repo = str(tmp_path)
1042
1043 cycle_calls = []
1044
1045 def fake_watch(path, interval=2.0, callback=None):
1046 if callback:
1047 # Simulate a cycle with changed files
1048 result = callback({"files": 3, "skipped": 10})
1049 cycle_calls.append(result)
1050 # Second call: simulate KeyboardInterrupt to exit
1051 raise KeyboardInterrupt
1052
1053 with patch("navegador.cli.commands._get_store") as mock_gs:
1054 mock_gs.return_value = _make_store()
1055 with patch("navegador.ingestion.RepoIngester") as MockRI:
1056 MockRI.return_value.watch.side_effect = fake_watch
1057 result = runner.invoke(main, ["ingest", repo, "--watch", "--interval", "1"])
1058
1059 assert result.exit_code == 0
1060 assert cycle_calls == [True]
1061
1062 def test_watch_callback_no_changed_files(self, tmp_path):
1063 """Callback with 0 changed files should still return True."""
1064 from click.testing import CliRunner
1065
1066 from navegador.cli.commands import main
1067
1068 runner = CliRunner()
1069
1070 def fake_watch(path, interval=2.0, callback=None):
1071 if callback:
1072 result = callback({"files": 0, "skipped": 5})
1073 assert result is True
1074 raise KeyboardInterrupt
1075
1076 with patch("navegador.cli.commands._get_store") as mock_gs:
1077 mock_gs.return_value = _make_store()
1078 with patch("navegador.ingestion.RepoIngester") as MockRI:
1079 MockRI.return_value.watch.side_effect = fake_watch
1080 result = runner.invoke(main, ["ingest", str(tmp_path), "--watch"])
1081
1082 assert result.exit_code == 0
1083
1084
1085 # ===========================================================================
1086 # navegador.cli.commands — additional uncovered CLI branches
1087 # ===========================================================================
1088
1089
1090 class TestCLIBranchesDeadcode:
1091 """Cover lines 1395-1407 (unreachable_classes and orphan_files branches)."""
1092
1093 def test_deadcode_shows_unreachable_classes(self, tmp_path):
1094 from click.testing import CliRunner
1095
1096 from navegador.cli.commands import main
1097 from navegador.analysis.deadcode import DeadCodeReport
1098
1099 runner = CliRunner()
1100 report = DeadCodeReport(
1101 unreachable_functions=[],
1102 unreachable_classes=[
1103 {"name": "OldClass", "file_path": "old.py", "line_start": 1, "type": "Class"}
1104 ],
1105 orphan_files=["orphan.py"],
1106 )
1107 with patch("navegador.cli.commands._get_store") as mock_gs:
1108 mock_gs.return_value = _make_store()
1109 with patch("navegador.analysis.deadcode.DeadCodeDetector") as MockDC:
1110 MockDC.return_value.detect.return_value = report
1111 result = runner.invoke(main, ["deadcode"])
1112 assert result.exit_code == 0
1113 assert "OldClass" in result.output
1114 assert "orphan.py" in result.output
1115
1116 def test_deadcode_no_dead_code_message(self, tmp_path):
1117 from click.testing import CliRunner
1118
1119 from navegador.cli.commands import main
1120 from navegador.analysis.deadcode import DeadCodeReport
1121
1122 runner = CliRunner()
1123 report = DeadCodeReport(
1124 unreachable_functions=[], unreachable_classes=[], orphan_files=[]
1125 )
1126 with patch("navegador.cli.commands._get_store") as mock_gs:
1127 mock_gs.return_value = _make_store()
1128 with patch("navegador.analysis.deadcode.DeadCodeDetector") as MockDC:
1129 MockDC.return_value.detect.return_value = report
1130 result = runner.invoke(main, ["deadcode"])
1131 assert result.exit_code == 0
1132 assert "No dead code" in result.output
1133
1134
1135 class TestCLIBranchesTestmap:
1136 """Cover lines 1439-1452 (testmap table and unmatched branches)."""
1137
1138 def test_testmap_shows_table_and_unmatched(self):
1139 from click.testing import CliRunner
1140
1141 from navegador.cli.commands import main
1142 from navegador.analysis.testmap import TestMapResult, TestLink
1143
1144 runner = CliRunner()
1145 link = TestLink(
1146 test_name="test_foo",
1147 test_file="test_foo.py",
1148 prod_name="foo",
1149 prod_file="foo.py",
1150 prod_type="Function",
1151 source="name",
1152 )
1153 result_obj = TestMapResult(
1154 links=[link],
1155 unmatched_tests=[{"name": "test_orphan", "file_path": "test_x.py"}],
1156 edges_created=1,
1157 )
1158 with patch("navegador.cli.commands._get_store") as mock_gs:
1159 mock_gs.return_value = _make_store()
1160 with patch("navegador.analysis.testmap.TestMapper") as MockTM:
1161 MockTM.return_value.map_tests.return_value = result_obj
1162 result = runner.invoke(main, ["testmap"])
1163 assert result.exit_code == 0
1164 assert "test_foo" in result.output
1165 assert "test_orphan" in result.output
1166
1167
1168 class TestCLIBranchesRename:
1169 """Cover lines 1640-1650 (rename non-JSON output)."""
1170
1171 def test_rename_preview_output(self):
1172 from click.testing import CliRunner
1173
1174 from navegador.cli.commands import main
1175 from navegador.refactor import RenameResult
1176
1177 runner = CliRunner()
1178 rename_result = RenameResult(
1179 old_name="old_func",
1180 new_name="new_func",
1181 affected_nodes=[{"name": "old_func", "file_path": "f.py", "type": "Function", "line_start": 1}],
1182 affected_files=["f.py", "g.py"],
1183 edges_updated=3,
1184 )
1185 with patch("navegador.cli.commands._get_store") as mock_gs:
1186 mock_gs.return_value = _make_store()
1187 with patch("navegador.refactor.SymbolRenamer") as MockSR:
1188 MockSR.return_value.preview_rename.return_value = rename_result
1189 result = runner.invoke(main, ["rename", "old_func", "new_func", "--preview"])
1190 assert result.exit_code == 0
1191 assert "old_func" in result.output
1192 assert "f.py" in result.output
1193
1194 def test_rename_apply_output(self):
1195 from click.testing import CliRunner
1196
1197 from navegador.cli.commands import main
1198 from navegador.refactor import RenameResult
1199
1200 runner = CliRunner()
1201 rename_result = RenameResult(
1202 old_name="old_func",
1203 new_name="new_func",
1204 affected_nodes=[],
1205 affected_files=[],
1206 edges_updated=0,
1207 )
1208 with patch("navegador.cli.commands._get_store") as mock_gs:
1209 mock_gs.return_value = _make_store()
1210 with patch("navegador.refactor.SymbolRenamer") as MockSR:
1211 MockSR.return_value.apply_rename.return_value = rename_result
1212 result = runner.invoke(main, ["rename", "old_func", "new_func"])
1213 assert result.exit_code == 0
1214 assert "Renamed" in result.output
1215
1216
1217 class TestCLIBranchesSemantic:
1218 """Cover lines 2068-2080 (semantic-search table output)."""
1219
1220 def test_semantic_search_table_output(self):
1221 from click.testing import CliRunner
1222
1223 from navegador.cli.commands import main
1224
1225 runner = CliRunner()
1226 search_results = [
1227 {"score": 0.95, "type": "Function", "name": "authenticate", "file_path": "auth.py"},
1228 ]
1229 with patch("navegador.cli.commands._get_store") as mock_gs:
1230 mock_gs.return_value = _make_store()
1231 with patch("navegador.intelligence.search.SemanticSearch") as MockSS:
1232 MockSS.return_value.search.return_value = search_results
1233 with patch("navegador.llm.auto_provider") as mock_ap:
1234 mock_ap.return_value = MagicMock()
1235 result = runner.invoke(main, ["semantic-search", "auth tokens"])
1236 assert result.exit_code == 0
1237 assert "authenticate" in result.output
1238
1239 def test_semantic_search_no_results(self):
1240 from click.testing import CliRunner
1241
1242 from navegador.cli.commands import main
1243
1244 runner = CliRunner()
1245 with patch("navegador.cli.commands._get_store") as mock_gs:
1246 mock_gs.return_value = _make_store()
1247 with patch("navegador.intelligence.search.SemanticSearch") as MockSS:
1248 MockSS.return_value.search.return_value = []
1249 with patch("navegador.llm.auto_provider") as mock_ap:
1250 mock_ap.return_value = MagicMock()
1251 result = runner.invoke(main, ["semantic-search", "nothing"])
1252 assert result.exit_code == 0
1253 assert "--index" in result.output
1254
1255 def test_semantic_search_with_index_flag(self):
1256 from click.testing import CliRunner
1257
1258 from navegador.cli.commands import main
1259
1260 runner = CliRunner()
1261 with patch("navegador.cli.commands._get_store") as mock_gs:
1262 mock_gs.return_value = _make_store()
1263 with patch("navegador.intelligence.search.SemanticSearch") as MockSS:
1264 inst = MockSS.return_value
1265 inst.index.return_value = 42
1266 inst.search.return_value = []
1267 with patch("navegador.llm.auto_provider") as mock_ap:
1268 mock_ap.return_value = MagicMock()
1269 result = runner.invoke(main, ["semantic-search", "auth", "--index"])
1270 assert result.exit_code == 0
1271 assert "42" in result.output
1272
1273
1274 class TestCLIBranchesRepoCommands:
1275 """Cover lines 1539-1572 (repo list/ingest-all table output)."""
1276
1277 def test_repo_list_table_output(self):
1278 from click.testing import CliRunner
1279
1280 from navegador.cli.commands import main
1281
1282 runner = CliRunner()
1283 repos = [{"name": "myrepo", "path": "/path/to/myrepo"}]
1284 with patch("navegador.cli.commands._get_store") as mock_gs:
1285 mock_gs.return_value = _make_store()
1286 with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1287 MockMRM.return_value.list_repos.return_value = repos
1288 result = runner.invoke(main, ["repo", "list"])
1289 assert result.exit_code == 0
1290 assert "myrepo" in result.output
1291
1292 def test_repo_list_empty(self):
1293 from click.testing import CliRunner
1294
1295 from navegador.cli.commands import main
1296
1297 runner = CliRunner()
1298 with patch("navegador.cli.commands._get_store") as mock_gs:
1299 mock_gs.return_value = _make_store()
1300 with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1301 MockMRM.return_value.list_repos.return_value = []
1302 result = runner.invoke(main, ["repo", "list"])
1303 assert result.exit_code == 0
1304 assert "No repositories" in result.output
1305
1306 def test_repo_ingest_all_table(self):
1307 from click.testing import CliRunner
1308
1309 from navegador.cli.commands import main
1310
1311 runner = CliRunner()
1312 summary = {"myrepo": {"files": 5, "functions": 10, "classes": 2}}
1313 with patch("navegador.cli.commands._get_store") as mock_gs:
1314 mock_gs.return_value = _make_store()
1315 with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1316 MockMRM.return_value.ingest_all.return_value = summary
1317 result = runner.invoke(main, ["repo", "ingest-all"])
1318 assert result.exit_code == 0
1319 assert "myrepo" in result.output
1320
1321 def test_repo_search_table(self):
1322 from click.testing import CliRunner
1323
1324 from navegador.cli.commands import main
1325
1326 runner = CliRunner()
1327 results = [{"label": "Function", "name": "foo", "file_path": "foo.py"}]
1328 with patch("navegador.cli.commands._get_store") as mock_gs:
1329 mock_gs.return_value = _make_store()
1330 with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1331 MockMRM.return_value.cross_repo_search.return_value = results
1332 result = runner.invoke(main, ["repo", "search", "foo"])
1333 assert result.exit_code == 0
1334 assert "foo" in result.output
1335
1336 def test_repo_search_empty(self):
1337 from click.testing import CliRunner
1338
1339 from navegador.cli.commands import main
1340
1341 runner = CliRunner()
1342 with patch("navegador.cli.commands._get_store") as mock_gs:
1343 mock_gs.return_value = _make_store()
1344 with patch("navegador.multirepo.MultiRepoManager") as MockMRM:
1345 MockMRM.return_value.cross_repo_search.return_value = []
1346 result = runner.invoke(main, ["repo", "search", "nothing"])
1347 assert result.exit_code == 0
1348 assert "No results" in result.output
1349
1350
1351 class TestCLIBranchesPM:
1352 """Cover lines 1793-1806 (pm ingest output)."""
1353
1354 def test_pm_ingest_no_github_raises(self):
1355 from click.testing import CliRunner
1356
1357 from navegador.cli.commands import main
1358
1359 runner = CliRunner()
1360 result = runner.invoke(main, ["pm", "ingest"])
1361 assert result.exit_code != 0
1362
1363 def test_pm_ingest_table_output(self):
1364 from click.testing import CliRunner
1365
1366 from navegador.cli.commands import main
1367
1368 runner = CliRunner()
1369 with patch("navegador.cli.commands._get_store") as mock_gs:
1370 mock_gs.return_value = _make_store()
1371 with patch("navegador.pm.TicketIngester") as MockTI:
1372 MockTI.return_value.ingest_github_issues.return_value = {
1373 "tickets": 5, "linked": 2
1374 }
1375 result = runner.invoke(main, ["pm", "ingest", "--github", "owner/repo"])
1376 assert result.exit_code == 0
1377 assert "5" in result.output
1378
1379 def test_pm_ingest_json(self):
1380 from click.testing import CliRunner
1381
1382 from navegador.cli.commands import main
1383
1384 runner = CliRunner()
1385 with patch("navegador.cli.commands._get_store") as mock_gs:
1386 mock_gs.return_value = _make_store()
1387 with patch("navegador.pm.TicketIngester") as MockTI:
1388 MockTI.return_value.ingest_github_issues.return_value = {
1389 "tickets": 3, "linked": 1
1390 }
1391 result = runner.invoke(
1392 main, ["pm", "ingest", "--github", "owner/repo", "--json"]
1393 )
1394 assert result.exit_code == 0
1395 data = json.loads(result.output)
1396 assert data["tickets"] == 3
1397
1398
1399 class TestCLIBranchesIngest:
1400 """Cover lines 179-185 (ingest --monorepo table output)."""
1401
1402 def test_ingest_monorepo_table(self, tmp_path):
1403 from click.testing import CliRunner
1404
1405 from navegador.cli.commands import main
1406
1407 runner = CliRunner()
1408 with patch("navegador.cli.commands._get_store") as mock_gs:
1409 mock_gs.return_value = _make_store()
1410 with patch("navegador.monorepo.MonorepoIngester") as MockMI:
1411 MockMI.return_value.ingest.return_value = {
1412 "files": 10, "functions": 20, "packages": 3, "workspace_type": "yarn"
1413 }
1414 result = runner.invoke(
1415 main, ["ingest", str(tmp_path), "--monorepo"]
1416 )
1417 assert result.exit_code == 0
1418
1419 def test_ingest_monorepo_json(self, tmp_path):
1420 from click.testing import CliRunner
1421
1422 from navegador.cli.commands import main
1423
1424 runner = CliRunner()
1425 with patch("navegador.cli.commands._get_store") as mock_gs:
1426 mock_gs.return_value = _make_store()
1427 with patch("navegador.monorepo.MonorepoIngester") as MockMI:
1428 MockMI.return_value.ingest.return_value = {
1429 "files": 5, "packages": 2
1430 }
1431 result = runner.invoke(
1432 main, ["ingest", str(tmp_path), "--monorepo", "--json"]
1433 )
1434 assert result.exit_code == 0
1435 data = json.loads(result.output)
1436 assert data["files"] == 5
1437
1438
1439 class TestCLIBranchesSubmodulesIngest:
1440 """Cover lines 1901-1916 (submodules ingest output)."""
1441
1442 def test_submodules_ingest_output(self, tmp_path):
1443 from click.testing import CliRunner
1444
1445 from navegador.cli.commands import main
1446
1447 runner = CliRunner()
1448 with patch("navegador.cli.commands._get_store") as mock_gs:
1449 mock_gs.return_value = _make_store()
1450 with patch("navegador.submodules.SubmoduleIngester") as MockSI:
1451 MockSI.return_value.ingest_with_submodules.return_value = {
1452 "total_files": 10,
1453 "submodules": {"sub1": {}, "sub2": {}},
1454 }
1455 result = runner.invoke(main, ["submodules", "ingest", str(tmp_path)])
1456 assert result.exit_code == 0
1457 assert "sub1" in result.output
1458
1459 def test_submodules_ingest_json(self, tmp_path):
1460 from click.testing import CliRunner
1461
1462 from navegador.cli.commands import main
1463
1464 runner = CliRunner()
1465 with patch("navegador.cli.commands._get_store") as mock_gs:
1466 mock_gs.return_value = _make_store()
1467 with patch("navegador.submodules.SubmoduleIngester") as MockSI:
1468 MockSI.return_value.ingest_with_submodules.return_value = {
1469 "total_files": 5,
1470 "submodules": {},
1471 }
1472 result = runner.invoke(
1473 main, ["submodules", "ingest", str(tmp_path), "--json"]
1474 )
1475 assert result.exit_code == 0
1476 data = json.loads(result.output)
1477 assert data["total_files"] == 5
1478
1479
1480 class TestCLIBranchesCommunities:
1481 """Cover lines 2105-2141 (communities store-labels + table)."""
1482
1483 def test_communities_store_labels(self):
1484 from click.testing import CliRunner
1485
1486 from navegador.cli.commands import main
1487 from navegador.intelligence.community import Community
1488
1489 runner = CliRunner()
1490 comm = Community(name="c1", members=["a", "b", "c"], density=0.5)
1491 with patch("navegador.cli.commands._get_store") as mock_gs:
1492 mock_gs.return_value = _make_store()
1493 with patch("navegador.intelligence.community.CommunityDetector") as MockCD:
1494 inst = MockCD.return_value
1495 inst.detect.return_value = [comm]
1496 inst.store_communities.return_value = 3
1497 result = runner.invoke(main, ["communities", "--store-labels"])
1498 assert result.exit_code == 0
1499 assert "3" in result.output
1500
1501 def test_communities_empty(self):
1502 from click.testing import CliRunner
1503
1504 from navegador.cli.commands import main
1505
1506 runner = CliRunner()
1507 with patch("navegador.cli.commands._get_store") as mock_gs:
1508 mock_gs.return_value = _make_store()
1509 with patch("navegador.intelligence.community.CommunityDetector") as MockCD:
1510 MockCD.return_value.detect.return_value = []
1511 result = runner.invoke(main, ["communities"])
1512 assert result.exit_code == 0
1513 assert "No communities" in result.output
1514
1515 def test_communities_large_preview(self):
1516 from click.testing import CliRunner
1517
1518 from navegador.cli.commands import main
1519 from navegador.intelligence.community import Community
1520
1521 runner = CliRunner()
1522 comm = Community(name="big", members=list("abcdefgh"), density=0.8)
1523 with patch("navegador.cli.commands._get_store") as mock_gs:
1524 mock_gs.return_value = _make_store()
1525 with patch("navegador.intelligence.community.CommunityDetector") as MockCD:
1526 MockCD.return_value.detect.return_value = [comm]
1527 result = runner.invoke(main, ["communities"])
1528 assert result.exit_code == 0
1529 assert "+" in result.output # preview truncation
1530
1531
1532 # ===========================================================================
1533 # navegador.cluster.messaging (86% → target ~95%)
1534 # ===========================================================================
1535
1536
1537 class TestMessageBus:
1538 """Cover lines 74-78, 154, 178-182, 208."""
1539
1540 def _make(self):
1541 from navegador.cluster.messaging import MessageBus
1542
1543 r = MagicMock()
1544 r.smembers.return_value = set()
1545 r.lrange.return_value = []
1546 return MessageBus("redis://localhost:6379", _redis_client=r), r
1547
1548 def test_send_returns_message_id(self):
1549 bus, r = self._make()
1550 msg_id = bus.send("agent1", "agent2", "task", {"k": "v"})
1551 assert isinstance(msg_id, str) and len(msg_id) == 36
1552 r.rpush.assert_called_once()
1553
1554 def test_receive_returns_unacked_messages(self):
1555 import json as _json
1556 from navegador.cluster.messaging import Message
1557 import time
1558
1559 bus, r = self._make()
1560 msg = Message(
1561 id="abc123", from_agent="a1", to_agent="a2",
1562 type="task", payload={}, timestamp=time.time()
1563 )
1564 r.lrange.return_value = [_json.dumps(msg.to_dict()).encode()]
1565 r.smembers.return_value = set()
1566 messages = bus.receive("a2")
1567 assert len(messages) == 1
1568 assert messages[0].id == "abc123"
1569
1570 def test_receive_filters_acked(self):
1571 import json as _json
1572 from navegador.cluster.messaging import Message
1573 import time
1574
1575 bus, r = self._make()
1576 msg = Message(
1577 id="acked_id", from_agent="a1", to_agent="a2",
1578 type="task", payload={}, timestamp=time.time()
1579 )
1580 r.lrange.return_value = [_json.dumps(msg.to_dict()).encode()]
1581 r.smembers.return_value = {b"acked_id"}
1582 messages = bus.receive("a2")
1583 assert messages == []
1584
1585 def test_acknowledge_with_agent_id(self):
1586 bus, r = self._make()
1587 bus.acknowledge("msg123", agent_id="a2")
1588 r.sadd.assert_called()
1589
1590 def test_acknowledge_without_agent_id_broadcasts(self):
1591 bus, r = self._make()
1592 r.smembers.return_value = {b"agent1", b"agent2"}
1593 bus.acknowledge("msg123")
1594 assert r.sadd.call_count == 2
1595
1596 def test_broadcast_skips_sender(self):
1597 bus, r = self._make()
1598 r.smembers.return_value = {b"sender", b"agent2", b"agent3"}
1599 with patch.object(bus, "send", return_value="mid") as mock_send:
1600 ids = bus.broadcast("sender", "task", {})
1601 assert len(ids) == 2
1602 for call_args in mock_send.call_args_list:
1603 assert call_args[0][0] == "sender"
1604 assert call_args[0][1] != "sender"
1605
1606 def test_client_lazy_init_raises_on_missing_redis(self):
1607 from navegador.cluster.messaging import MessageBus
1608
1609 bus = MessageBus("redis://localhost:6379")
1610 with patch.dict("sys.modules", {"redis": None}):
1611 with pytest.raises(ImportError, match="redis"):
1612 bus._client()
1613
1614
1615 # ===========================================================================
1616 # navegador.cluster.locking (90% → target ~97%)
1617 # ===========================================================================
1618
1619
1620 class TestDistributedLock:
1621 """Cover lines 72-76, 120."""
1622
1623 def _make(self):
1624 from navegador.cluster.locking import DistributedLock
1625
1626 r = MagicMock()
1627 return DistributedLock("redis://localhost:6379", "test-lock", _redis_client=r), r
1628
1629 def test_acquire_success(self):
1630 lock, r = self._make()
1631 r.set.return_value = True
1632 assert lock.acquire() is True
1633 assert lock._token is not None
1634
1635 def test_acquire_failure(self):
1636 lock, r = self._make()
1637 r.set.return_value = None
1638 assert lock.acquire() is False
1639
1640 def test_release_when_token_matches(self):
1641 lock, r = self._make()
1642 lock._token = "mytoken"
1643 r.get.return_value = b"mytoken"
1644 lock.release()
1645 r.delete.assert_called_once()
1646 assert lock._token is None
1647
1648 def test_release_when_token_not_held(self):
1649 lock, r = self._make()
1650 lock._token = None
1651 lock.release()
1652 r.delete.assert_not_called()
1653
1654 def test_release_when_stored_token_differs(self):
1655 lock, r = self._make()
1656 lock._token = "mytoken"
1657 r.get.return_value = b"other_token"
1658 lock.release()
1659 r.delete.assert_not_called()
1660
1661 def test_context_manager_acquires_and_releases(self):
1662 lock, r = self._make()
1663 r.set.return_value = True
1664 r.get.return_value = None # simulate already released
1665 with lock:
1666 pass
1667 assert lock._token is None
1668
1669 def test_context_manager_raises_on_timeout(self):
1670 import time
1671 from navegador.cluster.locking import LockTimeout
1672
1673 lock, r = self._make()
1674 r.set.return_value = None # never acquired
1675 lock._timeout = 0
1676 lock._retry_interval = 0
1677
1678 with pytest.raises(LockTimeout):
1679 with lock:
1680 pass
1681
1682 def test_client_lazy_init_raises_on_missing_redis(self):
1683 from navegador.cluster.locking import DistributedLock
1684
1685 lock = DistributedLock("redis://localhost:6379", "x")
1686 with patch.dict("sys.modules", {"redis": None}):
1687 with pytest.raises(ImportError, match="redis"):
1688 lock._client()
1689
1690
1691 # ===========================================================================
1692 # navegador.cluster.observability (89% → target ~97%)
1693 # ===========================================================================
1694
1695
1696 class TestSwarmDashboard:
1697 """Cover lines 44-48, 93, 108, 160."""
1698
1699 def _make(self):
1700 from navegador.cluster.observability import SwarmDashboard
1701
1702 r = MagicMock()
1703 r.keys.return_value = []
1704 r.get.return_value = None
1705 return SwarmDashboard("redis://localhost:6379", _redis_client=r), r
1706
1707 def test_register_agent(self):
1708 dash, r = self._make()
1709 dash.register_agent("agent1", metadata={"role": "ingester"})
1710 r.setex.assert_called_once()
1711
1712 def test_register_agent_no_metadata(self):
1713 dash, r = self._make()
1714 dash.register_agent("agent1")
1715 r.setex.assert_called_once()
1716
1717 def test_agent_status_empty(self):
1718 dash, r = self._make()
1719 r.keys.return_value = []
1720 agents = dash.agent_status()
1721 assert agents == []
1722
1723 def test_agent_status_returns_active_agents(self):
1724 import json as _json
1725 dash, r = self._make()
1726 payload = {"agent_id": "a1", "last_seen": 12345, "state": "active"}
1727 r.keys.return_value = [b"navegador:obs:agent:a1"]
1728 r.get.return_value = _json.dumps(payload).encode()
1729 agents = dash.agent_status()
1730 assert len(agents) == 1
1731 assert agents[0]["agent_id"] == "a1"
1732
1733 def test_task_metrics_default(self):
1734 dash, r = self._make()
1735 r.get.return_value = None
1736 metrics = dash.task_metrics()
1737 assert metrics == {"pending": 0, "active": 0, "completed": 0, "failed": 0}
1738
1739 def test_task_metrics_from_redis(self):
1740 import json as _json
1741 dash, r = self._make()
1742 stored = {"pending": 3, "active": 1, "completed": 10, "failed": 0}
1743 r.get.return_value = _json.dumps(stored).encode()
1744 metrics = dash.task_metrics()
1745 assert metrics["pending"] == 3
1746
1747 def test_update_task_metrics(self):
1748 import json as _json
1749 dash, r = self._make()
1750 r.get.return_value = _json.dumps(
1751 {"pending": 0, "active": 0, "completed": 0, "failed": 0}
1752 ).encode()
1753 dash.update_task_metrics(pending=5, active=2)
1754 r.set.assert_called_once()
1755
1756 def test_graph_metrics(self):
1757 store = _make_store()
1758 store.node_count.return_value = 42
1759 store.edge_count.return_value = 100
1760 dash, r = self._make()
1761 result = dash.graph_metrics(store)
1762 assert result["node_count"] == 42
1763 assert result["edge_count"] == 100
1764
1765 def test_to_json_structure(self):
1766 import json as _json
1767 dash, r = self._make()
1768 r.keys.return_value = []
1769 r.get.side_effect = [None, None] # graph_meta + task_metrics
1770 snapshot = _json.loads(dash.to_json())
1771 assert "agents" in snapshot
1772 assert "task_metrics" in snapshot
1773
1774 def test_client_lazy_init_raises_on_missing_redis(self):
1775 from navegador.cluster.observability import SwarmDashboard
1776
1777 dash = SwarmDashboard("redis://localhost:6379")
1778 with patch.dict("sys.modules", {"redis": None}):
1779 with pytest.raises(ImportError, match="redis"):
1780 dash._client()
1781
1782
1783 # ===========================================================================
1784 # navegador.cluster.pubsub (86% → target ~97%)
1785 # ===========================================================================
1786
1787
1788 class TestGraphNotifier:
1789 """Cover lines 72-76, 159-162."""
1790
1791 def _make(self):
1792 from navegador.cluster.pubsub import GraphNotifier
1793
1794 r = MagicMock()
1795 pubsub_mock = MagicMock()
1796 pubsub_mock.listen.return_value = iter([])
1797 r.pubsub.return_value = pubsub_mock
1798 r.publish.return_value = 1
1799 return GraphNotifier("redis://localhost:6379", redis_client=r), r
1800
1801 def test_publish_returns_receiver_count(self):
1802 from navegador.cluster.pubsub import EventType
1803
1804 notifier, r = self._make()
1805 count = notifier.publish(EventType.NODE_CREATED, {"name": "foo"})
1806 assert count == 1
1807 r.publish.assert_called_once()
1808
1809 def test_publish_with_string_event_type(self):
1810 notifier, r = self._make()
1811 count = notifier.publish("custom_event", {"key": "val"})
1812 assert count == 1
1813
1814 def test_subscribe_run_in_thread(self):
1815 from navegador.cluster.pubsub import EventType
1816
1817 notifier, r = self._make()
1818 import json as _json
1819 import threading
1820
1821 # Return one message then stop
1822 msg = {
1823 "type": "message",
1824 "data": _json.dumps({"event_type": "node_created", "data": {"k": "v"}}).encode(),
1825 }
1826 r.pubsub.return_value.listen.return_value = iter([msg])
1827
1828 received = []
1829
1830 def callback(et, data):
1831 received.append((et, data))
1832
1833 t = notifier.subscribe([EventType.NODE_CREATED], callback, run_in_thread=True)
1834 assert isinstance(t, threading.Thread)
1835 t.join(timeout=2)
1836 assert received == [("node_created", {"k": "v"})]
1837
1838 def test_close(self):
1839 notifier, r = self._make()
1840 notifier.close()
1841 r.close.assert_called_once()
1842
1843 def test_close_ignores_exception(self):
1844 notifier, r = self._make()
1845 r.close.side_effect = Exception("closed")
1846 notifier.close() # should not raise
1847
1848 def test_connect_redis_raises_on_missing_dep(self):
1849 from navegador.cluster.pubsub import GraphNotifier
1850
1851 with patch.dict("sys.modules", {"redis": None}):
1852 with pytest.raises(ImportError, match="redis"):
1853 GraphNotifier._connect_redis("redis://localhost")
1854
1855
1856 # ===========================================================================
1857 # navegador.ingestion.ruby — extra branches (66% → target ~85%)
1858 # ===========================================================================
1859
1860
1861 class TestRubyParserBranches:
1862 """Exercise _handle_class superclass, _handle_module body, _maybe_handle_require,
1863 _extract_calls and fallback paths."""
1864
1865 def _make_parser(self):
1866 with _mock_ts("tree_sitter_ruby"):
1867 from navegador.ingestion.ruby import RubyParser
1868 return RubyParser()
1869
1870 def test_handle_class_with_superclass(self):
1871 with _mock_ts("tree_sitter_ruby"):
1872 from navegador.ingestion.ruby import RubyParser
1873
1874 parser = RubyParser()
1875 store = _make_store()
1876
1877 name_node = _text_node(b"MyClass")
1878 superclass_node = _text_node(b"< BaseClass", "constant")
1879 class_node = MockNode("class", children=[name_node, superclass_node])
1880 class_node.set_field("name", name_node)
1881 class_node.set_field("superclass", superclass_node)
1882
1883 stats = {"functions": 0, "classes": 0, "edges": 0}
1884 parser._handle_class(class_node, b"class MyClass < BaseClass\nend", "f.rb", store, stats)
1885 assert stats["classes"] == 1
1886 assert stats["edges"] >= 2 # CONTAINS + INHERITS
1887
1888 def test_handle_class_no_name_node(self):
1889 with _mock_ts("tree_sitter_ruby"):
1890 from navegador.ingestion.ruby import RubyParser
1891
1892 parser = RubyParser()
1893 store = _make_store()
1894 anon_class = MockNode("class")
1895 stats = {"functions": 0, "classes": 0, "edges": 0}
1896 parser._handle_class(anon_class, b"class; end", "f.rb", store, stats)
1897 assert stats["classes"] == 0
1898
1899 def test_handle_module_with_body(self):
1900 with _mock_ts("tree_sitter_ruby"):
1901 from navegador.ingestion.ruby import RubyParser
1902
1903 parser = RubyParser()
1904 store = _make_store()
1905
1906 name_node = _text_node(b"MyModule")
1907 method_name = _text_node(b"my_method")
1908 method_node = MockNode("method", children=[method_name])
1909 method_node.set_field("name", method_name)
1910 body_node = MockNode("body_statement", children=[method_node])
1911 mod_node = MockNode("module", children=[name_node, body_node])
1912 mod_node.set_field("name", name_node)
1913 # body found via body_statement child (no "body" field)
1914
1915 stats = {"functions": 0, "classes": 0, "edges": 0}
1916 src = b"module MyModule\n def my_method; end\nend"
1917 parser._handle_module(mod_node, src, "f.rb", store, stats)
1918 assert stats["classes"] == 1
1919
1920 def test_handle_method_standalone_function(self):
1921 with _mock_ts("tree_sitter_ruby"):
1922 from navegador.ingestion.ruby import RubyParser
1923
1924 parser = RubyParser()
1925 store = _make_store()
1926
1927 name_node = _text_node(b"standalone_fn")
1928 fn_node = MockNode("method", children=[name_node])
1929 fn_node.set_field("name", name_node)
1930
1931 stats = {"functions": 0, "classes": 0, "edges": 0}
1932 parser._handle_method(fn_node, b"def standalone_fn; end", "f.rb", store, stats, class_name=None)
1933 assert stats["functions"] == 1
1934 # No class → Function node
1935 create_call = store.create_node.call_args_list[-1]
1936 from navegador.graph.schema import NodeLabel
1937 assert create_call[0][0] == NodeLabel.Function
1938
1939 def test_handle_method_no_name(self):
1940 with _mock_ts("tree_sitter_ruby"):
1941 from navegador.ingestion.ruby import RubyParser
1942
1943 parser = RubyParser()
1944 store = _make_store()
1945 anon_method = MockNode("method")
1946 stats = {"functions": 0, "classes": 0, "edges": 0}
1947 parser._handle_method(anon_method, b"", "f.rb", store, stats, class_name=None)
1948 assert stats["functions"] == 0
1949
1950 def test_maybe_handle_require(self):
1951 with _mock_ts("tree_sitter_ruby"):
1952 from navegador.ingestion.ruby import RubyParser
1953
1954 parser = RubyParser()
1955 store = _make_store()
1956
1957 method_node = _text_node(b"require", "identifier")
1958 string_node = _text_node(b"'json'", "string")
1959 args_node = MockNode("argument_list", children=[string_node])
1960 call_node = MockNode("call", children=[method_node, args_node])
1961 call_node.set_field("method", method_node)
1962 call_node.set_field("arguments", args_node)
1963
1964 stats = {"functions": 0, "classes": 0, "edges": 0}
1965 src = b"require 'json'"
1966 parser._maybe_handle_require(call_node, src, "f.rb", store, stats)
1967 store.create_node.assert_called_once()
1968
1969 def test_maybe_handle_require_skips_non_require(self):
1970 with _mock_ts("tree_sitter_ruby"):
1971 from navegador.ingestion.ruby import RubyParser
1972
1973 parser = RubyParser()
1974 store = _make_store()
1975
1976 method_node = _text_node(b"puts", "identifier")
1977 call_node = MockNode("call", children=[method_node])
1978 call_node.set_field("method", method_node)
1979
1980 stats = {"functions": 0, "classes": 0, "edges": 0}
1981 parser._maybe_handle_require(call_node, b"puts 'hi'", "f.rb", store, stats)
1982 store.create_node.assert_not_called()
1983
1984 def test_extract_calls(self):
1985 with _mock_ts("tree_sitter_ruby"):
1986 from navegador.ingestion.ruby import RubyParser
1987 from navegador.graph.schema import NodeLabel
1988
1989 parser = RubyParser()
1990 store = _make_store()
1991
1992 callee_node = _text_node(b"helper", "identifier")
1993 call_node = MockNode("call", children=[callee_node])
1994 call_node.set_field("method", callee_node)
1995 body_node = MockNode("body_statement", children=[call_node])
1996 fn_node = MockNode("method", children=[body_node])
1997
1998 stats = {"functions": 0, "classes": 0, "edges": 0}
1999 parser._extract_calls(fn_node, b"def foo; helper; end", "f.rb", "foo", NodeLabel.Function, store, stats)
2000 store.create_edge.assert_called()
2001
2002
2003 # ===========================================================================
2004 # navegador.ingestion.cpp — extra branches (73% → target ~90%)
2005 # ===========================================================================
2006
2007
2008 class TestCppParserBranches:
2009 def _make_parser(self):
2010 with _mock_ts("tree_sitter_cpp"):
2011 from navegador.ingestion.cpp import CppParser
2012 return CppParser()
2013
2014 def test_handle_class_with_inheritance(self):
2015 with _mock_ts("tree_sitter_cpp"):
2016 from navegador.ingestion.cpp import CppParser
2017
2018 parser = CppParser()
2019 store = _make_store()
2020
2021 name_node = _text_node(b"MyClass", "type_identifier")
2022 parent_node = _text_node(b"BaseClass", "type_identifier")
2023 base_clause_node = MockNode("base_class_clause", children=[parent_node])
2024 class_node = MockNode("class_specifier", children=[name_node, base_clause_node])
2025 class_node.set_field("name", name_node)
2026 class_node.set_field("base_clause", base_clause_node)
2027
2028 stats = {"functions": 0, "classes": 0, "edges": 0}
2029 parser._handle_class(class_node, b"class MyClass : public BaseClass {};", "f.cpp", store, stats)
2030 assert stats["classes"] == 1
2031 assert stats["edges"] >= 2
2032
2033 def test_handle_class_no_name(self):
2034 with _mock_ts("tree_sitter_cpp"):
2035 from navegador.ingestion.cpp import CppParser
2036
2037 parser = CppParser()
2038 store = _make_store()
2039 anon_class = MockNode("class_specifier")
2040 stats = {"functions": 0, "classes": 0, "edges": 0}
2041 parser._handle_class(anon_class, b"struct {};", "f.cpp", store, stats)
2042 assert stats["classes"] == 0
2043
2044 def test_handle_function_with_class_name(self):
2045 with _mock_ts("tree_sitter_cpp"):
2046 from navegador.ingestion.cpp import CppParser
2047
2048 parser = CppParser()
2049 store = _make_store()
2050
2051 fn_name_node = _text_node(b"myMethod", "identifier")
2052 declarator = MockNode("function_declarator", children=[fn_name_node])
2053 declarator.set_field("declarator", fn_name_node)
2054 body = MockNode("compound_statement")
2055 fn_node = MockNode("function_definition", children=[declarator, body])
2056 fn_node.set_field("declarator", declarator)
2057 fn_node.set_field("body", body)
2058
2059 stats = {"functions": 0, "classes": 0, "edges": 0}
2060 parser._handle_function(fn_node, b"void myMethod() {}", "f.cpp", store, stats, class_name="MyClass")
2061 assert stats["functions"] == 1
2062 from navegador.graph.schema import NodeLabel
2063 assert store.create_node.call_args[0][0] == NodeLabel.Method
2064
2065 def test_extract_function_name_qualified(self):
2066 with _mock_ts("tree_sitter_cpp"):
2067 from navegador.ingestion.cpp import CppParser
2068
2069 parser = CppParser()
2070 src = b"method"
2071 name_node = MockNode("identifier", start_byte=0, end_byte=len(src))
2072 qualified = MockNode("qualified_identifier", children=[name_node])
2073 qualified.set_field("name", name_node)
2074 result = parser._extract_function_name(qualified, src)
2075 assert result == "method"
2076
2077 def test_extract_function_name_qualified_fallback(self):
2078 with _mock_ts("tree_sitter_cpp"):
2079 from navegador.ingestion.cpp import CppParser
2080
2081 parser = CppParser()
2082 src = b"method"
2083 id_node = MockNode("identifier", start_byte=0, end_byte=len(src))
2084 qualified = MockNode("qualified_identifier", children=[id_node])
2085 # No name field → fallback to last identifier child
2086 result = parser._extract_function_name(qualified, src)
2087 assert result == "method"
2088
2089 def test_extract_function_name_pointer_declarator(self):
2090 with _mock_ts("tree_sitter_cpp"):
2091 from navegador.ingestion.cpp import CppParser
2092
2093 parser = CppParser()
2094 src = b"fp"
2095 inner = MockNode("identifier", start_byte=0, end_byte=len(src))
2096 ptr_decl = MockNode("pointer_declarator", children=[inner])
2097 ptr_decl.set_field("declarator", inner)
2098 result = parser._extract_function_name(ptr_decl, src)
2099 assert result == "fp"
2100
2101 def test_extract_function_name_fallback_child(self):
2102 with _mock_ts("tree_sitter_cpp"):
2103 from navegador.ingestion.cpp import CppParser
2104
2105 parser = CppParser()
2106 src = b"fallbackFn"
2107 id_node = MockNode("identifier", start_byte=0, end_byte=len(src))
2108 unknown_decl = MockNode("unknown_declarator", children=[id_node])
2109 result = parser._extract_function_name(unknown_decl, src)
2110 assert result == "fallbackFn"
2111
2112 def test_extract_function_name_none(self):
2113 with _mock_ts("tree_sitter_cpp"):
2114 from navegador.ingestion.cpp import CppParser
2115
2116 parser = CppParser()
2117 assert parser._extract_function_name(None, b"") is None
2118
2119 def test_handle_include(self):
2120 with _mock_ts("tree_sitter_cpp"):
2121 from navegador.ingestion.cpp import CppParser
2122
2123 parser = CppParser()
2124 store = _make_store()
2125
2126 path_node = _text_node(b'"vector"', "string_literal")
2127 include_node = MockNode("preproc_include", children=[path_node])
2128 include_node.set_field("path", path_node)
2129
2130 stats = {"functions": 0, "classes": 0, "edges": 0}
2131 parser._handle_include(include_node, b'#include "vector"', "f.cpp", store, stats)
2132 store.create_node.assert_called_once()
2133
2134 def test_namespace_recurse(self):
2135 with _mock_ts("tree_sitter_cpp"):
2136 from navegador.ingestion.cpp import CppParser
2137
2138 parser = CppParser()
2139 store = _make_store()
2140
2141 # namespace body contains a function
2142 fn_name = _text_node(b"inner_fn", "identifier")
2143 fn_decl = MockNode("function_declarator", children=[fn_name])
2144 fn_decl.set_field("declarator", fn_name)
2145 fn_body = MockNode("compound_statement")
2146 fn_def = MockNode("function_definition", children=[fn_decl, fn_body])
2147 fn_def.set_field("declarator", fn_decl)
2148
2149 decl_list = MockNode("declaration_list", children=[fn_def])
2150 ns_node = MockNode("namespace_definition", children=[decl_list])
2151
2152 stats = {"functions": 0, "classes": 0, "edges": 0}
2153 parser._walk(ns_node, b"namespace ns { void inner_fn(){} }", "f.cpp", store, stats, class_name=None)
2154 assert stats["functions"] == 1
2155
2156
2157 # ===========================================================================
2158 # navegador.ingestion.csharp — extra branches (79% → target ~90%)
2159 # ===========================================================================
2160
2161
2162 class TestCSharpParserBranches:
2163 def test_handle_class_with_bases(self):
2164 with _mock_ts("tree_sitter_c_sharp"):
2165 from navegador.ingestion.csharp import CSharpParser
2166
2167 parser = CSharpParser()
2168 store = _make_store()
2169
2170 name_node = _text_node(b"MyService", "identifier")
2171 base_id = _text_node(b"IService", "identifier")
2172 bases_node = MockNode("base_list", children=[base_id])
2173 class_node = MockNode("class_declaration", children=[name_node, bases_node])
2174 class_node.set_field("name", name_node)
2175 class_node.set_field("bases", bases_node)
2176
2177 stats = {"functions": 0, "classes": 0, "edges": 0}
2178 parser._handle_class(class_node, b"class MyService : IService {}", "f.cs", store, stats)
2179 assert stats["classes"] == 1
2180 assert stats["edges"] >= 2
2181
2182 def test_handle_class_no_name(self):
2183 with _mock_ts("tree_sitter_c_sharp"):
2184 from navegador.ingestion.csharp import CSharpParser
2185
2186 parser = CSharpParser()
2187 store = _make_store()
2188 anon = MockNode("class_declaration")
2189 stats = {"functions": 0, "classes": 0, "edges": 0}
2190 parser._handle_class(anon, b"", "f.cs", store, stats)
2191 assert stats["classes"] == 0
2192
2193 def test_handle_method_standalone(self):
2194 with _mock_ts("tree_sitter_c_sharp"):
2195 from navegador.ingestion.csharp import CSharpParser
2196
2197 parser = CSharpParser()
2198 store = _make_store()
2199
2200 name_node = _text_node(b"DoWork", "identifier")
2201 fn_node = MockNode("method_declaration", children=[name_node])
2202 fn_node.set_field("name", name_node)
2203
2204 stats = {"functions": 0, "classes": 0, "edges": 0}
2205 parser._handle_method(fn_node, b"void DoWork() {}", "f.cs", store, stats, class_name=None)
2206 assert stats["functions"] == 1
2207
2208 def test_handle_method_no_name(self):
2209 with _mock_ts("tree_sitter_c_sharp"):
2210 from navegador.ingestion.csharp import CSharpParser
2211
2212 parser = CSharpParser()
2213 store = _make_store()
2214 anon = MockNode("method_declaration")
2215 stats = {"functions": 0, "classes": 0, "edges": 0}
2216 parser._handle_method(anon, b"", "f.cs", store, stats, class_name=None)
2217 assert stats["functions"] == 0
2218
2219 def test_handle_using(self):
2220 with _mock_ts("tree_sitter_c_sharp"):
2221 from navegador.ingestion.csharp import CSharpParser
2222
2223 parser = CSharpParser()
2224 store = _make_store()
2225
2226 src = b"using System.Collections.Generic;"
2227 using_node = MockNode(
2228 "using_directive",
2229 start_byte=0,
2230 end_byte=len(src),
2231 start_point=(0, 0),
2232 end_point=(0, len(src)),
2233 )
2234 stats = {"functions": 0, "classes": 0, "edges": 0}
2235 parser._handle_using(using_node, src, "f.cs", store, stats)
2236 store.create_node.assert_called_once()
2237
2238 def test_extract_calls(self):
2239 with _mock_ts("tree_sitter_c_sharp"):
2240 from navegador.ingestion.csharp import CSharpParser
2241 from navegador.graph.schema import NodeLabel
2242
2243 parser = CSharpParser()
2244 store = _make_store()
2245
2246 callee_node = _text_node(b"DoWork", "identifier")
2247 invoke_node = MockNode("invocation_expression", children=[callee_node])
2248 invoke_node.set_field("function", callee_node)
2249 block_node = MockNode("block", children=[invoke_node])
2250 fn_node = MockNode("method_declaration", children=[block_node])
2251 fn_node.set_field("body", block_node)
2252
2253 stats = {"functions": 0, "classes": 0, "edges": 0}
2254 parser._extract_calls(fn_node, b"DoWork()", "f.cs", "Run", NodeLabel.Method, store, stats)
2255 store.create_edge.assert_called()
2256
2257
2258 # ===========================================================================
2259 # navegador.ingestion.kotlin — extra branches (79% → target ~90%)
2260 # ===========================================================================
2261
2262
2263 class TestKotlinParserBranches:
2264 def test_handle_class_no_name(self):
2265 with _mock_ts("tree_sitter_kotlin"):
2266 from navegador.ingestion.kotlin import KotlinParser
2267
2268 parser = KotlinParser()
2269 store = _make_store()
2270 anon = MockNode("class_declaration")
2271 stats = {"functions": 0, "classes": 0, "edges": 0}
2272 parser._handle_class(anon, b"", "f.kt", store, stats)
2273 assert stats["classes"] == 0
2274
2275 def test_handle_class_with_body(self):
2276 with _mock_ts("tree_sitter_kotlin"):
2277 from navegador.ingestion.kotlin import KotlinParser
2278
2279 parser = KotlinParser()
2280 store = _make_store()
2281
2282 name_node = _text_node(b"MyClass", "simple_identifier")
2283 fn_name = _text_node(b"doSomething", "simple_identifier")
2284 fn_node = MockNode("function_declaration", children=[fn_name])
2285 fn_node.set_field("name", fn_name)
2286 body_node = MockNode("class_body", children=[fn_node])
2287 class_node = MockNode("class_declaration", children=[name_node, body_node])
2288 class_node.set_field("name", name_node)
2289 class_node.set_field("body", body_node)
2290
2291 stats = {"functions": 0, "classes": 0, "edges": 0}
2292 src = b"class MyClass { fun doSomething() {} }"
2293 parser._handle_class(class_node, src, "f.kt", store, stats)
2294 assert stats["classes"] == 1
2295 assert stats["functions"] == 1
2296
2297 def test_handle_function_no_name(self):
2298 with _mock_ts("tree_sitter_kotlin"):
2299 from navegador.ingestion.kotlin import KotlinParser
2300
2301 parser = KotlinParser()
2302 store = _make_store()
2303 anon = MockNode("function_declaration")
2304 stats = {"functions": 0, "classes": 0, "edges": 0}
2305 parser._handle_function(anon, b"", "f.kt", store, stats, class_name=None)
2306 assert stats["functions"] == 0
2307
2308 def test_handle_import(self):
2309 with _mock_ts("tree_sitter_kotlin"):
2310 from navegador.ingestion.kotlin import KotlinParser
2311
2312 parser = KotlinParser()
2313 store = _make_store()
2314
2315 src = b"import kotlin.collections.List"
2316 import_node = MockNode(
2317 "import_header",
2318 start_byte=0,
2319 end_byte=len(src),
2320 start_point=(0, 0),
2321 end_point=(0, len(src)),
2322 )
2323 stats = {"functions": 0, "classes": 0, "edges": 0}
2324 parser._handle_import(import_node, src, "f.kt", store, stats)
2325 store.create_node.assert_called_once()
2326
2327
2328 # ===========================================================================
2329 # navegador.ingestion.php — extra branches (79% → target ~90%)
2330 # ===========================================================================
2331
2332
2333 class TestPHPParserBranches:
2334 def test_handle_class_no_name(self):
2335 with _mock_ts("tree_sitter_php"):
2336 from navegador.ingestion.php import PHPParser
2337
2338 parser = PHPParser()
2339 store = _make_store()
2340 anon = MockNode("class_declaration")
2341 stats = {"functions": 0, "classes": 0, "edges": 0}
2342 parser._handle_class(anon, b"", "f.php", store, stats)
2343 assert stats["classes"] == 0
2344
2345 def test_handle_function_no_name(self):
2346 with _mock_ts("tree_sitter_php"):
2347 from navegador.ingestion.php import PHPParser
2348
2349 parser = PHPParser()
2350 store = _make_store()
2351 anon = MockNode("function_definition")
2352 stats = {"functions": 0, "classes": 0, "edges": 0}
2353 parser._handle_function(anon, b"", "f.php", store, stats, class_name=None)
2354 assert stats["functions"] == 0
2355
2356 def test_handle_class_with_body_methods(self):
2357 with _mock_ts("tree_sitter_php"):
2358 from navegador.ingestion.php import PHPParser
2359
2360 parser = PHPParser()
2361 store = _make_store()
2362
2363 name_node = _text_node(b"MyController", "name")
2364 fn_name = _text_node(b"index", "name")
2365 method_node = MockNode("method_declaration", children=[fn_name])
2366 method_node.set_field("name", fn_name)
2367 body_node = MockNode("declaration_list", children=[method_node])
2368 class_node = MockNode("class_declaration", children=[name_node, body_node])
2369 class_node.set_field("name", name_node)
2370 class_node.set_field("body", body_node)
2371
2372 stats = {"functions": 0, "classes": 0, "edges": 0}
2373 src = b"class MyController { public function index() {} }"
2374 parser._handle_class(class_node, src, "f.php", store, stats)
2375 assert stats["classes"] == 1
2376 assert stats["functions"] >= 1
2377
2378
2379 # ===========================================================================
2380 # navegador.ingestion.swift — extra branches (79% → target ~90%)
2381 # ===========================================================================
2382
2383
2384 class TestSwiftParserBranches:
2385 def test_handle_class_no_name(self):
2386 with _mock_ts("tree_sitter_swift"):
2387 from navegador.ingestion.swift import SwiftParser
2388
2389 parser = SwiftParser()
2390 store = _make_store()
2391 anon = MockNode("class_declaration")
2392 stats = {"functions": 0, "classes": 0, "edges": 0}
2393 parser._handle_class(anon, b"", "f.swift", store, stats)
2394 assert stats["classes"] == 0
2395
2396 def test_handle_function_no_name(self):
2397 with _mock_ts("tree_sitter_swift"):
2398 from navegador.ingestion.swift import SwiftParser
2399
2400 parser = SwiftParser()
2401 store = _make_store()
2402 anon = MockNode("function_declaration")
2403 stats = {"functions": 0, "classes": 0, "edges": 0}
2404 parser._handle_function(anon, b"", "f.swift", store, stats, class_name=None)
2405 assert stats["functions"] == 0
2406
2407 def test_handle_import(self):
2408 with _mock_ts("tree_sitter_swift"):
2409 from navegador.ingestion.swift import SwiftParser
2410
2411 parser = SwiftParser()
2412 store = _make_store()
2413
2414 src = b"import Foundation"
2415 import_node = MockNode(
2416 "import_declaration",
2417 start_byte=0,
2418 end_byte=len(src),
2419 start_point=(0, 0),
2420 end_point=(0, len(src)),
2421 )
2422 stats = {"functions": 0, "classes": 0, "edges": 0}
2423 parser._handle_import(import_node, src, "f.swift", store, stats)
2424 store.create_node.assert_called_once()
2425
2426 def test_handle_class_with_body(self):
2427 with _mock_ts("tree_sitter_swift"):
2428 from navegador.ingestion.swift import SwiftParser
2429
2430 parser = SwiftParser()
2431 store = _make_store()
2432
2433 name_node = _text_node(b"MyView", "type_identifier")
2434 fn_name = _text_node(b"body", "simple_identifier")
2435 fn_node = MockNode("function_declaration", children=[fn_name])
2436 fn_node.set_field("name", fn_name)
2437 body_node = MockNode("class_body", children=[fn_node])
2438 class_node = MockNode("class_declaration", children=[name_node, body_node])
2439 class_node.set_field("name", name_node)
2440 class_node.set_field("body", body_node)
2441
2442 stats = {"functions": 0, "classes": 0, "edges": 0}
2443 src = b"class MyView { func body() {} }"
2444 parser._handle_class(class_node, src, "f.swift", store, stats)
2445 assert stats["classes"] == 1
2446 assert stats["functions"] == 1
2447
2448
2449 # ===========================================================================
2450 # navegador.ingestion.c — extra branches (76% → target ~90%)
2451 # ===========================================================================
2452
2453
2454 class TestCParserBranches:
2455 def test_handle_function(self):
2456 with _mock_ts("tree_sitter_c"):
2457 from navegador.ingestion.c import CParser
2458
2459 parser = CParser()
2460 store = _make_store()
2461
2462 fn_name_node = _text_node(b"myFunc", "identifier")
2463 declarator = MockNode("function_declarator", children=[fn_name_node])
2464 declarator.set_field("declarator", fn_name_node)
2465 body = MockNode("compound_statement")
2466 fn_node = MockNode("function_definition", children=[declarator, body])
2467 fn_node.set_field("declarator", declarator)
2468 fn_node.set_field("body", body)
2469
2470 stats = {"functions": 0, "classes": 0, "edges": 0}
2471 parser._handle_function(fn_node, b"void myFunc() {}", "f.c", store, stats)
2472 assert stats["functions"] == 1
2473
2474 def test_handle_struct(self):
2475 with _mock_ts("tree_sitter_c"):
2476 from navegador.ingestion.c import CParser
2477
2478 parser = CParser()
2479 store = _make_store()
2480
2481 name_node = _text_node(b"Point", "type_identifier")
2482 struct_node = MockNode("struct_specifier", children=[name_node])
2483 struct_node.set_field("name", name_node)
2484
2485 stats = {"functions": 0, "classes": 0, "edges": 0}
2486 parser._handle_struct(struct_node, b"struct Point {};", "f.c", store, stats)
2487 assert stats["classes"] == 1
2488
2489 def test_handle_struct_no_name(self):
2490 with _mock_ts("tree_sitter_c"):
2491 from navegador.ingestion.c import CParser
2492
2493 parser = CParser()
2494 store = _make_store()
2495 anon = MockNode("struct_specifier")
2496 stats = {"functions": 0, "classes": 0, "edges": 0}
2497 parser._handle_struct(anon, b"", "f.c", store, stats)
2498 assert stats["classes"] == 0
2499
2500 def test_handle_include(self):
2501 with _mock_ts("tree_sitter_c"):
2502 from navegador.ingestion.c import CParser
2503
2504 parser = CParser()
2505 store = _make_store()
2506
2507 path_node = _text_node(b"<stdio.h>", "system_lib_string")
2508 include_node = MockNode("preproc_include", children=[path_node])
2509 include_node.set_field("path", path_node)
2510
2511 stats = {"functions": 0, "classes": 0, "edges": 0}
2512 parser._handle_include(include_node, b"#include <stdio.h>", "f.c", store, stats)
2513 store.create_node.assert_called_once()
2514
2515 def test_handle_function_no_name(self):
2516 with _mock_ts("tree_sitter_c"):
2517 from navegador.ingestion.c import CParser
2518
2519 parser = CParser()
2520 store = _make_store()
2521
2522 fn_node = MockNode("function_definition")
2523 # No declarator field → _extract_function_name returns None
2524 stats = {"functions": 0, "classes": 0, "edges": 0}
2525 parser._handle_function(fn_node, b"", "f.c", store, stats)
2526 assert stats["functions"] == 0

Keyboard Shortcuts

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