Navegador

navegador / tests / test_enrichment_base.py
Source Blame History 413 lines
8c1f142… lmata 1 """Tests for navegador.enrichment — EnrichmentResult, FrameworkEnricher, and CLI."""
8c1f142… lmata 2
8c1f142… lmata 3 import json
8c1f142… lmata 4 from unittest.mock import MagicMock, call, patch
8c1f142… lmata 5
8c1f142… lmata 6 import pytest
8c1f142… lmata 7 from click.testing import CliRunner
8c1f142… lmata 8
8c1f142… lmata 9 from navegador.enrichment import EnrichmentResult, FrameworkEnricher
8c1f142… lmata 10 from navegador.enrichment.base import FrameworkEnricher as FrameworkEnricherBase
8c1f142… lmata 11 from navegador.graph.store import GraphStore
8c1f142… lmata 12
8c1f142… lmata 13
8c1f142… lmata 14 # ── Helpers ───────────────────────────────────────────────────────────────────
8c1f142… lmata 15
8c1f142… lmata 16
8c1f142… lmata 17 def _mock_store(result_set=None):
8c1f142… lmata 18 """Return a GraphStore backed by a mock FalkorDB graph."""
8c1f142… lmata 19 client = MagicMock()
8c1f142… lmata 20 graph = MagicMock()
8c1f142… lmata 21 graph.query.return_value = MagicMock(result_set=result_set)
8c1f142… lmata 22 client.select_graph.return_value = graph
8c1f142… lmata 23 store = GraphStore(client)
8c1f142… lmata 24 return store
8c1f142… lmata 25
8c1f142… lmata 26
8c1f142… lmata 27 class MockEnricher(FrameworkEnricher):
8c1f142… lmata 28 """Concrete enricher used in tests."""
8c1f142… lmata 29
8c1f142… lmata 30 @property
8c1f142… lmata 31 def framework_name(self) -> str:
8c1f142… lmata 32 return "mock"
8c1f142… lmata 33
8c1f142… lmata 34 @property
8c1f142… lmata 35 def detection_patterns(self) -> list[str]:
8c1f142… lmata 36 return ["mock_module", "mock_settings.py"]
8c1f142… lmata 37
8c1f142… lmata 38 def enrich(self) -> EnrichmentResult:
8c1f142… lmata 39 result = EnrichmentResult()
8c1f142… lmata 40 result.promoted = 3
8c1f142… lmata 41 result.edges_added = 2
8c1f142… lmata 42 result.patterns_found = {"mock_view": 3, "mock_model": 0}
8c1f142… lmata 43 return result
8c1f142… lmata 44
8c1f142… lmata 45
8c1f142… lmata 46 # ── EnrichmentResult defaults ─────────────────────────────────────────────────
8c1f142… lmata 47
8c1f142… lmata 48
8c1f142… lmata 49 class TestEnrichmentResult:
8c1f142… lmata 50 def test_promoted_defaults_to_zero(self):
8c1f142… lmata 51 r = EnrichmentResult()
8c1f142… lmata 52 assert r.promoted == 0
8c1f142… lmata 53
8c1f142… lmata 54 def test_edges_added_defaults_to_zero(self):
8c1f142… lmata 55 r = EnrichmentResult()
8c1f142… lmata 56 assert r.edges_added == 0
8c1f142… lmata 57
8c1f142… lmata 58 def test_patterns_found_defaults_to_empty_dict(self):
8c1f142… lmata 59 r = EnrichmentResult()
8c1f142… lmata 60 assert r.patterns_found == {}
8c1f142… lmata 61
8c1f142… lmata 62 def test_attributes_are_mutable(self):
8c1f142… lmata 63 r = EnrichmentResult()
8c1f142… lmata 64 r.promoted = 5
8c1f142… lmata 65 r.edges_added = 10
8c1f142… lmata 66 r.patterns_found["view"] = 7
8c1f142… lmata 67 assert r.promoted == 5
8c1f142… lmata 68 assert r.edges_added == 10
8c1f142… lmata 69 assert r.patterns_found["view"] == 7
8c1f142… lmata 70
8c1f142… lmata 71 def test_instances_are_independent(self):
8c1f142… lmata 72 r1 = EnrichmentResult()
8c1f142… lmata 73 r2 = EnrichmentResult()
8c1f142… lmata 74 r1.patterns_found["x"] = 1
8c1f142… lmata 75 assert "x" not in r2.patterns_found
8c1f142… lmata 76
8c1f142… lmata 77
8c1f142… lmata 78 # ── FrameworkEnricher.detect() ────────────────────────────────────────────────
8c1f142… lmata 79
8c1f142… lmata 80
8c1f142… lmata 81 class TestDetect:
8c1f142… lmata 82 def test_returns_true_when_pattern_matches(self):
8c1f142… lmata 83 store = _mock_store(result_set=[[1]])
8c1f142… lmata 84 enricher = MockEnricher(store)
8c1f142… lmata 85 assert enricher.detect() is True
8c1f142… lmata 86
8c1f142… lmata 87 def test_returns_false_when_no_match(self):
8c1f142… lmata 88 store = _mock_store(result_set=[[0]])
8c1f142… lmata 89 enricher = MockEnricher(store)
8c1f142… lmata 90 assert enricher.detect() is False
8c1f142… lmata 91
8c1f142… lmata 92 def test_returns_false_when_result_set_is_empty(self):
8c1f142… lmata 93 store = _mock_store(result_set=[])
8c1f142… lmata 94 enricher = MockEnricher(store)
8c1f142… lmata 95 assert enricher.detect() is False
8c1f142… lmata 96
8c1f142… lmata 97 def test_returns_false_when_result_set_is_none(self):
8c1f142… lmata 98 store = _mock_store(result_set=None)
8c1f142… lmata 99 enricher = MockEnricher(store)
8c1f142… lmata 100 assert enricher.detect() is False
8c1f142… lmata 101
8c1f142… lmata 102 def test_returns_true_on_second_pattern_if_first_misses(self):
8c1f142… lmata 103 """detect() short-circuits on the first positive match, but we verify
8c1f142… lmata 104 it tries subsequent patterns when earlier ones return zero."""
8c1f142… lmata 105 call_count = 0
8c1f142… lmata 106
8c1f142… lmata 107 def _side_effect(cypher, params):
8c1f142… lmata 108 nonlocal call_count
8c1f142… lmata 109 call_count += 1
8c1f142… lmata 110 # First pattern returns 0, second returns 1
8c1f142… lmata 111 count = 1 if call_count >= 2 else 0
8c1f142… lmata 112 return MagicMock(result_set=[[count]])
8c1f142… lmata 113
8c1f142… lmata 114 client = MagicMock()
8c1f142… lmata 115 graph = MagicMock()
8c1f142… lmata 116 graph.query.side_effect = _side_effect
8c1f142… lmata 117 client.select_graph.return_value = graph
8c1f142… lmata 118 store = GraphStore(client)
8c1f142… lmata 119
8c1f142… lmata 120 enricher = MockEnricher(store)
8c1f142… lmata 121 assert enricher.detect() is True
8c1f142… lmata 122 assert call_count == 2
8c1f142… lmata 123
8c1f142… lmata 124 def test_detect_queries_each_pattern_with_correct_param(self):
8c1f142… lmata 125 store = _mock_store(result_set=[[0]])
8c1f142… lmata 126 enricher = MockEnricher(store)
8c1f142… lmata 127 enricher.detect()
8c1f142… lmata 128
8c1f142… lmata 129 calls = store._graph.query.call_args_list
8c1f142… lmata 130 # Two patterns → two queries
8c1f142… lmata 131 assert len(calls) == 2
8c1f142… lmata 132 params0 = calls[0][0][1] if len(calls[0][0]) > 1 else calls[0][1].get("params", {})
8c1f142… lmata 133 params1 = calls[1][0][1] if len(calls[1][0]) > 1 else calls[1][1].get("params", {})
2c266d2… lmata 134 assert params0 == {"name": "mock_module"}
2c266d2… lmata 135 assert params1 == {"name": "mock_settings.py"}
8c1f142… lmata 136
8c1f142… lmata 137 def test_stops_early_when_first_pattern_matches(self):
8c1f142… lmata 138 store = _mock_store(result_set=[[5]])
8c1f142… lmata 139 enricher = MockEnricher(store)
8c1f142… lmata 140 assert enricher.detect() is True
8c1f142… lmata 141 # Should only query once (short-circuit on first match)
8c1f142… lmata 142 assert store._graph.query.call_count == 1
8c1f142… lmata 143
8c1f142… lmata 144
8c1f142… lmata 145 # ── FrameworkEnricher._promote_node() ────────────────────────────────────────
8c1f142… lmata 146
8c1f142… lmata 147
8c1f142… lmata 148 class TestPromoteNode:
8c1f142… lmata 149 def test_calls_store_query_with_correct_cypher_and_params(self):
8c1f142… lmata 150 store = _mock_store()
8c1f142… lmata 151 enricher = MockEnricher(store)
8c1f142… lmata 152 enricher._promote_node("MyView", "app/views.py", "DjangoView")
8c1f142… lmata 153
8c1f142… lmata 154 store._graph.query.assert_called_once()
8c1f142… lmata 155 cypher, params = store._graph.query.call_args[0]
8c1f142… lmata 156 assert "SET n.semantic_type = $semantic_type" in cypher
8c1f142… lmata 157 assert "n.name = $name" in cypher
8c1f142… lmata 158 assert "n.file_path = $file_path" in cypher
8c1f142… lmata 159 assert params["name"] == "MyView"
8c1f142… lmata 160 assert params["file_path"] == "app/views.py"
8c1f142… lmata 161 assert params["semantic_type"] == "DjangoView"
8c1f142… lmata 162
8c1f142… lmata 163 def test_extra_props_appended_to_set_clause(self):
8c1f142… lmata 164 store = _mock_store()
8c1f142… lmata 165 enricher = MockEnricher(store)
8c1f142… lmata 166 enricher._promote_node(
8c1f142… lmata 167 "MyModel", "app/models.py", "DjangoModel", props={"table": "my_table"}
8c1f142… lmata 168 )
8c1f142… lmata 169
8c1f142… lmata 170 cypher, params = store._graph.query.call_args[0]
8c1f142… lmata 171 assert "n.table = $table" in cypher
8c1f142… lmata 172 assert params["table"] == "my_table"
8c1f142… lmata 173
8c1f142… lmata 174 def test_no_extra_props_produces_clean_cypher(self):
8c1f142… lmata 175 store = _mock_store()
8c1f142… lmata 176 enricher = MockEnricher(store)
8c1f142… lmata 177 enricher._promote_node("Fn", "a.py", "endpoint")
8c1f142… lmata 178
8c1f142… lmata 179 cypher, _ = store._graph.query.call_args[0]
8c1f142… lmata 180 # Should not have a trailing comma or extra SET clause pieces
8c1f142… lmata 181 assert cypher.count("SET") == 1
8c1f142… lmata 182 assert "None" not in cypher
8c1f142… lmata 183
8c1f142… lmata 184 def test_multiple_extra_props(self):
8c1f142… lmata 185 store = _mock_store()
8c1f142… lmata 186 enricher = MockEnricher(store)
8c1f142… lmata 187 enricher._promote_node(
8c1f142… lmata 188 "Router", "routes.py", "FastAPIRouter", props={"prefix": "/api", "version": "v1"}
8c1f142… lmata 189 )
8c1f142… lmata 190
8c1f142… lmata 191 cypher, params = store._graph.query.call_args[0]
8c1f142… lmata 192 assert "n.prefix = $prefix" in cypher
8c1f142… lmata 193 assert "n.version = $version" in cypher
8c1f142… lmata 194 assert params["prefix"] == "/api"
8c1f142… lmata 195 assert params["version"] == "v1"
8c1f142… lmata 196
8c1f142… lmata 197
8c1f142… lmata 198 # ── FrameworkEnricher._add_semantic_edge() ────────────────────────────────────
8c1f142… lmata 199
8c1f142… lmata 200
8c1f142… lmata 201 class TestAddSemanticEdge:
8c1f142… lmata 202 def test_calls_store_query_with_correct_cypher_and_params(self):
8c1f142… lmata 203 store = _mock_store()
8c1f142… lmata 204 enricher = MockEnricher(store)
8c1f142… lmata 205 enricher._add_semantic_edge("UserView", "HANDLES", "UserModel")
8c1f142… lmata 206
8c1f142… lmata 207 store._graph.query.assert_called_once()
8c1f142… lmata 208 cypher, params = store._graph.query.call_args[0]
8c1f142… lmata 209 assert "MERGE (a)-[r:HANDLES]->(b)" in cypher
8c1f142… lmata 210 assert "a.name = $from_name" in cypher
8c1f142… lmata 211 assert "b.name = $to_name" in cypher
8c1f142… lmata 212 assert params["from_name"] == "UserView"
8c1f142… lmata 213 assert params["to_name"] == "UserModel"
8c1f142… lmata 214
8c1f142… lmata 215 def test_extra_props_produce_set_clause(self):
8c1f142… lmata 216 store = _mock_store()
8c1f142… lmata 217 enricher = MockEnricher(store)
8c1f142… lmata 218 enricher._add_semantic_edge(
8c1f142… lmata 219 "ViewA", "CALLS", "ViewB", props={"weight": 1, "layer": "http"}
8c1f142… lmata 220 )
8c1f142… lmata 221
8c1f142… lmata 222 cypher, params = store._graph.query.call_args[0]
8c1f142… lmata 223 assert "SET" in cypher
8c1f142… lmata 224 assert "r.weight = $weight" in cypher
8c1f142… lmata 225 assert "r.layer = $layer" in cypher
8c1f142… lmata 226 assert params["weight"] == 1
8c1f142… lmata 227 assert params["layer"] == "http"
8c1f142… lmata 228
8c1f142… lmata 229 def test_no_extra_props_no_set_clause(self):
8c1f142… lmata 230 store = _mock_store()
8c1f142… lmata 231 enricher = MockEnricher(store)
8c1f142… lmata 232 enricher._add_semantic_edge("A", "LINKS", "B")
8c1f142… lmata 233
8c1f142… lmata 234 cypher, _ = store._graph.query.call_args[0]
8c1f142… lmata 235 assert "SET" not in cypher
8c1f142… lmata 236
8c1f142… lmata 237 def test_edge_type_is_interpolated(self):
8c1f142… lmata 238 store = _mock_store()
8c1f142… lmata 239 enricher = MockEnricher(store)
8c1f142… lmata 240 enricher._add_semantic_edge("X", "CUSTOM_EDGE_TYPE", "Y")
8c1f142… lmata 241
8c1f142… lmata 242 cypher, _ = store._graph.query.call_args[0]
8c1f142… lmata 243 assert "CUSTOM_EDGE_TYPE" in cypher
8c1f142… lmata 244
8c1f142… lmata 245
8c1f142… lmata 246 # ── MockEnricher.enrich() contract ────────────────────────────────────────────
8c1f142… lmata 247
8c1f142… lmata 248
8c1f142… lmata 249 class TestMockEnricherEnrich:
8c1f142… lmata 250 def test_returns_enrichment_result(self):
8c1f142… lmata 251 store = _mock_store()
8c1f142… lmata 252 enricher = MockEnricher(store)
8c1f142… lmata 253 result = enricher.enrich()
8c1f142… lmata 254 assert isinstance(result, EnrichmentResult)
8c1f142… lmata 255
8c1f142… lmata 256 def test_result_values(self):
8c1f142… lmata 257 store = _mock_store()
8c1f142… lmata 258 enricher = MockEnricher(store)
8c1f142… lmata 259 result = enricher.enrich()
8c1f142… lmata 260 assert result.promoted == 3
8c1f142… lmata 261 assert result.edges_added == 2
8c1f142… lmata 262 assert result.patterns_found == {"mock_view": 3, "mock_model": 0}
8c1f142… lmata 263
8c1f142… lmata 264 def test_framework_name(self):
8c1f142… lmata 265 store = _mock_store()
8c1f142… lmata 266 assert MockEnricher(store).framework_name == "mock"
8c1f142… lmata 267
8c1f142… lmata 268 def test_detection_patterns(self):
8c1f142… lmata 269 store = _mock_store()
8c1f142… lmata 270 patterns = MockEnricher(store).detection_patterns
8c1f142… lmata 271 assert "mock_module" in patterns
8c1f142… lmata 272 assert "mock_settings.py" in patterns
8c1f142… lmata 273
8c1f142… lmata 274
8c1f142… lmata 275 # ── Abstract enforcement ───────────────────────────────────────────────────────
8c1f142… lmata 276
8c1f142… lmata 277
8c1f142… lmata 278 class TestAbstractEnforcement:
8c1f142… lmata 279 def test_cannot_instantiate_base_class_directly(self):
8c1f142… lmata 280 store = _mock_store()
8c1f142… lmata 281 with pytest.raises(TypeError):
8c1f142… lmata 282 FrameworkEnricher(store) # type: ignore[abstract]
8c1f142… lmata 283
8c1f142… lmata 284 def test_subclass_missing_framework_name_raises(self):
8c1f142… lmata 285 with pytest.raises(TypeError):
8c1f142… lmata 286
8c1f142… lmata 287 class Incomplete(FrameworkEnricher):
8c1f142… lmata 288 @property
8c1f142… lmata 289 def detection_patterns(self):
8c1f142… lmata 290 return []
8c1f142… lmata 291
8c1f142… lmata 292 def enrich(self):
8c1f142… lmata 293 return EnrichmentResult()
8c1f142… lmata 294
8c1f142… lmata 295 Incomplete(_mock_store())
8c1f142… lmata 296
8c1f142… lmata 297 def test_subclass_missing_detection_patterns_raises(self):
8c1f142… lmata 298 with pytest.raises(TypeError):
8c1f142… lmata 299
8c1f142… lmata 300 class Incomplete(FrameworkEnricher):
8c1f142… lmata 301 @property
8c1f142… lmata 302 def framework_name(self):
8c1f142… lmata 303 return "x"
8c1f142… lmata 304
8c1f142… lmata 305 def enrich(self):
8c1f142… lmata 306 return EnrichmentResult()
8c1f142… lmata 307
8c1f142… lmata 308 Incomplete(_mock_store())
8c1f142… lmata 309
8c1f142… lmata 310 def test_subclass_missing_enrich_raises(self):
8c1f142… lmata 311 with pytest.raises(TypeError):
8c1f142… lmata 312
8c1f142… lmata 313 class Incomplete(FrameworkEnricher):
8c1f142… lmata 314 @property
8c1f142… lmata 315 def framework_name(self):
8c1f142… lmata 316 return "x"
8c1f142… lmata 317
8c1f142… lmata 318 @property
8c1f142… lmata 319 def detection_patterns(self):
8c1f142… lmata 320 return []
8c1f142… lmata 321
8c1f142… lmata 322 Incomplete(_mock_store())
8c1f142… lmata 323
8c1f142… lmata 324
8c1f142… lmata 325 # ── CLI: navegador enrich ──────────────────────────────────────────────────────
8c1f142… lmata 326
8c1f142… lmata 327
8c1f142… lmata 328 class TestEnrichCLI:
8c1f142… lmata 329 def _runner(self):
8c1f142… lmata 330 return CliRunner()
8c1f142… lmata 331
8c1f142… lmata 332 def test_no_frameworks_detected_message(self):
8c1f142… lmata 333 from navegador.cli.commands import main
8c1f142… lmata 334
8c1f142… lmata 335 runner = self._runner()
8c1f142… lmata 336 with patch("navegador.cli.commands._get_store", return_value=_mock_store()):
8c1f142… lmata 337 # No enricher modules in package yet — message should appear
8c1f142… lmata 338 result = runner.invoke(main, ["enrich"])
8c1f142… lmata 339 assert result.exit_code == 0
8c1f142… lmata 340 assert "No frameworks detected" in result.output
8c1f142… lmata 341
8c1f142… lmata 342 def test_unknown_framework_exits_nonzero(self):
8c1f142… lmata 343 from navegador.cli.commands import main
8c1f142… lmata 344
8c1f142… lmata 345 runner = self._runner()
8c1f142… lmata 346 with patch("navegador.cli.commands._get_store", return_value=_mock_store()):
8c1f142… lmata 347 result = runner.invoke(main, ["enrich", "--framework", "nonexistent_xyz"])
8c1f142… lmata 348 assert result.exit_code != 0
8c1f142… lmata 349
8c1f142… lmata 350 def test_json_flag_produces_empty_object_when_no_frameworks(self):
8c1f142… lmata 351 from navegador.cli.commands import main
8c1f142… lmata 352
8c1f142… lmata 353 runner = self._runner()
8c1f142… lmata 354 with patch("navegador.cli.commands._get_store", return_value=_mock_store()):
8c1f142… lmata 355 result = runner.invoke(main, ["enrich", "--json"])
8c1f142… lmata 356 assert result.exit_code == 0
8c1f142… lmata 357 data = json.loads(result.output)
8c1f142… lmata 358 assert isinstance(data, dict)
8c1f142… lmata 359
8c1f142… lmata 360 def test_enrich_command_exists_in_main_group(self):
8c1f142… lmata 361 from navegador.cli.commands import main
8c1f142… lmata 362
8c1f142… lmata 363 assert "enrich" in main.commands
8c1f142… lmata 364
8c1f142… lmata 365 def test_enrich_runs_enricher_when_framework_registered(self):
8c1f142… lmata 366 """Patch the enrichment package discovery to inject MockEnricher."""
8c1f142… lmata 367 import pkgutil
8c1f142… lmata 368
8c1f142… lmata 369 from navegador.cli.commands import main
8c1f142… lmata 370
8c1f142… lmata 371 runner = self._runner()
8c1f142… lmata 372 store = _mock_store(result_set=[[1]])
8c1f142… lmata 373
8c1f142… lmata 374 fake_module = MagicMock()
8c1f142… lmata 375 fake_module.MockEnricher = MockEnricher
8c1f142… lmata 376
8c1f142… lmata 377 def fake_iter_modules(path):
8c1f142… lmata 378 yield MagicMock(), "mock_framework", False
8c1f142… lmata 379
8c1f142… lmata 380 with patch("navegador.cli.commands._get_store", return_value=store), \
8c1f142… lmata 381 patch("pkgutil.iter_modules", side_effect=fake_iter_modules), \
8c1f142… lmata 382 patch("importlib.import_module", return_value=fake_module):
8c1f142… lmata 383 result = runner.invoke(main, ["enrich", "--framework", "mock"])
8c1f142… lmata 384
8c1f142… lmata 385 assert result.exit_code == 0
8c1f142… lmata 386 assert "mock" in result.output.lower()
8c1f142… lmata 387
8c1f142… lmata 388 def test_enrich_json_output_structure(self):
8c1f142… lmata 389 """Verify JSON output shape when an enricher runs."""
8c1f142… lmata 390 import pkgutil
8c1f142… lmata 391
8c1f142… lmata 392 from navegador.cli.commands import main
8c1f142… lmata 393
8c1f142… lmata 394 runner = self._runner()
8c1f142… lmata 395 store = _mock_store(result_set=[[1]])
8c1f142… lmata 396
8c1f142… lmata 397 fake_module = MagicMock()
8c1f142… lmata 398 fake_module.MockEnricher = MockEnricher
8c1f142… lmata 399
8c1f142… lmata 400 def fake_iter_modules(path):
8c1f142… lmata 401 yield MagicMock(), "mock_framework", False
8c1f142… lmata 402
8c1f142… lmata 403 with patch("navegador.cli.commands._get_store", return_value=store), \
8c1f142… lmata 404 patch("pkgutil.iter_modules", side_effect=fake_iter_modules), \
8c1f142… lmata 405 patch("importlib.import_module", return_value=fake_module):
8c1f142… lmata 406 result = runner.invoke(main, ["enrich", "--framework", "mock", "--json"])
8c1f142… lmata 407
8c1f142… lmata 408 assert result.exit_code == 0
8c1f142… lmata 409 data = json.loads(result.output)
8c1f142… lmata 410 assert "mock" in data
8c1f142… lmata 411 assert data["mock"]["promoted"] == 3
8c1f142… lmata 412 assert data["mock"]["edges_added"] == 2
8c1f142… lmata 413 assert isinstance(data["mock"]["patterns_found"], dict)

Keyboard Shortcuts

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