Navegador

navegador / tests / test_sdk.py
Blame History Raw 550 lines
1
"""
2
Tests for the Navegador Python SDK (navegador/sdk.py).
3
4
All tests use a mock GraphStore so no real database is required.
5
"""
6
7
from unittest.mock import MagicMock, patch
8
9
import pytest
10
11
from navegador.sdk import Navegador
12
13
14
# ── Helpers ───────────────────────────────────────────────────────────────────
15
16
17
def _mock_store(rows=None):
18
"""Return a mock GraphStore whose .query() yields the given rows."""
19
store = MagicMock()
20
result = MagicMock()
21
result.result_set = rows or []
22
store.query.return_value = result
23
return store
24
25
26
def _nav(rows=None):
27
"""Return a Navegador instance wired to a mock store."""
28
return Navegador(_mock_store(rows))
29
30
31
# ── Constructor tests ─────────────────────────────────────────────────────────
32
33
34
class TestConstructors:
35
def test_direct_init_stores_store(self):
36
store = _mock_store()
37
nav = Navegador(store)
38
assert nav._store is store
39
40
def test_sqlite_classmethod(self):
41
fake_store = _mock_store()
42
with patch("navegador.graph.store.GraphStore.sqlite", return_value=fake_store) as mock_sqlite:
43
nav = Navegador.sqlite("/tmp/test.db")
44
mock_sqlite.assert_called_once_with("/tmp/test.db")
45
assert nav._store is fake_store
46
47
def test_sqlite_default_path(self):
48
fake_store = _mock_store()
49
with patch("navegador.graph.store.GraphStore.sqlite", return_value=fake_store) as mock_sqlite:
50
Navegador.sqlite()
51
mock_sqlite.assert_called_once_with(".navegador/graph.db")
52
53
def test_redis_classmethod(self):
54
fake_store = _mock_store()
55
with patch("navegador.graph.store.GraphStore.redis", return_value=fake_store) as mock_redis:
56
nav = Navegador.redis("redis://myhost:6379")
57
mock_redis.assert_called_once_with("redis://myhost:6379")
58
assert nav._store is fake_store
59
60
def test_redis_default_url(self):
61
fake_store = _mock_store()
62
with patch("navegador.graph.store.GraphStore.redis", return_value=fake_store) as mock_redis:
63
Navegador.redis()
64
mock_redis.assert_called_once_with("redis://localhost:6379")
65
66
67
# ── Ingestion ─────────────────────────────────────────────────────────────────
68
69
70
class TestIngest:
71
def test_ingest_delegates_to_repo_ingester(self):
72
store = _mock_store()
73
nav = Navegador(store)
74
expected = {"files": 3, "functions": 10, "classes": 2, "edges": 5, "skipped": 0}
75
76
with patch("navegador.ingestion.RepoIngester") as MockIngester:
77
mock_instance = MockIngester.return_value
78
mock_instance.ingest.return_value = expected
79
80
result = nav.ingest("/some/repo")
81
82
MockIngester.assert_called_once_with(store)
83
mock_instance.ingest.assert_called_once_with(
84
"/some/repo", clear=False, incremental=False
85
)
86
assert result == expected
87
88
def test_ingest_passes_clear_and_incremental(self):
89
store = _mock_store()
90
nav = Navegador(store)
91
92
with patch("navegador.ingestion.RepoIngester") as MockIngester:
93
mock_instance = MockIngester.return_value
94
mock_instance.ingest.return_value = {}
95
96
nav.ingest("/repo", clear=True, incremental=True)
97
mock_instance.ingest.assert_called_once_with(
98
"/repo", clear=True, incremental=True
99
)
100
101
102
# ── Context loading ───────────────────────────────────────────────────────────
103
104
105
class TestFileContext:
106
def test_returns_context_bundle(self):
107
from navegador.context.loader import ContextBundle, ContextNode
108
109
nav = _nav([])
110
bundle = nav.file_context("src/auth.py")
111
assert isinstance(bundle, ContextBundle)
112
assert bundle.target.type == "File"
113
assert bundle.target.name == "auth.py"
114
115
def test_passes_file_path(self):
116
store = _mock_store([])
117
nav = Navegador(store)
118
nav.file_context("src/auth.py")
119
# store.query must have been called with the file path param
120
call_args = store.query.call_args
121
assert call_args[0][1]["path"] == "src/auth.py"
122
123
124
class TestFunctionContext:
125
def test_returns_context_bundle(self):
126
from navegador.context.loader import ContextBundle
127
128
nav = _nav([])
129
bundle = nav.function_context("validate_token")
130
assert isinstance(bundle, ContextBundle)
131
assert bundle.target.name == "validate_token"
132
assert bundle.target.type == "Function"
133
134
def test_passes_file_path_and_depth(self):
135
store = _mock_store([])
136
nav = Navegador(store)
137
138
with patch("navegador.context.loader.ContextLoader.load_function") as mock_load:
139
from navegador.context.loader import ContextBundle, ContextNode
140
mock_load.return_value = ContextBundle(
141
target=ContextNode(type="Function", name="fn")
142
)
143
nav.function_context("fn", file_path="src/x.py", depth=3)
144
mock_load.assert_called_once_with("fn", file_path="src/x.py", depth=3)
145
146
def test_default_depth(self):
147
store = _mock_store([])
148
nav = Navegador(store)
149
150
with patch("navegador.context.loader.ContextLoader.load_function") as mock_load:
151
from navegador.context.loader import ContextBundle, ContextNode
152
mock_load.return_value = ContextBundle(
153
target=ContextNode(type="Function", name="fn")
154
)
155
nav.function_context("fn")
156
mock_load.assert_called_once_with("fn", file_path="", depth=2)
157
158
159
class TestClassContext:
160
def test_returns_context_bundle(self):
161
from navegador.context.loader import ContextBundle
162
163
nav = _nav([])
164
bundle = nav.class_context("AuthService")
165
assert isinstance(bundle, ContextBundle)
166
assert bundle.target.name == "AuthService"
167
assert bundle.target.type == "Class"
168
169
def test_passes_file_path(self):
170
store = _mock_store([])
171
nav = Navegador(store)
172
173
with patch("navegador.context.loader.ContextLoader.load_class") as mock_load:
174
from navegador.context.loader import ContextBundle, ContextNode
175
mock_load.return_value = ContextBundle(
176
target=ContextNode(type="Class", name="AuthService")
177
)
178
nav.class_context("AuthService", file_path="src/auth.py")
179
mock_load.assert_called_once_with("AuthService", file_path="src/auth.py")
180
181
182
class TestExplain:
183
def test_returns_context_bundle(self):
184
from navegador.context.loader import ContextBundle
185
186
nav = _nav([])
187
bundle = nav.explain("validate_token")
188
assert isinstance(bundle, ContextBundle)
189
assert bundle.metadata["query"] == "explain"
190
191
def test_passes_file_path(self):
192
store = _mock_store([])
193
nav = Navegador(store)
194
195
with patch("navegador.context.loader.ContextLoader.explain") as mock_explain:
196
from navegador.context.loader import ContextBundle, ContextNode
197
mock_explain.return_value = ContextBundle(
198
target=ContextNode(type="Node", name="fn")
199
)
200
nav.explain("fn", file_path="src/x.py")
201
mock_explain.assert_called_once_with("fn", file_path="src/x.py")
202
203
204
# ── Knowledge ─────────────────────────────────────────────────────────────────
205
206
207
class TestAddConcept:
208
def test_delegates_to_knowledge_ingester(self):
209
store = _mock_store()
210
nav = Navegador(store)
211
212
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
213
mock_k = MockK.return_value
214
nav.add_concept("JWT", description="Stateless token", domain="auth")
215
MockK.assert_called_once_with(store)
216
mock_k.add_concept.assert_called_once_with(
217
"JWT", description="Stateless token", domain="auth"
218
)
219
220
221
class TestAddRule:
222
def test_delegates_to_knowledge_ingester(self):
223
store = _mock_store()
224
nav = Navegador(store)
225
226
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
227
mock_k = MockK.return_value
228
nav.add_rule("tokens must expire", severity="critical")
229
mock_k.add_rule.assert_called_once_with(
230
"tokens must expire", severity="critical"
231
)
232
233
234
class TestAddDecision:
235
def test_delegates_to_knowledge_ingester(self):
236
store = _mock_store()
237
nav = Navegador(store)
238
239
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
240
mock_k = MockK.return_value
241
nav.add_decision("Use FalkorDB", rationale="Cypher + SQLite", status="accepted")
242
mock_k.add_decision.assert_called_once_with(
243
"Use FalkorDB", rationale="Cypher + SQLite", status="accepted"
244
)
245
246
247
class TestAddPerson:
248
def test_delegates_to_knowledge_ingester(self):
249
store = _mock_store()
250
nav = Navegador(store)
251
252
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
253
mock_k = MockK.return_value
254
nav.add_person("Alice", email="[email protected]", role="lead")
255
mock_k.add_person.assert_called_once_with(
256
"Alice", email="[email protected]", role="lead"
257
)
258
259
260
class TestAddDomain:
261
def test_delegates_to_knowledge_ingester(self):
262
store = _mock_store()
263
nav = Navegador(store)
264
265
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
266
mock_k = MockK.return_value
267
nav.add_domain("auth", description="Authentication layer")
268
mock_k.add_domain.assert_called_once_with(
269
"auth", description="Authentication layer"
270
)
271
272
273
class TestAnnotate:
274
def test_delegates_to_knowledge_ingester(self):
275
store = _mock_store()
276
nav = Navegador(store)
277
278
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
279
mock_k = MockK.return_value
280
nav.annotate("validate_token", "Function", concept="JWT")
281
mock_k.annotate_code.assert_called_once_with(
282
"validate_token", "Function", concept="JWT", rule=None
283
)
284
285
def test_passes_rule(self):
286
store = _mock_store()
287
nav = Navegador(store)
288
289
with patch("navegador.ingestion.KnowledgeIngester") as MockK:
290
mock_k = MockK.return_value
291
nav.annotate("validate_token", "Function", rule="tokens must expire")
292
mock_k.annotate_code.assert_called_once_with(
293
"validate_token", "Function", concept=None, rule="tokens must expire"
294
)
295
296
297
class TestConceptLoad:
298
def test_delegates_to_context_loader(self):
299
rows = [["JWT", "Stateless token auth", "active", "auth", [], [], [], []]]
300
nav = _nav(rows)
301
bundle = nav.concept("JWT")
302
assert bundle.target.name == "JWT"
303
assert bundle.target.type == "Concept"
304
305
def test_not_found_returns_bundle_with_found_false(self):
306
nav = _nav([])
307
bundle = nav.concept("NonExistent")
308
assert bundle.metadata.get("found") is False
309
310
311
class TestDomainLoad:
312
def test_delegates_to_context_loader(self):
313
rows = [["Function", "login", "src/auth.py", "Log in"]]
314
nav = _nav(rows)
315
bundle = nav.domain("auth")
316
assert bundle.target.name == "auth"
317
assert bundle.target.type == "Domain"
318
319
320
class TestDecisionLoad:
321
def test_delegates_to_context_loader(self):
322
rows = [[
323
"Use FalkorDB",
324
"Graph DB",
325
"Cypher queries",
326
"Neo4j",
327
"accepted",
328
"2026-01-01",
329
"infrastructure",
330
[],
331
[],
332
]]
333
nav = _nav(rows)
334
bundle = nav.decision("Use FalkorDB")
335
assert bundle.target.name == "Use FalkorDB"
336
assert bundle.target.type == "Decision"
337
assert bundle.target.rationale == "Cypher queries"
338
339
def test_not_found(self):
340
nav = _nav([])
341
bundle = nav.decision("Unknown")
342
assert bundle.metadata.get("found") is False
343
344
345
# ── Search ────────────────────────────────────────────────────────────────────
346
347
348
class TestSearch:
349
def test_search_returns_nodes(self):
350
from navegador.context.loader import ContextNode
351
352
rows = [["Function", "validate_token", "src/auth.py", 10, "Validate a token"]]
353
nav = _nav(rows)
354
results = nav.search("validate")
355
assert len(results) == 1
356
assert isinstance(results[0], ContextNode)
357
assert results[0].name == "validate_token"
358
359
def test_search_empty(self):
360
nav = _nav([])
361
assert nav.search("xyz") == []
362
363
def test_search_passes_limit(self):
364
store = _mock_store([])
365
nav = Navegador(store)
366
367
with patch("navegador.context.loader.ContextLoader.search") as mock_search:
368
mock_search.return_value = []
369
nav.search("auth", limit=5)
370
mock_search.assert_called_once_with("auth", limit=5)
371
372
373
class TestSearchAll:
374
def test_search_all_returns_nodes(self):
375
from navegador.context.loader import ContextNode
376
377
rows = [["Concept", "JWT", "", None, "Stateless token auth"]]
378
nav = _nav(rows)
379
results = nav.search_all("JWT")
380
assert len(results) == 1
381
assert results[0].type == "Concept"
382
383
def test_search_all_passes_limit(self):
384
store = _mock_store([])
385
nav = Navegador(store)
386
387
with patch("navegador.context.loader.ContextLoader.search_all") as mock_sa:
388
mock_sa.return_value = []
389
nav.search_all("auth", limit=10)
390
mock_sa.assert_called_once_with("auth", limit=10)
391
392
393
class TestSearchKnowledge:
394
def test_search_knowledge_returns_nodes(self):
395
from navegador.context.loader import ContextNode
396
397
rows = [["Concept", "JWT", "Stateless token auth", "auth", "active"]]
398
nav = _nav(rows)
399
results = nav.search_knowledge("JWT")
400
assert len(results) == 1
401
assert results[0].domain == "auth"
402
403
def test_search_knowledge_empty(self):
404
nav = _nav([])
405
assert nav.search_knowledge("missing") == []
406
407
def test_search_knowledge_passes_limit(self):
408
store = _mock_store([])
409
nav = Navegador(store)
410
411
with patch("navegador.context.loader.ContextLoader.search_knowledge") as mock_sk:
412
mock_sk.return_value = []
413
nav.search_knowledge("auth", limit=3)
414
mock_sk.assert_called_once_with("auth", limit=3)
415
416
417
# ── Graph ─────────────────────────────────────────────────────────────────────
418
419
420
class TestQuery:
421
def test_delegates_to_store(self):
422
store = _mock_store([[42]])
423
nav = Navegador(store)
424
result = nav.query("MATCH (n) RETURN count(n)")
425
store.query.assert_called_once_with("MATCH (n) RETURN count(n)", None)
426
assert result.result_set == [[42]]
427
428
def test_passes_params(self):
429
store = _mock_store([])
430
nav = Navegador(store)
431
nav.query("MATCH (n:Function {name: $name}) RETURN n", {"name": "foo"})
432
store.query.assert_called_once_with(
433
"MATCH (n:Function {name: $name}) RETURN n", {"name": "foo"}
434
)
435
436
437
class TestStats:
438
def test_returns_dict_with_expected_keys(self):
439
node_result = MagicMock()
440
node_result.result_set = [["Function", 5], ["Class", 2]]
441
edge_result = MagicMock()
442
edge_result.result_set = [["CALLS", 8], ["INHERITS", 1]]
443
444
store = MagicMock()
445
store.query.side_effect = [node_result, edge_result]
446
447
nav = Navegador(store)
448
s = nav.stats()
449
450
assert s["total_nodes"] == 7
451
assert s["total_edges"] == 9
452
assert s["nodes"]["Function"] == 5
453
assert s["nodes"]["Class"] == 2
454
assert s["edges"]["CALLS"] == 8
455
assert s["edges"]["INHERITS"] == 1
456
457
def test_empty_graph(self):
458
store = _mock_store([])
459
nav = Navegador(store)
460
s = nav.stats()
461
assert s["total_nodes"] == 0
462
assert s["total_edges"] == 0
463
assert s["nodes"] == {}
464
assert s["edges"] == {}
465
466
467
class TestExport:
468
def test_delegates_to_export_graph(self):
469
store = _mock_store()
470
nav = Navegador(store)
471
expected = {"nodes": 10, "edges": 5}
472
473
with patch("navegador.graph.export.export_graph", return_value=expected) as mock_export:
474
result = nav.export("/tmp/out.jsonl")
475
mock_export.assert_called_once_with(store, "/tmp/out.jsonl")
476
assert result == expected
477
478
479
class TestImportGraph:
480
def test_delegates_to_import_graph(self):
481
store = _mock_store()
482
nav = Navegador(store)
483
expected = {"nodes": 10, "edges": 5}
484
485
with patch("navegador.graph.export.import_graph", return_value=expected) as mock_import:
486
result = nav.import_graph("/tmp/in.jsonl")
487
mock_import.assert_called_once_with(store, "/tmp/in.jsonl", clear=True)
488
assert result == expected
489
490
def test_passes_clear_false(self):
491
store = _mock_store()
492
nav = Navegador(store)
493
494
with patch("navegador.graph.export.import_graph", return_value={}) as mock_import:
495
nav.import_graph("/tmp/in.jsonl", clear=False)
496
mock_import.assert_called_once_with(store, "/tmp/in.jsonl", clear=False)
497
498
499
class TestClear:
500
def test_delegates_to_store(self):
501
store = _mock_store()
502
nav = Navegador(store)
503
nav.clear()
504
store.clear.assert_called_once()
505
506
507
# ── Owners ────────────────────────────────────────────────────────────────────
508
509
510
class TestFindOwners:
511
def test_returns_person_nodes(self):
512
from navegador.context.loader import ContextNode
513
514
rows = [["Class", "AuthService", "Alice", "[email protected]", "lead", "auth"]]
515
nav = _nav(rows)
516
results = nav.find_owners("AuthService")
517
assert len(results) == 1
518
assert isinstance(results[0], ContextNode)
519
assert results[0].type == "Person"
520
assert results[0].name == "Alice"
521
522
def test_empty(self):
523
nav = _nav([])
524
assert nav.find_owners("nobody") == []
525
526
def test_passes_file_path(self):
527
store = _mock_store([])
528
nav = Navegador(store)
529
530
with patch("navegador.context.loader.ContextLoader.find_owners") as mock_fo:
531
mock_fo.return_value = []
532
nav.find_owners("AuthService", file_path="src/auth.py")
533
mock_fo.assert_called_once_with("AuthService", file_path="src/auth.py")
534
535
536
# ── Top-level import ──────────────────────────────────────────────────────────
537
538
539
class TestTopLevelImport:
540
def test_navegador_exported_from_package(self):
541
import navegador
542
543
assert hasattr(navegador, "Navegador")
544
assert navegador.Navegador is Navegador
545
546
def test_navegador_in_all(self):
547
import navegador
548
549
assert "Navegador" in navegador.__all__
550

Keyboard Shortcuts

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