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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- 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
|