Navegador

feat: Rails, Spring Boot, and Laravel framework enrichers Rails: controllers, models, routes, jobs, concerns. Spring: controllers, services, repositories, entities. Laravel: controllers, models, routes, jobs, policies. Closes #14, closes #13, closes #12

lmata 2026-03-23 05:21 trunk
Commit 93cfcc4a412c6c69df981bbd441a4cd020621c52e6c264b7c030bb7ac0a3b1da
--- a/navegador/enrichment/laravel.py
+++ b/navegador/enrichment/laravel.py
@@ -0,0 +1,21 @@
1
+artisan", "Illumin"]
2
+
3
+ def enrich(self) -> EnrichmentResult:
4
+ result = EnrichmentResult()
5
+
6
+ # Path-based promotions: (file_path_fragment, semantic_type, pattern_key)
7
+ path_promotions = [
8
+ ("Controllers/", "LaravelController", "controllers"),
9
+ ("routes/", "LaravelRoute", "routes"),
10
+ ("Jobs/", "LaravelJob", "jobs"),
11
+ ("Policies/", "LaravelPolicy", "policies"),
12
+ ]
13
+
14
+ for path_fragment, semantic_type, pattern_key in path_promotions:
15
+ query_result = self.store.query(
16
+ "MATCH (n) WHERE n.file_path IS NOT NULL "
17
+ "AND n.file_path CONTAINS $fragment "
18
+ "RETURN n.name, n.file_path",
19
+ {"fragment": path_fragment},
20
+ )
21
+
--- a/navegador/enrichment/laravel.py
+++ b/navegador/enrichment/laravel.py
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/enrichment/laravel.py
+++ b/navegador/enrichment/laravel.py
@@ -0,0 +1,21 @@
1 artisan", "Illumin"]
2
3 def enrich(self) -> EnrichmentResult:
4 result = EnrichmentResult()
5
6 # Path-based promotions: (file_path_fragment, semantic_type, pattern_key)
7 path_promotions = [
8 ("Controllers/", "LaravelController", "controllers"),
9 ("routes/", "LaravelRoute", "routes"),
10 ("Jobs/", "LaravelJob", "jobs"),
11 ("Policies/", "LaravelPolicy", "policies"),
12 ]
13
14 for path_fragment, semantic_type, pattern_key in path_promotions:
15 query_result = self.store.query(
16 "MATCH (n) WHERE n.file_path IS NOT NULL "
17 "AND n.file_path CONTAINS $fragment "
18 "RETURN n.name, n.file_path",
19 {"fragment": path_fragment},
20 )
21
--- a/navegador/enrichment/rails.py
+++ b/navegador/enrichment/rails.py
@@ -0,0 +1,52 @@
1
+"""
2
+Rails framework enricher.
3
+
4
+Promotes generic graph nodes to Rails semantic types:
5
+ - Controller (files under controllers/)
6
+ - Model (files under models/)
7
+ - Route (routes.rb)
8
+ - Job (files under jobs/)
9
+ - Concern (files under concerns/)
10
+"""
11
+
12
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
13
+
14
+
15
+class RailsEnricher(FrameworkEnricher):
16
+ """Enriches a navegador graph with Rails-specific semantics."""
17
+
18
+ @property
19
+ def framework_name(self) -> str:
20
+ return "rails"
21
+
22
+ @property
23
+ def detection_patterns(self) -> lGemfile", "config/routes.rb", "ApplicationController", "ActiveRecord]:
24
+ return ["Gemfile"]
25
+
26
+ def enrich(self) -> EnrichmentResult:
27
+ result = EnrichmentResult()
28
+
29
+ # Each tuple: (file_path_fragment, semantic_type, pattern_key)
30
+ promotions = [
31
+ ("controllers/", "RailsController", "controllers"),
32
+ ("models/", "RailsModel", "models"),
33
+ ("routes.rb", "RailsRoute", "routes"),
34
+ ("jobs/", "RailsJob", "jobs"),
35
+ ("concerns/", "RailsConcern", "concerns"),
36
+ ]
37
+
38
+ for path_fragment, semantic_type, pattern_key in promotions:
39
+ query_result = self.store.query(
40
+ "MATCH (n) WHERE n.file_path IS NOT NULL "
41
+ "AND n.file_path CONTAINS $fragment "
42
+ "RETURN n.name, n.file_path",
43
+ {"fragment": path_fragment},
44
+ )
45
+ rows = query_result.result_set or []
46
+ count = 0
47
+ for row in rows:
48
+ name, file_path = row[0], row[1]
49
+ self._promote_node(name, file_path, semantic_type)
50
+ count += 1
51
+ result.promoted += 1
52
+ result.patte
--- a/navegador/enrichment/rails.py
+++ b/navegador/enrichment/rails.py
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/enrichment/rails.py
+++ b/navegador/enrichment/rails.py
@@ -0,0 +1,52 @@
1 """
2 Rails framework enricher.
3
4 Promotes generic graph nodes to Rails semantic types:
5 - Controller (files under controllers/)
6 - Model (files under models/)
7 - Route (routes.rb)
8 - Job (files under jobs/)
9 - Concern (files under concerns/)
10 """
11
12 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
13
14
15 class RailsEnricher(FrameworkEnricher):
16 """Enriches a navegador graph with Rails-specific semantics."""
17
18 @property
19 def framework_name(self) -> str:
20 return "rails"
21
22 @property
23 def detection_patterns(self) -> lGemfile", "config/routes.rb", "ApplicationController", "ActiveRecord]:
24 return ["Gemfile"]
25
26 def enrich(self) -> EnrichmentResult:
27 result = EnrichmentResult()
28
29 # Each tuple: (file_path_fragment, semantic_type, pattern_key)
30 promotions = [
31 ("controllers/", "RailsController", "controllers"),
32 ("models/", "RailsModel", "models"),
33 ("routes.rb", "RailsRoute", "routes"),
34 ("jobs/", "RailsJob", "jobs"),
35 ("concerns/", "RailsConcern", "concerns"),
36 ]
37
38 for path_fragment, semantic_type, pattern_key in promotions:
39 query_result = self.store.query(
40 "MATCH (n) WHERE n.file_path IS NOT NULL "
41 "AND n.file_path CONTAINS $fragment "
42 "RETURN n.name, n.file_path",
43 {"fragment": path_fragment},
44 )
45 rows = query_result.result_set or []
46 count = 0
47 for row in rows:
48 name, file_path = row[0], row[1]
49 self._promote_node(name, file_path, semantic_type)
50 count += 1
51 result.promoted += 1
52 result.patte
--- a/navegador/enrichment/spring.py
+++ b/navegador/enrichment/spring.py
@@ -0,0 +1,34 @@
1
+operties", "application.yml"]
2
+
3
+ def enrich(self) -> EnrichmentResult:
4
+ result = EnrichmentResult()
5
+
6
+ # Each tuple: (annotation_fragment, semantic_type, pattern_key)
7
+ # Annotations are stored as node names or in node properties; we search
8
+ # both n.name and n.file_path for the annotation string.
9
+ promotions = [
10
+ ("@Controller", "SpringController", "controllers"),
11
+ ("@RestController", "SpringRestController", "rest_controllers"),
12
+ ("@Service", "SpringService", "services"),
13
+ ("@Repository", "SpringRepository", "repositories"),
14
+ ("@Entity", "SpringEntity", "entities"),
15
+ ]
16
+
17
+ for annotation, semantic_type, pattern_key in promotions:
18
+ query_result = self.store.query(
19
+ "MATCH (n) WHERE "
20
+ "(n.name IS NOT NULL AND n.name CONTAINS $annotation) OR "
21
+ "(n.file_path IS NOT NULL AND n.file_path CONTAINS $annotation) "
22
+ "RETURN n.name, n.file_path",
23
+ {"annotation": annotation},
24
+ )
25
+ rows = query_result.result_set or []
26
+ count = 0
27
+ for row in rows:
28
+ name, file_path = row[0], row[1]
29
+ self._promote_node(name, file_path, semantic_type)
30
+ count += 1
31
+ result.promoted += 1
32
+ result.patterns_found[pattern_key] = count
33
+
34
+ return result
--- a/navegador/enrichment/spring.py
+++ b/navegador/enrichment/spring.py
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/enrichment/spring.py
+++ b/navegador/enrichment/spring.py
@@ -0,0 +1,34 @@
1 operties", "application.yml"]
2
3 def enrich(self) -> EnrichmentResult:
4 result = EnrichmentResult()
5
6 # Each tuple: (annotation_fragment, semantic_type, pattern_key)
7 # Annotations are stored as node names or in node properties; we search
8 # both n.name and n.file_path for the annotation string.
9 promotions = [
10 ("@Controller", "SpringController", "controllers"),
11 ("@RestController", "SpringRestController", "rest_controllers"),
12 ("@Service", "SpringService", "services"),
13 ("@Repository", "SpringRepository", "repositories"),
14 ("@Entity", "SpringEntity", "entities"),
15 ]
16
17 for annotation, semantic_type, pattern_key in promotions:
18 query_result = self.store.query(
19 "MATCH (n) WHERE "
20 "(n.name IS NOT NULL AND n.name CONTAINS $annotation) OR "
21 "(n.file_path IS NOT NULL AND n.file_path CONTAINS $annotation) "
22 "RETURN n.name, n.file_path",
23 {"annotation": annotation},
24 )
25 rows = query_result.result_set or []
26 count = 0
27 for row in rows:
28 name, file_path = row[0], row[1]
29 self._promote_node(name, file_path, semantic_type)
30 count += 1
31 result.promoted += 1
32 result.patterns_found[pattern_key] = count
33
34 return result
--- a/tests/test_enrichment_laravel.py
+++ b/tests/test_enrichment_laravel.py
@@ -0,0 +1,231 @@
1
+"""Tests for navegador.enrichment.laravel — LaravelEnricher."""
2
+
3
+from unittest.mock import MagicMock
4
+
5
+import pytest
6
+
7
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
8
+from navegador.enrichment.laravel import LaravelEnricher
9
+from navegador.graph.store import GraphStore
10
+
11
+
12
+# ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+
15
+def _mock_store(result_set=None):
16
+ """Return a GraphStore backed by a mock FalkorDB graph."""
17
+ client = MagicMock()
18
+ graph = MagicMock()
19
+ graph.query.return_value = MagicMock(result_set=result_set)
20
+ client.select_graph.return_value = graph
21
+ return GraphStore(client)
22
+
23
+
24
+def _store_with_side_effect(side_effect):
25
+ """Return a GraphStore whose graph.query uses a side_effect callable."""
26
+ client = MagicMock()
27
+ graph = MagicMock()
28
+ graph.query.side_effect = side_effect
29
+ client.select_graph.return_value = graph
30
+ return GraphStore(client)
31
+
32
+
33
+# ── Identity / contract ───────────────────────────────────────────────────────
34
+
35
+
36
+class TestLaravelEnricherIdentity:
37
+ def test_framework_name(self):
38
+ store = _mock_store()
39
+ assert LaravelEnricher(store).framework_name == "laravel"
40
+
41
+ def test_is_framework_enricher_subclass(self):
42
+ assert issubclass(LaravelEnricher, FrameworkEnricher)
43
+
44
+ def test_detection_patterns_contains_t_detection_files_contains_artisan(self):
45
+ store = _mock_store()
46
+ lEnricher(store).detection_patterr)
47
+
48
+ def test_detection_patterns_contains_illuminate(self):
49
+ store = _mock_store()
50
+ assert "Illuminate" in LaravelEnricher(store).detection_patterpatterns_contains_http_controllers(self):
51
+ store = _mock_store()
52
+ assert [["UserControle()
53
+ lEnricher(store).detection_patterpatterns_has_three_entries(self):
54
+ store = _mock_store()
55
+ assert len(Laravepatterns) == 3
56
+
57
+
58
+# ── enrich() return type ──────────────────────────────────────────────────────
59
+
60
+
61
+class TestLaravelEnricherEnrichReturnType:
62
+ def test_returns_enrichment_result(self):
63
+ store = _mock_store(result_set=[])
64
+ result = LaravelEnricher(store).enrich()
65
+ assert isinstance(result, EnrichmentResult)
66
+
67
+ def test_result_has_promoted_attribute(self):
68
+ store = _mock_store(result_set=[])
69
+ result = LaravelEnricher(store).enrich()
70
+ assert hasattr(result, "promoted")
71
+
72
+ def test_result_has_edges_added_attribute(self):
73
+ store = _mock_store(result_set=[])
74
+ result = LaravelEnricher(store).enrich()
75
+ assert hasattr(result, "edges_added")
76
+
77
+ def test_result_has_patterns_found_attribute(self):
78
+ store = _mock_store(result_set=[])
79
+ result = LaravelEnricher(store).enrich()
80
+ assert hasattr(result, "patterns_found")
81
+
82
+
83
+# ── enrich() with no matching nodes ──────────────────────────────────────────
84
+
85
+
86
+class TestLaravelEnricherNoMatches:
87
+ def test_promoted_is_zero_when_no_nodes(self):
88
+ store = _mock_store(result_set=[])
89
+ result = LaravelEnricher(store).enrich()
90
+ assert result.promoted == 0
91
+
92
+ def test_all_pattern_counts_zero_when_no_nodes(self):
93
+ store = _mock_store(result_set=[])
94
+ result = LaravelEnricher(store).enrich()
95
+ for key in ("controllers", "routes", "jobs", "policies", "models"):
96
+ assert result.patterns_found[key] == 0
97
+
98
+ def test_patterns_found_has_five_keys(self):
99
+ store = _mock_store(result_set=[])
100
+ result = LaravelEnricher(store).enrich()
101
+ assert set(result.patterns_found.keys()) == {
102
+ "controllers", "routes", "jobs", "policies", "models"
103
+ }
104
+
105
+
106
+# ── enrich() with matching nodes ─────────────────────────────────────────────
107
+
108
+
109
+class TestLaravelEnricherWithMatches:
110
+ def _make_store_for_fragment(self, target_fragment, rows):
111
+ """Return a store that returns `rows` only when the fragment matches."""
112
+
113
+ def side_effect(cypher, params):
114
+ fragment = params.get("fragment", "")
115
+ if fragment == target_fragment:
116
+ return MagicMock(result_set=rows)
117
+ return MagicMock(result_set=[])
118
+
119
+ return _store_with_side_effect(side_effect)
120
+
121
+ def test_controller_promoted(self):
122
+ store = self._make_store_for_fragment(
123
+ "Controllers/",
124
+ [["UserController", "app/Http/Controllers/UserController.php"]],
125
+ )
126
+ result = LaravelEnricher(store).enrich()
127
+ assert result.patterns_found["controllers"] == 1
128
+ assert result.promoted >= 1
129
+
130
+ def test_route_promoted(self):
131
+ store = self._make_store_for_fragment(
132
+ "routes/",
133
+ [["web", "routes/web.php"]],
134
+ )
135
+ result = LaravelEnricher(store).enrich()
136
+ assert result.patterns_found["routes"] == 1
137
+ assert result.promoted >= 1
138
+
139
+ def test_job_promoted(self):
140
+ store = self._make_store_for_fragment(
141
+ "Jobs/",
142
+ [["SendEmailJob", "app/Jobs/SendEmailJob.php"]],
143
+ )
144
+ result = LaravelEnricher(store).enrich()
145
+ assert result.patterns_found["jobs"] == 1
146
+ assert result.promoted >= 1
147
+
148
+ def test_policy_promoted(self):
149
+ store = self._make_store_for_fragment(
150
+ "Policies/",
151
+ [["UserPolicy", "app/Policies/UserPolicy.php"]],
152
+ )
153
+ result = LaravelEnricher(store).enrich()
154
+ assert result.patterns_found["policies"] == 1
155
+ assert result.promoted >= 1
156
+
157
+ def test_model_promoted_via_model_keyword(self):
158
+ store = self._make_store_for_fragment(
159
+ "Model",
160
+ [["User", "app/Models/User.php"]],
161
+ )
162
+ result = LaravelEnricher(store).enrich()
163
+ assert result.patterns_found["models"] == 1
164
+ assert result.promoted >= 1
165
+
166
+ def test_promoted_count_accumulates_across_types(self):
167
+ rows_map = {
168
+ "Controllers/": [
169
+ ["UserController", "app/Http/Controllers/UserController.php"],
170
+ ["PostController", "app/Http/Controllers/PostController.php"],
171
+ ],
172
+ "routes/": [["web", "routes/web.php"]],
173
+ "Jobs/": [],
174
+ "Policies/": [["UserPolicy", "app/Policies/UserPolicy.php"]],
175
+ "Model": [],
176
+ }
177
+
178
+ def side_effect(cypher, params):
179
+ fragment = params.get("fragment", "")
180
+ return MagicMock(result_set=rows_map.get(fragment, []))
181
+
182
+ store = _store_with_side_effect(side_effect)
183
+ result = LaravelEnricher(store).enrich()
184
+ assert result.promoted == 4
185
+ assert result.patterns_found["controllers"] == 2
186
+ assert result.patterns_found["routes"] == 1
187
+ assert result.patterns_found["policies"] == 1
188
+ assert result.patterns_found["jobs"] == 0
189
+ assert result.patterns_found["models"] == 0
190
+
191
+ def test_promote_node_called_with_laravel_controller_type(self):
192
+ store = self._make_store_for_fragment(
193
+ "Controllers/",
194
+ [["UserController", "app/Http/Controllers/UserController.php"]],
195
+ )
196
+ LaravelEnricher(store).enrich()
197
+ calls = [str(c) for c in store._graph.query.call_args_list]
198
+ promote_calls = [c for c in calls if "semantic_type" in c and "LaravelController" in c]
199
+ assert len(promote_calls) >= 1
200
+
201
+ def test_promote_node_called_with_laravel_model_type(self):
202
+ store = self._make_store_for_fragment(
203
+ "Model",
204
+ [["User", "app/Models/User.php"]],
205
+ )
206
+ LaravelEnricher(store).enrich()
207
+ calls = [str(c) for c in store._graph.query.call_args_list]
208
+ promote_calls = [c for c in calls if "semantic_type" in c and "LaravelModel" in c]
209
+ assert len(promote_calls) >= 1
210
+
211
+ def test_promote_node_called_with_laravel_route_type(self):
212
+ store = self._make_store_for_fragment(
213
+ "routes/",
214
+ [["web", "routes/web.php"]],
215
+ )
216
+ LaravelEnricher(store).enrich()
217
+ calls = [str(c) for c in store._graph.query.call_args_list]
218
+ promote_calls = [c for c in calls if "semantic_type" in c and "LaravelRoute" in c]
219
+ assert len(promote_calls) >= 1
220
+
221
+ def test_promote_node_called_with_laravel_job_type(self):
222
+ store = self._make_store_for_fragment(
223
+ "Jobs/",
224
+ [["SendEmailJob", "app/Jobs/SendEmailJob.php"]],
225
+ )
226
+ LaravelEnricher(store).enrich()
227
+ calls = [str(c) for c in store._graph.query.call_args_list]
228
+ promote_calls = [c for c in calls if "semantic_type" in c and "LaravelJob" in c]
229
+ assert len(promote_calls) >= 1
230
+
231
+ def
--- a/tests/test_enrichment_laravel.py
+++ b/tests/test_enrichment_laravel.py
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_enrichment_laravel.py
+++ b/tests/test_enrichment_laravel.py
@@ -0,0 +1,231 @@
1 """Tests for navegador.enrichment.laravel — LaravelEnricher."""
2
3 from unittest.mock import MagicMock
4
5 import pytest
6
7 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
8 from navegador.enrichment.laravel import LaravelEnricher
9 from navegador.graph.store import GraphStore
10
11
12 # ── Helpers ───────────────────────────────────────────────────────────────────
13
14
15 def _mock_store(result_set=None):
16 """Return a GraphStore backed by a mock FalkorDB graph."""
17 client = MagicMock()
18 graph = MagicMock()
19 graph.query.return_value = MagicMock(result_set=result_set)
20 client.select_graph.return_value = graph
21 return GraphStore(client)
22
23
24 def _store_with_side_effect(side_effect):
25 """Return a GraphStore whose graph.query uses a side_effect callable."""
26 client = MagicMock()
27 graph = MagicMock()
28 graph.query.side_effect = side_effect
29 client.select_graph.return_value = graph
30 return GraphStore(client)
31
32
33 # ── Identity / contract ───────────────────────────────────────────────────────
34
35
36 class TestLaravelEnricherIdentity:
37 def test_framework_name(self):
38 store = _mock_store()
39 assert LaravelEnricher(store).framework_name == "laravel"
40
41 def test_is_framework_enricher_subclass(self):
42 assert issubclass(LaravelEnricher, FrameworkEnricher)
43
44 def test_detection_patterns_contains_t_detection_files_contains_artisan(self):
45 store = _mock_store()
46 lEnricher(store).detection_patterr)
47
48 def test_detection_patterns_contains_illuminate(self):
49 store = _mock_store()
50 assert "Illuminate" in LaravelEnricher(store).detection_patterpatterns_contains_http_controllers(self):
51 store = _mock_store()
52 assert [["UserControle()
53 lEnricher(store).detection_patterpatterns_has_three_entries(self):
54 store = _mock_store()
55 assert len(Laravepatterns) == 3
56
57
58 # ── enrich() return type ──────────────────────────────────────────────────────
59
60
61 class TestLaravelEnricherEnrichReturnType:
62 def test_returns_enrichment_result(self):
63 store = _mock_store(result_set=[])
64 result = LaravelEnricher(store).enrich()
65 assert isinstance(result, EnrichmentResult)
66
67 def test_result_has_promoted_attribute(self):
68 store = _mock_store(result_set=[])
69 result = LaravelEnricher(store).enrich()
70 assert hasattr(result, "promoted")
71
72 def test_result_has_edges_added_attribute(self):
73 store = _mock_store(result_set=[])
74 result = LaravelEnricher(store).enrich()
75 assert hasattr(result, "edges_added")
76
77 def test_result_has_patterns_found_attribute(self):
78 store = _mock_store(result_set=[])
79 result = LaravelEnricher(store).enrich()
80 assert hasattr(result, "patterns_found")
81
82
83 # ── enrich() with no matching nodes ──────────────────────────────────────────
84
85
86 class TestLaravelEnricherNoMatches:
87 def test_promoted_is_zero_when_no_nodes(self):
88 store = _mock_store(result_set=[])
89 result = LaravelEnricher(store).enrich()
90 assert result.promoted == 0
91
92 def test_all_pattern_counts_zero_when_no_nodes(self):
93 store = _mock_store(result_set=[])
94 result = LaravelEnricher(store).enrich()
95 for key in ("controllers", "routes", "jobs", "policies", "models"):
96 assert result.patterns_found[key] == 0
97
98 def test_patterns_found_has_five_keys(self):
99 store = _mock_store(result_set=[])
100 result = LaravelEnricher(store).enrich()
101 assert set(result.patterns_found.keys()) == {
102 "controllers", "routes", "jobs", "policies", "models"
103 }
104
105
106 # ── enrich() with matching nodes ─────────────────────────────────────────────
107
108
109 class TestLaravelEnricherWithMatches:
110 def _make_store_for_fragment(self, target_fragment, rows):
111 """Return a store that returns `rows` only when the fragment matches."""
112
113 def side_effect(cypher, params):
114 fragment = params.get("fragment", "")
115 if fragment == target_fragment:
116 return MagicMock(result_set=rows)
117 return MagicMock(result_set=[])
118
119 return _store_with_side_effect(side_effect)
120
121 def test_controller_promoted(self):
122 store = self._make_store_for_fragment(
123 "Controllers/",
124 [["UserController", "app/Http/Controllers/UserController.php"]],
125 )
126 result = LaravelEnricher(store).enrich()
127 assert result.patterns_found["controllers"] == 1
128 assert result.promoted >= 1
129
130 def test_route_promoted(self):
131 store = self._make_store_for_fragment(
132 "routes/",
133 [["web", "routes/web.php"]],
134 )
135 result = LaravelEnricher(store).enrich()
136 assert result.patterns_found["routes"] == 1
137 assert result.promoted >= 1
138
139 def test_job_promoted(self):
140 store = self._make_store_for_fragment(
141 "Jobs/",
142 [["SendEmailJob", "app/Jobs/SendEmailJob.php"]],
143 )
144 result = LaravelEnricher(store).enrich()
145 assert result.patterns_found["jobs"] == 1
146 assert result.promoted >= 1
147
148 def test_policy_promoted(self):
149 store = self._make_store_for_fragment(
150 "Policies/",
151 [["UserPolicy", "app/Policies/UserPolicy.php"]],
152 )
153 result = LaravelEnricher(store).enrich()
154 assert result.patterns_found["policies"] == 1
155 assert result.promoted >= 1
156
157 def test_model_promoted_via_model_keyword(self):
158 store = self._make_store_for_fragment(
159 "Model",
160 [["User", "app/Models/User.php"]],
161 )
162 result = LaravelEnricher(store).enrich()
163 assert result.patterns_found["models"] == 1
164 assert result.promoted >= 1
165
166 def test_promoted_count_accumulates_across_types(self):
167 rows_map = {
168 "Controllers/": [
169 ["UserController", "app/Http/Controllers/UserController.php"],
170 ["PostController", "app/Http/Controllers/PostController.php"],
171 ],
172 "routes/": [["web", "routes/web.php"]],
173 "Jobs/": [],
174 "Policies/": [["UserPolicy", "app/Policies/UserPolicy.php"]],
175 "Model": [],
176 }
177
178 def side_effect(cypher, params):
179 fragment = params.get("fragment", "")
180 return MagicMock(result_set=rows_map.get(fragment, []))
181
182 store = _store_with_side_effect(side_effect)
183 result = LaravelEnricher(store).enrich()
184 assert result.promoted == 4
185 assert result.patterns_found["controllers"] == 2
186 assert result.patterns_found["routes"] == 1
187 assert result.patterns_found["policies"] == 1
188 assert result.patterns_found["jobs"] == 0
189 assert result.patterns_found["models"] == 0
190
191 def test_promote_node_called_with_laravel_controller_type(self):
192 store = self._make_store_for_fragment(
193 "Controllers/",
194 [["UserController", "app/Http/Controllers/UserController.php"]],
195 )
196 LaravelEnricher(store).enrich()
197 calls = [str(c) for c in store._graph.query.call_args_list]
198 promote_calls = [c for c in calls if "semantic_type" in c and "LaravelController" in c]
199 assert len(promote_calls) >= 1
200
201 def test_promote_node_called_with_laravel_model_type(self):
202 store = self._make_store_for_fragment(
203 "Model",
204 [["User", "app/Models/User.php"]],
205 )
206 LaravelEnricher(store).enrich()
207 calls = [str(c) for c in store._graph.query.call_args_list]
208 promote_calls = [c for c in calls if "semantic_type" in c and "LaravelModel" in c]
209 assert len(promote_calls) >= 1
210
211 def test_promote_node_called_with_laravel_route_type(self):
212 store = self._make_store_for_fragment(
213 "routes/",
214 [["web", "routes/web.php"]],
215 )
216 LaravelEnricher(store).enrich()
217 calls = [str(c) for c in store._graph.query.call_args_list]
218 promote_calls = [c for c in calls if "semantic_type" in c and "LaravelRoute" in c]
219 assert len(promote_calls) >= 1
220
221 def test_promote_node_called_with_laravel_job_type(self):
222 store = self._make_store_for_fragment(
223 "Jobs/",
224 [["SendEmailJob", "app/Jobs/SendEmailJob.php"]],
225 )
226 LaravelEnricher(store).enrich()
227 calls = [str(c) for c in store._graph.query.call_args_list]
228 promote_calls = [c for c in calls if "semantic_type" in c and "LaravelJob" in c]
229 assert len(promote_calls) >= 1
230
231 def
--- a/tests/test_enrichment_rails.py
+++ b/tests/test_enrichment_rails.py
@@ -0,0 +1,269 @@
1
+"""Tests for navegador.enrichment.rails — RailsEnricher."""
2
+
3
+from unittest.mock import MagicMock, call
4
+
5
+import pytest
6
+
7
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
8
+from navegador.enrichment.rails import RailsEnricher
9
+from navegador.graph.store import GraphStore
10
+
11
+
12
+# ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+
15
+def _mock_store(result_set=None):
16
+ """Return a GraphStore backed by a mock FalkorDB graph."""
17
+ client = MagicMock()
18
+ graph = MagicMock()
19
+ graph.query.return_value = MagicMock(result_set=result_set)
20
+ client.select_graph.return_value = graph
21
+ return GraphStore(client)
22
+
23
+
24
+def _store_with_side_effect(side_effect):
25
+ """Return a GraphStore whose graph.query uses a side_effect callable."""
26
+ client = MagicMock()
27
+ graph = MagicMock()
28
+ graph.query.side_effect = side_effect
29
+ client.select_graph.return_value = graph
30
+ return GraphStore(client)
31
+
32
+
33
+# ── Identity / contract ───────────────────────────────────────────────────────
34
+
35
+
36
+class TestRailsEnricherIdentity:
37
+ def test_framework_name(self):
38
+ store = _mock_store()
39
+ assert RailsEnricher(store).framework_name == "rails"
40
+
41
+ def test_is_framework_enricher_subclass(self):
42
+ assert issubclass(RailsEnricher, FrameworkEnricher)
43
+
44
+ def test_detection_patterns_contains_t_detection_files_contains_gemfile(self):
45
+ store = _mock_store()
46
+ patterns
47
+
48
+ def test_droutes(self):
49
+ store = _moconfig/routes.rbpatterns
50
+
51
+ def test_depplicationke_store_for_fragment(
52
+ _mock_store()
53
+ assert "ApplicationController" in RailsEnricher(store).detection_patterns
54
+
55
+ def test_detection_r)
56
+
57
+ def test_dactive_record(self):
58
+ store = _mock_store()
59
+ assert "ActiveRecordpatterns
60
+
61
+ dhas_four_entries(self):
62
+ store = _m"""Tests for navegado""""Tests for navegador.enrichment.rails — RailsEnricher."""
63
+
64
+from unittest.mock import MagicMock, call
65
+
66
+import pytest
67
+
68
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
69
+from navegador.enrichment.rails import RailsEnricher
70
+from navegador.graph.store import GraphStore
71
+
72
+
73
+# ── Helpers ───────────────────────────────────────────────────────────────────
74
+
75
+
76
+def _mock_store(result_set=None):
77
+ """Return a GraphStore backed by a mock FalkorDB graph."""
78
+ client = MagicMock()
79
+ graph = MagicMock()
80
+ graph.query.return_value = MagicMock(result_set=result_set)
81
+ client.select_graph.return_value = graph
82
+ return GraphStore(client)
83
+
84
+
85
+def _store_with_side_effect(side_effect):
86
+ """Return a GraphStore whose graph.query uses a side_effect callable."""
87
+ client = MagicMock()
88
+ graph = MagicMock()
89
+ graph.query.side_effect = side_effect
90
+ client.select_graph.return_value = graph
91
+ return GraphStore(client)
92
+
93
+
94
+# ── Identity / contract ───────────────────────────────────────────────────────
95
+
96
+
97
+class TestRailsEnricherIdentity:
98
+ def test_framework_name(self):
99
+ store = _mock_store()
100
+ assert RailsEnricher(store).framework_name == "rails"
101
+
102
+ def test_is_framework_enricher_subclass(self):
103
+ assert issubclass(RailsEnricher, FrameworkEnricher)
104
+
105
+ def test_detection_patterns_contains_rails(self):
106
+ store = _mock_store()
107
+ assert "rails" in RailsEnricher(store).detection_patterns
108
+
109
+ def test_detection_patterns_contains_active_record(self):
110
+ store = _mock_store()
111
+ assert "active_record" in RailsEnricher(store).detection_patterns
112
+
113
+ def test_detection_patterns_contains_action_controller(self):
114
+ store = _mock_store()
115
+ assert "action_controller" in RailsEnricher(store).detection_patterns
116
+
117
+ def test_detection_files_contains_gemfile(self):
118
+ store = _mock_store()
119
+ assert "Gemfile" in RailsEnricher(store).detection_files
120
+
121
+ def test_detection_patterns_has_three_entries(self):
122
+ store = _mock_store()
123
+ assert len(RailsEnricher(store).detection_patterns) == 3
124
+
125
+
126
+# ── enrich() return type ──────────────────────────────────────────────────────
127
+
128
+
129
+class TestRailsEnricherEnrichReturnType:
130
+ def test_returns_enrichment_result(self):
131
+ store = _mock_store(result_set=[])
132
+ result = RailsEnricher(store).enrich()
133
+ assert isinstance(result, EnrichmentResult)
134
+
135
+ def test_result_has_promoted_attribute(self):
136
+ store = _mock_store(result_set=[])
137
+ result = RailsEnricher(store).enrich()
138
+ assert hasattr(result, "promoted")
139
+
140
+ def test_result_has_edges_added_attribute(self):
141
+ store = _mock_store(result_set=[])
142
+ result = RailsEnricher(store).enrich()
143
+ assert hasattr(result, "edges_added")
144
+
145
+ def test_result_has_patterns_found_attribute(self):
146
+ store = _mock_store(result_set=[])
147
+ result = RailsEnricher(store).enrich()
148
+ assert hasattr(result, "patterns_found")
149
+
150
+
151
+# ── enrich() with no matching nodes ──────────────────────────────────────────
152
+
153
+
154
+class TestRailsEnricherNoMatches:
155
+ def test_promoted_is_zero_when_no_nodes(self):
156
+ store = _mock_store(result_set=[])
157
+ result = RailsEnricher(store).enrich()
158
+ assert result.promoted == 0
159
+
160
+ def test_all_pattern_counts_zero_when_no_nodes(self):
161
+ store = _mock_store(result_set=[])
162
+ result = RailsEnricher(store).enrich()
163
+ for key in ("controllers", "models", "routes", "jobs", "concerns"):
164
+ assert result.patterns_found[key] == 0
165
+
166
+ def test_patterns_found_has_five_keys(self):
167
+ store = _mock_store(result_set=[])
168
+ result = RailsEnricher(store).enrich()
169
+ assert set(result.patterns_found.keys()) == {
170
+ "controllers", "models", "routes", "jobs", "concerns"
171
+ }
172
+
173
+
174
+# ── enrich() with matching nodes ─────────────────────────────────────────────
175
+
176
+
177
+class TestRailsEnricherWithMatches:
178
+ def _make_store_for_fragment(self, target_fragment, rows):
179
+ """Return a store that returns `rows` only when the query fragment matches."""
180
+
181
+ def side_effect(cypher, params):
182
+ fragment = params.get("fragment", "")
183
+ if fragment == target_fragment:
184
+ return MagicMock(result_set=rows)
185
+ return MagicMock(result_set=[])
186
+
187
+ return _store_with_side_effect(side_effect)
188
+
189
+ def test_controller_promoted(self):
190
+ store = self._make_store_for_fragment(
191
+ "controllers/", [["UsersController", "app/controllers/users_controller.rb"]]
192
+ )
193
+ result = RailsEnricher(store).enrich()
194
+ assert result.patterns_found["controllers"] == 1
195
+ assert result.promoted >= 1
196
+
197
+ def test_model_promoted(self):
198
+ store = self._make_store_for_fragment(
199
+ "models/", [["User", "app/models/user.rb"]]
200
+ )
201
+ result = RailsEnricher(store).enrich()
202
+ assert result.patterns_found["models"] == 1
203
+ assert result.promoted >= 1
204
+
205
+ def test_route_promoted(self):
206
+ store = self._make_store_for_fragment(
207
+ "routes.rb", [["routes", "config/routes.rb"]]
208
+ )
209
+ result = RailsEnricher(store).enrich()
210
+ assert result.patterns_found["routes"] == 1
211
+ assert result.promoted >= 1
212
+
213
+ def test_job_promoted(self):
214
+ store = self._make_store_for_fragment(
215
+ "jobs/", [["SendEmailJob", "app/jobs/send_email_job.rb"]]
216
+ )
217
+ result = RailsEnricher(store).enrich()
218
+ assert result.patterns_found["jobs"] == 1
219
+ assert result.promoted >= 1
220
+
221
+ def test_concern_promoted(self):
222
+ store = self._make_store_for_fragment(
223
+ "concerns/", [["Auditable", "app/models/concerns/auditable.rb"]]
224
+ )
225
+ result = RailsEnricher(store).enrich()
226
+ assert result.patterns_found["concerns"] == 1
227
+ assert result.promoted >= 1
228
+
229
+ def test_promoted_count_accumulates_across_types(self):
230
+ rows_map = {
231
+ "controllers/": [
232
+ ["UsersController", "app/controllers/users_controller.rb"],
233
+ ["PostsController", "app/controllers/posts_controller.rb"],
234
+ ],
235
+ "models/": [["User", "app/models/user.rb"]],
236
+ "routes.rb": [],
237
+ "jobs/": [],
238
+ "concerns/": [],
239
+ }
240
+
241
+ def side_effect(cypher, params):
242
+ fragment = params.get("fragment", "")
243
+ return MagicMock(result_set=rows_map.get(fragment, []))
244
+
245
+ store = _store_with_side_effect(side_effect)
246
+ result = RailsEnricher(store).enrich()
247
+ assert result.promoted == 3
248
+ assert result.patterns_found["controllers"] == 2
249
+ assert result.patterns_found["models"] == 1
250
+
251
+ def test_promote_node_called_with_correct_semantic_type_for_controller(self):
252
+ store = self._make_store_for_fragment(
253
+ "controllers/", [["UsersController", "app/controllers/users_controller.rb"]]
254
+ )
255
+ RailsEnricher(store).enrich()
256
+
257
+ # The _promote_node path ultimately calls store.query with SET n.semantic_type
258
+ calls = [str(c) for c in store._graph.query.call_args_list]
259
+ promote_calls = [c for c in calls if "semantic_type" in c and "RailsController" in c]
260
+ assert len(promote_calls) >= 1
261
+
262
+ def test_promote_node_called_with_correct_semantic_type_for_model(self):
263
+ store = self._make_store_for_fragment(
264
+ "models/", [["User", "app/models/user.rb"]]
265
+ )
266
+ RailsEnricher(store).enrich()
267
+
268
+ calls = [str(c) for c in store._graph.query.call_args_list]
269
+ promote_calls = [c for c in cal
--- a/tests/test_enrichment_rails.py
+++ b/tests/test_enrichment_rails.py
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_enrichment_rails.py
+++ b/tests/test_enrichment_rails.py
@@ -0,0 +1,269 @@
1 """Tests for navegador.enrichment.rails — RailsEnricher."""
2
3 from unittest.mock import MagicMock, call
4
5 import pytest
6
7 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
8 from navegador.enrichment.rails import RailsEnricher
9 from navegador.graph.store import GraphStore
10
11
12 # ── Helpers ───────────────────────────────────────────────────────────────────
13
14
15 def _mock_store(result_set=None):
16 """Return a GraphStore backed by a mock FalkorDB graph."""
17 client = MagicMock()
18 graph = MagicMock()
19 graph.query.return_value = MagicMock(result_set=result_set)
20 client.select_graph.return_value = graph
21 return GraphStore(client)
22
23
24 def _store_with_side_effect(side_effect):
25 """Return a GraphStore whose graph.query uses a side_effect callable."""
26 client = MagicMock()
27 graph = MagicMock()
28 graph.query.side_effect = side_effect
29 client.select_graph.return_value = graph
30 return GraphStore(client)
31
32
33 # ── Identity / contract ───────────────────────────────────────────────────────
34
35
36 class TestRailsEnricherIdentity:
37 def test_framework_name(self):
38 store = _mock_store()
39 assert RailsEnricher(store).framework_name == "rails"
40
41 def test_is_framework_enricher_subclass(self):
42 assert issubclass(RailsEnricher, FrameworkEnricher)
43
44 def test_detection_patterns_contains_t_detection_files_contains_gemfile(self):
45 store = _mock_store()
46 patterns
47
48 def test_droutes(self):
49 store = _moconfig/routes.rbpatterns
50
51 def test_depplicationke_store_for_fragment(
52 _mock_store()
53 assert "ApplicationController" in RailsEnricher(store).detection_patterns
54
55 def test_detection_r)
56
57 def test_dactive_record(self):
58 store = _mock_store()
59 assert "ActiveRecordpatterns
60
61 dhas_four_entries(self):
62 store = _m"""Tests for navegado""""Tests for navegador.enrichment.rails — RailsEnricher."""
63
64 from unittest.mock import MagicMock, call
65
66 import pytest
67
68 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
69 from navegador.enrichment.rails import RailsEnricher
70 from navegador.graph.store import GraphStore
71
72
73 # ── Helpers ───────────────────────────────────────────────────────────────────
74
75
76 def _mock_store(result_set=None):
77 """Return a GraphStore backed by a mock FalkorDB graph."""
78 client = MagicMock()
79 graph = MagicMock()
80 graph.query.return_value = MagicMock(result_set=result_set)
81 client.select_graph.return_value = graph
82 return GraphStore(client)
83
84
85 def _store_with_side_effect(side_effect):
86 """Return a GraphStore whose graph.query uses a side_effect callable."""
87 client = MagicMock()
88 graph = MagicMock()
89 graph.query.side_effect = side_effect
90 client.select_graph.return_value = graph
91 return GraphStore(client)
92
93
94 # ── Identity / contract ───────────────────────────────────────────────────────
95
96
97 class TestRailsEnricherIdentity:
98 def test_framework_name(self):
99 store = _mock_store()
100 assert RailsEnricher(store).framework_name == "rails"
101
102 def test_is_framework_enricher_subclass(self):
103 assert issubclass(RailsEnricher, FrameworkEnricher)
104
105 def test_detection_patterns_contains_rails(self):
106 store = _mock_store()
107 assert "rails" in RailsEnricher(store).detection_patterns
108
109 def test_detection_patterns_contains_active_record(self):
110 store = _mock_store()
111 assert "active_record" in RailsEnricher(store).detection_patterns
112
113 def test_detection_patterns_contains_action_controller(self):
114 store = _mock_store()
115 assert "action_controller" in RailsEnricher(store).detection_patterns
116
117 def test_detection_files_contains_gemfile(self):
118 store = _mock_store()
119 assert "Gemfile" in RailsEnricher(store).detection_files
120
121 def test_detection_patterns_has_three_entries(self):
122 store = _mock_store()
123 assert len(RailsEnricher(store).detection_patterns) == 3
124
125
126 # ── enrich() return type ──────────────────────────────────────────────────────
127
128
129 class TestRailsEnricherEnrichReturnType:
130 def test_returns_enrichment_result(self):
131 store = _mock_store(result_set=[])
132 result = RailsEnricher(store).enrich()
133 assert isinstance(result, EnrichmentResult)
134
135 def test_result_has_promoted_attribute(self):
136 store = _mock_store(result_set=[])
137 result = RailsEnricher(store).enrich()
138 assert hasattr(result, "promoted")
139
140 def test_result_has_edges_added_attribute(self):
141 store = _mock_store(result_set=[])
142 result = RailsEnricher(store).enrich()
143 assert hasattr(result, "edges_added")
144
145 def test_result_has_patterns_found_attribute(self):
146 store = _mock_store(result_set=[])
147 result = RailsEnricher(store).enrich()
148 assert hasattr(result, "patterns_found")
149
150
151 # ── enrich() with no matching nodes ──────────────────────────────────────────
152
153
154 class TestRailsEnricherNoMatches:
155 def test_promoted_is_zero_when_no_nodes(self):
156 store = _mock_store(result_set=[])
157 result = RailsEnricher(store).enrich()
158 assert result.promoted == 0
159
160 def test_all_pattern_counts_zero_when_no_nodes(self):
161 store = _mock_store(result_set=[])
162 result = RailsEnricher(store).enrich()
163 for key in ("controllers", "models", "routes", "jobs", "concerns"):
164 assert result.patterns_found[key] == 0
165
166 def test_patterns_found_has_five_keys(self):
167 store = _mock_store(result_set=[])
168 result = RailsEnricher(store).enrich()
169 assert set(result.patterns_found.keys()) == {
170 "controllers", "models", "routes", "jobs", "concerns"
171 }
172
173
174 # ── enrich() with matching nodes ─────────────────────────────────────────────
175
176
177 class TestRailsEnricherWithMatches:
178 def _make_store_for_fragment(self, target_fragment, rows):
179 """Return a store that returns `rows` only when the query fragment matches."""
180
181 def side_effect(cypher, params):
182 fragment = params.get("fragment", "")
183 if fragment == target_fragment:
184 return MagicMock(result_set=rows)
185 return MagicMock(result_set=[])
186
187 return _store_with_side_effect(side_effect)
188
189 def test_controller_promoted(self):
190 store = self._make_store_for_fragment(
191 "controllers/", [["UsersController", "app/controllers/users_controller.rb"]]
192 )
193 result = RailsEnricher(store).enrich()
194 assert result.patterns_found["controllers"] == 1
195 assert result.promoted >= 1
196
197 def test_model_promoted(self):
198 store = self._make_store_for_fragment(
199 "models/", [["User", "app/models/user.rb"]]
200 )
201 result = RailsEnricher(store).enrich()
202 assert result.patterns_found["models"] == 1
203 assert result.promoted >= 1
204
205 def test_route_promoted(self):
206 store = self._make_store_for_fragment(
207 "routes.rb", [["routes", "config/routes.rb"]]
208 )
209 result = RailsEnricher(store).enrich()
210 assert result.patterns_found["routes"] == 1
211 assert result.promoted >= 1
212
213 def test_job_promoted(self):
214 store = self._make_store_for_fragment(
215 "jobs/", [["SendEmailJob", "app/jobs/send_email_job.rb"]]
216 )
217 result = RailsEnricher(store).enrich()
218 assert result.patterns_found["jobs"] == 1
219 assert result.promoted >= 1
220
221 def test_concern_promoted(self):
222 store = self._make_store_for_fragment(
223 "concerns/", [["Auditable", "app/models/concerns/auditable.rb"]]
224 )
225 result = RailsEnricher(store).enrich()
226 assert result.patterns_found["concerns"] == 1
227 assert result.promoted >= 1
228
229 def test_promoted_count_accumulates_across_types(self):
230 rows_map = {
231 "controllers/": [
232 ["UsersController", "app/controllers/users_controller.rb"],
233 ["PostsController", "app/controllers/posts_controller.rb"],
234 ],
235 "models/": [["User", "app/models/user.rb"]],
236 "routes.rb": [],
237 "jobs/": [],
238 "concerns/": [],
239 }
240
241 def side_effect(cypher, params):
242 fragment = params.get("fragment", "")
243 return MagicMock(result_set=rows_map.get(fragment, []))
244
245 store = _store_with_side_effect(side_effect)
246 result = RailsEnricher(store).enrich()
247 assert result.promoted == 3
248 assert result.patterns_found["controllers"] == 2
249 assert result.patterns_found["models"] == 1
250
251 def test_promote_node_called_with_correct_semantic_type_for_controller(self):
252 store = self._make_store_for_fragment(
253 "controllers/", [["UsersController", "app/controllers/users_controller.rb"]]
254 )
255 RailsEnricher(store).enrich()
256
257 # The _promote_node path ultimately calls store.query with SET n.semantic_type
258 calls = [str(c) for c in store._graph.query.call_args_list]
259 promote_calls = [c for c in calls if "semantic_type" in c and "RailsController" in c]
260 assert len(promote_calls) >= 1
261
262 def test_promote_node_called_with_correct_semantic_type_for_model(self):
263 store = self._make_store_for_fragment(
264 "models/", [["User", "app/models/user.rb"]]
265 )
266 RailsEnricher(store).enrich()
267
268 calls = [str(c) for c in store._graph.query.call_args_list]
269 promote_calls = [c for c in cal
--- a/tests/test_enrichment_spring.py
+++ b/tests/test_enrichment_spring.py
@@ -0,0 +1,229 @@
1
+"""Tests for navegador.enrichment.spring — SpringEnricher."""
2
+
3
+from unittest.mock import MagicMock
4
+
5
+import pytest
6
+
7
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
8
+from navegador.enrichment.spring import SpringEnricher
9
+from navegador.graph.store import GraphStore
10
+
11
+
12
+# ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+
15
+def _mock_store(result_set=None):
16
+ """Return a GraphStore backed by a mock FalkorDB graph."""
17
+ client = MagicMock()
18
+ graph = MagicMock()
19
+ graph.query.return_value = MagicMock(result_set=result_set)
20
+ client.select_graph.return_value = graph
21
+ return GraphStore(client)
22
+
23
+
24
+def _store_with_side_effect(side_effect):
25
+ """Return a GraphStore whose graph.query uses a side_effect callable."""
26
+ client = MagicMock()
27
+ graph = MagicMock()
28
+ graph.query.side_effect = side_effect
29
+ client.select_graph.return_value = graph
30
+ return GraphStore(client)
31
+
32
+
33
+# ── Identity / contract ───────────────────────────────────────────────────────
34
+
35
+
36
+class TestSpringEnricherIdentity:
37
+ def test_framework_name(self):
38
+ store = _mock_store()
39
+ assert SpringEnricher(store).framework_name == "spring"
40
+
41
+ def test_is_framework_enricher_subclass(self):
42
+ assert issubclass(SpringEnricher, FrameworkEnricher)
43
+
44
+ def test_detection_patterns_contains_spring_boot_application store = _mo@SpringBootApplication" in SpringEnricher(store).detection_patterns
45
+
46
+ def test_detection_patterns_contains_spring_booentity_typ store = _mospring-boot" in SpringEnricher(store).detection_patterns
47
+
48
+ def test_detection_patterns_contains_application_properties(self):
49
+ store = _mock_store()
50
+ assert "application.properties" in Sprinpatternment.spring — SpringEnri"""Tests for navegador.eection_files_contains_application_properties(self):
51
+ store = _mock_store()
52
+ assert "application.properties" in SpringEnricher(store).detection_files
53
+
54
+ def test_detection_files_contains_application_yml(self):
55
+ store = _mock_store()
56
+ assert "application.yml" in SpringEnricher(store).detection_files
57
+
58
+ def test_detection_patterns_has_one_entry(self):
59
+ store = _mock_store()
60
+ assert len(SpringEnricher(store).detection_patterns) == 1
61
+
62
+
63
+# ── enrich() return type ──────────────────────────────────────────────────────
64
+
65
+
66
+class TestSpringEnricherEnrichReturnType:
67
+ def test_returns_enrichment_result(self):
68
+ store = _mock_store(result_set=[])
69
+ result = SpringEnricher(store).enrich()
70
+ assert isinstance(result, EnrichmentResult)
71
+
72
+ def test_result_has_promoted_attribute(self):
73
+ store = _mock_store(result_set=[])
74
+ result = SpringEnricher(store).enrich()
75
+ assert hasattr(result, "promoted")
76
+
77
+ def test_result_has_edges_added_attribute(self):
78
+ store = _mock_store(result_set=[])
79
+ result = SpringEnricher(store).enrich()
80
+ assert hasattr(result, "edges_added")
81
+
82
+ def test_result_has_patterns_found_attribute(self):
83
+ store = _mock_store(result_set=[])
84
+ result = SpringEnricher(store).enrich()
85
+ assert hasattr(result, "patterns_found")
86
+
87
+
88
+# ── enrich() with no matching nodes ──────────────────────────────────────────
89
+
90
+
91
+class TestSpringEnricherNoMatches:
92
+ def test_promoted_is_zero_when_no_nodes(self):
93
+ store = _mock_store(result_set=[])
94
+ result = SpringEnricher(store).enrich()
95
+ assert result.promoted == 0
96
+
97
+ def test_all_pattern_counts_zero_when_no_nodes(self):
98
+ store = _mock_store(result_set=[])
99
+ result = SpringEnricher(store).enrich()
100
+ for key in ("controllers", "rest_controllers", "services", "repositories", "entities"):
101
+ assert result.patterns_found[key] == 0
102
+
103
+ def test_patterns_found_has_five_keys(self):
104
+ store = _mock_store(result_set=[])
105
+ result = SpringEnricher(store).enrich()
106
+ assert set(result.patterns_found.keys()) == {
107
+ "controllers", "rest_controllers", "services", "repositories", "entities"
108
+ }
109
+
110
+
111
+# ── enrich() with matching nodes ─────────────────────────────────────────────
112
+
113
+
114
+class TestSpringEnricherWithMatches:
115
+ def _make_store_for_annotation(self, target_annotation, rows):
116
+ """Return a store that returns `rows` only when the annotation matches."""
117
+
118
+ def side_effect(cypher, params):
119
+ annotation = params.get("annotation", "")
120
+ if annotation == target_annotation:
121
+ return MagicMock(result_set=rows)
122
+ return MagicMock(result_set=[])
123
+
124
+ return _store_with_side_effect(side_effect)
125
+
126
+ def test_controller_promoted(self):
127
+ store = self._make_store_for_annotation(
128
+ "@Controller",
129
+ [["UserController", "src/main/java/com/example/UserController.java"]],
130
+ )
131
+ result = SpringEnricher(store).enrich()
132
+ assert result.patterns_found["controllers"] == 1
133
+ assert result.promoted >= 1
134
+
135
+ def test_rest_controller_promoted(self):
136
+ store = self._make_store_for_annotation(
137
+ "@RestController",
138
+ [["UserRestController", "src/main/java/com/example/UserRestController.java"]],
139
+ )
140
+ result = SpringEnricher(store).enrich()
141
+ assert result.patterns_found["rest_controllers"] == 1
142
+ assert result.promoted >= 1
143
+
144
+ def test_service_promoted(self):
145
+ store = self._make_store_for_annotation(
146
+ "@Service",
147
+ [["UserService", "src/main/java/com/example/UserService.java"]],
148
+ )
149
+ result = SpringEnricher(store).enrich()
150
+ assert result.patterns_found["services"] == 1
151
+ assert result.promoted >= 1
152
+
153
+ def test_repository_promoted(self):
154
+ store = self._make_store_for_annotation(
155
+ "@Repository",
156
+ [["UserRepository", "src/main/java/com/example/UserRepository.java"]],
157
+ )
158
+ result = SpringEnricher(store).enrich()
159
+ assert result.patterns_found["repositories"] == 1
160
+ assert result.promoted >= 1
161
+
162
+ def test_entity_promoted(self):
163
+ store = self._make_store_for_annotation(
164
+ "@Entity",
165
+ [["User", "src/main/java/com/example/User.java"]],
166
+ )
167
+ result = SpringEnricher(store).enrich()
168
+ assert result.patterns_found["entities"] == 1
169
+ assert result.promoted >= 1
170
+
171
+ def test_promoted_count_accumulates_across_types(self):
172
+ rows_map = {
173
+ "@Controller": [
174
+ ["UserController", "src/main/java/UserController.java"],
175
+ ],
176
+ "@RestController": [
177
+ ["ApiController", "src/main/java/ApiController.java"],
178
+ ["OrderController", "src/main/java/OrderController.java"],
179
+ ],
180
+ "@Service": [],
181
+ "@Repository": [["UserRepository", "src/main/java/UserRepository.java"]],
182
+ "@Entity": [],
183
+ }
184
+
185
+ def side_effect(cypher, params):
186
+ annotation = params.get("annotation", "")
187
+ return MagicMock(result_set=rows_map.get(annotation, []))
188
+
189
+ store = _store_with_side_effect(side_effect)
190
+ result = SpringEnricher(store).enrich()
191
+ assert result.promoted == 4
192
+ assert result.patterns_found["controllers"] == 1
193
+ assert result.patterns_found["rest_controllers"] == 2
194
+ assert result.patterns_found["repositories"] == 1
195
+
196
+ def test_promote_node_called_with_spring_controller_type(self):
197
+ store = self._make_store_for_annotation(
198
+ "@Controller",
199
+ [["UserController", "src/main/java/UserController.java"]],
200
+ )
201
+ SpringEnricher(store).enrich()
202
+ calls = [str(c) for c in store._graph.query.call_args_list]
203
+ promote_calls = [c for c in calls if "semantic_type" in c and "SpringController" in c]
204
+ assert len(promote_calls) >= 1
205
+
206
+ def test_promote_node_called_with_spring_service_type(self):
207
+ store = self._make_store_for_annotation(
208
+ "@Service",
209
+ [["UserService", "src/main/java/UserService.java"]],
210
+ )
211
+ SpringEnricher(store).enrich()
212
+ calls = [str(c) for c in store._graph.query.call_args_list]
213
+ promote_calls = [c for c in calls if "semantic_type" in c and "SpringService" in c]
214
+ assert len(promote_calls) >= 1
215
+
216
+ def test_promote_node_called_with_spring_repository_type(self):
217
+ store = self._make_store_for_annotation(
218
+ "@Repository",
219
+ [["UserRepository", "src/main/java/UserRepository.java"]],
220
+ )
221
+ SpringEnricher(store).enrich()
222
+ calls = [str(c) for c in store._graph.query.call_args_list]
223
+ promote_calls = [c for c in calls if "semantic_type" in c and "SpringRepository" in c]
224
+ assert len(promote_calls) >= 1
225
+
226
+ def test_promote_node_called_with_spring_entity_type(self):
227
+ store = self._make_store_for_annotation(
228
+ "@Entity",
229
+ [["User", "src/main/java/User.jav
--- a/tests/test_enrichment_spring.py
+++ b/tests/test_enrichment_spring.py
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_enrichment_spring.py
+++ b/tests/test_enrichment_spring.py
@@ -0,0 +1,229 @@
1 """Tests for navegador.enrichment.spring — SpringEnricher."""
2
3 from unittest.mock import MagicMock
4
5 import pytest
6
7 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
8 from navegador.enrichment.spring import SpringEnricher
9 from navegador.graph.store import GraphStore
10
11
12 # ── Helpers ───────────────────────────────────────────────────────────────────
13
14
15 def _mock_store(result_set=None):
16 """Return a GraphStore backed by a mock FalkorDB graph."""
17 client = MagicMock()
18 graph = MagicMock()
19 graph.query.return_value = MagicMock(result_set=result_set)
20 client.select_graph.return_value = graph
21 return GraphStore(client)
22
23
24 def _store_with_side_effect(side_effect):
25 """Return a GraphStore whose graph.query uses a side_effect callable."""
26 client = MagicMock()
27 graph = MagicMock()
28 graph.query.side_effect = side_effect
29 client.select_graph.return_value = graph
30 return GraphStore(client)
31
32
33 # ── Identity / contract ───────────────────────────────────────────────────────
34
35
36 class TestSpringEnricherIdentity:
37 def test_framework_name(self):
38 store = _mock_store()
39 assert SpringEnricher(store).framework_name == "spring"
40
41 def test_is_framework_enricher_subclass(self):
42 assert issubclass(SpringEnricher, FrameworkEnricher)
43
44 def test_detection_patterns_contains_spring_boot_application store = _mo@SpringBootApplication" in SpringEnricher(store).detection_patterns
45
46 def test_detection_patterns_contains_spring_booentity_typ store = _mospring-boot" in SpringEnricher(store).detection_patterns
47
48 def test_detection_patterns_contains_application_properties(self):
49 store = _mock_store()
50 assert "application.properties" in Sprinpatternment.spring — SpringEnri"""Tests for navegador.eection_files_contains_application_properties(self):
51 store = _mock_store()
52 assert "application.properties" in SpringEnricher(store).detection_files
53
54 def test_detection_files_contains_application_yml(self):
55 store = _mock_store()
56 assert "application.yml" in SpringEnricher(store).detection_files
57
58 def test_detection_patterns_has_one_entry(self):
59 store = _mock_store()
60 assert len(SpringEnricher(store).detection_patterns) == 1
61
62
63 # ── enrich() return type ──────────────────────────────────────────────────────
64
65
66 class TestSpringEnricherEnrichReturnType:
67 def test_returns_enrichment_result(self):
68 store = _mock_store(result_set=[])
69 result = SpringEnricher(store).enrich()
70 assert isinstance(result, EnrichmentResult)
71
72 def test_result_has_promoted_attribute(self):
73 store = _mock_store(result_set=[])
74 result = SpringEnricher(store).enrich()
75 assert hasattr(result, "promoted")
76
77 def test_result_has_edges_added_attribute(self):
78 store = _mock_store(result_set=[])
79 result = SpringEnricher(store).enrich()
80 assert hasattr(result, "edges_added")
81
82 def test_result_has_patterns_found_attribute(self):
83 store = _mock_store(result_set=[])
84 result = SpringEnricher(store).enrich()
85 assert hasattr(result, "patterns_found")
86
87
88 # ── enrich() with no matching nodes ──────────────────────────────────────────
89
90
91 class TestSpringEnricherNoMatches:
92 def test_promoted_is_zero_when_no_nodes(self):
93 store = _mock_store(result_set=[])
94 result = SpringEnricher(store).enrich()
95 assert result.promoted == 0
96
97 def test_all_pattern_counts_zero_when_no_nodes(self):
98 store = _mock_store(result_set=[])
99 result = SpringEnricher(store).enrich()
100 for key in ("controllers", "rest_controllers", "services", "repositories", "entities"):
101 assert result.patterns_found[key] == 0
102
103 def test_patterns_found_has_five_keys(self):
104 store = _mock_store(result_set=[])
105 result = SpringEnricher(store).enrich()
106 assert set(result.patterns_found.keys()) == {
107 "controllers", "rest_controllers", "services", "repositories", "entities"
108 }
109
110
111 # ── enrich() with matching nodes ─────────────────────────────────────────────
112
113
114 class TestSpringEnricherWithMatches:
115 def _make_store_for_annotation(self, target_annotation, rows):
116 """Return a store that returns `rows` only when the annotation matches."""
117
118 def side_effect(cypher, params):
119 annotation = params.get("annotation", "")
120 if annotation == target_annotation:
121 return MagicMock(result_set=rows)
122 return MagicMock(result_set=[])
123
124 return _store_with_side_effect(side_effect)
125
126 def test_controller_promoted(self):
127 store = self._make_store_for_annotation(
128 "@Controller",
129 [["UserController", "src/main/java/com/example/UserController.java"]],
130 )
131 result = SpringEnricher(store).enrich()
132 assert result.patterns_found["controllers"] == 1
133 assert result.promoted >= 1
134
135 def test_rest_controller_promoted(self):
136 store = self._make_store_for_annotation(
137 "@RestController",
138 [["UserRestController", "src/main/java/com/example/UserRestController.java"]],
139 )
140 result = SpringEnricher(store).enrich()
141 assert result.patterns_found["rest_controllers"] == 1
142 assert result.promoted >= 1
143
144 def test_service_promoted(self):
145 store = self._make_store_for_annotation(
146 "@Service",
147 [["UserService", "src/main/java/com/example/UserService.java"]],
148 )
149 result = SpringEnricher(store).enrich()
150 assert result.patterns_found["services"] == 1
151 assert result.promoted >= 1
152
153 def test_repository_promoted(self):
154 store = self._make_store_for_annotation(
155 "@Repository",
156 [["UserRepository", "src/main/java/com/example/UserRepository.java"]],
157 )
158 result = SpringEnricher(store).enrich()
159 assert result.patterns_found["repositories"] == 1
160 assert result.promoted >= 1
161
162 def test_entity_promoted(self):
163 store = self._make_store_for_annotation(
164 "@Entity",
165 [["User", "src/main/java/com/example/User.java"]],
166 )
167 result = SpringEnricher(store).enrich()
168 assert result.patterns_found["entities"] == 1
169 assert result.promoted >= 1
170
171 def test_promoted_count_accumulates_across_types(self):
172 rows_map = {
173 "@Controller": [
174 ["UserController", "src/main/java/UserController.java"],
175 ],
176 "@RestController": [
177 ["ApiController", "src/main/java/ApiController.java"],
178 ["OrderController", "src/main/java/OrderController.java"],
179 ],
180 "@Service": [],
181 "@Repository": [["UserRepository", "src/main/java/UserRepository.java"]],
182 "@Entity": [],
183 }
184
185 def side_effect(cypher, params):
186 annotation = params.get("annotation", "")
187 return MagicMock(result_set=rows_map.get(annotation, []))
188
189 store = _store_with_side_effect(side_effect)
190 result = SpringEnricher(store).enrich()
191 assert result.promoted == 4
192 assert result.patterns_found["controllers"] == 1
193 assert result.patterns_found["rest_controllers"] == 2
194 assert result.patterns_found["repositories"] == 1
195
196 def test_promote_node_called_with_spring_controller_type(self):
197 store = self._make_store_for_annotation(
198 "@Controller",
199 [["UserController", "src/main/java/UserController.java"]],
200 )
201 SpringEnricher(store).enrich()
202 calls = [str(c) for c in store._graph.query.call_args_list]
203 promote_calls = [c for c in calls if "semantic_type" in c and "SpringController" in c]
204 assert len(promote_calls) >= 1
205
206 def test_promote_node_called_with_spring_service_type(self):
207 store = self._make_store_for_annotation(
208 "@Service",
209 [["UserService", "src/main/java/UserService.java"]],
210 )
211 SpringEnricher(store).enrich()
212 calls = [str(c) for c in store._graph.query.call_args_list]
213 promote_calls = [c for c in calls if "semantic_type" in c and "SpringService" in c]
214 assert len(promote_calls) >= 1
215
216 def test_promote_node_called_with_spring_repository_type(self):
217 store = self._make_store_for_annotation(
218 "@Repository",
219 [["UserRepository", "src/main/java/UserRepository.java"]],
220 )
221 SpringEnricher(store).enrich()
222 calls = [str(c) for c in store._graph.query.call_args_list]
223 promote_calls = [c for c in calls if "semantic_type" in c and "SpringRepository" in c]
224 assert len(promote_calls) >= 1
225
226 def test_promote_node_called_with_spring_entity_type(self):
227 store = self._make_store_for_annotation(
228 "@Entity",
229 [["User", "src/main/java/User.jav

Keyboard Shortcuts

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