Navegador

navegador / tests / test_v04_batch3.py
Source Blame History 1234 lines
1d4baaf… lmata 1 """
1d4baaf… lmata 2 Tests for navegador v0.4 batch 3 — issues #7, #18, #53, #55, #58, #61, #62.
1d4baaf… lmata 3
1d4baaf… lmata 4 Covers:
1d4baaf… lmata 5 #7 / #18 — PlanopticonPipeline (pipeline, action items, decision timeline, auto-link)
1d4baaf… lmata 6 #53 — TicketIngester (GitHub, Linear stub, Jira stub)
1d4baaf… lmata 7 #55 — FossilAdapter (current_branch, changed_files, file_history, blame)
1d4baaf… lmata 8 #58 — DependencyIngester (npm, pip/requirements.txt, pip/pyproject.toml, cargo)
1d4baaf… lmata 9 #61 — SubmoduleIngester (detect_submodules, ingest_with_submodules)
1d4baaf… lmata 10 #62 — WorkspaceMode enum, WorkspaceManager (unified + federated)
1d4baaf… lmata 11 """
1d4baaf… lmata 12
1d4baaf… lmata 13 from __future__ import annotations
1d4baaf… lmata 14
1d4baaf… lmata 15 import json
1d4baaf… lmata 16 import subprocess
1d4baaf… lmata 17 import tempfile
1d4baaf… lmata 18 from pathlib import Path
1d4baaf… lmata 19 from unittest.mock import MagicMock, patch
1d4baaf… lmata 20
1d4baaf… lmata 21 import pytest
1d4baaf… lmata 22
1d4baaf… lmata 23
1d4baaf… lmata 24 # ── Shared mock store factory ─────────────────────────────────────────────────
1d4baaf… lmata 25
1d4baaf… lmata 26
1d4baaf… lmata 27 def _make_store():
1d4baaf… lmata 28 store = MagicMock()
1d4baaf… lmata 29 store.query.return_value = MagicMock(result_set=[])
1d4baaf… lmata 30 return store
1d4baaf… lmata 31
1d4baaf… lmata 32
1d4baaf… lmata 33 # =============================================================================
1d4baaf… lmata 34 # #7 / #18 — PlanopticonPipeline
1d4baaf… lmata 35 # =============================================================================
1d4baaf… lmata 36
1d4baaf… lmata 37
1d4baaf… lmata 38 class TestPlanopticonPipelineDetectInput:
1d4baaf… lmata 39 """_detect_input correctly identifies file types from path."""
1d4baaf… lmata 40
1d4baaf… lmata 41 from navegador.planopticon_pipeline import PlanopticonPipeline as _Pipeline
1d4baaf… lmata 42
1d4baaf… lmata 43 def test_manifest_file(self, tmp_path):
1d4baaf… lmata 44 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 45
1d4baaf… lmata 46 f = tmp_path / "manifest.json"
1d4baaf… lmata 47 f.write_text("{}")
1d4baaf… lmata 48 itype, resolved = PlanopticonPipeline._detect_input(f)
1d4baaf… lmata 49 assert itype == "manifest"
1d4baaf… lmata 50 assert resolved == f
1d4baaf… lmata 51
1d4baaf… lmata 52 def test_interchange_file(self, tmp_path):
1d4baaf… lmata 53 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 54
1d4baaf… lmata 55 f = tmp_path / "interchange.json"
1d4baaf… lmata 56 f.write_text("{}")
1d4baaf… lmata 57 itype, _ = PlanopticonPipeline._detect_input(f)
1d4baaf… lmata 58 assert itype == "interchange"
1d4baaf… lmata 59
1d4baaf… lmata 60 def test_batch_file(self, tmp_path):
1d4baaf… lmata 61 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 62
1d4baaf… lmata 63 f = tmp_path / "batch_manifest.json"
1d4baaf… lmata 64 f.write_text("{}")
1d4baaf… lmata 65 itype, _ = PlanopticonPipeline._detect_input(f)
1d4baaf… lmata 66 assert itype == "batch"
1d4baaf… lmata 67
1d4baaf… lmata 68 def test_kg_file_default(self, tmp_path):
1d4baaf… lmata 69 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 70
1d4baaf… lmata 71 f = tmp_path / "knowledge_graph.json"
1d4baaf… lmata 72 f.write_text("{}")
1d4baaf… lmata 73 itype, _ = PlanopticonPipeline._detect_input(f)
1d4baaf… lmata 74 assert itype == "kg"
1d4baaf… lmata 75
1d4baaf… lmata 76 def test_directory_with_manifest(self, tmp_path):
1d4baaf… lmata 77 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 78
1d4baaf… lmata 79 (tmp_path / "manifest.json").write_text("{}")
1d4baaf… lmata 80 itype, resolved = PlanopticonPipeline._detect_input(tmp_path)
1d4baaf… lmata 81 assert itype == "manifest"
1d4baaf… lmata 82
1d4baaf… lmata 83 def test_directory_without_known_files_raises(self, tmp_path):
1d4baaf… lmata 84 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 85
1d4baaf… lmata 86 with pytest.raises(FileNotFoundError):
1d4baaf… lmata 87 PlanopticonPipeline._detect_input(tmp_path)
1d4baaf… lmata 88
1d4baaf… lmata 89
1d4baaf… lmata 90 class TestPlanopticonPipelineRun:
1d4baaf… lmata 91 """PlanopticonPipeline.run delegates to PlanopticonIngester and auto-links."""
1d4baaf… lmata 92
1d4baaf… lmata 93 def test_run_returns_stats_with_linked_key(self, tmp_path):
1d4baaf… lmata 94 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 95
1d4baaf… lmata 96 kg_data = {"nodes": [], "relationships": [], "sources": []}
1d4baaf… lmata 97 kg_file = tmp_path / "knowledge_graph.json"
1d4baaf… lmata 98 kg_file.write_text(json.dumps(kg_data))
1d4baaf… lmata 99
1d4baaf… lmata 100 store = _make_store()
1d4baaf… lmata 101 pipeline = PlanopticonPipeline(store, source_tag="test")
1d4baaf… lmata 102 stats = pipeline.run(str(kg_file))
1d4baaf… lmata 103
1d4baaf… lmata 104 assert "nodes" in stats
1d4baaf… lmata 105 assert "linked" in stats
1d4baaf… lmata 106
1d4baaf… lmata 107 def test_run_calls_ingester(self, tmp_path):
1d4baaf… lmata 108 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 109
1d4baaf… lmata 110 kg_data = {
1d4baaf… lmata 111 "nodes": [{"id": "n1", "type": "concept", "name": "Auth"}],
1d4baaf… lmata 112 "relationships": [],
1d4baaf… lmata 113 "sources": [],
1d4baaf… lmata 114 }
1d4baaf… lmata 115 kg_file = tmp_path / "knowledge_graph.json"
1d4baaf… lmata 116 kg_file.write_text(json.dumps(kg_data))
1d4baaf… lmata 117
1d4baaf… lmata 118 store = _make_store()
1d4baaf… lmata 119 pipeline = PlanopticonPipeline(store)
1d4baaf… lmata 120 stats = pipeline.run(str(kg_file), source_tag="Meeting")
1d4baaf… lmata 121
1d4baaf… lmata 122 assert isinstance(stats, dict)
1d4baaf… lmata 123 # create_node should have been called at least once for the concept node
1d4baaf… lmata 124 store.create_node.assert_called()
1d4baaf… lmata 125
1d4baaf… lmata 126
1d4baaf… lmata 127 class TestExtractActionItems:
1d4baaf… lmata 128 """extract_action_items pulls action items from various KG data formats."""
1d4baaf… lmata 129
1d4baaf… lmata 130 def test_action_items_list(self):
1d4baaf… lmata 131 from navegador.planopticon_pipeline import ActionItem, PlanopticonPipeline
1d4baaf… lmata 132
1d4baaf… lmata 133 kg_data = {
1d4baaf… lmata 134 "action_items": [
1d4baaf… lmata 135 {"action": "Write tests", "assignee": "Alice", "priority": "high"},
1d4baaf… lmata 136 {"action": "Deploy service", "assignee": "", "priority": "info"},
1d4baaf… lmata 137 ]
1d4baaf… lmata 138 }
1d4baaf… lmata 139 items = PlanopticonPipeline.extract_action_items(kg_data)
1d4baaf… lmata 140 assert len(items) == 2
1d4baaf… lmata 141 assert all(isinstance(i, ActionItem) for i in items)
1d4baaf… lmata 142 assert items[0].action == "Write tests"
1d4baaf… lmata 143 assert items[0].assignee == "Alice"
1d4baaf… lmata 144 assert items[1].action == "Deploy service"
1d4baaf… lmata 145
1d4baaf… lmata 146 def test_blank_actions_skipped(self):
1d4baaf… lmata 147 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 148
1d4baaf… lmata 149 kg_data = {"action_items": [{"action": " ", "assignee": "Bob"}]}
1d4baaf… lmata 150 items = PlanopticonPipeline.extract_action_items(kg_data)
1d4baaf… lmata 151 assert items == []
1d4baaf… lmata 152
1d4baaf… lmata 153 def test_entities_with_task_type(self):
1d4baaf… lmata 154 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 155
1d4baaf… lmata 156 kg_data = {
1d4baaf… lmata 157 "entities": [
1d4baaf… lmata 158 {"planning_type": "task", "name": "Refactor auth module"},
1d4baaf… lmata 159 {"planning_type": "decision", "name": "Use PostgreSQL"},
1d4baaf… lmata 160 ]
1d4baaf… lmata 161 }
1d4baaf… lmata 162 items = PlanopticonPipeline.extract_action_items(kg_data)
1d4baaf… lmata 163 assert len(items) == 1
1d4baaf… lmata 164 assert items[0].action == "Refactor auth module"
1d4baaf… lmata 165
1d4baaf… lmata 166 def test_nodes_with_action_item_type(self):
1d4baaf… lmata 167 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 168
1d4baaf… lmata 169 kg_data = {
1d4baaf… lmata 170 "nodes": [
1d4baaf… lmata 171 {"type": "action_item", "name": "Update documentation"},
1d4baaf… lmata 172 ]
1d4baaf… lmata 173 }
1d4baaf… lmata 174 items = PlanopticonPipeline.extract_action_items(kg_data)
1d4baaf… lmata 175 assert len(items) == 1
1d4baaf… lmata 176 assert items[0].action == "Update documentation"
1d4baaf… lmata 177
1d4baaf… lmata 178 def test_empty_data_returns_empty_list(self):
1d4baaf… lmata 179 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 180
1d4baaf… lmata 181 assert PlanopticonPipeline.extract_action_items({}) == []
1d4baaf… lmata 182
1d4baaf… lmata 183 def test_action_item_to_dict(self):
1d4baaf… lmata 184 from navegador.planopticon_pipeline import ActionItem
1d4baaf… lmata 185
1d4baaf… lmata 186 item = ActionItem(action="Do thing", assignee="Carol", priority="critical")
1d4baaf… lmata 187 d = item.to_dict()
1d4baaf… lmata 188 assert d["action"] == "Do thing"
1d4baaf… lmata 189 assert d["assignee"] == "Carol"
1d4baaf… lmata 190 assert d["priority"] == "critical"
1d4baaf… lmata 191
1d4baaf… lmata 192
1d4baaf… lmata 193 class TestBuildDecisionTimeline:
1d4baaf… lmata 194 """build_decision_timeline queries the store and returns chronological list."""
1d4baaf… lmata 195
1d4baaf… lmata 196 def test_returns_list_from_store(self):
1d4baaf… lmata 197 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 198
1d4baaf… lmata 199 store = _make_store()
1d4baaf… lmata 200 store.query.return_value = MagicMock(
1d4baaf… lmata 201 result_set=[
1d4baaf… lmata 202 ["Use microservices", "Split monolith", "arch", "accepted", "Scalability", "2024-01-10"],
1d4baaf… lmata 203 ["Use PostgreSQL", "Relational DB", "data", "accepted", "ACID", "2024-02-01"],
1d4baaf… lmata 204 ]
1d4baaf… lmata 205 )
1d4baaf… lmata 206 timeline = PlanopticonPipeline.build_decision_timeline(store)
1d4baaf… lmata 207 assert len(timeline) == 2
1d4baaf… lmata 208 assert timeline[0]["name"] == "Use microservices"
1d4baaf… lmata 209 assert timeline[0]["date"] == "2024-01-10"
1d4baaf… lmata 210
1d4baaf… lmata 211 def test_returns_empty_on_query_failure(self):
1d4baaf… lmata 212 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 213
1d4baaf… lmata 214 store = _make_store()
1d4baaf… lmata 215 store.query.side_effect = Exception("DB error")
1d4baaf… lmata 216 timeline = PlanopticonPipeline.build_decision_timeline(store)
1d4baaf… lmata 217 assert timeline == []
1d4baaf… lmata 218
1d4baaf… lmata 219 def test_entry_has_required_keys(self):
1d4baaf… lmata 220 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 221
1d4baaf… lmata 222 store = _make_store()
1d4baaf… lmata 223 store.query.return_value = MagicMock(
1d4baaf… lmata 224 result_set=[["D1", "Desc", "domain", "accepted", "rationale", "2024-01-01"]]
1d4baaf… lmata 225 )
1d4baaf… lmata 226 timeline = PlanopticonPipeline.build_decision_timeline(store)
1d4baaf… lmata 227 required_keys = {"name", "description", "domain", "status", "rationale", "date"}
1d4baaf… lmata 228 assert required_keys.issubset(timeline[0].keys())
1d4baaf… lmata 229
1d4baaf… lmata 230
1d4baaf… lmata 231 class TestAutoLinkToCode:
1d4baaf… lmata 232 """auto_link_to_code matches knowledge nodes to code by name similarity."""
1d4baaf… lmata 233
1d4baaf… lmata 234 def test_returns_zero_when_no_nodes(self):
1d4baaf… lmata 235 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 236
1d4baaf… lmata 237 store = _make_store()
1d4baaf… lmata 238 store.query.return_value = MagicMock(result_set=[])
1d4baaf… lmata 239 assert PlanopticonPipeline.auto_link_to_code(store) == 0
1d4baaf… lmata 240
1d4baaf… lmata 241 def test_links_matching_nodes(self):
1d4baaf… lmata 242 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 243
1d4baaf… lmata 244 store = _make_store()
1d4baaf… lmata 245
1d4baaf… lmata 246 # First call: knowledge nodes; second: code nodes; subsequent: merge queries
1d4baaf… lmata 247 call_count = 0
1d4baaf… lmata 248 def _query(cypher, params=None):
1d4baaf… lmata 249 nonlocal call_count
1d4baaf… lmata 250 call_count += 1
1d4baaf… lmata 251 if call_count == 1:
1d4baaf… lmata 252 # knowledge nodes — use "authenticate" (12 chars) which IS in "authenticate_user"
1d4baaf… lmata 253 return MagicMock(result_set=[["Concept", "authenticate handler"]])
1d4baaf… lmata 254 elif call_count == 2:
1d4baaf… lmata 255 # code nodes
1d4baaf… lmata 256 return MagicMock(result_set=[["Function", "authenticate_user"]])
1d4baaf… lmata 257 else:
1d4baaf… lmata 258 # MERGE query — no result needed
1d4baaf… lmata 259 return MagicMock(result_set=[])
1d4baaf… lmata 260
1d4baaf… lmata 261 store.query.side_effect = _query
1d4baaf… lmata 262 linked = PlanopticonPipeline.auto_link_to_code(store)
1d4baaf… lmata 263 # "authenticate" (12 chars, ≥4) is contained in "authenticate_user"
1d4baaf… lmata 264 assert linked >= 1
1d4baaf… lmata 265
1d4baaf… lmata 266 def test_short_tokens_skipped(self):
1d4baaf… lmata 267 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 268
1d4baaf… lmata 269 store = _make_store()
1d4baaf… lmata 270
1d4baaf… lmata 271 call_count = 0
1d4baaf… lmata 272 def _query(cypher, params=None):
1d4baaf… lmata 273 nonlocal call_count
1d4baaf… lmata 274 call_count += 1
1d4baaf… lmata 275 if call_count == 1:
1d4baaf… lmata 276 return MagicMock(result_set=[["Concept", "API"]]) # all tokens < 4 chars
1d4baaf… lmata 277 elif call_count == 2:
1d4baaf… lmata 278 return MagicMock(result_set=[["Function", "api_handler"]])
1d4baaf… lmata 279 return MagicMock(result_set=[])
1d4baaf… lmata 280
1d4baaf… lmata 281 store.query.side_effect = _query
1d4baaf… lmata 282 linked = PlanopticonPipeline.auto_link_to_code(store)
1d4baaf… lmata 283 # "api" is only 3 chars — should not match
1d4baaf… lmata 284 assert linked == 0
1d4baaf… lmata 285
1d4baaf… lmata 286 def test_returns_zero_on_query_failure(self):
1d4baaf… lmata 287 from navegador.planopticon_pipeline import PlanopticonPipeline
1d4baaf… lmata 288
1d4baaf… lmata 289 store = _make_store()
1d4baaf… lmata 290 store.query.side_effect = Exception("boom")
1d4baaf… lmata 291 result = PlanopticonPipeline.auto_link_to_code(store)
1d4baaf… lmata 292 assert result == 0
1d4baaf… lmata 293
1d4baaf… lmata 294
1d4baaf… lmata 295 # =============================================================================
1d4baaf… lmata 296 # #53 — TicketIngester
1d4baaf… lmata 297 # =============================================================================
1d4baaf… lmata 298
1d4baaf… lmata 299
1d4baaf… lmata 300 class TestTicketIngesterGitHub:
1d4baaf… lmata 301 """TicketIngester.ingest_github_issues fetches and ingests GitHub issues."""
1d4baaf… lmata 302
1d4baaf… lmata 303 def _make_issue(self, number=1, title="Fix bug", body="Details", labels=None, assignees=None):
1d4baaf… lmata 304 return {
1d4baaf… lmata 305 "number": number,
1d4baaf… lmata 306 "title": title,
1d4baaf… lmata 307 "body": body,
1d4baaf… lmata 308 "html_url": f"https://github.com/owner/repo/issues/{number}",
1d4baaf… lmata 309 "labels": [{"name": l} for l in (labels or [])],
1d4baaf… lmata 310 "assignees": [{"login": a} for a in (assignees or [])],
1d4baaf… lmata 311 }
1d4baaf… lmata 312
1d4baaf… lmata 313 def test_ingest_creates_ticket_nodes(self):
1d4baaf… lmata 314 from navegador.pm import TicketIngester
1d4baaf… lmata 315
1d4baaf… lmata 316 store = _make_store()
1d4baaf… lmata 317 store.query.return_value = MagicMock(result_set=[])
1d4baaf… lmata 318 ing = TicketIngester(store)
1d4baaf… lmata 319
1d4baaf… lmata 320 issues = [self._make_issue(1, "Bug report"), self._make_issue(2, "Feature request")]
1d4baaf… lmata 321 with patch("urllib.request.urlopen") as mock_open:
1d4baaf… lmata 322 cm = MagicMock()
1d4baaf… lmata 323 cm.__enter__ = MagicMock(return_value=cm)
1d4baaf… lmata 324 cm.__exit__ = MagicMock(return_value=False)
1d4baaf… lmata 325 cm.read.return_value = json.dumps(issues).encode()
1d4baaf… lmata 326 mock_open.return_value = cm
1d4baaf… lmata 327
1d4baaf… lmata 328 stats = ing.ingest_github_issues("owner/repo", token="test_token")
1d4baaf… lmata 329
1d4baaf… lmata 330 assert stats["tickets"] == 2
1d4baaf… lmata 331 assert "linked" in stats
1d4baaf… lmata 332
1d4baaf… lmata 333 def test_pull_requests_filtered_out(self):
1d4baaf… lmata 334 from navegador.pm import TicketIngester
1d4baaf… lmata 335
1d4baaf… lmata 336 store = _make_store()
1d4baaf… lmata 337 store.query.return_value = MagicMock(result_set=[])
1d4baaf… lmata 338 ing = TicketIngester(store)
1d4baaf… lmata 339
1d4baaf… lmata 340 # Mix of issue and PR
1d4baaf… lmata 341 issue = self._make_issue(1, "Real issue")
1d4baaf… lmata 342 pr = {**self._make_issue(2, "A PR"), "pull_request": {"url": "..."}}
1d4baaf… lmata 343
1d4baaf… lmata 344 with patch("urllib.request.urlopen") as mock_open:
1d4baaf… lmata 345 cm = MagicMock()
1d4baaf… lmata 346 cm.__enter__ = MagicMock(return_value=cm)
1d4baaf… lmata 347 cm.__exit__ = MagicMock(return_value=False)
1d4baaf… lmata 348 cm.read.return_value = json.dumps([issue, pr]).encode()
1d4baaf… lmata 349 mock_open.return_value = cm
1d4baaf… lmata 350
1d4baaf… lmata 351 stats = ing.ingest_github_issues("owner/repo")
1d4baaf… lmata 352
1d4baaf… lmata 353 assert stats["tickets"] == 1 # PR filtered out
1d4baaf… lmata 354
1d4baaf… lmata 355 def test_assignees_become_person_nodes(self):
1d4baaf… lmata 356 from navegador.pm import TicketIngester
1d4baaf… lmata 357
1d4baaf… lmata 358 store = _make_store()
1d4baaf… lmata 359 store.query.return_value = MagicMock(result_set=[])
1d4baaf… lmata 360 ing = TicketIngester(store)
1d4baaf… lmata 361
1d4baaf… lmata 362 issue = self._make_issue(1, "Assign me", assignees=["alice"])
1d4baaf… lmata 363
1d4baaf… lmata 364 with patch("urllib.request.urlopen") as mock_open:
1d4baaf… lmata 365 cm = MagicMock()
1d4baaf… lmata 366 cm.__enter__ = MagicMock(return_value=cm)
1d4baaf… lmata 367 cm.__exit__ = MagicMock(return_value=False)
1d4baaf… lmata 368 cm.read.return_value = json.dumps([issue]).encode()
1d4baaf… lmata 369 mock_open.return_value = cm
1d4baaf… lmata 370
1d4baaf… lmata 371 ing.ingest_github_issues("owner/repo")
1d4baaf… lmata 372
1d4baaf… lmata 373 # Person node created for alice
1d4baaf… lmata 374 person_calls = [
1d4baaf… lmata 375 c for c in store.create_node.call_args_list
1d4baaf… lmata 376 if c.args and hasattr(c.args[0], "value") and c.args[0].value == "Person"
1d4baaf… lmata 377 ]
1d4baaf… lmata 378 assert len(person_calls) >= 1
1d4baaf… lmata 379
1d4baaf… lmata 380 def test_network_error_raises_runtime_error(self):
1d4baaf… lmata 381 from navegador.pm import TicketIngester
1d4baaf… lmata 382
1d4baaf… lmata 383 store = _make_store()
1d4baaf… lmata 384 ing = TicketIngester(store)
1d4baaf… lmata 385
1d4baaf… lmata 386 with patch("urllib.request.urlopen", side_effect=Exception("network error")):
1d4baaf… lmata 387 with pytest.raises(RuntimeError, match="Failed to fetch GitHub issues"):
1d4baaf… lmata 388 ing.ingest_github_issues("owner/repo")
1d4baaf… lmata 389
1d4baaf… lmata 390
1d4baaf… lmata 391 class TestTicketIngesterSeverity:
1d4baaf… lmata 392 """_github_severity maps label names to severity levels."""
1d4baaf… lmata 393
1d4baaf… lmata 394 def test_critical_label(self):
1d4baaf… lmata 395 from navegador.pm import TicketIngester
1d4baaf… lmata 396
1d4baaf… lmata 397 assert TicketIngester._github_severity(["critical"]) == "critical"
1d4baaf… lmata 398 assert TicketIngester._github_severity(["blocker"]) == "critical"
1d4baaf… lmata 399
1d4baaf… lmata 400 def test_warning_label(self):
1d4baaf… lmata 401 from navegador.pm import TicketIngester
1d4baaf… lmata 402
1d4baaf… lmata 403 assert TicketIngester._github_severity(["bug"]) == "warning"
1d4baaf… lmata 404 assert TicketIngester._github_severity(["high"]) == "warning"
1d4baaf… lmata 405
1d4baaf… lmata 406 def test_default_info(self):
1d4baaf… lmata 407 from navegador.pm import TicketIngester
1d4baaf… lmata 408
1d4baaf… lmata 409 assert TicketIngester._github_severity([]) == "info"
1d4baaf… lmata 410 assert TicketIngester._github_severity(["enhancement"]) == "info"
1d4baaf… lmata 411
1d4baaf… lmata 412
1d4baaf… lmata 413 class TestTicketIngesterStubs:
1d4baaf… lmata 414 """Linear and Jira raise NotImplementedError with helpful messages."""
1d4baaf… lmata 415
1d4baaf… lmata 416 def test_linear_raises_not_implemented(self):
1d4baaf… lmata 417 from navegador.pm import TicketIngester
1d4baaf… lmata 418
1d4baaf… lmata 419 ing = TicketIngester(_make_store())
1d4baaf… lmata 420 with pytest.raises(NotImplementedError, match="Linear"):
1d4baaf… lmata 421 ing.ingest_linear("lin_apikey")
1d4baaf… lmata 422
1d4baaf… lmata 423 def test_jira_raises_not_implemented(self):
1d4baaf… lmata 424 from navegador.pm import TicketIngester
1d4baaf… lmata 425
1d4baaf… lmata 426 ing = TicketIngester(_make_store())
1d4baaf… lmata 427 with pytest.raises(NotImplementedError, match="Jira"):
1d4baaf… lmata 428 ing.ingest_jira("https://company.atlassian.net", token="tok")
1d4baaf… lmata 429
1d4baaf… lmata 430 def test_linear_message_contains_guidance(self):
1d4baaf… lmata 431 from navegador.pm import TicketIngester
1d4baaf… lmata 432
1d4baaf… lmata 433 ing = TicketIngester(_make_store())
1d4baaf… lmata 434 with pytest.raises(NotImplementedError) as exc_info:
1d4baaf… lmata 435 ing.ingest_linear("lin_key", project="MyProject")
1d4baaf… lmata 436 assert "53" in str(exc_info.value) or "Linear" in str(exc_info.value)
1d4baaf… lmata 437
1d4baaf… lmata 438 def test_jira_message_contains_guidance(self):
1d4baaf… lmata 439 from navegador.pm import TicketIngester
1d4baaf… lmata 440
1d4baaf… lmata 441 ing = TicketIngester(_make_store())
1d4baaf… lmata 442 with pytest.raises(NotImplementedError) as exc_info:
1d4baaf… lmata 443 ing.ingest_jira("https://x.atlassian.net")
1d4baaf… lmata 444 assert "Jira" in str(exc_info.value) or "jira" in str(exc_info.value).lower()
1d4baaf… lmata 445
1d4baaf… lmata 446
1d4baaf… lmata 447 # =============================================================================
1d4baaf… lmata 448 # #55 — FossilAdapter
1d4baaf… lmata 449 # =============================================================================
1d4baaf… lmata 450
1d4baaf… lmata 451
1d4baaf… lmata 452 @pytest.fixture()
1d4baaf… lmata 453 def fossil_dir(tmp_path):
1d4baaf… lmata 454 d = tmp_path / "fossil_repo"
1d4baaf… lmata 455 d.mkdir()
1d4baaf… lmata 456 (d / ".fslckout").touch()
1d4baaf… lmata 457 return d
1d4baaf… lmata 458
1d4baaf… lmata 459
1d4baaf… lmata 460 class TestFossilAdapterCurrentBranch:
1d4baaf… lmata 461 """current_branch calls 'fossil branch current' and returns stripped output."""
1d4baaf… lmata 462
1d4baaf… lmata 463 def test_returns_branch_name(self, fossil_dir):
1d4baaf… lmata 464 from navegador.vcs import FossilAdapter
1d4baaf… lmata 465
1d4baaf… lmata 466 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 467 mock_result = MagicMock()
1d4baaf… lmata 468 mock_result.stdout = "trunk\n"
1d4baaf… lmata 469
1d4baaf… lmata 470 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 471 branch = adapter.current_branch()
1d4baaf… lmata 472
1d4baaf… lmata 473 assert branch == "trunk"
1d4baaf… lmata 474
1d4baaf… lmata 475 def test_strips_whitespace(self, fossil_dir):
1d4baaf… lmata 476 from navegador.vcs import FossilAdapter
1d4baaf… lmata 477
1d4baaf… lmata 478 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 479 mock_result = MagicMock()
1d4baaf… lmata 480 mock_result.stdout = " feature-branch \n"
1d4baaf… lmata 481
1d4baaf… lmata 482 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 483 branch = adapter.current_branch()
1d4baaf… lmata 484
1d4baaf… lmata 485 assert branch == "feature-branch"
1d4baaf… lmata 486
1d4baaf… lmata 487 def test_calls_fossil_branch_current(self, fossil_dir):
1d4baaf… lmata 488 from navegador.vcs import FossilAdapter
1d4baaf… lmata 489
1d4baaf… lmata 490 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 491 mock_result = MagicMock()
1d4baaf… lmata 492 mock_result.stdout = "main\n"
1d4baaf… lmata 493
1d4baaf… lmata 494 with patch("subprocess.run", return_value=mock_result) as mock_run:
1d4baaf… lmata 495 adapter.current_branch()
1d4baaf… lmata 496
1d4baaf… lmata 497 call_args = mock_run.call_args
1d4baaf… lmata 498 assert call_args[0][0] == ["fossil", "branch", "current"]
1d4baaf… lmata 499
1d4baaf… lmata 500
1d4baaf… lmata 501 class TestFossilAdapterChangedFiles:
1d4baaf… lmata 502 """changed_files calls 'fossil changes --differ' and parses output."""
1d4baaf… lmata 503
1d4baaf… lmata 504 def test_returns_changed_file_list(self, fossil_dir):
1d4baaf… lmata 505 from navegador.vcs import FossilAdapter
1d4baaf… lmata 506
1d4baaf… lmata 507 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 508 mock_result = MagicMock()
1d4baaf… lmata 509 mock_result.stdout = "EDITED src/main.py\nADDED tests/test_new.py\n"
1d4baaf… lmata 510
1d4baaf… lmata 511 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 512 files = adapter.changed_files()
1d4baaf… lmata 513
1d4baaf… lmata 514 assert "src/main.py" in files
1d4baaf… lmata 515 assert "tests/test_new.py" in files
1d4baaf… lmata 516
1d4baaf… lmata 517 def test_empty_output_returns_empty_list(self, fossil_dir):
1d4baaf… lmata 518 from navegador.vcs import FossilAdapter
1d4baaf… lmata 519
1d4baaf… lmata 520 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 521 mock_result = MagicMock()
1d4baaf… lmata 522 mock_result.stdout = ""
1d4baaf… lmata 523
1d4baaf… lmata 524 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 525 files = adapter.changed_files()
1d4baaf… lmata 526
1d4baaf… lmata 527 assert files == []
1d4baaf… lmata 528
1d4baaf… lmata 529 def test_calls_fossil_changes_differ(self, fossil_dir):
1d4baaf… lmata 530 from navegador.vcs import FossilAdapter
1d4baaf… lmata 531
1d4baaf… lmata 532 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 533 mock_result = MagicMock()
1d4baaf… lmata 534 mock_result.stdout = ""
1d4baaf… lmata 535
1d4baaf… lmata 536 with patch("subprocess.run", return_value=mock_result) as mock_run:
1d4baaf… lmata 537 adapter.changed_files()
1d4baaf… lmata 538
1d4baaf… lmata 539 call_args = mock_run.call_args
1d4baaf… lmata 540 assert call_args[0][0] == ["fossil", "changes", "--differ"]
1d4baaf… lmata 541
1d4baaf… lmata 542 def test_returns_list(self, fossil_dir):
1d4baaf… lmata 543 from navegador.vcs import FossilAdapter
1d4baaf… lmata 544
1d4baaf… lmata 545 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 546 mock_result = MagicMock()
1d4baaf… lmata 547 mock_result.stdout = "EDITED foo.py\n"
1d4baaf… lmata 548
1d4baaf… lmata 549 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 550 result = adapter.changed_files()
1d4baaf… lmata 551
1d4baaf… lmata 552 assert isinstance(result, list)
1d4baaf… lmata 553
1d4baaf… lmata 554
1d4baaf… lmata 555 class TestFossilAdapterFileHistory:
1d4baaf… lmata 556 """file_history calls 'fossil timeline' and parses output into entry dicts."""
1d4baaf… lmata 557
1d4baaf… lmata 558 SAMPLE_TIMELINE = """\
1d4baaf… lmata 559 === 2024-01-15 ===
1d4baaf… lmata 560 14:23:07 [abc123def456] Add feature. (user: alice, tags: trunk)
1d4baaf… lmata 561 09:00:00 [deadbeef1234] Fix typo. (user: bob, tags: trunk)
1d4baaf… lmata 562 === 2024-01-14 ===
1d4baaf… lmata 563 22:10:00 [cafe0000abcd] Initial commit. (user: alice, tags: initial)
1d4baaf… lmata 564 """
1d4baaf… lmata 565
1d4baaf… lmata 566 def test_returns_list_of_dicts(self, fossil_dir):
1d4baaf… lmata 567 from navegador.vcs import FossilAdapter
1d4baaf… lmata 568
1d4baaf… lmata 569 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 570 mock_result = MagicMock()
1d4baaf… lmata 571 mock_result.stdout = self.SAMPLE_TIMELINE
1d4baaf… lmata 572
1d4baaf… lmata 573 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 574 history = adapter.file_history("src/main.py")
1d4baaf… lmata 575
1d4baaf… lmata 576 assert isinstance(history, list)
1d4baaf… lmata 577 assert len(history) >= 1
1d4baaf… lmata 578
1d4baaf… lmata 579 def test_entry_has_required_keys(self, fossil_dir):
1d4baaf… lmata 580 from navegador.vcs import FossilAdapter
1d4baaf… lmata 581
1d4baaf… lmata 582 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 583 mock_result = MagicMock()
1d4baaf… lmata 584 mock_result.stdout = self.SAMPLE_TIMELINE
1d4baaf… lmata 585
1d4baaf… lmata 586 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 587 history = adapter.file_history("src/main.py")
1d4baaf… lmata 588
1d4baaf… lmata 589 for entry in history:
1d4baaf… lmata 590 assert "hash" in entry
1d4baaf… lmata 591 assert "author" in entry
1d4baaf… lmata 592 assert "date" in entry
1d4baaf… lmata 593 assert "message" in entry
1d4baaf… lmata 594
1d4baaf… lmata 595 def test_limit_passed_to_fossil(self, fossil_dir):
1d4baaf… lmata 596 from navegador.vcs import FossilAdapter
1d4baaf… lmata 597
1d4baaf… lmata 598 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 599 mock_result = MagicMock()
1d4baaf… lmata 600 mock_result.stdout = ""
1d4baaf… lmata 601
1d4baaf… lmata 602 with patch("subprocess.run", return_value=mock_result) as mock_run:
1d4baaf… lmata 603 adapter.file_history("src/main.py", limit=5)
1d4baaf… lmata 604
1d4baaf… lmata 605 args = mock_run.call_args[0][0]
1d4baaf… lmata 606 assert "5" in args
1d4baaf… lmata 607
1d4baaf… lmata 608 def test_empty_output_returns_empty_list(self, fossil_dir):
1d4baaf… lmata 609 from navegador.vcs import FossilAdapter
1d4baaf… lmata 610
1d4baaf… lmata 611 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 612 mock_result = MagicMock()
1d4baaf… lmata 613 mock_result.stdout = ""
1d4baaf… lmata 614
1d4baaf… lmata 615 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 616 history = adapter.file_history("nonexistent.py")
1d4baaf… lmata 617
1d4baaf… lmata 618 assert history == []
1d4baaf… lmata 619
1d4baaf… lmata 620
1d4baaf… lmata 621 class TestFossilAdapterBlame:
1d4baaf… lmata 622 """blame calls 'fossil annotate --log' and parses per-line output."""
1d4baaf… lmata 623
1d4baaf… lmata 624 SAMPLE_ANNOTATE = """\
1d4baaf… lmata 625 1.1 alice 2024-01-15: def main():
1d4baaf… lmata 626 1.1 alice 2024-01-15: pass
1d4baaf… lmata 627 1.2 bob 2024-01-20: # added comment
1d4baaf… lmata 628 """
1d4baaf… lmata 629
1d4baaf… lmata 630 def test_returns_list(self, fossil_dir):
1d4baaf… lmata 631 from navegador.vcs import FossilAdapter
1d4baaf… lmata 632
1d4baaf… lmata 633 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 634 mock_result = MagicMock()
1d4baaf… lmata 635 mock_result.stdout = self.SAMPLE_ANNOTATE
1d4baaf… lmata 636
1d4baaf… lmata 637 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 638 result = adapter.blame("src/main.py")
1d4baaf… lmata 639
1d4baaf… lmata 640 assert isinstance(result, list)
1d4baaf… lmata 641 assert len(result) >= 1
1d4baaf… lmata 642
1d4baaf… lmata 643 def test_entry_has_required_keys(self, fossil_dir):
1d4baaf… lmata 644 from navegador.vcs import FossilAdapter
1d4baaf… lmata 645
1d4baaf… lmata 646 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 647 mock_result = MagicMock()
1d4baaf… lmata 648 mock_result.stdout = self.SAMPLE_ANNOTATE
1d4baaf… lmata 649
1d4baaf… lmata 650 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 651 result = adapter.blame("src/main.py")
1d4baaf… lmata 652
1d4baaf… lmata 653 for entry in result:
1d4baaf… lmata 654 assert "line" in entry
1d4baaf… lmata 655 assert "hash" in entry
1d4baaf… lmata 656 assert "author" in entry
1d4baaf… lmata 657 assert "content" in entry
1d4baaf… lmata 658
1d4baaf… lmata 659 def test_line_numbers_sequential(self, fossil_dir):
1d4baaf… lmata 660 from navegador.vcs import FossilAdapter
1d4baaf… lmata 661
1d4baaf… lmata 662 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 663 mock_result = MagicMock()
1d4baaf… lmata 664 mock_result.stdout = self.SAMPLE_ANNOTATE
1d4baaf… lmata 665
1d4baaf… lmata 666 with patch("subprocess.run", return_value=mock_result):
1d4baaf… lmata 667 result = adapter.blame("src/main.py")
1d4baaf… lmata 668
1d4baaf… lmata 669 if len(result) >= 2:
1d4baaf… lmata 670 assert result[1]["line"] > result[0]["line"]
1d4baaf… lmata 671
1d4baaf… lmata 672 def test_calls_fossil_annotate(self, fossil_dir):
1d4baaf… lmata 673 from navegador.vcs import FossilAdapter
1d4baaf… lmata 674
1d4baaf… lmata 675 adapter = FossilAdapter(fossil_dir)
1d4baaf… lmata 676 mock_result = MagicMock()
1d4baaf… lmata 677 mock_result.stdout = ""
1d4baaf… lmata 678
1d4baaf… lmata 679 with patch("subprocess.run", return_value=mock_result) as mock_run:
1d4baaf… lmata 680 adapter.blame("src/main.py")
1d4baaf… lmata 681
1d4baaf… lmata 682 args = mock_run.call_args[0][0]
1d4baaf… lmata 683 assert "fossil" in args
1d4baaf… lmata 684 assert "annotate" in args
1d4baaf… lmata 685
1d4baaf… lmata 686
1d4baaf… lmata 687 # =============================================================================
1d4baaf… lmata 688 # #58 — DependencyIngester
1d4baaf… lmata 689 # =============================================================================
1d4baaf… lmata 690
1d4baaf… lmata 691
1d4baaf… lmata 692 class TestDependencyIngesterNPM:
1d4baaf… lmata 693 """ingest_npm parses package.json and creates dependency nodes."""
1d4baaf… lmata 694
1d4baaf… lmata 695 def test_ingests_dependencies(self, tmp_path):
1d4baaf… lmata 696 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 697
1d4baaf… lmata 698 pkg = {
1d4baaf… lmata 699 "name": "myapp",
1d4baaf… lmata 700 "dependencies": {"react": "^18.0.0", "lodash": "4.17.21"},
1d4baaf… lmata 701 "devDependencies": {"jest": "^29.0.0"},
1d4baaf… lmata 702 }
1d4baaf… lmata 703 pkg_file = tmp_path / "package.json"
1d4baaf… lmata 704 pkg_file.write_text(json.dumps(pkg))
1d4baaf… lmata 705
1d4baaf… lmata 706 store = _make_store()
1d4baaf… lmata 707 ing = DependencyIngester(store)
1d4baaf… lmata 708 stats = ing.ingest_npm(str(pkg_file))
1d4baaf… lmata 709
1d4baaf… lmata 710 assert stats["packages"] == 3 # 2 deps + 1 devDep
1d4baaf… lmata 711 assert store.create_node.call_count >= 3
1d4baaf… lmata 712
1d4baaf… lmata 713 def test_empty_dependencies(self, tmp_path):
1d4baaf… lmata 714 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 715
1d4baaf… lmata 716 pkg = {"name": "empty", "dependencies": {}}
1d4baaf… lmata 717 pkg_file = tmp_path / "package.json"
1d4baaf… lmata 718 pkg_file.write_text(json.dumps(pkg))
1d4baaf… lmata 719
1d4baaf… lmata 720 store = _make_store()
1d4baaf… lmata 721 stats = DependencyIngester(store).ingest_npm(str(pkg_file))
1d4baaf… lmata 722 assert stats["packages"] == 0
1d4baaf… lmata 723
1d4baaf… lmata 724 def test_peer_dependencies_included(self, tmp_path):
1d4baaf… lmata 725 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 726
1d4baaf… lmata 727 pkg = {
1d4baaf… lmata 728 "peerDependencies": {"react": ">=17"},
1d4baaf… lmata 729 }
1d4baaf… lmata 730 pkg_file = tmp_path / "package.json"
1d4baaf… lmata 731 pkg_file.write_text(json.dumps(pkg))
1d4baaf… lmata 732
1d4baaf… lmata 733 store = _make_store()
1d4baaf… lmata 734 stats = DependencyIngester(store).ingest_npm(str(pkg_file))
1d4baaf… lmata 735 assert stats["packages"] == 1
1d4baaf… lmata 736
1d4baaf… lmata 737 def test_creates_depends_on_edge(self, tmp_path):
1d4baaf… lmata 738 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 739
1d4baaf… lmata 740 pkg = {"dependencies": {"axios": "^1.0.0"}}
1d4baaf… lmata 741 pkg_file = tmp_path / "package.json"
1d4baaf… lmata 742 pkg_file.write_text(json.dumps(pkg))
1d4baaf… lmata 743
1d4baaf… lmata 744 store = _make_store()
1d4baaf… lmata 745 DependencyIngester(store).ingest_npm(str(pkg_file))
1d4baaf… lmata 746 store.create_edge.assert_called()
1d4baaf… lmata 747
1d4baaf… lmata 748
1d4baaf… lmata 749 class TestDependencyIngesterPip:
1d4baaf… lmata 750 """ingest_pip parses requirements.txt and creates dependency nodes."""
1d4baaf… lmata 751
1d4baaf… lmata 752 def test_requirements_txt(self, tmp_path):
1d4baaf… lmata 753 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 754
1d4baaf… lmata 755 req_file = tmp_path / "requirements.txt"
1d4baaf… lmata 756 req_file.write_text(
1d4baaf… lmata 757 "requests>=2.28.0\n"
1d4baaf… lmata 758 "flask[async]==2.3.0\n"
1d4baaf… lmata 759 "# a comment\n"
1d4baaf… lmata 760 "\n"
1d4baaf… lmata 761 "pytest>=7.0 # dev\n"
1d4baaf… lmata 762 )
1d4baaf… lmata 763
1d4baaf… lmata 764 store = _make_store()
1d4baaf… lmata 765 stats = DependencyIngester(store).ingest_pip(str(req_file))
1d4baaf… lmata 766 assert stats["packages"] == 3
1d4baaf… lmata 767
1d4baaf… lmata 768 def test_skips_comments_and_blanks(self, tmp_path):
1d4baaf… lmata 769 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 770
1d4baaf… lmata 771 req_file = tmp_path / "requirements.txt"
1d4baaf… lmata 772 req_file.write_text("# comment\n\n-r other.txt\n")
1d4baaf… lmata 773
1d4baaf… lmata 774 store = _make_store()
1d4baaf… lmata 775 stats = DependencyIngester(store).ingest_pip(str(req_file))
1d4baaf… lmata 776 assert stats["packages"] == 0
1d4baaf… lmata 777
1d4baaf… lmata 778 def test_pyproject_toml(self, tmp_path):
1d4baaf… lmata 779 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 780
1d4baaf… lmata 781 toml_content = """\
1d4baaf… lmata 782 [project]
1d4baaf… lmata 783 name = "myproject"
1d4baaf… lmata 784 dependencies = [
1d4baaf… lmata 785 "click>=8.0",
1d4baaf… lmata 786 "rich>=12.0",
1d4baaf… lmata 787 "pydantic>=2.0",
1d4baaf… lmata 788 ]
1d4baaf… lmata 789 """
1d4baaf… lmata 790 pyproject = tmp_path / "pyproject.toml"
1d4baaf… lmata 791 pyproject.write_text(toml_content)
1d4baaf… lmata 792
1d4baaf… lmata 793 store = _make_store()
1d4baaf… lmata 794 stats = DependencyIngester(store).ingest_pip(str(pyproject))
1d4baaf… lmata 795 assert stats["packages"] >= 3
1d4baaf… lmata 796
1d4baaf… lmata 797
1d4baaf… lmata 798 class TestDependencyIngesterCargo:
1d4baaf… lmata 799 """ingest_cargo parses Cargo.toml and creates dependency nodes."""
1d4baaf… lmata 800
1d4baaf… lmata 801 def test_basic_cargo_toml(self, tmp_path):
1d4baaf… lmata 802 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 803
1d4baaf… lmata 804 cargo_content = """\
1d4baaf… lmata 805 [package]
1d4baaf… lmata 806 name = "myapp"
1d4baaf… lmata 807
1d4baaf… lmata 808 [dependencies]
1d4baaf… lmata 809 serde = "1.0"
1d4baaf… lmata 810 tokio = { version = "1.0", features = ["full"] }
1d4baaf… lmata 811
1d4baaf… lmata 812 [dev-dependencies]
1d4baaf… lmata 813 criterion = "0.4"
1d4baaf… lmata 814 """
1d4baaf… lmata 815 cargo_file = tmp_path / "Cargo.toml"
1d4baaf… lmata 816 cargo_file.write_text(cargo_content)
1d4baaf… lmata 817
1d4baaf… lmata 818 store = _make_store()
1d4baaf… lmata 819 stats = DependencyIngester(store).ingest_cargo(str(cargo_file))
1d4baaf… lmata 820 assert stats["packages"] == 3 # serde, tokio, criterion
1d4baaf… lmata 821
1d4baaf… lmata 822 def test_empty_cargo_toml(self, tmp_path):
1d4baaf… lmata 823 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 824
1d4baaf… lmata 825 cargo_file = tmp_path / "Cargo.toml"
1d4baaf… lmata 826 cargo_file.write_text("[package]\nname = \"empty\"\n")
1d4baaf… lmata 827
1d4baaf… lmata 828 store = _make_store()
1d4baaf… lmata 829 stats = DependencyIngester(store).ingest_cargo(str(cargo_file))
1d4baaf… lmata 830 assert stats["packages"] == 0
1d4baaf… lmata 831
1d4baaf… lmata 832 def test_build_dependencies_included(self, tmp_path):
1d4baaf… lmata 833 from navegador.dependencies import DependencyIngester
1d4baaf… lmata 834
1d4baaf… lmata 835 cargo_content = "[build-dependencies]\nbuild-helper = \"0.3\"\n"
1d4baaf… lmata 836 cargo_file = tmp_path / "Cargo.toml"
1d4baaf… lmata 837 cargo_file.write_text(cargo_content)
1d4baaf… lmata 838
1d4baaf… lmata 839 store = _make_store()
1d4baaf… lmata 840 stats = DependencyIngester(store).ingest_cargo(str(cargo_file))
1d4baaf… lmata 841 assert stats["packages"] == 1
1d4baaf… lmata 842
1d4baaf… lmata 843
1d4baaf… lmata 844 # =============================================================================
1d4baaf… lmata 845 # #61 — SubmoduleIngester
1d4baaf… lmata 846 # =============================================================================
1d4baaf… lmata 847
1d4baaf… lmata 848
1d4baaf… lmata 849 class TestDetectSubmodules:
1d4baaf… lmata 850 """detect_submodules parses .gitmodules into structured dicts."""
1d4baaf… lmata 851
1d4baaf… lmata 852 def test_no_gitmodules_returns_empty(self, tmp_path):
1d4baaf… lmata 853 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 854
1d4baaf… lmata 855 result = SubmoduleIngester(_make_store()).detect_submodules(tmp_path)
1d4baaf… lmata 856 assert result == []
1d4baaf… lmata 857
1d4baaf… lmata 858 def test_single_submodule(self, tmp_path):
1d4baaf… lmata 859 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 860
1d4baaf… lmata 861 gitmodules = tmp_path / ".gitmodules"
1d4baaf… lmata 862 gitmodules.write_text(
1d4baaf… lmata 863 '[submodule "vendor/lib"]\n'
1d4baaf… lmata 864 " path = vendor/lib\n"
1d4baaf… lmata 865 " url = https://github.com/org/lib.git\n"
1d4baaf… lmata 866 )
1d4baaf… lmata 867
1d4baaf… lmata 868 result = SubmoduleIngester(_make_store()).detect_submodules(tmp_path)
1d4baaf… lmata 869 assert len(result) == 1
1d4baaf… lmata 870 assert result[0]["name"] == "vendor/lib"
1d4baaf… lmata 871 assert result[0]["path"] == "vendor/lib"
1d4baaf… lmata 872 assert result[0]["url"] == "https://github.com/org/lib.git"
1d4baaf… lmata 873 assert result[0]["abs_path"] == str(tmp_path / "vendor/lib")
1d4baaf… lmata 874
1d4baaf… lmata 875 def test_multiple_submodules(self, tmp_path):
1d4baaf… lmata 876 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 877
1d4baaf… lmata 878 gitmodules = tmp_path / ".gitmodules"
1d4baaf… lmata 879 gitmodules.write_text(
1d4baaf… lmata 880 '[submodule "a"]\n path = sub/a\n url = https://example.com/a.git\n'
1d4baaf… lmata 881 '[submodule "b"]\n path = sub/b\n url = https://example.com/b.git\n'
1d4baaf… lmata 882 )
1d4baaf… lmata 883
1d4baaf… lmata 884 result = SubmoduleIngester(_make_store()).detect_submodules(tmp_path)
1d4baaf… lmata 885 assert len(result) == 2
1d4baaf… lmata 886 names = {r["name"] for r in result}
1d4baaf… lmata 887 assert names == {"a", "b"}
1d4baaf… lmata 888
1d4baaf… lmata 889 def test_missing_url_returns_empty_string(self, tmp_path):
1d4baaf… lmata 890 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 891
1d4baaf… lmata 892 gitmodules = tmp_path / ".gitmodules"
1d4baaf… lmata 893 gitmodules.write_text('[submodule "x"]\n path = sub/x\n')
1d4baaf… lmata 894
1d4baaf… lmata 895 result = SubmoduleIngester(_make_store()).detect_submodules(tmp_path)
1d4baaf… lmata 896 assert result[0]["url"] == ""
1d4baaf… lmata 897
1d4baaf… lmata 898
1d4baaf… lmata 899 class TestIngestWithSubmodules:
1d4baaf… lmata 900 """ingest_with_submodules ingests parent + submodules, creates DEPENDS_ON edges."""
1d4baaf… lmata 901
1d4baaf… lmata 902 def test_no_gitmodules_ingests_parent_only(self, tmp_path):
1d4baaf… lmata 903 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 904
1d4baaf… lmata 905 store = _make_store()
1d4baaf… lmata 906 ing = SubmoduleIngester(store)
1d4baaf… lmata 907
1d4baaf… lmata 908 with patch("navegador.ingestion.parser.RepoIngester") as MockIngester:
1d4baaf… lmata 909 mock_inst = MagicMock()
1d4baaf… lmata 910 mock_inst.ingest.return_value = {"files": 5, "nodes": 10}
1d4baaf… lmata 911 MockIngester.return_value = mock_inst
1d4baaf… lmata 912
1d4baaf… lmata 913 stats = ing.ingest_with_submodules(str(tmp_path))
1d4baaf… lmata 914
1d4baaf… lmata 915 assert stats["parent"]["files"] == 5
1d4baaf… lmata 916 assert stats["submodules"] == {}
1d4baaf… lmata 917 assert stats["total_files"] == 5
1d4baaf… lmata 918
1d4baaf… lmata 919 def test_missing_submodule_path_recorded_as_error(self, tmp_path):
1d4baaf… lmata 920 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 921
1d4baaf… lmata 922 gitmodules = tmp_path / ".gitmodules"
1d4baaf… lmata 923 gitmodules.write_text(
1d4baaf… lmata 924 '[submodule "missing"]\n path = does/not/exist\n url = https://x.com/r.git\n'
1d4baaf… lmata 925 )
1d4baaf… lmata 926
1d4baaf… lmata 927 store = _make_store()
1d4baaf… lmata 928 ing = SubmoduleIngester(store)
1d4baaf… lmata 929
1d4baaf… lmata 930 with patch("navegador.ingestion.parser.RepoIngester") as MockIngester:
1d4baaf… lmata 931 mock_inst = MagicMock()
1d4baaf… lmata 932 mock_inst.ingest.return_value = {"files": 3, "nodes": 6}
1d4baaf… lmata 933 MockIngester.return_value = mock_inst
1d4baaf… lmata 934
1d4baaf… lmata 935 stats = ing.ingest_with_submodules(str(tmp_path))
1d4baaf… lmata 936
1d4baaf… lmata 937 assert "missing" in stats["submodules"]
1d4baaf… lmata 938 assert "error" in stats["submodules"]["missing"]
1d4baaf… lmata 939
1d4baaf… lmata 940 def test_existing_submodule_ingested(self, tmp_path):
1d4baaf… lmata 941 from navegador.submodules import SubmoduleIngester
1d4baaf… lmata 942
1d4baaf… lmata 943 sub_dir = tmp_path / "libs" / "core"
1d4baaf… lmata 944 sub_dir.mkdir(parents=True)
1d4baaf… lmata 945
1d4baaf… lmata 946 gitmodules = tmp_path / ".gitmodules"
1d4baaf… lmata 947 gitmodules.write_text(
1d4baaf… lmata 948 '[submodule "core"]\n path = libs/core\n url = https://x.com/core.git\n'
1d4baaf… lmata 949 )
1d4baaf… lmata 950
1d4baaf… lmata 951 store = _make_store()
1d4baaf… lmata 952 ing = SubmoduleIngester(store)
1d4baaf… lmata 953
1d4baaf… lmata 954 with patch("navegador.ingestion.parser.RepoIngester") as MockIngester:
1d4baaf… lmata 955 mock_inst = MagicMock()
1d4baaf… lmata 956 mock_inst.ingest.return_value = {"files": 4, "nodes": 8}
1d4baaf… lmata 957 MockIngester.return_value = mock_inst
1d4baaf… lmata 958
1d4baaf… lmata 959 stats = ing.ingest_with_submodules(str(tmp_path))
1d4baaf… lmata 960
1d4baaf… lmata 961 assert "core" in stats["submodules"]
1d4baaf… lmata 962 assert stats["submodules"]["core"]["files"] == 4
1d4baaf… lmata 963 assert stats["total_files"] == 8 # parent 4 + submodule 4
1d4baaf… lmata 964
1d4baaf… lmata 965 # DEPENDS_ON edge from parent → submodule
1d4baaf… lmata 966 store.create_edge.assert_called()
1d4baaf… lmata 967
1d4baaf… lmata 968
1d4baaf… lmata 969 # =============================================================================
1d4baaf… lmata 970 # #62 — WorkspaceMode + WorkspaceManager
1d4baaf… lmata 971 # =============================================================================
1d4baaf… lmata 972
1d4baaf… lmata 973
1d4baaf… lmata 974 class TestWorkspaceMode:
1d4baaf… lmata 975 """WorkspaceMode enum has UNIFIED and FEDERATED values."""
1d4baaf… lmata 976
1d4baaf… lmata 977 def test_has_unified(self):
1d4baaf… lmata 978 from navegador.multirepo import WorkspaceMode
1d4baaf… lmata 979
1d4baaf… lmata 980 assert WorkspaceMode.UNIFIED == "unified"
1d4baaf… lmata 981
1d4baaf… lmata 982 def test_has_federated(self):
1d4baaf… lmata 983 from navegador.multirepo import WorkspaceMode
1d4baaf… lmata 984
1d4baaf… lmata 985 assert WorkspaceMode.FEDERATED == "federated"
1d4baaf… lmata 986
1d4baaf… lmata 987 def test_is_str_enum(self):
1d4baaf… lmata 988 from navegador.multirepo import WorkspaceMode
1d4baaf… lmata 989
1d4baaf… lmata 990 assert isinstance(WorkspaceMode.UNIFIED, str)
1d4baaf… lmata 991 assert isinstance(WorkspaceMode.FEDERATED, str)
1d4baaf… lmata 992
1d4baaf… lmata 993 def test_from_string(self):
1d4baaf… lmata 994 from navegador.multirepo import WorkspaceMode
1d4baaf… lmata 995
1d4baaf… lmata 996 assert WorkspaceMode("unified") == WorkspaceMode.UNIFIED
1d4baaf… lmata 997 assert WorkspaceMode("federated") == WorkspaceMode.FEDERATED
1d4baaf… lmata 998
1d4baaf… lmata 999
1d4baaf… lmata 1000 class TestWorkspaceManagerUnified:
1d4baaf… lmata 1001 """WorkspaceManager in UNIFIED mode uses a single shared graph."""
1d4baaf… lmata 1002
1d4baaf… lmata 1003 def test_add_repo_creates_repository_node(self, tmp_path):
1d4baaf… lmata 1004 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1005
1d4baaf… lmata 1006 store = _make_store()
1d4baaf… lmata 1007 wm = WorkspaceManager(store, mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1008 wm.add_repo("backend", str(tmp_path))
1d4baaf… lmata 1009
1d4baaf… lmata 1010 store.create_node.assert_called()
1d4baaf… lmata 1011
1d4baaf… lmata 1012 def test_list_repos(self, tmp_path):
1d4baaf… lmata 1013 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1014
1d4baaf… lmata 1015 store = _make_store()
1d4baaf… lmata 1016 wm = WorkspaceManager(store, mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1017 wm.add_repo("backend", str(tmp_path))
1d4baaf… lmata 1018 wm.add_repo("frontend", str(tmp_path))
1d4baaf… lmata 1019
1d4baaf… lmata 1020 repos = wm.list_repos()
1d4baaf… lmata 1021 names = {r["name"] for r in repos}
1d4baaf… lmata 1022 assert names == {"backend", "frontend"}
1d4baaf… lmata 1023
1d4baaf… lmata 1024 def test_ingest_all_calls_repo_ingester(self, tmp_path):
1d4baaf… lmata 1025 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1026
1d4baaf… lmata 1027 store = _make_store()
1d4baaf… lmata 1028 wm = WorkspaceManager(store, mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1029 wm.add_repo("repo1", str(tmp_path))
1d4baaf… lmata 1030
1d4baaf… lmata 1031 with patch("navegador.ingestion.parser.RepoIngester") as MockIngester:
1d4baaf… lmata 1032 mock_inst = MagicMock()
1d4baaf… lmata 1033 mock_inst.ingest.return_value = {"files": 2, "nodes": 5}
1d4baaf… lmata 1034 MockIngester.return_value = mock_inst
1d4baaf… lmata 1035
1d4baaf… lmata 1036 summary = wm.ingest_all()
1d4baaf… lmata 1037
1d4baaf… lmata 1038 assert "repo1" in summary
1d4baaf… lmata 1039 assert summary["repo1"]["files"] == 2
1d4baaf… lmata 1040
1d4baaf… lmata 1041 def test_ingest_all_no_repos_returns_empty(self):
1d4baaf… lmata 1042 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1043
1d4baaf… lmata 1044 wm = WorkspaceManager(_make_store(), mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1045 assert wm.ingest_all() == {}
1d4baaf… lmata 1046
1d4baaf… lmata 1047 def test_search_unified_queries_single_store(self):
1d4baaf… lmata 1048 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1049
1d4baaf… lmata 1050 store = _make_store()
1d4baaf… lmata 1051 store.query.return_value = MagicMock(
1d4baaf… lmata 1052 result_set=[["Function", "authenticate", "/src/auth.py"]]
1d4baaf… lmata 1053 )
1d4baaf… lmata 1054 wm = WorkspaceManager(store, mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1055 wm.add_repo("repo", "/tmp/repo")
1d4baaf… lmata 1056
1d4baaf… lmata 1057 results = wm.search("authenticate")
1d4baaf… lmata 1058 assert len(results) >= 1
1d4baaf… lmata 1059 assert results[0]["name"] == "authenticate"
1d4baaf… lmata 1060
1d4baaf… lmata 1061 def test_ingest_error_recorded_in_summary(self, tmp_path):
1d4baaf… lmata 1062 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1063
1d4baaf… lmata 1064 store = _make_store()
1d4baaf… lmata 1065 wm = WorkspaceManager(store, mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1066 wm.add_repo("broken", str(tmp_path))
1d4baaf… lmata 1067
1d4baaf… lmata 1068 with patch("navegador.ingestion.parser.RepoIngester") as MockIngester:
1d4baaf… lmata 1069 MockIngester.return_value.ingest.side_effect = RuntimeError("parse error")
1d4baaf… lmata 1070 summary = wm.ingest_all()
1d4baaf… lmata 1071
1d4baaf… lmata 1072 assert "broken" in summary
1d4baaf… lmata 1073 assert "error" in summary["broken"]
1d4baaf… lmata 1074
1d4baaf… lmata 1075
1d4baaf… lmata 1076 class TestWorkspaceManagerFederated:
1d4baaf… lmata 1077 """WorkspaceManager in FEDERATED mode creates per-repo graphs."""
1d4baaf… lmata 1078
1d4baaf… lmata 1079 def test_add_repo_sets_federated_graph_name(self, tmp_path):
1d4baaf… lmata 1080 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1081
1d4baaf… lmata 1082 store = _make_store()
1d4baaf… lmata 1083 wm = WorkspaceManager(store, mode=WorkspaceMode.FEDERATED)
1d4baaf… lmata 1084 wm.add_repo("api", str(tmp_path))
1d4baaf… lmata 1085
1d4baaf… lmata 1086 repos = wm.list_repos()
1d4baaf… lmata 1087 assert repos[0]["graph_name"] == "navegador_api"
1d4baaf… lmata 1088
1d4baaf… lmata 1089 def test_unified_graph_name_is_navegador(self, tmp_path):
1d4baaf… lmata 1090 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1091
1d4baaf… lmata 1092 store = _make_store()
1d4baaf… lmata 1093 wm = WorkspaceManager(store, mode=WorkspaceMode.UNIFIED)
1d4baaf… lmata 1094 wm.add_repo("api", str(tmp_path))
1d4baaf… lmata 1095
1d4baaf… lmata 1096 repos = wm.list_repos()
1d4baaf… lmata 1097 assert repos[0]["graph_name"] == "navegador"
1d4baaf… lmata 1098
1d4baaf… lmata 1099 def test_federated_ingest_uses_per_repo_store(self, tmp_path):
1d4baaf… lmata 1100 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1101
1d4baaf… lmata 1102 store = _make_store()
1d4baaf… lmata 1103 # select_graph returns a different mock each time
1d4baaf… lmata 1104 store._client.select_graph.return_value = MagicMock()
1d4baaf… lmata 1105
1d4baaf… lmata 1106 wm = WorkspaceManager(store, mode=WorkspaceMode.FEDERATED)
1d4baaf… lmata 1107 wm.add_repo("svc", str(tmp_path))
1d4baaf… lmata 1108
1d4baaf… lmata 1109 with patch("navegador.ingestion.parser.RepoIngester") as MockIngester:
1d4baaf… lmata 1110 mock_inst = MagicMock()
1d4baaf… lmata 1111 mock_inst.ingest.return_value = {"files": 1, "nodes": 3}
1d4baaf… lmata 1112 MockIngester.return_value = mock_inst
1d4baaf… lmata 1113
1d4baaf… lmata 1114 summary = wm.ingest_all()
1d4baaf… lmata 1115
1d4baaf… lmata 1116 assert "svc" in summary
1d4baaf… lmata 1117 # select_graph should have been called with "navegador_svc"
1d4baaf… lmata 1118 called_graphs = [
1d4baaf… lmata 1119 c.args[0] for c in store._client.select_graph.call_args_list
1d4baaf… lmata 1120 ]
1d4baaf… lmata 1121 assert any("navegador_svc" in g for g in called_graphs)
1d4baaf… lmata 1122
1d4baaf… lmata 1123 def test_federated_search_merges_results(self):
1d4baaf… lmata 1124 from navegador.multirepo import WorkspaceManager, WorkspaceMode
1d4baaf… lmata 1125
1d4baaf… lmata 1126 store = _make_store()
1d4baaf… lmata 1127
1d4baaf… lmata 1128 # Each per-repo graph returns a result
1d4baaf… lmata 1129 per_repo_store_mock = MagicMock()
1d4baaf… lmata 1130 per_repo_store_mock.query.return_value = MagicMock(
1d4baaf… lmata 1131 result_set=[["Function", "auth_check", "/src/auth.py"]]
1d4baaf… lmata 1132 )
1d4baaf… lmata 1133 store._client.select_graph.return_value = per_repo_store_mock
1d4baaf… lmata 1134
1d4baaf… lmata 1135 wm = WorkspaceManager(store, mode=WorkspaceMode.FEDERATED)
1d4baaf… lmata 1136 wm._repos = {
1d4baaf… lmata 1137 "backend": {"path": "/tmp/backend", "graph_name": "navegador_backend"},
1d4baaf… lmata 1138 "frontend": {"path": "/tmp/frontend", "graph_name": "navegador_frontend"},
1d4baaf… lmata 1139 }
1d4baaf… lmata 1140
1d4baaf… lmata 1141 results = wm.search("auth")
1d4baaf… lmata 1142 # Two repos each return one result → 2 total (deduplicated to 1 because same name)
1d4baaf… lmata 1143 assert len(results) >= 1
1d4baaf… lmata 1144
1d4baaf… lmata 1145
1d4baaf… lmata 1146 # =============================================================================
1d4baaf… lmata 1147 # CLI smoke tests
1d4baaf… lmata 1148 # =============================================================================
1d4baaf… lmata 1149
1d4baaf… lmata 1150
1d4baaf… lmata 1151 class TestCLIPMGroup:
1d4baaf… lmata 1152 """pm group is registered on the main CLI."""
1d4baaf… lmata 1153
1d4baaf… lmata 1154 def test_pm_group_exists(self):
1d4baaf… lmata 1155 from click.testing import CliRunner
1d4baaf… lmata 1156
1d4baaf… lmata 1157 from navegador.cli.commands import main
1d4baaf… lmata 1158
1d4baaf… lmata 1159 runner = CliRunner()
1d4baaf… lmata 1160 result = runner.invoke(main, ["pm", "--help"])
1d4baaf… lmata 1161 assert result.exit_code == 0
1d4baaf… lmata 1162 assert "ingest" in result.output
1d4baaf… lmata 1163
1d4baaf… lmata 1164 def test_pm_ingest_requires_github(self):
1d4baaf… lmata 1165 from click.testing import CliRunner
1d4baaf… lmata 1166
1d4baaf… lmata 1167 from navegador.cli.commands import main
1d4baaf… lmata 1168
1d4baaf… lmata 1169 runner = CliRunner()
1d4baaf… lmata 1170 result = runner.invoke(main, ["pm", "ingest"])
1d4baaf… lmata 1171 assert result.exit_code != 0
1d4baaf… lmata 1172
1d4baaf… lmata 1173
1d4baaf… lmata 1174 class TestCLIDepsGroup:
1d4baaf… lmata 1175 """deps group is registered on the main CLI."""
1d4baaf… lmata 1176
1d4baaf… lmata 1177 def test_deps_group_exists(self):
1d4baaf… lmata 1178 from click.testing import CliRunner
1d4baaf… lmata 1179
1d4baaf… lmata 1180 from navegador.cli.commands import main
1d4baaf… lmata 1181
1d4baaf… lmata 1182 runner = CliRunner()
1d4baaf… lmata 1183 result = runner.invoke(main, ["deps", "--help"])
1d4baaf… lmata 1184 assert result.exit_code == 0
1d4baaf… lmata 1185 assert "ingest" in result.output
1d4baaf… lmata 1186
1d4baaf… lmata 1187
1d4baaf… lmata 1188 class TestCLISubmodulesGroup:
1d4baaf… lmata 1189 """submodules group is registered on the main CLI."""
1d4baaf… lmata 1190
1d4baaf… lmata 1191 def test_submodules_group_exists(self):
1d4baaf… lmata 1192 from click.testing import CliRunner
1d4baaf… lmata 1193
1d4baaf… lmata 1194 from navegador.cli.commands import main
1d4baaf… lmata 1195
1d4baaf… lmata 1196 runner = CliRunner()
1d4baaf… lmata 1197 result = runner.invoke(main, ["submodules", "--help"])
1d4baaf… lmata 1198 assert result.exit_code == 0
1d4baaf… lmata 1199
1d4baaf… lmata 1200 def test_submodules_list_empty(self, tmp_path):
1d4baaf… lmata 1201 from click.testing import CliRunner
1d4baaf… lmata 1202
1d4baaf… lmata 1203 from navegador.cli.commands import main
1d4baaf… lmata 1204
1d4baaf… lmata 1205 runner = CliRunner()
1d4baaf… lmata 1206 result = runner.invoke(main, ["submodules", "list", str(tmp_path)])
1d4baaf… lmata 1207 assert result.exit_code == 0
1d4baaf… lmata 1208 assert "No submodules" in result.output
1d4baaf… lmata 1209
1d4baaf… lmata 1210
1d4baaf… lmata 1211 class TestCLIWorkspaceGroup:
1d4baaf… lmata 1212 """workspace group is registered on the main CLI."""
1d4baaf… lmata 1213
1d4baaf… lmata 1214 def test_workspace_group_exists(self):
1d4baaf… lmata 1215 from click.testing import CliRunner
1d4baaf… lmata 1216
1d4baaf… lmata 1217 from navegador.cli.commands import main
1d4baaf… lmata 1218
1d4baaf… lmata 1219 runner = CliRunner()
1d4baaf… lmata 1220 result = runner.invoke(main, ["workspace", "--help"])
1d4baaf… lmata 1221 assert result.exit_code == 0
1d4baaf… lmata 1222 assert "ingest" in result.output
1d4baaf… lmata 1223
1d4baaf… lmata 1224 def test_workspace_ingest_requires_repos(self, tmp_path):
1d4baaf… lmata 1225 from click.testing import CliRunner
1d4baaf… lmata 1226
1d4baaf… lmata 1227 from navegador.cli.commands import main
1d4baaf… lmata 1228
1d4baaf… lmata 1229 runner = CliRunner()
1d4baaf… lmata 1230 result = runner.invoke(
1d4baaf… lmata 1231 main,
1d4baaf… lmata 1232 ["workspace", "ingest", "--db", str(tmp_path / "g.db")],
1d4baaf… lmata 1233 )
1d4baaf… lmata 1234 assert result.exit_code != 0

Keyboard Shortcuts

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