Navegador

navegador / tests / test_v04_features.py
Source Blame History 754 lines
dda2b32… lmata 1 """
dda2b32… lmata 2 Tests for navegador v0.4 features:
dda2b32… lmata 3 #16 — Multi-repo support (MultiRepoManager)
dda2b32… lmata 4 #26 — Coordinated rename (SymbolRenamer)
dda2b32… lmata 5 #39 — CODEOWNERS integration (CodeownersIngester)
dda2b32… lmata 6 #40 — ADR ingestion (ADRIngester)
dda2b32… lmata 7 #41 — OpenAPI / GraphQL schema (APISchemaIngester)
dda2b32… lmata 8 """
dda2b32… lmata 9
dda2b32… lmata 10 from __future__ import annotations
dda2b32… lmata 11
dda2b32… lmata 12 import json
dda2b32… lmata 13 import tempfile
dda2b32… lmata 14 from pathlib import Path
dda2b32… lmata 15 from unittest.mock import MagicMock, call, patch
dda2b32… lmata 16
dda2b32… lmata 17 import pytest
dda2b32… lmata 18 from click.testing import CliRunner
dda2b32… lmata 19
dda2b32… lmata 20 from navegador.cli.commands import main
dda2b32… lmata 21
dda2b32… lmata 22
dda2b32… lmata 23 # ── Shared helpers ────────────────────────────────────────────────────────────
dda2b32… lmata 24
dda2b32… lmata 25
dda2b32… lmata 26 def _mock_store():
dda2b32… lmata 27 store = MagicMock()
dda2b32… lmata 28 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 29 return store
dda2b32… lmata 30
dda2b32… lmata 31
dda2b32… lmata 32 def _write(path: Path, content: str) -> None:
dda2b32… lmata 33 path.parent.mkdir(parents=True, exist_ok=True)
dda2b32… lmata 34 path.write_text(content, encoding="utf-8")
dda2b32… lmata 35
dda2b32… lmata 36
dda2b32… lmata 37 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 38 # #16 — MultiRepoManager
dda2b32… lmata 39 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 40
dda2b32… lmata 41
dda2b32… lmata 42 class TestMultiRepoManagerAddRepo:
dda2b32… lmata 43 def test_creates_repository_node(self, tmp_path):
dda2b32… lmata 44 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 45
dda2b32… lmata 46 store = _mock_store()
dda2b32… lmata 47 mgr = MultiRepoManager(store)
dda2b32… lmata 48 mgr.add_repo("backend", str(tmp_path))
dda2b32… lmata 49 store.create_node.assert_called_once()
dda2b32… lmata 50 args = store.create_node.call_args[0]
dda2b32… lmata 51 assert args[0] == "Repository"
dda2b32… lmata 52 assert args[1]["name"] == "backend"
dda2b32… lmata 53
dda2b32… lmata 54 def test_resolves_path(self, tmp_path):
dda2b32… lmata 55 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 56
dda2b32… lmata 57 store = _mock_store()
dda2b32… lmata 58 mgr = MultiRepoManager(store)
dda2b32… lmata 59 mgr.add_repo("x", str(tmp_path))
dda2b32… lmata 60 props = store.create_node.call_args[0][1]
dda2b32… lmata 61 assert Path(props["path"]).is_absolute()
dda2b32… lmata 62
dda2b32… lmata 63
dda2b32… lmata 64 class TestMultiRepoManagerListRepos:
dda2b32… lmata 65 def test_returns_empty_list_when_no_repos(self):
dda2b32… lmata 66 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 67
dda2b32… lmata 68 store = _mock_store()
dda2b32… lmata 69 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 70 mgr = MultiRepoManager(store)
dda2b32… lmata 71 assert mgr.list_repos() == []
dda2b32… lmata 72
dda2b32… lmata 73 def test_parses_result_set(self):
dda2b32… lmata 74 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 75
dda2b32… lmata 76 store = _mock_store()
dda2b32… lmata 77 store.query.return_value = MagicMock(
dda2b32… lmata 78 result_set=[["backend", "/repos/backend"], ["frontend", "/repos/frontend"]]
dda2b32… lmata 79 )
dda2b32… lmata 80 mgr = MultiRepoManager(store)
dda2b32… lmata 81 repos = mgr.list_repos()
dda2b32… lmata 82 assert len(repos) == 2
dda2b32… lmata 83 assert repos[0] == {"name": "backend", "path": "/repos/backend"}
dda2b32… lmata 84 assert repos[1] == {"name": "frontend", "path": "/repos/frontend"}
dda2b32… lmata 85
dda2b32… lmata 86
dda2b32… lmata 87 class TestMultiRepoManagerIngestAll:
dda2b32… lmata 88 def test_calls_repo_ingester_for_each_repo(self, tmp_path):
dda2b32… lmata 89 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 90
dda2b32… lmata 91 store = _mock_store()
dda2b32… lmata 92 # list_repos() is called first; return one repo
dda2b32… lmata 93 store.query.return_value = MagicMock(
dda2b32… lmata 94 result_set=[["svc", str(tmp_path)]]
dda2b32… lmata 95 )
dda2b32… lmata 96 mgr = MultiRepoManager(store)
dda2b32… lmata 97
dda2b32… lmata 98 mock_ingester_instance = MagicMock()
dda2b32… lmata 99 mock_ingester_instance.ingest.return_value = {"files": 3, "functions": 10}
dda2b32… lmata 100 mock_ingester_cls = MagicMock(return_value=mock_ingester_instance)
dda2b32… lmata 101
dda2b32… lmata 102 # Patch the lazy import inside ingest_all
dda2b32… lmata 103 with patch("navegador.ingestion.parser.RepoIngester", mock_ingester_cls):
dda2b32… lmata 104 # Also patch the name that is imported lazily inside the method
dda2b32… lmata 105 import navegador.multirepo as _m
dda2b32… lmata 106 import navegador.ingestion.parser as _p
dda2b32… lmata 107 original = getattr(_p, "RepoIngester", None)
dda2b32… lmata 108 _p.RepoIngester = mock_ingester_cls
dda2b32… lmata 109 try:
dda2b32… lmata 110 summary = mgr.ingest_all()
dda2b32… lmata 111 finally:
dda2b32… lmata 112 if original is not None:
dda2b32… lmata 113 _p.RepoIngester = original
dda2b32… lmata 114
dda2b32… lmata 115 assert "svc" in summary
dda2b32… lmata 116 assert summary["svc"]["files"] == 3
dda2b32… lmata 117
dda2b32… lmata 118 def test_returns_empty_when_no_repos(self):
dda2b32… lmata 119 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 120
dda2b32… lmata 121 store = _mock_store()
dda2b32… lmata 122 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 123 mgr = MultiRepoManager(store)
dda2b32… lmata 124 assert mgr.ingest_all() == {}
dda2b32… lmata 125
dda2b32… lmata 126 def test_clear_flag_calls_store_clear_when_repos_exist(self, tmp_path):
dda2b32… lmata 127 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 128
dda2b32… lmata 129 store = _mock_store()
dda2b32… lmata 130 # Return one repo so ingest_all proceeds past the empty check
dda2b32… lmata 131 store.query.return_value = MagicMock(
dda2b32… lmata 132 result_set=[["svc", str(tmp_path)]]
dda2b32… lmata 133 )
dda2b32… lmata 134 mgr = MultiRepoManager(store)
dda2b32… lmata 135
dda2b32… lmata 136 mock_ingester_instance = MagicMock()
dda2b32… lmata 137 mock_ingester_instance.ingest.return_value = {"files": 1}
dda2b32… lmata 138 mock_ingester_cls = MagicMock(return_value=mock_ingester_instance)
dda2b32… lmata 139
dda2b32… lmata 140 import navegador.ingestion.parser as _p
dda2b32… lmata 141 original = getattr(_p, "RepoIngester", None)
dda2b32… lmata 142 _p.RepoIngester = mock_ingester_cls
dda2b32… lmata 143 try:
dda2b32… lmata 144 mgr.ingest_all(clear=True)
dda2b32… lmata 145 finally:
dda2b32… lmata 146 if original is not None:
dda2b32… lmata 147 _p.RepoIngester = original
dda2b32… lmata 148
dda2b32… lmata 149 store.clear.assert_called_once()
dda2b32… lmata 150
dda2b32… lmata 151
dda2b32… lmata 152 class TestMultiRepoManagerCrossRepoSearch:
dda2b32… lmata 153 def test_returns_results(self):
dda2b32… lmata 154 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 155
dda2b32… lmata 156 store = _mock_store()
dda2b32… lmata 157 store.query.return_value = MagicMock(
dda2b32… lmata 158 result_set=[["Function", "authenticate", "auth.py"]]
dda2b32… lmata 159 )
dda2b32… lmata 160 mgr = MultiRepoManager(store)
dda2b32… lmata 161 results = mgr.cross_repo_search("authenticate")
dda2b32… lmata 162 assert len(results) == 1
dda2b32… lmata 163 assert results[0]["name"] == "authenticate"
dda2b32… lmata 164
dda2b32… lmata 165 def test_empty_when_no_match(self):
dda2b32… lmata 166 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 167
dda2b32… lmata 168 store = _mock_store()
dda2b32… lmata 169 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 170 mgr = MultiRepoManager(store)
dda2b32… lmata 171 assert mgr.cross_repo_search("zzz_nonexistent") == []
dda2b32… lmata 172
dda2b32… lmata 173 def test_limit_is_applied(self):
dda2b32… lmata 174 from navegador.multirepo import MultiRepoManager
dda2b32… lmata 175
dda2b32… lmata 176 store = _mock_store()
dda2b32… lmata 177 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 178 mgr = MultiRepoManager(store)
dda2b32… lmata 179 mgr.cross_repo_search("foo", limit=5)
dda2b32… lmata 180 cypher = store.query.call_args[0][0]
dda2b32… lmata 181 assert "LIMIT 5" in cypher
dda2b32… lmata 182
dda2b32… lmata 183
dda2b32… lmata 184 # ── CLI: repo ──────────────────────────────────────────────────────────────
dda2b32… lmata 185
dda2b32… lmata 186
dda2b32… lmata 187 class TestRepoCLI:
dda2b32… lmata 188 def test_repo_add(self, tmp_path):
dda2b32… lmata 189 runner = CliRunner()
dda2b32… lmata 190 store = _mock_store()
dda2b32… lmata 191 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 192 result = runner.invoke(
dda2b32… lmata 193 main, ["repo", "add", "myapp", str(tmp_path)]
dda2b32… lmata 194 )
dda2b32… lmata 195 assert result.exit_code == 0
dda2b32… lmata 196 assert "myapp" in result.output
dda2b32… lmata 197
dda2b32… lmata 198 def test_repo_list_empty(self):
dda2b32… lmata 199 runner = CliRunner()
dda2b32… lmata 200 store = _mock_store()
dda2b32… lmata 201 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 202 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 203 result = runner.invoke(main, ["repo", "list"])
dda2b32… lmata 204 assert result.exit_code == 0
dda2b32… lmata 205
dda2b32… lmata 206 def test_repo_search(self):
dda2b32… lmata 207 runner = CliRunner()
dda2b32… lmata 208 store = _mock_store()
dda2b32… lmata 209 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 210 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 211 result = runner.invoke(main, ["repo", "search", "foo"])
dda2b32… lmata 212 assert result.exit_code == 0
dda2b32… lmata 213
dda2b32… lmata 214
dda2b32… lmata 215 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 216 # #26 — SymbolRenamer
dda2b32… lmata 217 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 218
dda2b32… lmata 219
dda2b32… lmata 220 class TestSymbolRenamerFindReferences:
dda2b32… lmata 221 def test_returns_references(self):
dda2b32… lmata 222 from navegador.refactor import SymbolRenamer
dda2b32… lmata 223
dda2b32… lmata 224 store = _mock_store()
dda2b32… lmata 225 store.query.return_value = MagicMock(
dda2b32… lmata 226 result_set=[["Function", "foo", "a.py", 10]]
dda2b32… lmata 227 )
dda2b32… lmata 228 renamer = SymbolRenamer(store)
dda2b32… lmata 229 refs = renamer.find_references("foo")
dda2b32… lmata 230 assert len(refs) == 1
dda2b32… lmata 231 assert refs[0]["name"] == "foo"
dda2b32… lmata 232 assert refs[0]["file_path"] == "a.py"
dda2b32… lmata 233
dda2b32… lmata 234 def test_filters_by_file_path(self):
dda2b32… lmata 235 from navegador.refactor import SymbolRenamer
dda2b32… lmata 236
dda2b32… lmata 237 store = _mock_store()
dda2b32… lmata 238 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 239 renamer = SymbolRenamer(store)
dda2b32… lmata 240 renamer.find_references("foo", file_path="a.py")
dda2b32… lmata 241 cypher = store.query.call_args[0][0]
dda2b32… lmata 242 assert "file_path" in cypher
dda2b32… lmata 243
dda2b32… lmata 244 def test_returns_empty_list_when_no_matches(self):
dda2b32… lmata 245 from navegador.refactor import SymbolRenamer
dda2b32… lmata 246
dda2b32… lmata 247 store = _mock_store()
dda2b32… lmata 248 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 249 renamer = SymbolRenamer(store)
dda2b32… lmata 250 assert renamer.find_references("nonexistent") == []
dda2b32… lmata 251
dda2b32… lmata 252
dda2b32… lmata 253 class TestSymbolRenamerPreview:
dda2b32… lmata 254 def test_preview_does_not_update_graph(self):
dda2b32… lmata 255 from navegador.refactor import SymbolRenamer
dda2b32… lmata 256
dda2b32… lmata 257 store = _mock_store()
dda2b32… lmata 258 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 259 renamer = SymbolRenamer(store)
dda2b32… lmata 260 preview = renamer.preview_rename("old", "new")
dda2b32… lmata 261 # No SET query should have been issued
dda2b32… lmata 262 for c in store.query.call_args_list:
dda2b32… lmata 263 assert "SET n.name" not in (c[0][0] if c[0] else "")
dda2b32… lmata 264
dda2b32… lmata 265 assert preview.old_name == "old"
dda2b32… lmata 266 assert preview.new_name == "new"
dda2b32… lmata 267
dda2b32… lmata 268 def test_preview_collects_affected_files(self):
dda2b32… lmata 269 from navegador.refactor import SymbolRenamer
dda2b32… lmata 270
dda2b32… lmata 271 store = _mock_store()
dda2b32… lmata 272
dda2b32… lmata 273 def _side(cypher, params=None):
dda2b32… lmata 274 if "SET" not in cypher:
dda2b32… lmata 275 return MagicMock(
dda2b32… lmata 276 result_set=[["Function", "old", "a.py", 1], ["Function", "old", "b.py", 5]]
dda2b32… lmata 277 )
dda2b32… lmata 278 return MagicMock(result_set=[])
dda2b32… lmata 279
dda2b32… lmata 280 store.query.side_effect = _side
dda2b32… lmata 281 renamer = SymbolRenamer(store)
dda2b32… lmata 282 preview = renamer.preview_rename("old", "new")
dda2b32… lmata 283 assert set(preview.affected_files) == {"a.py", "b.py"}
dda2b32… lmata 284
dda2b32… lmata 285
dda2b32… lmata 286 class TestSymbolRenamerApply:
dda2b32… lmata 287 def test_apply_issues_set_query(self):
dda2b32… lmata 288 from navegador.refactor import SymbolRenamer
dda2b32… lmata 289
dda2b32… lmata 290 store = _mock_store()
dda2b32… lmata 291 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 292 renamer = SymbolRenamer(store)
dda2b32… lmata 293 renamer.apply_rename("old", "new")
dda2b32… lmata 294 cypher_calls = [c[0][0] for c in store.query.call_args_list]
dda2b32… lmata 295 assert any("SET n.name" in c for c in cypher_calls)
dda2b32… lmata 296
dda2b32… lmata 297 def test_apply_returns_result_with_names(self):
dda2b32… lmata 298 from navegador.refactor import SymbolRenamer
dda2b32… lmata 299
dda2b32… lmata 300 store = _mock_store()
dda2b32… lmata 301 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 302 renamer = SymbolRenamer(store)
dda2b32… lmata 303 result = renamer.apply_rename("alpha", "beta")
dda2b32… lmata 304 assert result.old_name == "alpha"
dda2b32… lmata 305 assert result.new_name == "beta"
dda2b32… lmata 306
dda2b32… lmata 307
dda2b32… lmata 308 # ── CLI: rename ───────────────────────────────────────────────────────────────
dda2b32… lmata 309
dda2b32… lmata 310
dda2b32… lmata 311 class TestRenameCLI:
dda2b32… lmata 312 def test_rename_preview(self):
dda2b32… lmata 313 runner = CliRunner()
dda2b32… lmata 314 store = _mock_store()
dda2b32… lmata 315 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 316 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 317 result = runner.invoke(main, ["rename", "old_fn", "new_fn", "--preview"])
dda2b32… lmata 318 assert result.exit_code == 0
dda2b32… lmata 319
dda2b32… lmata 320 def test_rename_apply(self):
dda2b32… lmata 321 runner = CliRunner()
dda2b32… lmata 322 store = _mock_store()
dda2b32… lmata 323 store.query.return_value = MagicMock(result_set=[])
dda2b32… lmata 324 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 325 result = runner.invoke(main, ["rename", "old_fn", "new_fn"])
dda2b32… lmata 326 assert result.exit_code == 0
dda2b32… lmata 327
dda2b32… lmata 328
dda2b32… lmata 329 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 330 # #39 — CodeownersIngester
dda2b32… lmata 331 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 332
dda2b32… lmata 333
dda2b32… lmata 334 class TestCodeownersIngesterParseFile:
dda2b32… lmata 335 def test_parses_basic_entries(self, tmp_path):
dda2b32… lmata 336 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 337
dda2b32… lmata 338 co = tmp_path / "CODEOWNERS"
dda2b32… lmata 339 co.write_text("*.py @alice @bob\ndocs/ @carol\n")
dda2b32… lmata 340 ingester = CodeownersIngester(_mock_store())
dda2b32… lmata 341 entries = ingester._parse_codeowners(co)
dda2b32… lmata 342 assert len(entries) == 2
dda2b32… lmata 343 assert entries[0] == ("*.py", ["@alice", "@bob"])
dda2b32… lmata 344 assert entries[1] == ("docs/", ["@carol"])
dda2b32… lmata 345
dda2b32… lmata 346 def test_ignores_comments(self, tmp_path):
dda2b32… lmata 347 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 348
dda2b32… lmata 349 co = tmp_path / "CODEOWNERS"
dda2b32… lmata 350 co.write_text("# comment\n*.py @alice\n")
dda2b32… lmata 351 ingester = CodeownersIngester(_mock_store())
dda2b32… lmata 352 entries = ingester._parse_codeowners(co)
dda2b32… lmata 353 assert len(entries) == 1
dda2b32… lmata 354
dda2b32… lmata 355 def test_ignores_blank_lines(self, tmp_path):
dda2b32… lmata 356 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 357
dda2b32… lmata 358 co = tmp_path / "CODEOWNERS"
dda2b32… lmata 359 co.write_text("\n\n*.py @alice\n\n")
dda2b32… lmata 360 ingester = CodeownersIngester(_mock_store())
dda2b32… lmata 361 entries = ingester._parse_codeowners(co)
dda2b32… lmata 362 assert len(entries) == 1
dda2b32… lmata 363
dda2b32… lmata 364 def test_handles_email_owner(self, tmp_path):
dda2b32… lmata 365 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 366
dda2b32… lmata 367 co = tmp_path / "CODEOWNERS"
dda2b32… lmata 368 co.write_text("*.go [email protected]\n")
dda2b32… lmata 369 ingester = CodeownersIngester(_mock_store())
dda2b32… lmata 370 entries = ingester._parse_codeowners(co)
dda2b32… lmata 371 assert entries[0][1] == ["[email protected]"]
dda2b32… lmata 372
dda2b32… lmata 373
dda2b32… lmata 374 class TestCodeownersIngesterIngest:
dda2b32… lmata 375 def test_creates_person_nodes(self, tmp_path):
dda2b32… lmata 376 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 377
dda2b32… lmata 378 co = tmp_path / "CODEOWNERS"
dda2b32… lmata 379 co.write_text("*.py @alice\n")
dda2b32… lmata 380 store = _mock_store()
dda2b32… lmata 381 ingester = CodeownersIngester(store)
dda2b32… lmata 382 stats = ingester.ingest(str(tmp_path))
dda2b32… lmata 383 assert stats["owners"] == 1
dda2b32… lmata 384 assert stats["patterns"] == 1
dda2b32… lmata 385 assert stats["edges"] == 1
dda2b32… lmata 386
dda2b32… lmata 387 def test_deduplicates_owners(self, tmp_path):
dda2b32… lmata 388 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 389
dda2b32… lmata 390 co = tmp_path / "CODEOWNERS"
dda2b32… lmata 391 co.write_text("*.py @alice\ndocs/ @alice\n")
dda2b32… lmata 392 store = _mock_store()
dda2b32… lmata 393 ingester = CodeownersIngester(store)
dda2b32… lmata 394 stats = ingester.ingest(str(tmp_path))
dda2b32… lmata 395 # alice appears in both patterns but should only be created once
dda2b32… lmata 396 assert stats["owners"] == 1
dda2b32… lmata 397 assert stats["patterns"] == 2
dda2b32… lmata 398
dda2b32… lmata 399 def test_returns_zeros_when_no_codeowners(self, tmp_path):
dda2b32… lmata 400 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 401
dda2b32… lmata 402 store = _mock_store()
dda2b32… lmata 403 stats = CodeownersIngester(store).ingest(str(tmp_path))
dda2b32… lmata 404 assert stats == {"owners": 0, "patterns": 0, "edges": 0}
dda2b32… lmata 405
dda2b32… lmata 406 def test_finds_github_codeowners(self, tmp_path):
dda2b32… lmata 407 from navegador.codeowners import CodeownersIngester
dda2b32… lmata 408
dda2b32… lmata 409 gh = tmp_path / ".github"
dda2b32… lmata 410 gh.mkdir()
dda2b32… lmata 411 (gh / "CODEOWNERS").write_text("* @team\n")
dda2b32… lmata 412 store = _mock_store()
dda2b32… lmata 413 stats = CodeownersIngester(store).ingest(str(tmp_path))
dda2b32… lmata 414 assert stats["owners"] == 1
dda2b32… lmata 415
dda2b32… lmata 416
dda2b32… lmata 417 # ── CLI: codeowners ───────────────────────────────────────────────────────────
dda2b32… lmata 418
dda2b32… lmata 419
dda2b32… lmata 420 class TestCodeownersCLI:
dda2b32… lmata 421 def test_cli_codeowners(self, tmp_path):
dda2b32… lmata 422 runner = CliRunner()
dda2b32… lmata 423 (tmp_path / "CODEOWNERS").write_text("*.py @alice\n")
dda2b32… lmata 424 store = _mock_store()
dda2b32… lmata 425 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 426 result = runner.invoke(main, ["codeowners", str(tmp_path)])
dda2b32… lmata 427 assert result.exit_code == 0
dda2b32… lmata 428 assert "owner" in result.output
dda2b32… lmata 429
dda2b32… lmata 430
dda2b32… lmata 431 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 432 # #40 — ADRIngester
dda2b32… lmata 433 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 434
dda2b32… lmata 435
dda2b32… lmata 436 _SAMPLE_ADR = """\
dda2b32… lmata 437 # Use FalkorDB as the graph database
dda2b32… lmata 438
dda2b32… lmata 439 ## Status
dda2b32… lmata 440
dda2b32… lmata 441 Accepted
dda2b32… lmata 442
dda2b32… lmata 443 ## Context
dda2b32… lmata 444
dda2b32… lmata 445 We need a property graph DB.
dda2b32… lmata 446
dda2b32… lmata 447 ## Decision
dda2b32… lmata 448
dda2b32… lmata 449 We will use FalkorDB.
dda2b32… lmata 450
dda2b32… lmata 451 ## Rationale
dda2b32… lmata 452
dda2b32… lmata 453 Best performance for our use case. Supports Cypher.
dda2b32… lmata 454
dda2b32… lmata 455 ## Date
dda2b32… lmata 456
dda2b32… lmata 457 2024-01-15
dda2b32… lmata 458 """
dda2b32… lmata 459
dda2b32… lmata 460
dda2b32… lmata 461 class TestADRIngesterParse:
dda2b32… lmata 462 def test_parses_title(self, tmp_path):
dda2b32… lmata 463 from navegador.adr import ADRIngester
dda2b32… lmata 464
dda2b32… lmata 465 f = tmp_path / "0001-use-falkordb.md"
dda2b32… lmata 466 f.write_text(_SAMPLE_ADR)
dda2b32… lmata 467 ingester = ADRIngester(_mock_store())
dda2b32… lmata 468 parsed = ingester._parse_adr(f)
dda2b32… lmata 469 assert parsed is not None
dda2b32… lmata 470 assert "FalkorDB" in parsed["description"]
dda2b32… lmata 471
dda2b32… lmata 472 def test_parses_status(self, tmp_path):
dda2b32… lmata 473 from navegador.adr import ADRIngester
dda2b32… lmata 474
dda2b32… lmata 475 f = tmp_path / "0001-test.md"
dda2b32… lmata 476 f.write_text(_SAMPLE_ADR)
dda2b32… lmata 477 ingester = ADRIngester(_mock_store())
dda2b32… lmata 478 parsed = ingester._parse_adr(f)
dda2b32… lmata 479 assert parsed["status"] == "accepted"
dda2b32… lmata 480
dda2b32… lmata 481 def test_parses_rationale(self, tmp_path):
dda2b32… lmata 482 from navegador.adr import ADRIngester
dda2b32… lmata 483
dda2b32… lmata 484 f = tmp_path / "0001-test.md"
dda2b32… lmata 485 f.write_text(_SAMPLE_ADR)
dda2b32… lmata 486 ingester = ADRIngester(_mock_store())
dda2b32… lmata 487 parsed = ingester._parse_adr(f)
dda2b32… lmata 488 assert "performance" in parsed["rationale"].lower()
dda2b32… lmata 489
dda2b32… lmata 490 def test_parses_date(self, tmp_path):
dda2b32… lmata 491 from navegador.adr import ADRIngester
dda2b32… lmata 492
dda2b32… lmata 493 f = tmp_path / "0001-test.md"
dda2b32… lmata 494 f.write_text(_SAMPLE_ADR)
dda2b32… lmata 495 ingester = ADRIngester(_mock_store())
dda2b32… lmata 496 parsed = ingester._parse_adr(f)
dda2b32… lmata 497 assert parsed["date"] == "2024-01-15"
dda2b32… lmata 498
dda2b32… lmata 499 def test_uses_stem_as_name(self, tmp_path):
dda2b32… lmata 500 from navegador.adr import ADRIngester
dda2b32… lmata 501
dda2b32… lmata 502 f = tmp_path / "0042-my-decision.md"
dda2b32… lmata 503 f.write_text(_SAMPLE_ADR)
dda2b32… lmata 504 ingester = ADRIngester(_mock_store())
dda2b32… lmata 505 parsed = ingester._parse_adr(f)
dda2b32… lmata 506 assert parsed["name"] == "0042-my-decision"
dda2b32… lmata 507
dda2b32… lmata 508 def test_returns_none_for_non_adr(self, tmp_path):
dda2b32… lmata 509 from navegador.adr import ADRIngester
dda2b32… lmata 510
dda2b32… lmata 511 f = tmp_path / "readme.md"
dda2b32… lmata 512 f.write_text("No heading here.")
dda2b32… lmata 513 ingester = ADRIngester(_mock_store())
dda2b32… lmata 514 assert ingester._parse_adr(f) is None
dda2b32… lmata 515
dda2b32… lmata 516
dda2b32… lmata 517 class TestADRIngesterIngest:
dda2b32… lmata 518 def test_creates_decision_nodes(self, tmp_path):
dda2b32… lmata 519 from navegador.adr import ADRIngester
dda2b32… lmata 520
dda2b32… lmata 521 (tmp_path / "0001-first.md").write_text(_SAMPLE_ADR)
dda2b32… lmata 522 (tmp_path / "0002-second.md").write_text(_SAMPLE_ADR)
dda2b32… lmata 523 store = _mock_store()
dda2b32… lmata 524 stats = ADRIngester(store).ingest(str(tmp_path))
dda2b32… lmata 525 assert stats["decisions"] == 2
dda2b32… lmata 526 assert stats["skipped"] == 0
dda2b32… lmata 527
dda2b32… lmata 528 def test_skips_files_without_h1(self, tmp_path):
dda2b32… lmata 529 from navegador.adr import ADRIngester
dda2b32… lmata 530
dda2b32… lmata 531 (tmp_path / "empty.md").write_text("no heading\n")
dda2b32… lmata 532 store = _mock_store()
dda2b32… lmata 533 stats = ADRIngester(store).ingest(str(tmp_path))
dda2b32… lmata 534 assert stats["skipped"] == 1
dda2b32… lmata 535
dda2b32… lmata 536 def test_returns_zeros_for_empty_dir(self, tmp_path):
dda2b32… lmata 537 from navegador.adr import ADRIngester
dda2b32… lmata 538
dda2b32… lmata 539 store = _mock_store()
dda2b32… lmata 540 stats = ADRIngester(store).ingest(str(tmp_path))
dda2b32… lmata 541 assert stats == {"decisions": 0, "skipped": 0}
dda2b32… lmata 542
dda2b32… lmata 543 def test_nonexistent_dir_returns_zeros(self, tmp_path):
dda2b32… lmata 544 from navegador.adr import ADRIngester
dda2b32… lmata 545
dda2b32… lmata 546 store = _mock_store()
dda2b32… lmata 547 stats = ADRIngester(store).ingest(str(tmp_path / "no_such_dir"))
dda2b32… lmata 548 assert stats == {"decisions": 0, "skipped": 0}
dda2b32… lmata 549
dda2b32… lmata 550
dda2b32… lmata 551 # ── CLI: adr ─────────────────────────────────────────────────────────────────
dda2b32… lmata 552
dda2b32… lmata 553
dda2b32… lmata 554 class TestADRCLI:
dda2b32… lmata 555 def test_adr_ingest(self, tmp_path):
dda2b32… lmata 556 runner = CliRunner()
dda2b32… lmata 557 (tmp_path / "0001-test.md").write_text(_SAMPLE_ADR)
dda2b32… lmata 558 store = _mock_store()
dda2b32… lmata 559 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 560 result = runner.invoke(main, ["adr", "ingest", str(tmp_path)])
dda2b32… lmata 561 assert result.exit_code == 0
dda2b32… lmata 562 assert "decision" in result.output.lower()
dda2b32… lmata 563
dda2b32… lmata 564
dda2b32… lmata 565 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 566 # #41 — APISchemaIngester
dda2b32… lmata 567 # ═════════════════════════════════════════════════════════════════════════════
dda2b32… lmata 568
dda2b32… lmata 569
dda2b32… lmata 570 _OPENAPI_YAML = """\
dda2b32… lmata 571 openapi: "3.0.0"
dda2b32… lmata 572 info:
dda2b32… lmata 573 title: Test API
dda2b32… lmata 574 version: "1.0"
dda2b32… lmata 575 paths:
dda2b32… lmata 576 /users:
dda2b32… lmata 577 get:
dda2b32… lmata 578 operationId: listUsers
dda2b32… lmata 579 summary: List all users
dda2b32… lmata 580 tags:
dda2b32… lmata 581 - users
dda2b32… lmata 582 post:
dda2b32… lmata 583 operationId: createUser
dda2b32… lmata 584 summary: Create a user
dda2b32… lmata 585 components:
dda2b32… lmata 586 schemas:
dda2b32… lmata 587 User:
dda2b32… lmata 588 description: A user object
dda2b32… lmata 589 type: object
dda2b32… lmata 590 """
dda2b32… lmata 591
dda2b32… lmata 592 _OPENAPI_JSON = {
dda2b32… lmata 593 "openapi": "3.0.0",
dda2b32… lmata 594 "info": {"title": "Test API", "version": "1.0"},
dda2b32… lmata 595 "paths": {
dda2b32… lmata 596 "/items": {
dda2b32… lmata 597 "get": {"operationId": "listItems", "summary": "List items"},
dda2b32… lmata 598 "post": {"summary": "Create item"},
dda2b32… lmata 599 }
dda2b32… lmata 600 },
dda2b32… lmata 601 "components": {
dda2b32… lmata 602 "schemas": {
dda2b32… lmata 603 "Item": {"description": "An item", "type": "object"}
dda2b32… lmata 604 }
dda2b32… lmata 605 },
dda2b32… lmata 606 }
dda2b32… lmata 607
dda2b32… lmata 608 _GRAPHQL_SCHEMA = """\
dda2b32… lmata 609 type Query {
dda2b32… lmata 610 users: [User]
dda2b32… lmata 611 user(id: ID!): User
dda2b32… lmata 612 }
dda2b32… lmata 613
dda2b32… lmata 614 type Mutation {
dda2b32… lmata 615 createUser(name: String!): User
dda2b32… lmata 616 }
dda2b32… lmata 617
dda2b32… lmata 618 type User {
dda2b32… lmata 619 id: ID!
dda2b32… lmata 620 name: String!
dda2b32… lmata 621 email: String
dda2b32… lmata 622 }
dda2b32… lmata 623
dda2b32… lmata 624 input CreateUserInput {
dda2b32… lmata 625 name: String!
dda2b32… lmata 626 email: String
dda2b32… lmata 627 }
dda2b32… lmata 628 """
dda2b32… lmata 629
dda2b32… lmata 630
dda2b32… lmata 631 class TestAPISchemaIngesterOpenAPI:
dda2b32… lmata 632 def test_ingest_openapi_json(self, tmp_path):
dda2b32… lmata 633 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 634
dda2b32… lmata 635 p = tmp_path / "api.json"
dda2b32… lmata 636 p.write_text(json.dumps(_OPENAPI_JSON))
dda2b32… lmata 637 store = _mock_store()
dda2b32… lmata 638 stats = APISchemaIngester(store).ingest_openapi(str(p))
dda2b32… lmata 639 assert stats["endpoints"] >= 2
dda2b32… lmata 640 assert stats["schemas"] >= 1
dda2b32… lmata 641
dda2b32… lmata 642 def test_ingest_creates_function_nodes(self, tmp_path):
dda2b32… lmata 643 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 644
dda2b32… lmata 645 p = tmp_path / "api.json"
dda2b32… lmata 646 p.write_text(json.dumps(_OPENAPI_JSON))
dda2b32… lmata 647 store = _mock_store()
dda2b32… lmata 648 APISchemaIngester(store).ingest_openapi(str(p))
dda2b32… lmata 649 labels = [c[0][0] for c in store.create_node.call_args_list]
dda2b32… lmata 650 assert "Function" in labels
dda2b32… lmata 651
dda2b32… lmata 652 def test_ingest_creates_class_nodes_for_schemas(self, tmp_path):
dda2b32… lmata 653 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 654
dda2b32… lmata 655 p = tmp_path / "api.json"
dda2b32… lmata 656 p.write_text(json.dumps(_OPENAPI_JSON))
dda2b32… lmata 657 store = _mock_store()
dda2b32… lmata 658 APISchemaIngester(store).ingest_openapi(str(p))
dda2b32… lmata 659 labels = [c[0][0] for c in store.create_node.call_args_list]
dda2b32… lmata 660 assert "Class" in labels
dda2b32… lmata 661
dda2b32… lmata 662 def test_missing_file_returns_zeros(self, tmp_path):
dda2b32… lmata 663 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 664
dda2b32… lmata 665 store = _mock_store()
dda2b32… lmata 666 stats = APISchemaIngester(store).ingest_openapi(str(tmp_path / "no.yaml"))
dda2b32… lmata 667 assert stats == {"endpoints": 0, "schemas": 0}
dda2b32… lmata 668
dda2b32… lmata 669 def test_empty_paths_returns_zeros(self, tmp_path):
dda2b32… lmata 670 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 671
dda2b32… lmata 672 p = tmp_path / "empty.json"
dda2b32… lmata 673 p.write_text(json.dumps({"openapi": "3.0.0", "info": {}}))
dda2b32… lmata 674 store = _mock_store()
dda2b32… lmata 675 stats = APISchemaIngester(store).ingest_openapi(str(p))
dda2b32… lmata 676 assert stats == {"endpoints": 0, "schemas": 0}
dda2b32… lmata 677
dda2b32… lmata 678
dda2b32… lmata 679 class TestAPISchemaIngesterGraphQL:
dda2b32… lmata 680 def test_ingest_graphql_types(self, tmp_path):
dda2b32… lmata 681 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 682
dda2b32… lmata 683 p = tmp_path / "schema.graphql"
dda2b32… lmata 684 p.write_text(_GRAPHQL_SCHEMA)
dda2b32… lmata 685 store = _mock_store()
dda2b32… lmata 686 stats = APISchemaIngester(store).ingest_graphql(str(p))
dda2b32… lmata 687 # User + CreateUserInput → type nodes
dda2b32… lmata 688 assert stats["types"] >= 1
dda2b32… lmata 689
dda2b32… lmata 690 def test_ingest_graphql_query_fields(self, tmp_path):
dda2b32… lmata 691 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 692
dda2b32… lmata 693 p = tmp_path / "schema.graphql"
dda2b32… lmata 694 p.write_text(_GRAPHQL_SCHEMA)
dda2b32… lmata 695 store = _mock_store()
dda2b32… lmata 696 stats = APISchemaIngester(store).ingest_graphql(str(p))
dda2b32… lmata 697 # Query.users, Query.user, Mutation.createUser
dda2b32… lmata 698 assert stats["fields"] >= 2
dda2b32… lmata 699
dda2b32… lmata 700 def test_missing_file_returns_zeros(self, tmp_path):
dda2b32… lmata 701 from navegador.api_schema import APISchemaIngester
dda2b32… lmata 702
dda2b32… lmata 703 store = _mock_store()
dda2b32… lmata 704 stats = APISchemaIngester(store).ingest_graphql(str(tmp_path / "no.graphql"))
dda2b32… lmata 705 assert stats == {"types": 0, "fields": 0}
dda2b32… lmata 706
dda2b32… lmata 707
dda2b32… lmata 708 # ── CLI: api ──────────────────────────────────────────────────────────────────
dda2b32… lmata 709
dda2b32… lmata 710
dda2b32… lmata 711 class TestAPICLI:
dda2b32… lmata 712 def test_api_ingest_openapi_json(self, tmp_path):
dda2b32… lmata 713 runner = CliRunner()
dda2b32… lmata 714 p = tmp_path / "api.json"
dda2b32… lmata 715 p.write_text(json.dumps(_OPENAPI_JSON))
dda2b32… lmata 716 store = _mock_store()
dda2b32… lmata 717 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 718 result = runner.invoke(
dda2b32… lmata 719 main, ["api", "ingest", str(p), "--type", "openapi"]
dda2b32… lmata 720 )
dda2b32… lmata 721 assert result.exit_code == 0
dda2b32… lmata 722
dda2b32… lmata 723 def test_api_ingest_graphql(self, tmp_path):
dda2b32… lmata 724 runner = CliRunner()
dda2b32… lmata 725 p = tmp_path / "schema.graphql"
dda2b32… lmata 726 p.write_text(_GRAPHQL_SCHEMA)
dda2b32… lmata 727 store = _mock_store()
dda2b32… lmata 728 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 729 result = runner.invoke(
dda2b32… lmata 730 main, ["api", "ingest", str(p), "--type", "graphql"]
dda2b32… lmata 731 )
dda2b32… lmata 732 assert result.exit_code == 0
dda2b32… lmata 733
dda2b32… lmata 734 def test_api_ingest_auto_detects_graphql(self, tmp_path):
dda2b32… lmata 735 runner = CliRunner()
dda2b32… lmata 736 p = tmp_path / "schema.graphql"
dda2b32… lmata 737 p.write_text(_GRAPHQL_SCHEMA)
dda2b32… lmata 738 store = _mock_store()
dda2b32… lmata 739 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 740 result = runner.invoke(main, ["api", "ingest", str(p)])
dda2b32… lmata 741 assert result.exit_code == 0
dda2b32… lmata 742
dda2b32… lmata 743 def test_api_ingest_json_output(self, tmp_path):
dda2b32… lmata 744 runner = CliRunner()
dda2b32… lmata 745 p = tmp_path / "api.json"
dda2b32… lmata 746 p.write_text(json.dumps(_OPENAPI_JSON))
dda2b32… lmata 747 store = _mock_store()
dda2b32… lmata 748 with patch("navegador.cli.commands._get_store", return_value=store):
dda2b32… lmata 749 result = runner.invoke(
dda2b32… lmata 750 main, ["api", "ingest", str(p), "--type", "openapi", "--json"]
dda2b32… lmata 751 )
dda2b32… lmata 752 assert result.exit_code == 0
dda2b32… lmata 753 data = json.loads(result.output)
dda2b32… lmata 754 assert "endpoints" in data

Keyboard Shortcuts

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