codexa 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,814 @@
1
+ """Tests for Phase 15 — CI/CD and Contribution Safety Pipeline.
2
+
3
+ Covers: quality analyzers, PR intelligence, CI templates, pre-commit
4
+ hooks, CLI commands, router registration, version bump, module imports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import textwrap
11
+ from pathlib import Path
12
+ from unittest.mock import patch
13
+
14
+ import pytest
15
+ from click.testing import CliRunner
16
+
17
+ from semantic_code_intelligence.parsing.parser import Symbol
18
+
19
+ # =========================================================================
20
+ # Quality analyzer tests
21
+ # =========================================================================
22
+
23
+
24
+ class TestComplexity:
25
+ """Tests for cyclomatic complexity analysis."""
26
+
27
+ def _sym(self, name: str, body: str, kind: str = "function") -> Symbol:
28
+ return Symbol(
29
+ name=name,
30
+ kind=kind,
31
+ file_path="test.py",
32
+ start_line=1,
33
+ end_line=10,
34
+ start_col=0,
35
+ end_col=0,
36
+ body=body,
37
+ )
38
+
39
+ def test_simple_function_low(self):
40
+ from semantic_code_intelligence.ci.quality import compute_complexity
41
+
42
+ sym = self._sym("hello", "def hello():\n return 1\n")
43
+ result = compute_complexity(sym)
44
+ assert result.complexity >= 1
45
+ assert result.rating == "low"
46
+
47
+ def test_branching_function(self):
48
+ from semantic_code_intelligence.ci.quality import compute_complexity
49
+
50
+ body = textwrap.dedent("""\
51
+ def check(x):
52
+ if x > 0:
53
+ if x > 10:
54
+ return "big"
55
+ elif x > 5:
56
+ return "medium"
57
+ else:
58
+ return "small"
59
+ for i in range(x):
60
+ if i % 2 == 0:
61
+ continue
62
+ while x > 0:
63
+ x -= 1
64
+ return x
65
+ """)
66
+ sym = self._sym("check", body)
67
+ result = compute_complexity(sym)
68
+ # 1 base + if + if + elif + for + if + while = 7
69
+ assert result.complexity >= 7
70
+ assert result.rating in ("moderate", "high")
71
+
72
+ def test_analyze_complexity_threshold(self):
73
+ from semantic_code_intelligence.ci.quality import analyze_complexity
74
+
75
+ simple = self._sym("simple", "def simple():\n pass\n")
76
+ complex_body = "\n".join(
77
+ f" if x == {i}:" for i in range(12)
78
+ )
79
+ hard = self._sym("hard", f"def hard(x):\n{complex_body}\n")
80
+ results = analyze_complexity([simple, hard], threshold=5)
81
+ assert len(results) >= 1
82
+ assert results[0].symbol_name == "hard"
83
+
84
+ def test_skips_classes(self):
85
+ from semantic_code_intelligence.ci.quality import analyze_complexity
86
+
87
+ cls = self._sym("MyClass", "class MyClass:\n pass\n", kind="class")
88
+ results = analyze_complexity([cls], threshold=1)
89
+ assert len(results) == 0
90
+
91
+ def test_rating_scale(self):
92
+ from semantic_code_intelligence.ci.quality import _rate_complexity
93
+
94
+ assert _rate_complexity(3) == "low"
95
+ assert _rate_complexity(8) == "moderate"
96
+ assert _rate_complexity(15) == "high"
97
+ assert _rate_complexity(25) == "very_high"
98
+
99
+
100
+ class TestDeadCode:
101
+ """Tests for dead code detection."""
102
+
103
+ def _sym(self, name: str, kind: str = "function", body: str = "pass") -> Symbol:
104
+ return Symbol(
105
+ name=name,
106
+ kind=kind,
107
+ file_path="test.py",
108
+ start_line=1,
109
+ end_line=2,
110
+ start_col=0,
111
+ end_col=0,
112
+ body=body,
113
+ )
114
+
115
+ def test_empty_input(self):
116
+ from semantic_code_intelligence.ci.quality import detect_dead_code
117
+
118
+ assert detect_dead_code([]) == []
119
+
120
+ def test_entry_point_excluded(self):
121
+ from semantic_code_intelligence.ci.quality import detect_dead_code
122
+
123
+ sym = self._sym("main")
124
+ assert detect_dead_code([sym]) == []
125
+
126
+ def test_test_functions_excluded(self):
127
+ from semantic_code_intelligence.ci.quality import detect_dead_code
128
+
129
+ sym = self._sym("test_something")
130
+ assert detect_dead_code([sym]) == []
131
+
132
+ def test_unreferenced_detected(self):
133
+ from semantic_code_intelligence.ci.quality import detect_dead_code
134
+
135
+ sym = self._sym("orphan_func", body="def orphan_func():\n return 42\n")
136
+ results = detect_dead_code([sym])
137
+ assert len(results) == 1
138
+ assert results[0].symbol_name == "orphan_func"
139
+
140
+ def test_referenced_not_dead(self):
141
+ from semantic_code_intelligence.ci.quality import detect_dead_code
142
+
143
+ caller = self._sym("caller", body="def caller():\n helper()\n")
144
+ helper = self._sym("helper", body="def helper():\n pass\n")
145
+ results = detect_dead_code([caller, helper])
146
+ helper_names = [r.symbol_name for r in results]
147
+ assert "helper" not in helper_names
148
+
149
+ def test_imports_excluded(self):
150
+ from semantic_code_intelligence.ci.quality import detect_dead_code
151
+
152
+ imp = self._sym("os", kind="import")
153
+ assert detect_dead_code([imp]) == []
154
+
155
+
156
+ class TestDuplicates:
157
+ """Tests for duplicate logic detection."""
158
+
159
+ def _sym(self, name: str, body: str, file_path: str = "a.py") -> Symbol:
160
+ return Symbol(
161
+ name=name,
162
+ kind="function",
163
+ file_path=file_path,
164
+ start_line=1,
165
+ end_line=10,
166
+ start_col=0,
167
+ end_col=0,
168
+ body=body,
169
+ )
170
+
171
+ def test_empty_input(self):
172
+ from semantic_code_intelligence.ci.quality import detect_duplicates
173
+
174
+ assert detect_duplicates([]) == []
175
+
176
+ def test_identical_bodies(self):
177
+ from semantic_code_intelligence.ci.quality import detect_duplicates
178
+
179
+ body = "def f(x):\n result = x + 1\n result *= 2\n return result\n # extra\n"
180
+ sym_a = self._sym("func_a", body, "a.py")
181
+ sym_b = self._sym("func_b", body, "b.py")
182
+ results = detect_duplicates([sym_a, sym_b], threshold=0.7)
183
+ assert len(results) == 1
184
+ assert results[0].similarity >= 0.9
185
+
186
+ def test_different_bodies(self):
187
+ from semantic_code_intelligence.ci.quality import detect_duplicates
188
+
189
+ body_a = "def a():\n return 1\n x = 2\n y = 3\n"
190
+ body_b = "def b():\n for i in range(100):\n print(i)\n z = i * 2\n"
191
+ sym_a = self._sym("a", body_a)
192
+ sym_b = self._sym("b", body_b)
193
+ results = detect_duplicates([sym_a, sym_b], threshold=0.9)
194
+ assert len(results) == 0
195
+
196
+ def test_short_bodies_skipped(self):
197
+ from semantic_code_intelligence.ci.quality import detect_duplicates
198
+
199
+ body = "pass"
200
+ sym_a = self._sym("a", body)
201
+ sym_b = self._sym("b", body)
202
+ results = detect_duplicates([sym_a, sym_b], min_lines=4)
203
+ assert len(results) == 0
204
+
205
+ def test_jaccard_basics(self):
206
+ from semantic_code_intelligence.ci.quality import _jaccard
207
+
208
+ assert _jaccard(set(), set()) == 1.0
209
+ assert _jaccard({"a", "b"}, {"a", "b"}) == 1.0
210
+ assert _jaccard({"a"}, set()) == 0.0
211
+
212
+
213
+ class TestQualityReport:
214
+ """Tests for aggregate quality report."""
215
+
216
+ def test_report_to_dict(self):
217
+ from semantic_code_intelligence.ci.quality import QualityReport
218
+
219
+ report = QualityReport(files_analyzed=5, symbol_count=20)
220
+ d = report.to_dict()
221
+ assert d["files_analyzed"] == 5
222
+ assert d["symbol_count"] == 20
223
+ assert d["issue_count"] == 0
224
+
225
+ def test_issue_count_aggregation(self):
226
+ from semantic_code_intelligence.ci.quality import (
227
+ QualityReport,
228
+ ComplexityResult,
229
+ DeadCodeResult,
230
+ DuplicateResult,
231
+ )
232
+ from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
233
+
234
+ report = QualityReport(
235
+ complexity_issues=[ComplexityResult("f", "a.py", 1, 10, 15, "high")],
236
+ dead_code=[DeadCodeResult("g", "function", "b.py", 1)],
237
+ duplicates=[DuplicateResult("a", "a.py", 1, "b", "b.py", 1, 0.9)],
238
+ safety=SafetyReport(safe=False, issues=[SafetyIssue("p", "d", 1)]),
239
+ )
240
+ assert report.issue_count == 4
241
+
242
+
243
+ class TestAnalyzeProject:
244
+ """Tests for project-level analysis."""
245
+
246
+ def test_analyze_empty_dir(self, tmp_path):
247
+ from semantic_code_intelligence.ci.quality import analyze_project
248
+
249
+ report = analyze_project(tmp_path)
250
+ assert report.files_analyzed == 0
251
+ assert report.issue_count == 0
252
+
253
+ def test_analyze_with_file(self, tmp_path):
254
+ from semantic_code_intelligence.ci.quality import analyze_project
255
+
256
+ py_file = tmp_path / "hello.py"
257
+ py_file.write_text("def hello():\n return 1\n", encoding="utf-8")
258
+ report = analyze_project(tmp_path, file_paths=[str(py_file)])
259
+ assert report.files_analyzed == 1
260
+ assert report.symbol_count >= 1
261
+
262
+
263
+ # =========================================================================
264
+ # PR intelligence tests
265
+ # =========================================================================
266
+
267
+
268
+ class TestChangeSummary:
269
+ """Tests for change summary generation."""
270
+
271
+ def test_empty_files(self):
272
+ from semantic_code_intelligence.ci.pr import build_change_summary
273
+
274
+ result = build_change_summary([])
275
+ assert result.files_changed == 0
276
+ assert result.to_dict()["files_changed"] == 0
277
+
278
+ def test_with_python_file(self, tmp_path):
279
+ from semantic_code_intelligence.ci.pr import build_change_summary
280
+
281
+ f = tmp_path / "test.py"
282
+ f.write_text("def hello():\n pass\n", encoding="utf-8")
283
+ result = build_change_summary([str(f)])
284
+ assert result.files_changed == 1
285
+ assert "python" in result.languages
286
+
287
+ def test_nonsupported_file(self, tmp_path):
288
+ from semantic_code_intelligence.ci.pr import build_change_summary
289
+
290
+ f = tmp_path / "readme.txt"
291
+ f.write_text("Hello world", encoding="utf-8")
292
+ result = build_change_summary([str(f)])
293
+ assert result.files_changed == 1
294
+ d = result.file_details[0]
295
+ assert d.language is None
296
+
297
+ def test_symbols_detected(self, tmp_path):
298
+ from semantic_code_intelligence.ci.pr import build_change_summary
299
+
300
+ f = tmp_path / "main.py"
301
+ f.write_text("def foo():\n pass\n\ndef bar():\n pass\n", encoding="utf-8")
302
+ result = build_change_summary([str(f)])
303
+ fd = result.file_details[0]
304
+ assert "foo" in fd.symbols_added
305
+ assert "bar" in fd.symbols_added
306
+
307
+
308
+ class TestImpactAnalysis:
309
+ """Tests for semantic impact analysis."""
310
+
311
+ def test_impact_empty(self, tmp_path):
312
+ from semantic_code_intelligence.ci.pr import analyze_impact
313
+
314
+ result = analyze_impact([], tmp_path)
315
+ assert result.changed_symbols == []
316
+ assert result.to_dict()["affected_files"] == []
317
+
318
+ def test_impact_with_file(self, tmp_path):
319
+ from semantic_code_intelligence.ci.pr import analyze_impact
320
+
321
+ f = tmp_path / "lib.py"
322
+ f.write_text("def helper():\n pass\n", encoding="utf-8")
323
+ result = analyze_impact([str(f)], tmp_path)
324
+ assert "helper" in result.changed_symbols
325
+
326
+
327
+ class TestSuggestReviewers:
328
+ """Tests for reviewer suggestion."""
329
+
330
+ def test_empty(self):
331
+ from semantic_code_intelligence.ci.pr import suggest_reviewers
332
+
333
+ assert suggest_reviewers([]) == []
334
+
335
+ def test_domain_grouping(self):
336
+ from semantic_code_intelligence.ci.pr import suggest_reviewers
337
+
338
+ files = ["src/auth/login.py", "src/auth/logout.py", "src/db/models.py"]
339
+ result = suggest_reviewers(files)
340
+ domains = [r["domain"] for r in result]
341
+ assert len(result) >= 2
342
+ assert any("auth" in d for d in domains)
343
+
344
+
345
+ class TestRiskScoring:
346
+ """Tests for risk severity scoring."""
347
+
348
+ def test_zero_risk(self):
349
+ from semantic_code_intelligence.ci.pr import compute_risk, ChangeSummary
350
+
351
+ cs = ChangeSummary(files_changed=0)
352
+ risk = compute_risk(cs)
353
+ assert risk.score == 0
354
+ assert risk.level == "low"
355
+
356
+ def test_large_changeset(self):
357
+ from semantic_code_intelligence.ci.pr import compute_risk, ChangeSummary
358
+
359
+ cs = ChangeSummary(files_changed=25, total_symbols_removed=15)
360
+ risk = compute_risk(cs)
361
+ assert risk.score >= 30
362
+ assert risk.level in ("medium", "high", "critical")
363
+
364
+ def test_safety_issues_increase_risk(self):
365
+ from semantic_code_intelligence.ci.pr import compute_risk, ChangeSummary
366
+ from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
367
+
368
+ cs = ChangeSummary(files_changed=1)
369
+ safety = SafetyReport(safe=False, issues=[
370
+ SafetyIssue("p", "eval detected", 1),
371
+ SafetyIssue("p", "exec detected", 2),
372
+ ])
373
+ risk = compute_risk(cs, safety_report=safety)
374
+ assert risk.score >= 20
375
+ assert any("safety" in f.lower() for f in risk.factors)
376
+
377
+ def test_risk_level_function(self):
378
+ from semantic_code_intelligence.ci.pr import _risk_level
379
+
380
+ assert _risk_level(10) == "low"
381
+ assert _risk_level(30) == "medium"
382
+ assert _risk_level(60) == "high"
383
+ assert _risk_level(80) == "critical"
384
+
385
+
386
+ class TestPRReport:
387
+ """Tests for full PR report generation."""
388
+
389
+ def test_report_to_dict(self):
390
+ from semantic_code_intelligence.ci.pr import (
391
+ PRReport,
392
+ ChangeSummary,
393
+ RiskScore,
394
+ )
395
+
396
+ report = PRReport(
397
+ change_summary=ChangeSummary(files_changed=1),
398
+ risk=RiskScore(score=10, level="low"),
399
+ )
400
+ d = report.to_dict()
401
+ assert d["change_summary"]["files_changed"] == 1
402
+ assert d["risk"]["score"] == 10
403
+
404
+ def test_generate_pr_report_empty(self, tmp_path):
405
+ from semantic_code_intelligence.ci.pr import generate_pr_report
406
+
407
+ report = generate_pr_report([], tmp_path)
408
+ assert report.change_summary.files_changed == 0
409
+ assert report.risk is not None
410
+
411
+
412
+ # =========================================================================
413
+ # CI template tests
414
+ # =========================================================================
415
+
416
+
417
+ class TestTemplates:
418
+ """Tests for CI workflow template generation."""
419
+
420
+ def test_analysis_workflow(self):
421
+ from semantic_code_intelligence.ci.templates import generate_analysis_workflow
422
+
423
+ content = generate_analysis_workflow()
424
+ assert "CodexA Analysis" in content
425
+ assert "codexa quality" in content
426
+ assert "codexa pr-summary" in content
427
+ assert "pull_request" in content
428
+
429
+ def test_analysis_custom_python(self):
430
+ from semantic_code_intelligence.ci.templates import generate_analysis_workflow
431
+
432
+ content = generate_analysis_workflow(python_version="3.13")
433
+ assert "3.13" in content
434
+
435
+ def test_safety_workflow(self):
436
+ from semantic_code_intelligence.ci.templates import generate_safety_workflow
437
+
438
+ content = generate_safety_workflow()
439
+ assert "CodexA Safety" in content
440
+ assert "--safety-only" in content
441
+
442
+ def test_precommit_config(self):
443
+ from semantic_code_intelligence.ci.templates import generate_precommit_config
444
+
445
+ content = generate_precommit_config()
446
+ assert "codexa-safety" in content
447
+ assert "codexa-quality" in content
448
+ assert "pre-commit" in content.lower()
449
+
450
+ def test_template_registry(self):
451
+ from semantic_code_intelligence.ci.templates import TEMPLATES
452
+
453
+ assert "analysis" in TEMPLATES
454
+ assert "safety" in TEMPLATES
455
+ assert "precommit" in TEMPLATES
456
+
457
+ def test_get_template(self):
458
+ from semantic_code_intelligence.ci.templates import get_template
459
+
460
+ content = get_template("analysis")
461
+ assert "codexa quality" in content
462
+
463
+ def test_get_template_unknown(self):
464
+ from semantic_code_intelligence.ci.templates import get_template
465
+
466
+ with pytest.raises(KeyError):
467
+ get_template("nonexistent")
468
+
469
+
470
+ # =========================================================================
471
+ # Pre-commit hook tests
472
+ # =========================================================================
473
+
474
+
475
+ class TestPrecommitHooks:
476
+ """Tests for pre-commit validation hooks."""
477
+
478
+ def test_empty_files(self):
479
+ from semantic_code_intelligence.ci.hooks import run_precommit_check
480
+
481
+ result = run_precommit_check([])
482
+ assert result.passed is True
483
+ assert result.files_checked == 0
484
+
485
+ def test_safe_file(self, tmp_path):
486
+ from semantic_code_intelligence.ci.hooks import run_precommit_check
487
+
488
+ f = tmp_path / "safe.py"
489
+ f.write_text("def hello():\n return 1\n", encoding="utf-8")
490
+ result = run_precommit_check([str(f)])
491
+ assert result.passed is True
492
+
493
+ def test_unsafe_file(self, tmp_path):
494
+ from semantic_code_intelligence.ci.hooks import run_precommit_check
495
+
496
+ f = tmp_path / "unsafe.py"
497
+ f.write_text("import os\nos.system('rm -rf /')\n", encoding="utf-8")
498
+ result = run_precommit_check([str(f)])
499
+ assert result.passed is False
500
+ assert result.safety is not None
501
+ assert not result.safety.safe
502
+
503
+ def test_result_to_dict(self):
504
+ from semantic_code_intelligence.ci.hooks import HookResult
505
+
506
+ result = HookResult(passed=True, files_checked=3)
507
+ d = result.to_dict()
508
+ assert d["passed"] is True
509
+ assert d["files_checked"] == 3
510
+
511
+
512
+ # =========================================================================
513
+ # CLI command tests
514
+ # =========================================================================
515
+
516
+
517
+ class TestQualityCLI:
518
+ """Tests for the `codexa quality` command."""
519
+
520
+ @pytest.fixture
521
+ def runner(self):
522
+ return CliRunner()
523
+
524
+ def test_help(self, runner):
525
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
526
+
527
+ result = runner.invoke(quality_cmd, ["--help"])
528
+ assert result.exit_code == 0
529
+ assert "quality" in result.output.lower() or "complexity" in result.output.lower()
530
+
531
+ def test_has_json_option(self, runner):
532
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
533
+
534
+ result = runner.invoke(quality_cmd, ["--help"])
535
+ assert "--json" in result.output
536
+
537
+ def test_has_safety_only(self, runner):
538
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
539
+
540
+ result = runner.invoke(quality_cmd, ["--help"])
541
+ assert "--safety-only" in result.output
542
+
543
+ def test_has_pipe(self, runner):
544
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
545
+
546
+ result = runner.invoke(quality_cmd, ["--help"])
547
+ assert "--pipe" in result.output
548
+
549
+ def test_json_output(self, runner, tmp_path):
550
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
551
+
552
+ result = runner.invoke(quality_cmd, ["--json", "--path", str(tmp_path)])
553
+ assert result.exit_code == 0
554
+ data = json.loads(result.output)
555
+ assert "files_analyzed" in data
556
+
557
+ def test_safety_only_mode(self, runner, tmp_path):
558
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
559
+
560
+ result = runner.invoke(quality_cmd, [
561
+ "--safety-only", "--pipe", "--path", str(tmp_path)
562
+ ])
563
+ assert result.exit_code == 0
564
+ assert "PASS" in result.output or "FAIL" in result.output
565
+
566
+ def test_pipe_mode(self, runner, tmp_path):
567
+ from semantic_code_intelligence.cli.commands.quality_cmd import quality_cmd
568
+
569
+ result = runner.invoke(quality_cmd, ["--pipe", "--path", str(tmp_path)])
570
+ assert result.exit_code == 0
571
+ assert "Files:" in result.output
572
+
573
+
574
+ class TestPRSummaryCLI:
575
+ """Tests for the `codexa pr-summary` command."""
576
+
577
+ @pytest.fixture
578
+ def runner(self):
579
+ return CliRunner()
580
+
581
+ def test_help(self, runner):
582
+ from semantic_code_intelligence.cli.commands.pr_summary_cmd import pr_summary_cmd
583
+
584
+ result = runner.invoke(pr_summary_cmd, ["--help"])
585
+ assert result.exit_code == 0
586
+ assert "pr-summary" in result.output.lower() or "pull request" in result.output.lower()
587
+
588
+ def test_has_json_option(self, runner):
589
+ from semantic_code_intelligence.cli.commands.pr_summary_cmd import pr_summary_cmd
590
+
591
+ result = runner.invoke(pr_summary_cmd, ["--help"])
592
+ assert "--json" in result.output
593
+
594
+ def test_has_files_option(self, runner):
595
+ from semantic_code_intelligence.cli.commands.pr_summary_cmd import pr_summary_cmd
596
+
597
+ result = runner.invoke(pr_summary_cmd, ["--help"])
598
+ assert "--files" in result.output or "-f" in result.output
599
+
600
+ def test_json_with_specific_file(self, runner, tmp_path):
601
+ from semantic_code_intelligence.cli.commands.pr_summary_cmd import pr_summary_cmd
602
+
603
+ f = tmp_path / "main.py"
604
+ f.write_text("def hello():\n pass\n", encoding="utf-8")
605
+ result = runner.invoke(pr_summary_cmd, [
606
+ "--json", "-f", str(f), "--path", str(tmp_path)
607
+ ])
608
+ assert result.exit_code == 0
609
+ data = json.loads(result.output)
610
+ assert "change_summary" in data
611
+
612
+ def test_pipe_mode(self, runner, tmp_path):
613
+ from semantic_code_intelligence.cli.commands.pr_summary_cmd import pr_summary_cmd
614
+
615
+ f = tmp_path / "main.py"
616
+ f.write_text("def foo():\n pass\n", encoding="utf-8")
617
+ result = runner.invoke(pr_summary_cmd, [
618
+ "--pipe", "-f", str(f), "--path", str(tmp_path)
619
+ ])
620
+ assert result.exit_code == 0
621
+ assert "Changed:" in result.output
622
+
623
+
624
+ class TestCIGenCLI:
625
+ """Tests for the `codexa ci-gen` command."""
626
+
627
+ @pytest.fixture
628
+ def runner(self):
629
+ return CliRunner()
630
+
631
+ def test_help(self, runner):
632
+ from semantic_code_intelligence.cli.commands.ci_gen_cmd import ci_gen_cmd
633
+
634
+ result = runner.invoke(ci_gen_cmd, ["--help"])
635
+ assert result.exit_code == 0
636
+ assert "ci-gen" in result.output.lower() or "template" in result.output.lower()
637
+
638
+ def test_analysis_template(self, runner):
639
+ from semantic_code_intelligence.cli.commands.ci_gen_cmd import ci_gen_cmd
640
+
641
+ result = runner.invoke(ci_gen_cmd, ["analysis"])
642
+ assert result.exit_code == 0
643
+ assert "CodexA Analysis" in result.output
644
+
645
+ def test_safety_template(self, runner):
646
+ from semantic_code_intelligence.cli.commands.ci_gen_cmd import ci_gen_cmd
647
+
648
+ result = runner.invoke(ci_gen_cmd, ["safety"])
649
+ assert result.exit_code == 0
650
+ assert "CodexA Safety" in result.output
651
+
652
+ def test_precommit_template(self, runner):
653
+ from semantic_code_intelligence.ci.templates import generate_precommit_config
654
+ from semantic_code_intelligence.cli.commands.ci_gen_cmd import ci_gen_cmd
655
+
656
+ result = runner.invoke(ci_gen_cmd, ["precommit"])
657
+ assert result.exit_code == 0
658
+ assert "pre-commit" in result.output.lower()
659
+
660
+ def test_output_to_file(self, runner, tmp_path):
661
+ from semantic_code_intelligence.cli.commands.ci_gen_cmd import ci_gen_cmd
662
+
663
+ outfile = tmp_path / "workflow.yml"
664
+ result = runner.invoke(ci_gen_cmd, ["analysis", "-o", str(outfile)])
665
+ assert result.exit_code == 0
666
+ assert outfile.exists()
667
+ content = outfile.read_text()
668
+ assert "CodexA Analysis" in content
669
+
670
+ def test_custom_python_version(self, runner):
671
+ from semantic_code_intelligence.cli.commands.ci_gen_cmd import ci_gen_cmd
672
+
673
+ result = runner.invoke(ci_gen_cmd, ["safety", "--python-version", "3.13"])
674
+ assert result.exit_code == 0
675
+ assert "3.13" in result.output
676
+
677
+
678
+ # =========================================================================
679
+ # Router, version, and module structure tests
680
+ # =========================================================================
681
+
682
+
683
+ class TestRouterPhase15:
684
+ """Tests for CLI router registration."""
685
+
686
+ def test_register_commands_count(self):
687
+ import click
688
+ from semantic_code_intelligence.cli.router import register_commands
689
+
690
+ group = click.Group("test")
691
+ register_commands(group)
692
+ assert len(group.commands) == 39
693
+
694
+ def test_quality_command_registered(self):
695
+ from semantic_code_intelligence.cli.main import cli
696
+
697
+ assert "quality" in cli.commands
698
+
699
+ def test_pr_summary_command_registered(self):
700
+ from semantic_code_intelligence.cli.main import cli
701
+
702
+ assert "pr-summary" in cli.commands
703
+
704
+ def test_ci_gen_command_registered(self):
705
+ from semantic_code_intelligence.cli.main import cli
706
+
707
+ assert "ci-gen" in cli.commands
708
+
709
+
710
+ class TestVersionBump:
711
+ """Test version is 0.19.0."""
712
+
713
+ def test_version_is_015(self):
714
+ from semantic_code_intelligence import __version__
715
+
716
+ assert __version__ == "0.4.0"
717
+
718
+
719
+ class TestCIModuleStructure:
720
+ """Tests for module import structure."""
721
+
722
+ def test_import_ci_package(self):
723
+ import semantic_code_intelligence.ci
724
+
725
+ def test_import_quality(self):
726
+ from semantic_code_intelligence.ci.quality import (
727
+ analyze_complexity,
728
+ detect_dead_code,
729
+ detect_duplicates,
730
+ analyze_project,
731
+ QualityReport,
732
+ )
733
+
734
+ def test_import_pr(self):
735
+ from semantic_code_intelligence.ci.pr import (
736
+ build_change_summary,
737
+ analyze_impact,
738
+ suggest_reviewers,
739
+ compute_risk,
740
+ generate_pr_report,
741
+ )
742
+
743
+ def test_import_templates(self):
744
+ from semantic_code_intelligence.ci.templates import (
745
+ generate_analysis_workflow,
746
+ generate_safety_workflow,
747
+ generate_precommit_config,
748
+ get_template,
749
+ TEMPLATES,
750
+ )
751
+
752
+ def test_import_hooks(self):
753
+ from semantic_code_intelligence.ci.hooks import (
754
+ run_precommit_check,
755
+ HookResult,
756
+ )
757
+
758
+
759
+ class TestDocsGenerator:
760
+ """Tests for CI doc generation."""
761
+
762
+ def test_generate_ci_reference(self):
763
+ from semantic_code_intelligence.docs import generate_ci_reference
764
+
765
+ md = generate_ci_reference()
766
+ assert "CI/CD" in md
767
+ assert "Quality" in md
768
+ assert "codexa quality" in md
769
+ assert "codexa pr-summary" in md
770
+ assert "codexa ci-gen" in md
771
+
772
+ def test_generate_all_docs_includes_ci(self, tmp_path):
773
+ from semantic_code_intelligence.docs import generate_all_docs
774
+
775
+ generated = generate_all_docs(tmp_path)
776
+ assert "CI.md" in generated
777
+
778
+
779
+ # =========================================================================
780
+ # Backward compatibility tests
781
+ # =========================================================================
782
+
783
+
784
+ class TestBackwardCompatibility:
785
+ """Ensure Phase 13 and Phase 14 modules still work."""
786
+
787
+ def test_web_module_imports(self):
788
+ from semantic_code_intelligence.web.api import APIHandler
789
+ from semantic_code_intelligence.web.ui import page_search
790
+ from semantic_code_intelligence.web.visualize import render_call_graph
791
+ from semantic_code_intelligence.web.server import WebServer
792
+
793
+ def test_docs_module_imports(self):
794
+ from semantic_code_intelligence.docs import (
795
+ generate_cli_reference,
796
+ generate_plugin_reference,
797
+ generate_bridge_reference,
798
+ generate_tool_reference,
799
+ generate_web_reference,
800
+ generate_ci_reference,
801
+ )
802
+
803
+ def test_plugin_hooks_intact(self):
804
+ from semantic_code_intelligence.plugins import PluginHook
805
+
806
+ assert len(PluginHook) == 22
807
+ assert PluginHook.CUSTOM_VALIDATION.value == "custom_validation"
808
+
809
+ def test_safety_validator_intact(self):
810
+ from semantic_code_intelligence.llm.safety import SafetyValidator
811
+
812
+ validator = SafetyValidator()
813
+ report = validator.validate("x = 1\n")
814
+ assert report.safe is True