Navegador

navegador / tests / test_intelligence.py
Blame History Raw 851 lines
1
"""
2
Tests for the navegador intelligence layer.
3
4
Covers:
5
- SemanticSearch._cosine_similarity
6
- SemanticSearch.index / search (mock graph + mock LLM)
7
- CommunityDetector.detect / store_communities (mock graph)
8
- NLPEngine.natural_query / name_communities / generate_docs (mock LLM)
9
- DocGenerator template mode and LLM mode (mock LLM)
10
- CLI commands: semantic-search, communities, ask, generate-docs, docs
11
12
All LLM providers are mocked — no real API calls are made.
13
"""
14
15
from __future__ import annotations
16
17
import json
18
import math
19
from unittest.mock import MagicMock, patch
20
21
import pytest
22
from click.testing import CliRunner
23
24
from navegador.cli.commands import main
25
26
27
# ── Helpers ───────────────────────────────────────────────────────────────────
28
29
30
def _mock_store(result_rows=None):
31
"""Return a MagicMock GraphStore whose .query() returns the given rows."""
32
store = MagicMock()
33
result = MagicMock()
34
result.result_set = result_rows if result_rows is not None else []
35
store.query.return_value = result
36
return store
37
38
39
def _mock_provider(complete_return="mocked answer", embed_return=None):
40
"""Return a MagicMock LLMProvider."""
41
if embed_return is None:
42
embed_return = [0.1, 0.2, 0.3, 0.4]
43
provider = MagicMock()
44
provider.complete.return_value = complete_return
45
provider.embed.return_value = embed_return
46
provider.name = "mock"
47
provider.model = "mock-model"
48
return provider
49
50
51
# ── SemanticSearch: _cosine_similarity ────────────────────────────────────────
52
53
54
class TestCosineSimilarity:
55
def setup_method(self):
56
from navegador.intelligence.search import SemanticSearch
57
58
self.cls = SemanticSearch
59
60
def test_identical_vectors_return_one(self):
61
v = [1.0, 0.0, 0.0]
62
assert self.cls._cosine_similarity(v, v) == pytest.approx(1.0)
63
64
def test_orthogonal_vectors_return_zero(self):
65
a = [1.0, 0.0]
66
b = [0.0, 1.0]
67
assert self.cls._cosine_similarity(a, b) == pytest.approx(0.0)
68
69
def test_opposite_vectors_return_minus_one(self):
70
a = [1.0, 0.0]
71
b = [-1.0, 0.0]
72
assert self.cls._cosine_similarity(a, b) == pytest.approx(-1.0)
73
74
def test_zero_vector_returns_zero(self):
75
a = [0.0, 0.0]
76
b = [1.0, 2.0]
77
assert self.cls._cosine_similarity(a, b) == 0.0
78
79
def test_different_length_vectors_return_zero(self):
80
a = [1.0, 2.0]
81
b = [1.0, 2.0, 3.0]
82
assert self.cls._cosine_similarity(a, b) == 0.0
83
84
def test_known_similarity(self):
85
a = [1.0, 1.0]
86
b = [1.0, 0.0]
87
# cos(45°) = 1/sqrt(2)
88
expected = 1.0 / math.sqrt(2)
89
assert self.cls._cosine_similarity(a, b) == pytest.approx(expected, abs=1e-6)
90
91
def test_general_non_unit_vectors(self):
92
a = [3.0, 4.0]
93
b = [3.0, 4.0]
94
# Same direction → 1.0 regardless of magnitude
95
assert self.cls._cosine_similarity(a, b) == pytest.approx(1.0)
96
97
98
# ── SemanticSearch: index ─────────────────────────────────────────────────────
99
100
101
class TestSemanticSearchIndex:
102
def test_index_embeds_and_stores(self):
103
from navegador.intelligence.search import SemanticSearch
104
105
rows = [
106
["Function", "my_func", "app.py", "Does something important"],
107
["Class", "MyClass", "app.py", "A useful class"],
108
]
109
store = _mock_store(rows)
110
provider = _mock_provider(embed_return=[0.1, 0.2, 0.3])
111
112
ss = SemanticSearch(store, provider)
113
count = ss.index(limit=10)
114
115
assert count == 2
116
# embed called once per node
117
assert provider.embed.call_count == 2
118
# SET query called for each node
119
assert store.query.call_count >= 3 # 1 fetch + 2 set
120
121
def test_index_skips_nodes_without_text(self):
122
from navegador.intelligence.search import SemanticSearch
123
124
rows = [
125
["Function", "no_doc", "app.py", ""], # empty text
126
["Class", "HasDoc", "app.py", "Some docstring"],
127
]
128
store = _mock_store(rows)
129
provider = _mock_provider(embed_return=[0.1, 0.2])
130
131
ss = SemanticSearch(store, provider)
132
count = ss.index()
133
134
assert count == 1 # only the node with text
135
assert provider.embed.call_count == 1
136
137
def test_index_returns_zero_for_empty_graph(self):
138
from navegador.intelligence.search import SemanticSearch
139
140
store = _mock_store([])
141
provider = _mock_provider()
142
ss = SemanticSearch(store, provider)
143
assert ss.index() == 0
144
provider.embed.assert_not_called()
145
146
147
# ── SemanticSearch: search ────────────────────────────────────────────────────
148
149
150
class TestSemanticSearchSearch:
151
def test_search_returns_sorted_results(self):
152
from navegador.intelligence.search import SemanticSearch
153
154
# Two nodes with known embeddings
155
# Node A: parallel to query → similarity 1.0
156
# Node B: orthogonal to query → similarity 0.0
157
query_vec = [1.0, 0.0]
158
node_a_vec = [1.0, 0.0] # sim = 1.0
159
node_b_vec = [0.0, 1.0] # sim = 0.0
160
161
rows = [
162
["Function", "node_a", "a.py", "doc a", json.dumps(node_a_vec)],
163
["Class", "node_b", "b.py", "doc b", json.dumps(node_b_vec)],
164
]
165
store = _mock_store(rows)
166
provider = _mock_provider(embed_return=query_vec)
167
168
ss = SemanticSearch(store, provider)
169
results = ss.search("find something", limit=10)
170
171
assert len(results) == 2
172
assert results[0]["name"] == "node_a"
173
assert results[0]["score"] == pytest.approx(1.0)
174
assert results[1]["name"] == "node_b"
175
assert results[1]["score"] == pytest.approx(0.0)
176
177
def test_search_respects_limit(self):
178
from navegador.intelligence.search import SemanticSearch
179
180
rows = [
181
["Function", f"func_{i}", "app.py", f"doc {i}", json.dumps([float(i), 0.0])]
182
for i in range(1, 6)
183
]
184
store = _mock_store(rows)
185
provider = _mock_provider(embed_return=[1.0, 0.0])
186
187
ss = SemanticSearch(store, provider)
188
results = ss.search("query", limit=3)
189
assert len(results) == 3
190
191
def test_search_handles_invalid_embedding_json(self):
192
from navegador.intelligence.search import SemanticSearch
193
194
rows = [
195
["Function", "bad_node", "app.py", "doc", "not-valid-json"],
196
["Function", "good_node", "app.py", "doc", json.dumps([1.0, 0.0])],
197
]
198
store = _mock_store(rows)
199
provider = _mock_provider(embed_return=[1.0, 0.0])
200
201
ss = SemanticSearch(store, provider)
202
results = ss.search("q", limit=10)
203
# Only good_node should appear
204
assert len(results) == 1
205
assert results[0]["name"] == "good_node"
206
207
def test_search_empty_graph_returns_empty_list(self):
208
from navegador.intelligence.search import SemanticSearch
209
210
store = _mock_store([])
211
provider = _mock_provider()
212
ss = SemanticSearch(store, provider)
213
assert ss.search("anything") == []
214
215
216
# ── CommunityDetector ─────────────────────────────────────────────────────────
217
218
219
class TestCommunityDetector:
220
"""Tests use a fully in-memory mock — no real FalkorDB required."""
221
222
def _make_store(self, node_rows, edge_rows):
223
"""
224
Return a MagicMock store that returns different rows for the first vs
225
subsequent query calls (nodes query, edges query).
226
"""
227
store = MagicMock()
228
229
node_result = MagicMock()
230
node_result.result_set = node_rows
231
edge_result = MagicMock()
232
edge_result.result_set = edge_rows
233
234
# First call → node query, second call → edge query, rest → set_community
235
store.query.side_effect = [node_result, edge_result] + [
236
MagicMock(result_set=[]) for _ in range(100)
237
]
238
return store
239
240
def test_two_cliques_form_separate_communities(self):
241
from navegador.intelligence.community import CommunityDetector
242
243
# Nodes: 0-1-2 form a triangle (clique), 3-4 form a pair
244
# They have no edges between groups → two communities
245
node_rows = [
246
[0, "func_a", "a.py", "Function"],
247
[1, "func_b", "a.py", "Function"],
248
[2, "func_c", "a.py", "Function"],
249
[3, "func_d", "b.py", "Function"],
250
[4, "func_e", "b.py", "Function"],
251
]
252
edge_rows = [
253
[0, 1], [1, 2], [0, 2], # triangle
254
[3, 4], # pair
255
]
256
store = self._make_store(node_rows, edge_rows)
257
detector = CommunityDetector(store)
258
communities = detector.detect(min_size=2)
259
260
assert len(communities) == 2
261
sizes = sorted(c.size for c in communities)
262
assert sizes == [2, 3]
263
264
def test_min_size_filters_small_communities(self):
265
from navegador.intelligence.community import CommunityDetector
266
267
node_rows = [
268
[0, "a", "x.py", "Function"],
269
[1, "b", "x.py", "Function"],
270
[2, "c", "x.py", "Function"], # isolated
271
]
272
edge_rows = [[0, 1]]
273
store = self._make_store(node_rows, edge_rows)
274
detector = CommunityDetector(store)
275
276
communities = detector.detect(min_size=2)
277
# Only the pair {a, b} passes; isolated node c gets size=1 (filtered)
278
assert all(c.size >= 2 for c in communities)
279
280
def test_empty_graph_returns_empty_list(self):
281
from navegador.intelligence.community import CommunityDetector
282
283
store = self._make_store([], [])
284
detector = CommunityDetector(store)
285
communities = detector.detect()
286
assert communities == []
287
288
def test_community_density_is_one_for_complete_graph(self):
289
from navegador.intelligence.community import CommunityDetector
290
291
# 3-node complete graph
292
node_rows = [
293
[0, "x", "", "Function"],
294
[1, "y", "", "Function"],
295
[2, "z", "", "Function"],
296
]
297
edge_rows = [[0, 1], [1, 2], [0, 2]]
298
store = self._make_store(node_rows, edge_rows)
299
detector = CommunityDetector(store)
300
communities = detector.detect(min_size=3)
301
302
assert len(communities) == 1
303
assert communities[0].density == pytest.approx(1.0)
304
305
def test_community_members_are_strings(self):
306
from navegador.intelligence.community import CommunityDetector
307
308
node_rows = [
309
[0, "func_alpha", "f.py", "Function"],
310
[1, "func_beta", "f.py", "Function"],
311
]
312
edge_rows = [[0, 1]]
313
store = self._make_store(node_rows, edge_rows)
314
detector = CommunityDetector(store)
315
communities = detector.detect(min_size=2)
316
317
members = communities[0].members
318
assert all(isinstance(m, str) for m in members)
319
assert set(members) == {"func_alpha", "func_beta"}
320
321
def test_store_communities_calls_query_for_each_node(self):
322
from navegador.intelligence.community import CommunityDetector
323
324
node_rows = [
325
[10, "n1", "", "Function"],
326
[11, "n2", "", "Function"],
327
]
328
edge_rows = [[10, 11]]
329
store = self._make_store(node_rows, edge_rows)
330
detector = CommunityDetector(store)
331
detector.detect(min_size=2)
332
333
# Reset side_effect so store_communities calls work cleanly
334
store.query.side_effect = None
335
store.query.return_value = MagicMock(result_set=[])
336
337
updated = detector.store_communities()
338
assert updated == 2 # two nodes
339
assert store.query.call_count >= 2
340
341
def test_community_sorted_largest_first(self):
342
from navegador.intelligence.community import CommunityDetector
343
344
# 4-node clique + 2-node pair with a bridge → label propagation may merge
345
# Use two fully disconnected groups of sizes 4 and 2
346
node_rows = [
347
[0, "a", "", "F"], [1, "b", "", "F"], [2, "c", "", "F"], [3, "d", "", "F"],
348
[4, "e", "", "F"], [5, "f", "", "F"],
349
]
350
edge_rows = [
351
[0, 1], [1, 2], [2, 3], [0, 3], # 4-cycle (all same community)
352
[4, 5], # pair
353
]
354
store = self._make_store(node_rows, edge_rows)
355
detector = CommunityDetector(store)
356
communities = detector.detect(min_size=2)
357
sizes = [c.size for c in communities]
358
assert sizes == sorted(sizes, reverse=True)
359
360
361
# ── NLPEngine ─────────────────────────────────────────────────────────────────
362
363
364
class TestNLPEngine:
365
def test_natural_query_calls_complete_twice(self):
366
"""Should call complete once for Cypher generation, once for formatting."""
367
from navegador.intelligence.nlp import NLPEngine
368
369
cypher_response = "MATCH (n:Function) RETURN n.name LIMIT 5"
370
format_response = "There are 5 functions: ..."
371
provider = MagicMock()
372
provider.complete.side_effect = [cypher_response, format_response]
373
374
store = _mock_store([["func_a"], ["func_b"]])
375
engine = NLPEngine(store, provider)
376
377
result = engine.natural_query("List all functions")
378
assert result == format_response
379
assert provider.complete.call_count == 2
380
381
def test_natural_query_handles_query_error(self):
382
"""When the generated Cypher fails, return an error message."""
383
from navegador.intelligence.nlp import NLPEngine
384
385
provider = _mock_provider(complete_return="INVALID CYPHER !!!")
386
store = MagicMock()
387
store.query.side_effect = Exception("syntax error")
388
389
engine = NLPEngine(store, provider)
390
result = engine.natural_query("broken question")
391
392
assert "Failed" in result or "Error" in result or "syntax error" in result
393
394
def test_natural_query_strips_markdown_fences(self):
395
"""LLM output with ```cypher fences should still execute."""
396
from navegador.intelligence.nlp import NLPEngine
397
398
fenced_cypher = "```cypher\nMATCH (n) RETURN n.name LIMIT 1\n```"
399
provider = MagicMock()
400
provider.complete.side_effect = [fenced_cypher, "One node found."]
401
402
store = _mock_store([["some_node"]])
403
engine = NLPEngine(store, provider)
404
result = engine.natural_query("find a node")
405
406
assert result == "One node found."
407
# Verify the actual query executed was the clean Cypher (no fences)
408
executed_cypher = store.query.call_args[0][0]
409
assert "```" not in executed_cypher
410
411
def test_name_communities_returns_one_entry_per_community(self):
412
from navegador.intelligence.community import Community
413
from navegador.intelligence.nlp import NLPEngine
414
415
store = _mock_store()
416
provider = _mock_provider(complete_return="Authentication Services")
417
418
comms = [
419
Community(name="community_1", members=["login", "logout", "verify_token"], size=3),
420
Community(name="community_2", members=["fetch_data", "store_record"], size=2),
421
]
422
engine = NLPEngine(store, provider)
423
named = engine.name_communities(comms)
424
425
assert len(named) == 2
426
assert all("suggested_name" in n for n in named)
427
assert all("original_name" in n for n in named)
428
assert provider.complete.call_count == 2
429
430
def test_name_communities_fallback_on_llm_error(self):
431
"""If LLM raises, the original name is used."""
432
from navegador.intelligence.community import Community
433
from navegador.intelligence.nlp import NLPEngine
434
435
store = _mock_store()
436
provider = MagicMock()
437
provider.complete.side_effect = RuntimeError("API down")
438
439
comm = Community(name="community_0", members=["a", "b"], size=2)
440
engine = NLPEngine(store, provider)
441
named = engine.name_communities([comm])
442
443
assert named[0]["suggested_name"] == "community_0"
444
445
def test_generate_docs_returns_llm_string(self):
446
from navegador.intelligence.nlp import NLPEngine
447
448
expected_docs = "## my_func\nDoes great things."
449
store = _mock_store([
450
["Function", "my_func", "app.py", "Does great things.", "def my_func():"]
451
])
452
# Make subsequent query calls (callers, callees) also return empty
453
store.query.side_effect = [
454
MagicMock(result_set=[["Function", "my_func", "app.py", "Does great things.", "def my_func():"]]),
455
MagicMock(result_set=[]),
456
MagicMock(result_set=[]),
457
]
458
provider = _mock_provider(complete_return=expected_docs)
459
460
engine = NLPEngine(store, provider)
461
result = engine.generate_docs("my_func", file_path="app.py")
462
463
assert result == expected_docs
464
provider.complete.assert_called_once()
465
466
def test_generate_docs_works_when_node_not_found(self):
467
"""When node doesn't exist, still calls LLM with empty context."""
468
from navegador.intelligence.nlp import NLPEngine
469
470
store = MagicMock()
471
store.query.return_value = MagicMock(result_set=[])
472
provider = _mock_provider(complete_return="No docs available.")
473
474
engine = NLPEngine(store, provider)
475
result = engine.generate_docs("nonexistent_func")
476
477
assert "No docs available." in result
478
479
480
# ── DocGenerator (template mode) ─────────────────────────────────────────────
481
482
483
class TestDocGeneratorTemplateMode:
484
def test_generate_file_docs_returns_markdown_with_symbols(self):
485
from navegador.intelligence.docgen import DocGenerator
486
487
rows = [
488
["Function", "greet", "Does greeting", "def greet():", 10],
489
["Class", "Greeter", "A greeter class", "class Greeter:", 20],
490
]
491
store = _mock_store(rows)
492
gen = DocGenerator(store, provider=None)
493
494
docs = gen.generate_file_docs("app.py")
495
496
assert "app.py" in docs
497
assert "greet" in docs
498
assert "Greeter" in docs
499
assert "Does greeting" in docs
500
501
def test_generate_file_docs_handles_empty_file(self):
502
from navegador.intelligence.docgen import DocGenerator
503
504
store = _mock_store([])
505
gen = DocGenerator(store, provider=None)
506
507
docs = gen.generate_file_docs("empty.py")
508
assert "No symbols" in docs
509
510
def test_generate_module_docs_groups_by_file(self):
511
from navegador.intelligence.docgen import DocGenerator
512
513
rows = [
514
["Function", "func_a", "nav/graph/store.py", "Store a node", "def func_a():"],
515
["Class", "GraphStore", "nav/graph/store.py", "Wraps the graph.", "class GraphStore:"],
516
["Function", "func_b", "nav/graph/queries.py", "Query helper", "def func_b():"],
517
]
518
store = _mock_store(rows)
519
gen = DocGenerator(store, provider=None)
520
521
docs = gen.generate_module_docs("nav.graph")
522
assert "nav/graph/store.py" in docs
523
assert "nav/graph/queries.py" in docs
524
assert "func_a" in docs
525
assert "GraphStore" in docs
526
527
def test_generate_module_docs_handles_no_results(self):
528
from navegador.intelligence.docgen import DocGenerator
529
530
store = _mock_store([])
531
gen = DocGenerator(store, provider=None)
532
533
docs = gen.generate_module_docs("empty.module")
534
assert "No symbols" in docs
535
536
def test_generate_project_docs_includes_stats_and_files(self):
537
from navegador.intelligence.docgen import DocGenerator
538
539
store = MagicMock()
540
541
stats_result = MagicMock()
542
stats_result.result_set = [
543
["Function", 42],
544
["Class", 10],
545
]
546
files_result = MagicMock()
547
files_result.result_set = [
548
["navegador/graph/store.py"],
549
["navegador/cli/commands.py"],
550
]
551
sym_result = MagicMock()
552
sym_result.result_set = [
553
["Function", "my_func", "navegador/graph/store.py", "Does things"],
554
]
555
store.query.side_effect = [stats_result, files_result, sym_result]
556
557
gen = DocGenerator(store, provider=None)
558
docs = gen.generate_project_docs()
559
560
assert "Project Documentation" in docs
561
assert "Function" in docs
562
assert "42" in docs
563
assert "navegador/graph/store.py" in docs
564
565
def test_signature_included_when_present(self):
566
from navegador.intelligence.docgen import DocGenerator
567
568
rows = [["Function", "my_func", "My doc", "def my_func(x: int) -> str:", 5]]
569
store = _mock_store(rows)
570
gen = DocGenerator(store, provider=None)
571
572
docs = gen.generate_file_docs("f.py")
573
assert "def my_func(x: int) -> str:" in docs
574
575
576
# ── DocGenerator (LLM mode) ───────────────────────────────────────────────────
577
578
579
class TestDocGeneratorLLMMode:
580
def test_generate_file_docs_uses_nlp_engine(self):
581
from navegador.intelligence.docgen import DocGenerator
582
583
rows = [["Function", "my_func", "Generated docs for my_func", "def my_func():", 1]]
584
store = MagicMock()
585
# 1st call: _FILE_SYMBOLS 2nd+: NLPEngine internal calls
586
store.query.return_value = MagicMock(result_set=rows)
587
588
provider = _mock_provider(complete_return="## my_func\nLLM-generated content.")
589
gen = DocGenerator(store, provider=provider)
590
docs = gen.generate_file_docs("app.py")
591
592
assert "app.py" in docs
593
provider.complete.assert_called()
594
595
def test_generate_project_docs_uses_llm(self):
596
from navegador.intelligence.docgen import DocGenerator
597
598
store = MagicMock()
599
# Return empty for template sub-calls
600
store.query.return_value = MagicMock(result_set=[])
601
602
provider = _mock_provider(complete_return="# Project README\nLLM wrote this.")
603
gen = DocGenerator(store, provider=provider)
604
docs = gen.generate_project_docs()
605
606
assert "Project README" in docs or "LLM wrote this" in docs
607
provider.complete.assert_called_once()
608
609
610
# ── CLI: semantic-search ──────────────────────────────────────────────────────
611
612
613
class TestSemanticSearchCLI:
614
def test_search_outputs_table(self):
615
runner = CliRunner()
616
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
617
patch("navegador.llm.auto_provider") as mock_auto:
618
store = _mock_store([])
619
mock_store_fn.return_value = store
620
mock_provider = _mock_provider(embed_return=[1.0, 0.0])
621
mock_auto.return_value = mock_provider
622
623
# search returns no results
624
from navegador.intelligence.search import SemanticSearch
625
with patch.object(SemanticSearch, "search", return_value=[]):
626
result = runner.invoke(main, ["semantic-search", "test query"])
627
assert result.exit_code == 0
628
629
def test_search_with_index_flag(self):
630
runner = CliRunner()
631
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
632
patch("navegador.llm.auto_provider") as mock_auto:
633
store = _mock_store([])
634
mock_store_fn.return_value = store
635
mock_provider = _mock_provider()
636
mock_auto.return_value = mock_provider
637
638
from navegador.intelligence.search import SemanticSearch
639
with patch.object(SemanticSearch, "index", return_value=5) as mock_index, \
640
patch.object(SemanticSearch, "search", return_value=[]):
641
result = runner.invoke(main, ["semantic-search", "test", "--index"])
642
assert result.exit_code == 0
643
mock_index.assert_called_once()
644
645
def test_search_json_output(self):
646
runner = CliRunner()
647
fake_results = [
648
{"type": "Function", "name": "foo", "file_path": "a.py", "text": "doc", "score": 0.95}
649
]
650
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
651
patch("navegador.llm.auto_provider") as mock_auto:
652
store = _mock_store([])
653
mock_store_fn.return_value = store
654
mock_auto.return_value = _mock_provider()
655
656
from navegador.intelligence.search import SemanticSearch
657
with patch.object(SemanticSearch, "search", return_value=fake_results):
658
result = runner.invoke(main, ["semantic-search", "foo", "--json"])
659
assert result.exit_code == 0
660
data = json.loads(result.output)
661
assert isinstance(data, list)
662
assert data[0]["name"] == "foo"
663
664
665
# ── CLI: communities ──────────────────────────────────────────────────────────
666
667
668
class TestCommunitiesCLI:
669
def _make_communities(self):
670
from navegador.intelligence.community import Community
671
672
return [
673
Community(name="community_0", members=["a", "b", "c"], size=3, density=1.0),
674
Community(name="community_1", members=["x", "y"], size=2, density=1.0),
675
]
676
677
def test_communities_outputs_table(self):
678
runner = CliRunner()
679
with patch("navegador.cli.commands._get_store") as mock_store_fn:
680
mock_store_fn.return_value = _mock_store()
681
from navegador.intelligence.community import CommunityDetector
682
with patch.object(CommunityDetector, "detect", return_value=self._make_communities()):
683
result = runner.invoke(main, ["communities"])
684
assert result.exit_code == 0
685
686
def test_communities_json_output(self):
687
runner = CliRunner()
688
with patch("navegador.cli.commands._get_store") as mock_store_fn:
689
mock_store_fn.return_value = _mock_store()
690
from navegador.intelligence.community import CommunityDetector
691
with patch.object(CommunityDetector, "detect", return_value=self._make_communities()):
692
result = runner.invoke(main, ["communities", "--json"])
693
assert result.exit_code == 0
694
data = json.loads(result.output)
695
assert len(data) == 2
696
assert data[0]["name"] == "community_0"
697
698
def test_communities_min_size_passed(self):
699
runner = CliRunner()
700
with patch("navegador.cli.commands._get_store") as mock_store_fn:
701
mock_store_fn.return_value = _mock_store()
702
from navegador.intelligence.community import CommunityDetector
703
with patch.object(CommunityDetector, "detect", return_value=[]) as mock_detect:
704
runner.invoke(main, ["communities", "--min-size", "5"])
705
mock_detect.assert_called_once_with(min_size=5)
706
707
def test_communities_empty_graph_message(self):
708
runner = CliRunner()
709
with patch("navegador.cli.commands._get_store") as mock_store_fn:
710
mock_store_fn.return_value = _mock_store()
711
from navegador.intelligence.community import CommunityDetector
712
with patch.object(CommunityDetector, "detect", return_value=[]):
713
result = runner.invoke(main, ["communities"])
714
assert result.exit_code == 0
715
assert "No communities" in result.output or result.exit_code == 0
716
717
def test_communities_store_labels_flag(self):
718
runner = CliRunner()
719
with patch("navegador.cli.commands._get_store") as mock_store_fn:
720
mock_store_fn.return_value = _mock_store()
721
from navegador.intelligence.community import CommunityDetector
722
with patch.object(CommunityDetector, "detect", return_value=self._make_communities()), \
723
patch.object(CommunityDetector, "store_communities", return_value=5) as mock_store:
724
result = runner.invoke(main, ["communities", "--store-labels"])
725
assert result.exit_code == 0
726
mock_store.assert_called_once()
727
728
729
# ── CLI: ask ──────────────────────────────────────────────────────────────────
730
731
732
class TestAskCLI:
733
def test_ask_prints_answer(self):
734
runner = CliRunner()
735
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
736
patch("navegador.llm.auto_provider") as mock_auto:
737
mock_store_fn.return_value = _mock_store()
738
mock_auto.return_value = _mock_provider()
739
740
from navegador.intelligence.nlp import NLPEngine
741
with patch.object(NLPEngine, "natural_query", return_value="The answer is 42."):
742
result = runner.invoke(main, ["ask", "What is the answer?"])
743
assert result.exit_code == 0
744
assert "42" in result.output
745
746
def test_ask_with_explicit_provider(self):
747
runner = CliRunner()
748
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
749
patch("navegador.llm.get_provider") as mock_get:
750
mock_store_fn.return_value = _mock_store()
751
mock_get.return_value = _mock_provider()
752
753
from navegador.intelligence.nlp import NLPEngine
754
with patch.object(NLPEngine, "natural_query", return_value="Answer."):
755
result = runner.invoke(
756
main, ["ask", "question", "--provider", "openai"]
757
)
758
assert result.exit_code == 0
759
mock_get.assert_called_once_with("openai", model="")
760
761
762
# ── CLI: generate-docs ────────────────────────────────────────────────────────
763
764
765
class TestGenerateDocsCLI:
766
def test_generate_docs_prints_output(self):
767
runner = CliRunner()
768
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
769
patch("navegador.llm.auto_provider") as mock_auto:
770
mock_store_fn.return_value = _mock_store()
771
mock_auto.return_value = _mock_provider()
772
773
from navegador.intelligence.nlp import NLPEngine
774
with patch.object(NLPEngine, "generate_docs", return_value="## my_func\nDocs here."):
775
result = runner.invoke(main, ["generate-docs", "my_func"])
776
assert result.exit_code == 0
777
assert "my_func" in result.output or "Docs" in result.output
778
779
def test_generate_docs_with_file_option(self):
780
runner = CliRunner()
781
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
782
patch("navegador.llm.auto_provider") as mock_auto:
783
mock_store_fn.return_value = _mock_store()
784
mock_auto.return_value = _mock_provider()
785
786
from navegador.intelligence.nlp import NLPEngine
787
with patch.object(NLPEngine, "generate_docs", return_value="Docs.") as mock_gd:
788
runner.invoke(
789
main, ["generate-docs", "my_func", "--file", "app.py"]
790
)
791
mock_gd.assert_called_once_with("my_func", file_path="app.py")
792
793
794
# ── CLI: docs ─────────────────────────────────────────────────────────────────
795
796
797
class TestDocsCLI:
798
def test_docs_file_path(self):
799
runner = CliRunner()
800
with patch("navegador.cli.commands._get_store") as mock_store_fn:
801
mock_store_fn.return_value = _mock_store()
802
from navegador.intelligence.docgen import DocGenerator
803
with patch.object(DocGenerator, "generate_file_docs", return_value="# File docs") as mock_fd:
804
result = runner.invoke(main, ["docs", "app/store.py"])
805
assert result.exit_code == 0
806
mock_fd.assert_called_once_with("app/store.py")
807
808
def test_docs_module_name(self):
809
runner = CliRunner()
810
with patch("navegador.cli.commands._get_store") as mock_store_fn:
811
mock_store_fn.return_value = _mock_store()
812
from navegador.intelligence.docgen import DocGenerator
813
with patch.object(DocGenerator, "generate_module_docs", return_value="# Module docs") as mock_md:
814
result = runner.invoke(main, ["docs", "navegador.graph"])
815
assert result.exit_code == 0
816
mock_md.assert_called_once_with("navegador.graph")
817
818
def test_docs_project_flag(self):
819
runner = CliRunner()
820
with patch("navegador.cli.commands._get_store") as mock_store_fn:
821
mock_store_fn.return_value = _mock_store()
822
from navegador.intelligence.docgen import DocGenerator
823
with patch.object(DocGenerator, "generate_project_docs", return_value="# Project") as mock_pd:
824
result = runner.invoke(main, ["docs", ".", "--project"])
825
assert result.exit_code == 0
826
mock_pd.assert_called_once()
827
828
def test_docs_json_output(self):
829
runner = CliRunner()
830
with patch("navegador.cli.commands._get_store") as mock_store_fn:
831
mock_store_fn.return_value = _mock_store()
832
from navegador.intelligence.docgen import DocGenerator
833
with patch.object(DocGenerator, "generate_project_docs", return_value="# Project"):
834
result = runner.invoke(main, ["docs", ".", "--project", "--json"])
835
assert result.exit_code == 0
836
data = json.loads(result.output)
837
assert "docs" in data
838
839
def test_docs_with_llm_provider(self):
840
runner = CliRunner()
841
with patch("navegador.cli.commands._get_store") as mock_store_fn, \
842
patch("navegador.intelligence.docgen.DocGenerator.generate_file_docs", return_value="# Docs"):
843
mock_store_fn.return_value = _mock_store()
844
with patch("navegador.llm.get_provider") as mock_get:
845
mock_get.return_value = _mock_provider()
846
result = runner.invoke(
847
main, ["docs", "app/store.py", "--provider", "openai"]
848
)
849
assert result.exit_code == 0
850
mock_get.assert_called_once_with("openai", model="")
851

Keyboard Shortcuts

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