Navegador

navegador / tests / test_coverage_boost.py
Blame History Raw 2527 lines
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
2527

Keyboard Shortcuts

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