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