Navegador

navegador / tests / test_context.py
Blame History Raw 576 lines
1
"""Tests for ContextBundle serialization and ContextLoader with mock store."""
2
3
import json
4
from unittest.mock import MagicMock
5
6
from navegador.context.loader import ContextBundle, ContextLoader, ContextNode
7
8
# ── Fixtures ──────────────────────────────────────────────────────────────────
9
10
def _make_bundle():
11
target = ContextNode(
12
type="Function",
13
name="get_user",
14
file_path="src/auth.py",
15
line_start=42,
16
docstring="Return a user by ID.",
17
signature="def get_user(user_id: int) -> User:",
18
)
19
nodes = [
20
ContextNode(type="Function", name="validate_token", file_path="src/auth.py", line_start=10),
21
ContextNode(type="Class", name="User", file_path="src/models.py", line_start=5),
22
]
23
edges = [{"from": "get_user", "type": "CALLS", "to": "validate_token"}]
24
return ContextBundle(target=target, nodes=nodes, edges=edges,
25
metadata={"query": "function_context"})
26
27
28
def _make_knowledge_bundle():
29
target = ContextNode(
30
type="Concept", name="JWT", description="Stateless token auth", domain="auth"
31
)
32
nodes = [
33
ContextNode(type="Rule", name="Tokens must expire"),
34
ContextNode(type="WikiPage", name="Auth Overview"),
35
]
36
edges = [
37
{"from": "Tokens must expire", "type": "GOVERNS", "to": "JWT"},
38
{"from": "Auth Overview", "type": "DOCUMENTS", "to": "JWT"},
39
]
40
return ContextBundle(target=target, nodes=nodes, edges=edges)
41
42
43
def _mock_store(rows=None):
44
store = MagicMock()
45
result = MagicMock()
46
result.result_set = rows or []
47
store.query.return_value = result
48
return store
49
50
51
# ── ContextNode ───────────────────────────────────────────────────────────────
52
53
class TestContextNode:
54
def test_defaults(self):
55
n = ContextNode(type="Function", name="foo")
56
assert n.file_path == ""
57
assert n.line_start is None
58
assert n.docstring is None
59
assert n.domain is None
60
61
def test_knowledge_fields(self):
62
n = ContextNode(type="Concept", name="Payment", description="A payment", domain="billing")
63
assert n.description == "A payment"
64
assert n.domain == "billing"
65
66
67
# ── ContextBundle.to_dict ─────────────────────────────────────────────────────
68
69
class TestContextBundleDict:
70
def test_structure(self):
71
b = _make_bundle()
72
d = b.to_dict()
73
assert d["target"]["name"] == "get_user"
74
assert len(d["nodes"]) == 2
75
assert len(d["edges"]) == 1
76
assert d["metadata"]["query"] == "function_context"
77
78
def test_roundtrip(self):
79
b = _make_bundle()
80
d = b.to_dict()
81
assert d["target"]["type"] == "Function"
82
assert d["nodes"][0]["name"] == "validate_token"
83
84
85
# ── ContextBundle.to_json ─────────────────────────────────────────────────────
86
87
class TestContextBundleJson:
88
def test_valid_json(self):
89
b = _make_bundle()
90
data = json.loads(b.to_json())
91
assert data["target"]["name"] == "get_user"
92
93
def test_indent(self):
94
b = _make_bundle()
95
raw = b.to_json(indent=4)
96
assert " " in raw # 4-space indent
97
98
99
# ── ContextBundle.to_markdown ─────────────────────────────────────────────────
100
101
class TestContextBundleMarkdown:
102
def test_contains_name(self):
103
b = _make_bundle()
104
md = b.to_markdown()
105
assert "get_user" in md
106
107
def test_contains_edge(self):
108
md = _make_bundle().to_markdown()
109
assert "CALLS" in md
110
assert "validate_token" in md
111
112
def test_contains_docstring(self):
113
md = _make_bundle().to_markdown()
114
assert "Return a user by ID." in md
115
116
def test_contains_signature(self):
117
md = _make_bundle().to_markdown()
118
assert "def get_user" in md
119
120
def test_knowledge_bundle(self):
121
md = _make_knowledge_bundle().to_markdown()
122
assert "JWT" in md
123
assert "auth" in md
124
assert "GOVERNS" in md
125
126
def test_empty_nodes(self):
127
target = ContextNode(type="File", name="empty.py", file_path="empty.py")
128
b = ContextBundle(target=target)
129
md = b.to_markdown()
130
assert "empty.py" in md
131
132
133
# ── ContextLoader ─────────────────────────────────────────────────────────────
134
135
class TestContextLoaderFile:
136
def test_load_file_empty(self):
137
store = _mock_store([])
138
loader = ContextLoader(store)
139
bundle = loader.load_file("src/auth.py")
140
assert bundle.target.name == "auth.py"
141
assert bundle.target.type == "File"
142
assert bundle.nodes == []
143
144
def test_load_file_with_rows(self):
145
rows = [["Function", "get_user", 10, "Get a user", "def get_user()"]]
146
store = _mock_store(rows)
147
loader = ContextLoader(store)
148
bundle = loader.load_file("src/auth.py")
149
assert len(bundle.nodes) == 1
150
assert bundle.nodes[0].name == "get_user"
151
assert bundle.nodes[0].type == "Function"
152
153
154
class TestContextLoaderFunction:
155
def test_load_function_no_results(self):
156
store = _mock_store([])
157
loader = ContextLoader(store)
158
bundle = loader.load_function("get_user", file_path="src/auth.py")
159
assert bundle.target.name == "get_user"
160
assert bundle.nodes == []
161
assert bundle.edges == []
162
163
def test_load_function_with_callee(self):
164
def side_effect(query, params):
165
result = MagicMock()
166
if "CALLEES" in query or "callee" in query.lower():
167
result.result_set = [["Function", "validate_token", "src/auth.py", 5]]
168
elif "CALLERS" in query or "caller" in query.lower():
169
result.result_set = []
170
else:
171
result.result_set = []
172
return result
173
174
store = MagicMock()
175
store.query.side_effect = side_effect
176
loader = ContextLoader(store)
177
loader.load_function("get_user")
178
# Should have called query multiple times
179
assert store.query.called
180
181
def test_load_function_default_file_path(self):
182
store = _mock_store([])
183
loader = ContextLoader(store)
184
bundle = loader.load_function("foo")
185
assert bundle.target.file_path == ""
186
187
188
class TestContextLoaderClass:
189
def test_load_class_empty(self):
190
store = _mock_store([])
191
loader = ContextLoader(store)
192
bundle = loader.load_class("AuthService")
193
assert bundle.target.name == "AuthService"
194
assert bundle.target.type == "Class"
195
196
def test_load_class_with_parent(self):
197
call_count = [0]
198
199
def side_effect(query, params=None):
200
result = MagicMock()
201
call_count[0] += 1
202
if call_count[0] == 1:
203
result.result_set = [["BaseService", "src/base.py"]]
204
else:
205
result.result_set = []
206
return result
207
208
store = MagicMock()
209
store.query.side_effect = side_effect
210
loader = ContextLoader(store)
211
bundle = loader.load_class("AuthService")
212
assert len(bundle.nodes) >= 1
213
214
215
class TestContextLoaderExplain:
216
def test_explain_empty(self):
217
store = _mock_store([])
218
loader = ContextLoader(store)
219
bundle = loader.explain("get_user")
220
assert bundle.target.name == "get_user"
221
assert bundle.metadata["query"] == "explain"
222
223
224
class TestContextLoaderSearch:
225
def test_search_empty(self):
226
store = _mock_store([])
227
loader = ContextLoader(store)
228
results = loader.search("auth")
229
assert results == []
230
231
def test_search_returns_nodes(self):
232
rows = [["Function", "authenticate", "src/auth.py", 10, "Authenticate a user"]]
233
store = _mock_store(rows)
234
loader = ContextLoader(store)
235
results = loader.search("auth")
236
assert len(results) == 1
237
assert results[0].name == "authenticate"
238
assert results[0].type == "Function"
239
240
def test_search_all(self):
241
rows = [["Concept", "Authentication", "", None, "The auth concept"]]
242
store = _mock_store(rows)
243
loader = ContextLoader(store)
244
results = loader.search_all("auth")
245
assert len(results) == 1
246
247
def test_search_by_docstring(self):
248
rows = [["Function", "login", "src/auth.py", 5, "Log in a user"]]
249
store = _mock_store(rows)
250
loader = ContextLoader(store)
251
results = loader.search_by_docstring("log in")
252
assert len(results) == 1
253
254
def test_decorated_by(self):
255
rows = [["Function", "protected_view", "src/views.py", 20]]
256
store = _mock_store(rows)
257
loader = ContextLoader(store)
258
results = loader.decorated_by("login_required")
259
assert len(results) == 1
260
assert results[0].name == "protected_view"
261
262
263
class TestContextLoaderConcept:
264
def test_load_concept_not_found(self):
265
store = _mock_store([])
266
loader = ContextLoader(store)
267
bundle = loader.load_concept("Unknown")
268
assert bundle.metadata.get("found") is False
269
270
def test_load_concept_found(self):
271
rows = [["JWT", "Stateless token auth", "active", "auth", [], [], [], []]]
272
store = _mock_store(rows)
273
loader = ContextLoader(store)
274
bundle = loader.load_concept("JWT")
275
assert bundle.target.name == "JWT"
276
assert bundle.target.description == "Stateless token auth"
277
278
def test_load_domain(self):
279
rows = [["Function", "login", "src/auth.py", "Log in"]]
280
store = _mock_store(rows)
281
loader = ContextLoader(store)
282
bundle = loader.load_domain("auth")
283
assert bundle.target.name == "auth"
284
assert bundle.target.type == "Domain"
285
286
287
# ── to_markdown with status and node docstrings ───────────────────────────────
288
289
class TestContextBundleMarkdownBranches:
290
def test_markdown_includes_status(self):
291
target = ContextNode(type="Concept", name="JWT", status="active")
292
b = ContextBundle(target=target)
293
md = b.to_markdown()
294
assert "active" in md
295
296
def test_markdown_node_with_docstring(self):
297
target = ContextNode(type="File", name="app.py", file_path="app.py")
298
node = ContextNode(type="Function", name="foo", file_path="app.py",
299
docstring="Does something useful.")
300
b = ContextBundle(target=target, nodes=[node])
301
md = b.to_markdown()
302
assert "Does something useful." in md
303
304
def test_markdown_node_with_description_fallback(self):
305
target = ContextNode(type="Concept", name="JWT")
306
node = ContextNode(type="Rule", name="must_expire",
307
description="Tokens must expire.")
308
b = ContextBundle(target=target, nodes=[node])
309
md = b.to_markdown()
310
assert "Tokens must expire." in md
311
312
313
# ── load_function with callers and decorators ─────────────────────────────────
314
315
class TestContextLoaderFunctionBranches:
316
def test_load_function_with_callers(self):
317
call_count = [0]
318
319
def side_effect(query, params):
320
result = MagicMock()
321
call_count[0] += 1
322
if call_count[0] == 1:
323
result.result_set = [] # callees empty
324
elif call_count[0] == 2:
325
result.result_set = [["Function", "caller_fn", "src/x.py", 5]]
326
else:
327
result.result_set = [] # decorators empty
328
return result
329
330
store = MagicMock()
331
store.query.side_effect = side_effect
332
loader = ContextLoader(store)
333
bundle = loader.load_function("get_user", file_path="src/auth.py")
334
assert any(n.name == "caller_fn" for n in bundle.nodes)
335
assert any(e["type"] == "CALLS" for e in bundle.edges)
336
337
def test_load_function_with_decorators(self):
338
call_count = [0]
339
340
def side_effect(query, params):
341
result = MagicMock()
342
call_count[0] += 1
343
if call_count[0] <= 2:
344
result.result_set = [] # callees and callers empty
345
else:
346
result.result_set = [["login_required", "src/decorators.py"]]
347
return result
348
349
store = MagicMock()
350
store.query.side_effect = side_effect
351
loader = ContextLoader(store)
352
bundle = loader.load_function("my_view", file_path="src/views.py")
353
assert any(n.name == "login_required" for n in bundle.nodes)
354
355
356
# ── load_class with subs and refs ─────────────────────────────────────────────
357
358
class TestContextLoaderClassBranches:
359
def test_load_class_with_subclasses(self):
360
call_count = [0]
361
362
def side_effect(query, params):
363
result = MagicMock()
364
call_count[0] += 1
365
if call_count[0] == 1:
366
result.result_set = [] # parents empty
367
elif call_count[0] == 2:
368
result.result_set = [["ChildService", "src/child.py"]]
369
else:
370
result.result_set = [] # refs empty
371
return result
372
373
store = MagicMock()
374
store.query.side_effect = side_effect
375
loader = ContextLoader(store)
376
bundle = loader.load_class("BaseService")
377
assert any(n.name == "ChildService" for n in bundle.nodes)
378
assert any(e["type"] == "INHERITS" for e in bundle.edges)
379
380
def test_load_class_with_references(self):
381
call_count = [0]
382
383
def side_effect(query, params):
384
result = MagicMock()
385
call_count[0] += 1
386
if call_count[0] <= 2:
387
result.result_set = [] # parents and subs empty
388
else:
389
result.result_set = [["Function", "use_service", "src/x.py", 10]]
390
return result
391
392
store = MagicMock()
393
store.query.side_effect = side_effect
394
loader = ContextLoader(store)
395
bundle = loader.load_class("AuthService")
396
assert any(n.name == "use_service" for n in bundle.nodes)
397
398
399
# ── explain with data ─────────────────────────────────────────────────────────
400
401
class TestContextLoaderExplainBranches:
402
def test_explain_with_outbound_data(self):
403
call_count = [0]
404
405
def side_effect(query, params):
406
result = MagicMock()
407
call_count[0] += 1
408
if call_count[0] == 1:
409
result.result_set = [["CALLS", "Function", "helper", "src/utils.py"]]
410
else:
411
result.result_set = []
412
return result
413
414
store = MagicMock()
415
store.query.side_effect = side_effect
416
loader = ContextLoader(store)
417
bundle = loader.explain("main_fn")
418
assert any(n.name == "helper" for n in bundle.nodes)
419
assert any(e["type"] == "CALLS" for e in bundle.edges)
420
421
def test_explain_with_inbound_data(self):
422
call_count = [0]
423
424
def side_effect(query, params):
425
result = MagicMock()
426
call_count[0] += 1
427
if call_count[0] == 1:
428
result.result_set = []
429
else:
430
result.result_set = [["CALLS", "Function", "caller", "src/main.py"]]
431
return result
432
433
store = MagicMock()
434
store.query.side_effect = side_effect
435
loader = ContextLoader(store)
436
bundle = loader.explain("helper_fn")
437
assert any(n.name == "caller" for n in bundle.nodes)
438
439
440
# ── load_concept with populated related nodes ─────────────────────────────────
441
442
class TestContextLoaderConceptBranches:
443
def test_load_concept_with_related_concepts_rules_wiki_implements(self):
444
rows = [[
445
"JWT",
446
"Stateless token auth",
447
"active",
448
"auth",
449
["OAuth"], # related concepts
450
["Tokens must expire"], # rules
451
["Auth Overview"], # wiki pages
452
["validate_token"], # implementing code
453
]]
454
store = _mock_store(rows)
455
loader = ContextLoader(store)
456
bundle = loader.load_concept("JWT")
457
names = {n.name for n in bundle.nodes}
458
assert "OAuth" in names
459
assert "Tokens must expire" in names
460
assert "Auth Overview" in names
461
assert "validate_token" in names
462
types = {e["type"] for e in bundle.edges}
463
assert "RELATED_TO" in types
464
assert "GOVERNS" in types
465
assert "DOCUMENTS" in types
466
assert "IMPLEMENTS" in types
467
468
469
# ── load_decision ───────────────────────────────────────────────────────────
470
471
class TestContextLoaderDecision:
472
def test_load_decision_not_found(self):
473
store = _mock_store([])
474
loader = ContextLoader(store)
475
bundle = loader.load_decision("Nonexistent")
476
assert bundle.metadata.get("found") is False
477
assert bundle.target.type == "Decision"
478
479
def test_load_decision_found(self):
480
rows = [[
481
"Use FalkorDB",
482
"Graph DB for navegador",
483
"Cypher queries, SQLite backend",
484
"Neo4j, ArangoDB",
485
"accepted",
486
"2026-03-01",
487
"infrastructure",
488
[], # documents
489
[], # decided_by
490
[], # domains
491
]]
492
store = _mock_store(rows)
493
loader = ContextLoader(store)
494
bundle = loader.load_decision("Use FalkorDB")
495
assert bundle.target.name == "Use FalkorDB"
496
assert bundle.target.rationale == "Cypher queries, SQLite backend"
497
assert bundle.target.alternatives == "Neo4j, ArangoDB"
498
assert bundle.target.status == "accepted"
499
500
def test_load_decision_with_related_nodes(self):
501
rows = [[
502
"Use FalkorDB",
503
"Graph DB",
504
"Cypher",
505
"Neo4j",
506
"accepted",
507
"2026-03-01",
508
"infra",
509
["GraphStore"], # documents
510
["Alice"], # decided_by
511
["infrastructure"], # domains
512
]]
513
store = _mock_store(rows)
514
loader = ContextLoader(store)
515
bundle = loader.load_decision("Use FalkorDB")
516
names = {n.name for n in bundle.nodes}
517
assert "GraphStore" in names
518
assert "Alice" in names
519
edge_types = {e["type"] for e in bundle.edges}
520
assert "DOCUMENTS" in edge_types
521
assert "DECIDED_BY" in edge_types
522
523
524
# ── find_owners ──────────────────────────────────────────────────────────────
525
526
class TestContextLoaderFindOwners:
527
def test_find_owners_empty(self):
528
store = _mock_store([])
529
loader = ContextLoader(store)
530
results = loader.find_owners("AuthService")
531
assert results == []
532
533
def test_find_owners_returns_people(self):
534
rows = [["Class", "AuthService", "Alice", "[email protected]", "lead", "auth"]]
535
store = _mock_store(rows)
536
loader = ContextLoader(store)
537
results = loader.find_owners("AuthService")
538
assert len(results) == 1
539
assert results[0].name == "Alice"
540
assert results[0].type == "Person"
541
542
def test_find_owners_passes_file_path(self):
543
store = _mock_store([])
544
loader = ContextLoader(store)
545
loader.find_owners("foo", file_path="src/foo.py")
546
store.query.assert_called_once()
547
args = store.query.call_args
548
assert args[0][1]["file_path"] == "src/foo.py"
549
550
551
# ── search_knowledge ────────────────────────────────────────────────────────
552
553
class TestContextLoaderSearchKnowledge:
554
def test_search_knowledge_empty(self):
555
store = _mock_store([])
556
loader = ContextLoader(store)
557
results = loader.search_knowledge("xyz")
558
assert results == []
559
560
def test_search_knowledge_returns_nodes(self):
561
rows = [["Concept", "JWT", "Stateless token auth", "auth", "active"]]
562
store = _mock_store(rows)
563
loader = ContextLoader(store)
564
results = loader.search_knowledge("JWT")
565
assert len(results) == 1
566
assert results[0].name == "JWT"
567
assert results[0].type == "Concept"
568
assert results[0].domain == "auth"
569
570
def test_search_knowledge_passes_limit(self):
571
store = _mock_store([])
572
loader = ContextLoader(store)
573
loader.search_knowledge("auth", limit=5)
574
args = store.query.call_args
575
assert args[0][1]["limit"] == 5
576

Keyboard Shortcuts

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