Navegador

navegador / tests / test_cli.py
Blame History Raw 768 lines
1
"""Tests for navegador CLI commands via click CliRunner."""
2
3
import json
4
from pathlib import Path
5
from unittest.mock import MagicMock, patch
6
7
from click.testing import CliRunner
8
9
from navegador.cli.commands import main
10
from navegador.context.loader import ContextBundle, ContextNode
11
12
# ── Helpers ───────────────────────────────────────────────────────────────────
13
14
def _mock_store():
15
store = MagicMock()
16
store.query.return_value = MagicMock(result_set=[])
17
return store
18
19
20
def _node(name="foo", type_="Function", file_path="app.py"):
21
return ContextNode(name=name, type=type_, file_path=file_path)
22
23
24
def _empty_bundle(name="target", type_="Function"):
25
"""Return a ContextBundle with a minimal target for testing."""
26
return ContextBundle(target=_node(name, type_), nodes=[])
27
28
29
# ── init ──────────────────────────────────────────────────────────────────────
30
31
class TestInitCommand:
32
def test_creates_navegador_dir(self):
33
runner = CliRunner()
34
with runner.isolated_filesystem():
35
result = runner.invoke(main, ["init", "."])
36
assert result.exit_code == 0
37
assert Path(".navegador").exists()
38
39
def test_shows_redis_hint_when_url_provided(self):
40
runner = CliRunner()
41
with runner.isolated_filesystem():
42
result = runner.invoke(main, ["init", ".", "--redis", "redis://localhost:6379"])
43
assert result.exit_code == 0
44
assert "redis://localhost:6379" in result.output
45
46
def test_shows_sqlite_hint_by_default(self):
47
runner = CliRunner()
48
with runner.isolated_filesystem():
49
result = runner.invoke(main, ["init", "."])
50
assert result.exit_code == 0
51
assert "Local SQLite" in result.output
52
53
def test_llm_provider_shown(self):
54
runner = CliRunner()
55
with runner.isolated_filesystem():
56
result = runner.invoke(main, ["init", ".", "--llm-provider", "anthropic"])
57
assert result.exit_code == 0
58
assert "anthropic" in result.output
59
60
def test_cluster_flag_shown(self):
61
runner = CliRunner()
62
with runner.isolated_filesystem():
63
result = runner.invoke(main, ["init", ".", "--cluster"])
64
assert result.exit_code == 0
65
assert "Cluster mode" in result.output
66
67
68
# ── ingest ────────────────────────────────────────────────────────────────────
69
70
class TestIngestCommand:
71
def test_outputs_table_on_success(self):
72
runner = CliRunner()
73
with runner.isolated_filesystem():
74
Path("src").mkdir()
75
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
76
patch("navegador.ingestion.RepoIngester") as MockRI:
77
MockRI.return_value.ingest.return_value = {"files": 5, "functions": 20}
78
result = runner.invoke(main, ["ingest", "src"])
79
assert result.exit_code == 0
80
81
def test_json_flag_outputs_json(self):
82
runner = CliRunner()
83
with runner.isolated_filesystem():
84
Path("src").mkdir()
85
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
86
patch("navegador.ingestion.RepoIngester") as MockRI:
87
MockRI.return_value.ingest.return_value = {"files": 5}
88
result = runner.invoke(main, ["ingest", "src", "--json"])
89
assert result.exit_code == 0
90
data = json.loads(result.output)
91
assert data["files"] == 5
92
93
def test_incremental_flag_passes_through(self):
94
runner = CliRunner()
95
with runner.isolated_filesystem():
96
Path("src").mkdir()
97
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
98
patch("navegador.ingestion.RepoIngester") as MockRI:
99
MockRI.return_value.ingest.return_value = {
100
"files": 2, "functions": 5, "classes": 1, "edges": 3, "skipped": 8
101
}
102
result = runner.invoke(main, ["ingest", "src", "--incremental"])
103
assert result.exit_code == 0
104
MockRI.return_value.ingest.assert_called_once()
105
_, kwargs = MockRI.return_value.ingest.call_args
106
assert kwargs["incremental"] is True
107
108
def test_watch_flag_calls_watch(self):
109
runner = CliRunner()
110
with runner.isolated_filesystem():
111
Path("src").mkdir()
112
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
113
patch("navegador.ingestion.RepoIngester") as MockRI:
114
# watch should be called, simulate immediate stop
115
MockRI.return_value.watch.side_effect = KeyboardInterrupt()
116
result = runner.invoke(main, ["ingest", "src", "--watch", "--interval", "0.1"])
117
assert result.exit_code == 0
118
MockRI.return_value.watch.assert_called_once()
119
120
def test_watch_with_interval(self):
121
runner = CliRunner()
122
with runner.isolated_filesystem():
123
Path("src").mkdir()
124
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
125
patch("navegador.ingestion.RepoIngester") as MockRI:
126
MockRI.return_value.watch.side_effect = KeyboardInterrupt()
127
runner.invoke(main, ["ingest", "src", "--watch", "--interval", "5.0"])
128
_, kwargs = MockRI.return_value.watch.call_args
129
assert kwargs["interval"] == 5.0
130
131
132
# ── context ───────────────────────────────────────────────────────────────────
133
134
class TestContextCommand:
135
def test_json_format(self):
136
runner = CliRunner()
137
bundle = ContextBundle(target=_node("MyClass", "Class"), nodes=[])
138
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
139
patch("navegador.context.ContextLoader") as MockCL:
140
MockCL.return_value.load_file.return_value = bundle
141
result = runner.invoke(main, ["context", "app.py", "--format", "json"])
142
assert result.exit_code == 0
143
data = json.loads(result.output)
144
assert isinstance(data, dict)
145
146
def test_markdown_format(self):
147
runner = CliRunner()
148
bundle = ContextBundle(target=_node(), nodes=[])
149
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
150
patch("navegador.context.ContextLoader") as MockCL:
151
MockCL.return_value.load_file.return_value = bundle
152
result = runner.invoke(main, ["context", "app.py"])
153
assert result.exit_code == 0
154
155
156
# ── function ──────────────────────────────────────────────────────────────────
157
158
class TestFunctionCommand:
159
def test_function_json(self):
160
runner = CliRunner()
161
bundle = ContextBundle(target=_node("my_func"), nodes=[])
162
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
163
patch("navegador.context.ContextLoader") as MockCL:
164
MockCL.return_value.load_function.return_value = bundle
165
result = runner.invoke(main, ["function", "my_func", "--format", "json"])
166
assert result.exit_code == 0
167
json.loads(result.output) # must be valid JSON
168
169
def test_function_with_file_option(self):
170
runner = CliRunner()
171
bundle = _empty_bundle()
172
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
173
patch("navegador.context.ContextLoader") as MockCL:
174
MockCL.return_value.load_function.return_value = bundle
175
result = runner.invoke(main, ["function", "foo", "--file", "bar.py"])
176
MockCL.return_value.load_function.assert_called_with(
177
"foo", file_path="bar.py", depth=2)
178
assert result.exit_code == 0
179
180
181
# ── class ─────────────────────────────────────────────────────────────────────
182
183
class TestClassCommand:
184
def test_class_json(self):
185
runner = CliRunner()
186
bundle = ContextBundle(target=_node("MyClass", "Class"), nodes=[])
187
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
188
patch("navegador.context.ContextLoader") as MockCL:
189
MockCL.return_value.load_class.return_value = bundle
190
result = runner.invoke(main, ["class", "MyClass", "--format", "json"])
191
assert result.exit_code == 0
192
json.loads(result.output)
193
194
195
# ── explain ───────────────────────────────────────────────────────────────────
196
197
class TestExplainCommand:
198
def test_explain_json(self):
199
runner = CliRunner()
200
bundle = _empty_bundle()
201
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
202
patch("navegador.context.ContextLoader") as MockCL:
203
MockCL.return_value.explain.return_value = bundle
204
result = runner.invoke(main, ["explain", "SomeName", "--format", "json"])
205
assert result.exit_code == 0
206
json.loads(result.output)
207
208
209
# ── search ────────────────────────────────────────────────────────────────────
210
211
class TestSearchCommand:
212
def test_search_json_no_results(self):
213
runner = CliRunner()
214
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
215
patch("navegador.context.ContextLoader") as MockCL:
216
MockCL.return_value.search.return_value = []
217
result = runner.invoke(main, ["search", "foo", "--format", "json"])
218
assert result.exit_code == 0
219
assert json.loads(result.output) == []
220
221
def test_search_all_flag(self):
222
runner = CliRunner()
223
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
224
patch("navegador.context.ContextLoader") as MockCL:
225
MockCL.return_value.search_all.return_value = []
226
result = runner.invoke(main, ["search", "foo", "--all", "--format", "json"])
227
assert result.exit_code == 0
228
MockCL.return_value.search_all.assert_called_once()
229
230
def test_search_docs_flag(self):
231
runner = CliRunner()
232
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
233
patch("navegador.context.ContextLoader") as MockCL:
234
MockCL.return_value.search_by_docstring.return_value = []
235
result = runner.invoke(main, ["search", "foo", "--docs", "--format", "json"])
236
assert result.exit_code == 0
237
MockCL.return_value.search_by_docstring.assert_called_once()
238
239
def test_search_markdown_no_results(self):
240
runner = CliRunner()
241
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
242
patch("navegador.context.ContextLoader") as MockCL:
243
MockCL.return_value.search.return_value = []
244
result = runner.invoke(main, ["search", "nothing"])
245
assert result.exit_code == 0
246
247
def test_search_with_results(self):
248
runner = CliRunner()
249
node = _node("result_fn")
250
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
251
patch("navegador.context.ContextLoader") as MockCL:
252
MockCL.return_value.search.return_value = [node]
253
result = runner.invoke(main, ["search", "result", "--format", "json"])
254
assert result.exit_code == 0
255
data = json.loads(result.output)
256
assert len(data) == 1
257
assert data[0]["name"] == "result_fn"
258
259
260
# ── decorated ─────────────────────────────────────────────────────────────────
261
262
class TestDecoratedCommand:
263
def test_decorated_json_no_results(self):
264
runner = CliRunner()
265
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
266
patch("navegador.context.ContextLoader") as MockCL:
267
MockCL.return_value.decorated_by.return_value = []
268
result = runner.invoke(main, ["decorated", "login_required", "--format", "json"])
269
assert result.exit_code == 0
270
assert json.loads(result.output) == []
271
272
def test_decorated_with_results(self):
273
runner = CliRunner()
274
node = _node("my_view", "Function", "views.py")
275
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
276
patch("navegador.context.ContextLoader") as MockCL:
277
MockCL.return_value.decorated_by.return_value = [node]
278
result = runner.invoke(main, ["decorated", "login_required", "--format", "json"])
279
data = json.loads(result.output)
280
assert data[0]["name"] == "my_view"
281
282
283
# ── query ─────────────────────────────────────────────────────────────────────
284
285
class TestQueryCommand:
286
def test_returns_json(self):
287
runner = CliRunner()
288
store = _mock_store()
289
store.query.return_value = MagicMock(result_set=[["Node1", "Node2"]])
290
with patch("navegador.cli.commands._get_store", return_value=store):
291
result = runner.invoke(main, ["query", "MATCH (n) RETURN n.name"])
292
assert result.exit_code == 0
293
data = json.loads(result.output)
294
assert data == [["Node1", "Node2"]]
295
296
def test_empty_result(self):
297
runner = CliRunner()
298
store = _mock_store()
299
with patch("navegador.cli.commands._get_store", return_value=store):
300
result = runner.invoke(main, ["query", "MATCH (n) RETURN n"])
301
assert result.exit_code == 0
302
assert json.loads(result.output) == []
303
304
305
# ── add concept / rule / decision / person / domain ───────────────────────────
306
307
class TestAddCommands:
308
def _run_add(self, *args):
309
runner = CliRunner()
310
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
311
patch("navegador.ingestion.KnowledgeIngester") as MockKI:
312
MockKI.return_value = MagicMock()
313
result = runner.invoke(main, list(args))
314
return result, MockKI
315
316
def test_add_concept(self):
317
result, MockKI = self._run_add("add", "concept", "Payment", "--desc", "Handles money")
318
assert result.exit_code == 0
319
MockKI.return_value.add_concept.assert_called_once()
320
321
def test_add_rule(self):
322
result, MockKI = self._run_add("add", "rule", "NoNullIds", "--severity", "critical")
323
assert result.exit_code == 0
324
MockKI.return_value.add_rule.assert_called_once()
325
326
def test_add_decision(self):
327
result, MockKI = self._run_add("add", "decision", "Use PostgreSQL")
328
assert result.exit_code == 0
329
MockKI.return_value.add_decision.assert_called_once()
330
331
def test_add_person(self):
332
result, MockKI = self._run_add("add", "person", "Alice", "--email", "[email protected]")
333
assert result.exit_code == 0
334
MockKI.return_value.add_person.assert_called_once()
335
336
def test_add_domain(self):
337
result, MockKI = self._run_add("add", "domain", "Billing", "--desc", "All billing logic")
338
assert result.exit_code == 0
339
MockKI.return_value.add_domain.assert_called_once()
340
341
342
# ── annotate ──────────────────────────────────────────────────────────────────
343
344
class TestAnnotateCommand:
345
def test_annotate_with_concept(self):
346
runner = CliRunner()
347
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
348
patch("navegador.ingestion.KnowledgeIngester") as MockKI:
349
MockKI.return_value = MagicMock()
350
result = runner.invoke(main, ["annotate", "process_payment", "--concept", "Payment"])
351
assert result.exit_code == 0
352
MockKI.return_value.annotate_code.assert_called_once()
353
354
def test_annotate_with_rule(self):
355
runner = CliRunner()
356
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
357
patch("navegador.ingestion.KnowledgeIngester") as MockKI:
358
MockKI.return_value = MagicMock()
359
result = runner.invoke(main, ["annotate", "validate_card", "--rule", "PCI"])
360
assert result.exit_code == 0
361
362
363
# ── domain / concept ──────────────────────────────────────────────────────────
364
365
class TestDomainConceptCommands:
366
def test_domain_json(self):
367
runner = CliRunner()
368
bundle = _empty_bundle()
369
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
370
patch("navegador.context.ContextLoader") as MockCL:
371
MockCL.return_value.load_domain.return_value = bundle
372
result = runner.invoke(main, ["domain", "Billing", "--format", "json"])
373
assert result.exit_code == 0
374
json.loads(result.output)
375
376
def test_concept_json(self):
377
runner = CliRunner()
378
bundle = _empty_bundle()
379
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
380
patch("navegador.context.ContextLoader") as MockCL:
381
MockCL.return_value.load_concept.return_value = bundle
382
result = runner.invoke(main, ["concept", "Payment", "--format", "json"])
383
assert result.exit_code == 0
384
json.loads(result.output)
385
386
387
# ── wiki ingest ───────────────────────────────────────────────────────────────
388
389
class TestWikiIngestCommand:
390
def test_ingest_local_dir(self):
391
runner = CliRunner()
392
with runner.isolated_filesystem():
393
Path("wiki").mkdir()
394
(Path("wiki") / "home.md").write_text("# Home")
395
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
396
patch("navegador.ingestion.WikiIngester") as MockWI:
397
MockWI.return_value.ingest_local.return_value = {"pages": 1, "links": 0}
398
result = runner.invoke(main, ["wiki", "ingest", "--dir", "wiki"])
399
assert result.exit_code == 0
400
assert "1" in result.output
401
402
def test_error_without_repo_or_dir(self):
403
runner = CliRunner()
404
with patch("navegador.cli.commands._get_store", return_value=_mock_store()):
405
result = runner.invoke(main, ["wiki", "ingest"])
406
assert result.exit_code != 0
407
408
def test_ingest_github_api(self):
409
runner = CliRunner()
410
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
411
patch("navegador.ingestion.WikiIngester") as MockWI:
412
MockWI.return_value.ingest_github_api.return_value = {"pages": 3, "links": 2}
413
result = runner.invoke(main, ["wiki", "ingest", "--repo", "owner/repo", "--api"])
414
assert result.exit_code == 0
415
416
417
# ── stats ─────────────────────────────────────────────────────────────────────
418
419
class TestStatsCommand:
420
def test_json_output(self):
421
runner = CliRunner()
422
store = _mock_store()
423
store.query.side_effect = [
424
MagicMock(result_set=[["Function", 10], ["Class", 5]]),
425
MagicMock(result_set=[["CALLS", 20]]),
426
]
427
with patch("navegador.cli.commands._get_store", return_value=store):
428
result = runner.invoke(main, ["stats", "--json"])
429
assert result.exit_code == 0
430
data = json.loads(result.output)
431
assert data["total_nodes"] == 15
432
assert data["total_edges"] == 20
433
434
def test_table_output(self):
435
runner = CliRunner()
436
store = _mock_store()
437
store.query.side_effect = [
438
MagicMock(result_set=[["Function", 10]]),
439
MagicMock(result_set=[["CALLS", 5]]),
440
]
441
with patch("navegador.cli.commands._get_store", return_value=store):
442
result = runner.invoke(main, ["stats"])
443
assert result.exit_code == 0
444
445
def test_empty_graph(self):
446
runner = CliRunner()
447
store = _mock_store()
448
store.query.side_effect = [
449
MagicMock(result_set=[]),
450
MagicMock(result_set=[]),
451
]
452
with patch("navegador.cli.commands._get_store", return_value=store):
453
result = runner.invoke(main, ["stats", "--json"])
454
assert result.exit_code == 0
455
data = json.loads(result.output)
456
assert data["total_nodes"] == 0
457
458
459
# ── planopticon ingest ────────────────────────────────────────────────────────
460
461
class TestPlanopticonIngestCommand:
462
def test_auto_detect_kg(self):
463
runner = CliRunner()
464
with runner.isolated_filesystem():
465
Path("knowledge_graph.json").write_text('{"nodes":[],"relationships":[],"sources":[]}')
466
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
467
patch("navegador.ingestion.PlanopticonIngester") as MockPI:
468
MockPI.return_value.ingest_kg.return_value = {"nodes": 0, "edges": 0}
469
result = runner.invoke(main, ["planopticon", "ingest", "knowledge_graph.json"])
470
assert result.exit_code == 0
471
472
def test_auto_detect_manifest(self):
473
runner = CliRunner()
474
with runner.isolated_filesystem():
475
Path("manifest.json").write_text("{}")
476
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
477
patch("navegador.ingestion.PlanopticonIngester") as MockPI:
478
MockPI.return_value.ingest_manifest.return_value = {"nodes": 0, "edges": 0}
479
result = runner.invoke(main, ["planopticon", "ingest", "manifest.json"])
480
assert result.exit_code == 0
481
MockPI.return_value.ingest_manifest.assert_called_once()
482
483
def test_json_output(self):
484
runner = CliRunner()
485
with runner.isolated_filesystem():
486
Path("kg.json").write_text('{"nodes":[],"relationships":[],"sources":[]}')
487
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
488
patch("navegador.ingestion.PlanopticonIngester") as MockPI:
489
MockPI.return_value.ingest_kg.return_value = {"nodes": 3, "edges": 1}
490
result = runner.invoke(main, ["planopticon", "ingest", "kg.json", "--json"])
491
assert result.exit_code == 0
492
data = json.loads(result.output)
493
assert data["nodes"] == 3
494
495
def test_directory_resolves_manifest(self):
496
runner = CliRunner()
497
with runner.isolated_filesystem():
498
Path("output").mkdir()
499
Path("output/manifest.json").write_text("{}")
500
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
501
patch("navegador.ingestion.PlanopticonIngester") as MockPI:
502
MockPI.return_value.ingest_manifest.return_value = {"nodes": 0, "edges": 0}
503
result = runner.invoke(main, ["planopticon", "ingest", "output"])
504
assert result.exit_code == 0
505
MockPI.return_value.ingest_manifest.assert_called_once()
506
507
508
# ── --help smoke tests ─────────────────────────────────────────────────────────
509
510
class TestHelp:
511
def test_main_help(self):
512
runner = CliRunner()
513
result = runner.invoke(main, ["--help"])
514
assert result.exit_code == 0
515
assert "navegador" in result.output.lower() or "knowledge" in result.output.lower()
516
517
def test_add_help(self):
518
runner = CliRunner()
519
result = runner.invoke(main, ["add", "--help"])
520
assert result.exit_code == 0
521
522
def test_wiki_help(self):
523
runner = CliRunner()
524
result = runner.invoke(main, ["wiki", "--help"])
525
assert result.exit_code == 0
526
527
def test_planopticon_help(self):
528
runner = CliRunner()
529
result = runner.invoke(main, ["planopticon", "--help"])
530
assert result.exit_code == 0
531
532
533
# ── _get_store with custom db path (lines 31-32) ──────────────────────────────
534
535
class TestGetStoreCustomPath:
536
def test_get_store_calls_get_store_with_custom_path(self):
537
"""_get_store body: custom path is forwarded to config.get_store."""
538
from navegador.cli.commands import _get_store
539
with patch("navegador.config.get_store", return_value=_mock_store()) as mock_gs:
540
_get_store("/custom/path.db")
541
mock_gs.assert_called_once_with("/custom/path.db")
542
543
def test_get_store_passes_none_for_default_path(self):
544
from navegador.cli.commands import _get_store
545
from navegador.config import DEFAULT_DB_PATH
546
with patch("navegador.config.get_store", return_value=_mock_store()) as mock_gs:
547
_get_store(DEFAULT_DB_PATH)
548
mock_gs.assert_called_once_with(None)
549
550
551
# ── search table output with results (lines 208-216) ─────────────────────────
552
553
class TestSearchTableOutput:
554
def test_search_renders_table_with_results(self):
555
runner = CliRunner()
556
node = _node("process_payment", "Function", "payments.py")
557
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
558
patch("navegador.context.ContextLoader") as MockCL:
559
MockCL.return_value.search.return_value = [node]
560
result = runner.invoke(main, ["search", "payment"])
561
assert result.exit_code == 0
562
assert "process_payment" in result.output
563
564
565
# ── decorated table output with results (lines 237-248) ──────────────────────
566
567
class TestDecoratedTableOutput:
568
def test_decorated_no_results_table(self):
569
runner = CliRunner()
570
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
571
patch("navegador.context.ContextLoader") as MockCL:
572
MockCL.return_value.decorated_by.return_value = []
573
result = runner.invoke(main, ["decorated", "login_required"])
574
assert result.exit_code == 0
575
assert "login_required" in result.output
576
577
def test_decorated_renders_table_with_results(self):
578
runner = CliRunner()
579
node = _node("my_view", "Function", "views.py")
580
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
581
patch("navegador.context.ContextLoader") as MockCL:
582
MockCL.return_value.decorated_by.return_value = [node]
583
result = runner.invoke(main, ["decorated", "login_required"])
584
assert result.exit_code == 0
585
assert "my_view" in result.output
586
587
588
# ── wiki ingest without --api flag (line 410) ─────────────────────────────────
589
590
class TestWikiIngestGithubNoApi:
591
def test_ingest_github_without_api_flag(self):
592
runner = CliRunner()
593
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
594
patch("navegador.ingestion.WikiIngester") as MockWI:
595
MockWI.return_value.ingest_github.return_value = {"pages": 5, "links": 3}
596
result = runner.invoke(main, ["wiki", "ingest", "--repo", "owner/repo"])
597
assert result.exit_code == 0
598
MockWI.return_value.ingest_github.assert_called_once()
599
assert "5" in result.output
600
601
602
# ── planopticon dir with no recognised files (line 497) ──────────────────────
603
604
class TestPlanopticonIngestNoKnownFiles:
605
def test_empty_directory_raises_usage_error(self):
606
runner = CliRunner()
607
with runner.isolated_filesystem():
608
Path("output").mkdir()
609
# No manifest.json, knowledge_graph.json, or interchange.json
610
Path("output/readme.txt").write_text("nothing")
611
with patch("navegador.cli.commands._get_store", return_value=_mock_store()):
612
result = runner.invoke(main, ["planopticon", "ingest", "output"])
613
assert result.exit_code != 0
614
615
616
# ── planopticon auto-detect interchange/batch (lines 505, 507) ───────────────
617
618
class TestPlanopticonAutoDetect:
619
def test_auto_detect_interchange(self):
620
runner = CliRunner()
621
with runner.isolated_filesystem():
622
Path("interchange.json").write_text("{}")
623
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
624
patch("navegador.ingestion.PlanopticonIngester") as MockPI:
625
MockPI.return_value.ingest_interchange.return_value = {"nodes": 0, "edges": 0}
626
result = runner.invoke(main, ["planopticon", "ingest", "interchange.json"])
627
assert result.exit_code == 0
628
MockPI.return_value.ingest_interchange.assert_called_once()
629
630
def test_auto_detect_batch(self):
631
runner = CliRunner()
632
with runner.isolated_filesystem():
633
Path("batch.json").write_text("{}")
634
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
635
patch("navegador.ingestion.PlanopticonIngester") as MockPI:
636
MockPI.return_value.ingest_batch.return_value = {"nodes": 0, "edges": 0}
637
result = runner.invoke(main, ["planopticon", "ingest", "batch.json"])
638
assert result.exit_code == 0
639
MockPI.return_value.ingest_batch.assert_called_once()
640
641
642
# ── mcp command (lines 538-549) ───────────────────────────────────────────────
643
644
# ── export / import ──────────────────────────────────────────────────────────
645
646
class TestExportCommand:
647
def test_export_success(self):
648
runner = CliRunner()
649
with runner.isolated_filesystem():
650
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
651
patch("navegador.graph.export.export_graph", return_value={"nodes": 10, "edges": 5}):
652
result = runner.invoke(main, ["export", "graph.jsonl"])
653
assert result.exit_code == 0
654
assert "10 nodes" in result.output
655
656
def test_export_json(self):
657
runner = CliRunner()
658
with runner.isolated_filesystem():
659
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
660
patch("navegador.graph.export.export_graph", return_value={"nodes": 10, "edges": 5}):
661
result = runner.invoke(main, ["export", "graph.jsonl", "--json"])
662
assert result.exit_code == 0
663
data = json.loads(result.output)
664
assert data["nodes"] == 10
665
666
667
class TestImportCommand:
668
def test_import_success(self):
669
runner = CliRunner()
670
with runner.isolated_filesystem():
671
Path("graph.jsonl").write_text("")
672
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
673
patch("navegador.graph.export.import_graph", return_value={"nodes": 10, "edges": 5}):
674
result = runner.invoke(main, ["import", "graph.jsonl"])
675
assert result.exit_code == 0
676
assert "10 nodes" in result.output
677
678
def test_import_json(self):
679
runner = CliRunner()
680
with runner.isolated_filesystem():
681
Path("graph.jsonl").write_text("")
682
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
683
patch("navegador.graph.export.import_graph", return_value={"nodes": 8, "edges": 3}):
684
result = runner.invoke(main, ["import", "graph.jsonl", "--json"])
685
assert result.exit_code == 0
686
data = json.loads(result.output)
687
assert data["nodes"] == 8
688
689
def test_import_no_clear(self):
690
runner = CliRunner()
691
with runner.isolated_filesystem():
692
Path("graph.jsonl").write_text("")
693
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
694
patch("navegador.graph.export.import_graph", return_value={"nodes": 0, "edges": 0}) as mock_imp:
695
runner.invoke(main, ["import", "graph.jsonl", "--no-clear"])
696
mock_imp.assert_called_once()
697
assert mock_imp.call_args[1]["clear"] is False
698
699
700
# ── migrate ──────────────────────────────────────────────────────────────────
701
702
class TestMigrateCommand:
703
def test_migrate_applies_migrations(self):
704
runner = CliRunner()
705
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
706
patch("navegador.graph.migrations.get_schema_version", return_value=0), \
707
patch("navegador.graph.migrations.migrate", return_value=[1, 2]) as mock_migrate, \
708
patch("navegador.graph.migrations.CURRENT_SCHEMA_VERSION", 2):
709
result = runner.invoke(main, ["migrate"])
710
assert result.exit_code == 0
711
assert "Migrated" in result.output
712
713
def test_migrate_already_current(self):
714
runner = CliRunner()
715
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
716
patch("navegador.graph.migrations.get_schema_version", return_value=2), \
717
patch("navegador.graph.migrations.migrate", return_value=[]):
718
result = runner.invoke(main, ["migrate"])
719
assert result.exit_code == 0
720
assert "up to date" in result.output
721
722
def test_migrate_check_needed(self):
723
runner = CliRunner()
724
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
725
patch("navegador.graph.migrations.get_schema_version", return_value=0), \
726
patch("navegador.graph.migrations.needs_migration", return_value=True), \
727
patch("navegador.graph.migrations.CURRENT_SCHEMA_VERSION", 2):
728
result = runner.invoke(main, ["migrate", "--check"])
729
assert result.exit_code == 0
730
assert "Migration needed" in result.output
731
732
def test_migrate_check_not_needed(self):
733
runner = CliRunner()
734
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
735
patch("navegador.graph.migrations.get_schema_version", return_value=2), \
736
patch("navegador.graph.migrations.needs_migration", return_value=False):
737
result = runner.invoke(main, ["migrate", "--check"])
738
assert result.exit_code == 0
739
assert "up to date" in result.output
740
741
742
class TestMcpCommand:
743
def test_mcp_command_runs_server(self):
744
from contextlib import asynccontextmanager
745
746
runner = CliRunner()
747
748
@asynccontextmanager
749
async def _fake_stdio():
750
yield (MagicMock(), MagicMock())
751
752
async def _fake_run(*args, **kwargs):
753
pass
754
755
mock_server = MagicMock()
756
mock_server.create_initialization_options.return_value = {}
757
mock_server.run = _fake_run
758
759
with patch("navegador.cli.commands._get_store", return_value=_mock_store()), \
760
patch.dict("sys.modules", {
761
"mcp": MagicMock(),
762
"mcp.server": MagicMock(),
763
"mcp.server.stdio": MagicMock(stdio_server=_fake_stdio),
764
}), \
765
patch("navegador.mcp.create_mcp_server", return_value=mock_server):
766
result = runner.invoke(main, ["mcp"])
767
assert result.exit_code == 0
768

Keyboard Shortcuts

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