|
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 |