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,934 @@
|
|
|
1
|
+
"""Tests for Phase 18 — Developer Workflow Intelligence.
|
|
2
|
+
|
|
3
|
+
Covers: hotspot detection, impact analysis, symbol trace, CLI commands,
|
|
4
|
+
router registration (30 commands), version (0.18.0), docs generation,
|
|
5
|
+
plugin hooks, and module structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from click.testing import CliRunner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# =========================================================================
|
|
18
|
+
# Test helpers
|
|
19
|
+
# =========================================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _write_sample_project(root: Path) -> None:
|
|
23
|
+
"""Write a multi-file project with known call relationships."""
|
|
24
|
+
src = root / "src"
|
|
25
|
+
src.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
(src / "core.py").write_text(
|
|
28
|
+
"def helper():\n"
|
|
29
|
+
" return 42\n"
|
|
30
|
+
"\n"
|
|
31
|
+
"def compute(x):\n"
|
|
32
|
+
" return helper() + x\n"
|
|
33
|
+
"\n"
|
|
34
|
+
"def orchestrate(a, b):\n"
|
|
35
|
+
" return compute(a) + compute(b)\n",
|
|
36
|
+
encoding="utf-8",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
(src / "api.py").write_text(
|
|
40
|
+
"from src.core import orchestrate\n"
|
|
41
|
+
"\n"
|
|
42
|
+
"def handle_request(data):\n"
|
|
43
|
+
" result = orchestrate(data, data)\n"
|
|
44
|
+
" return result\n"
|
|
45
|
+
"\n"
|
|
46
|
+
"def validate(data):\n"
|
|
47
|
+
" if not data:\n"
|
|
48
|
+
" return False\n"
|
|
49
|
+
" if len(data) > 100:\n"
|
|
50
|
+
" return False\n"
|
|
51
|
+
" return True\n",
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
(src / "utils.py").write_text(
|
|
56
|
+
"def format_output(value):\n"
|
|
57
|
+
" return str(value)\n"
|
|
58
|
+
"\n"
|
|
59
|
+
"def log_event(event):\n"
|
|
60
|
+
" pass\n",
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _build_context(root: Path):
|
|
66
|
+
"""Index the project and return (symbols, call_graph, dep_map)."""
|
|
67
|
+
from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
|
|
68
|
+
|
|
69
|
+
builder = ContextBuilder()
|
|
70
|
+
dep_map = DependencyMap()
|
|
71
|
+
|
|
72
|
+
for fp in sorted(root.rglob("*.py")):
|
|
73
|
+
content = fp.read_text(encoding="utf-8")
|
|
74
|
+
builder.index_file(str(fp), content)
|
|
75
|
+
dep_map.add_file(str(fp), content)
|
|
76
|
+
|
|
77
|
+
symbols = builder.get_all_symbols()
|
|
78
|
+
cg = CallGraph()
|
|
79
|
+
cg.build(symbols)
|
|
80
|
+
return symbols, cg, dep_map
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# =========================================================================
|
|
84
|
+
# HotspotFactor / Hotspot / HotspotReport dataclass tests
|
|
85
|
+
# =========================================================================
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestHotspotDataclasses:
|
|
89
|
+
"""Tests for hotspot data structures."""
|
|
90
|
+
|
|
91
|
+
def test_hotspot_factor_to_dict(self):
|
|
92
|
+
from semantic_code_intelligence.ci.hotspots import HotspotFactor
|
|
93
|
+
|
|
94
|
+
f = HotspotFactor(name="complexity", raw_value=12.0, normalized=0.8, weight=0.3)
|
|
95
|
+
d = f.to_dict()
|
|
96
|
+
assert d["name"] == "complexity"
|
|
97
|
+
assert d["raw_value"] == 12.0
|
|
98
|
+
assert d["normalized"] == 0.8
|
|
99
|
+
assert d["weight"] == 0.3
|
|
100
|
+
|
|
101
|
+
def test_hotspot_to_dict(self):
|
|
102
|
+
from semantic_code_intelligence.ci.hotspots import Hotspot, HotspotFactor
|
|
103
|
+
|
|
104
|
+
h = Hotspot(
|
|
105
|
+
name="process",
|
|
106
|
+
file_path="src/api.py",
|
|
107
|
+
kind="symbol",
|
|
108
|
+
risk_score=72.5,
|
|
109
|
+
factors=[HotspotFactor("complexity", 10.0, 0.7, 0.3)],
|
|
110
|
+
)
|
|
111
|
+
d = h.to_dict()
|
|
112
|
+
assert d["name"] == "process"
|
|
113
|
+
assert d["risk_score"] == 72.5
|
|
114
|
+
assert len(d["factors"]) == 1
|
|
115
|
+
|
|
116
|
+
def test_hotspot_report_to_dict(self):
|
|
117
|
+
from semantic_code_intelligence.ci.hotspots import Hotspot, HotspotReport
|
|
118
|
+
|
|
119
|
+
r = HotspotReport(
|
|
120
|
+
files_analyzed=5,
|
|
121
|
+
symbols_analyzed=20,
|
|
122
|
+
hotspots=[Hotspot("f1", "/a.py", "symbol", 50.0)],
|
|
123
|
+
)
|
|
124
|
+
d = r.to_dict()
|
|
125
|
+
assert d["files_analyzed"] == 5
|
|
126
|
+
assert d["symbols_analyzed"] == 20
|
|
127
|
+
assert d["hotspot_count"] == 1
|
|
128
|
+
|
|
129
|
+
def test_hotspot_report_empty(self):
|
|
130
|
+
from semantic_code_intelligence.ci.hotspots import HotspotReport
|
|
131
|
+
|
|
132
|
+
r = HotspotReport(files_analyzed=0, symbols_analyzed=0)
|
|
133
|
+
assert r.to_dict()["hotspot_count"] == 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# =========================================================================
|
|
137
|
+
# analyze_hotspots tests
|
|
138
|
+
# =========================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestAnalyzeHotspots:
|
|
142
|
+
"""Tests for the hotspot detection engine."""
|
|
143
|
+
|
|
144
|
+
def test_basic_analysis(self, tmp_path):
|
|
145
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
146
|
+
|
|
147
|
+
_write_sample_project(tmp_path)
|
|
148
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
149
|
+
|
|
150
|
+
report = analyze_hotspots(
|
|
151
|
+
symbols, cg, dep_map, tmp_path, include_git=False,
|
|
152
|
+
)
|
|
153
|
+
assert report.files_analyzed >= 1
|
|
154
|
+
assert report.symbols_analyzed >= 1
|
|
155
|
+
assert isinstance(report.hotspots, list)
|
|
156
|
+
|
|
157
|
+
def test_top_n_limits(self, tmp_path):
|
|
158
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
159
|
+
|
|
160
|
+
_write_sample_project(tmp_path)
|
|
161
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
162
|
+
|
|
163
|
+
report = analyze_hotspots(
|
|
164
|
+
symbols, cg, dep_map, tmp_path,
|
|
165
|
+
top_n=2, include_git=False,
|
|
166
|
+
)
|
|
167
|
+
assert len(report.hotspots) <= 2
|
|
168
|
+
|
|
169
|
+
def test_custom_weights(self, tmp_path):
|
|
170
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
171
|
+
|
|
172
|
+
_write_sample_project(tmp_path)
|
|
173
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
174
|
+
|
|
175
|
+
report = analyze_hotspots(
|
|
176
|
+
symbols, cg, dep_map, tmp_path,
|
|
177
|
+
include_git=False,
|
|
178
|
+
weights={"complexity": 1.0, "duplication": 0.0, "fan_in": 0.0, "fan_out": 0.0, "churn": 0.0},
|
|
179
|
+
)
|
|
180
|
+
assert report.symbols_analyzed >= 1
|
|
181
|
+
|
|
182
|
+
def test_no_git_redistributes_weights(self, tmp_path):
|
|
183
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
184
|
+
|
|
185
|
+
_write_sample_project(tmp_path)
|
|
186
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
187
|
+
|
|
188
|
+
report = analyze_hotspots(
|
|
189
|
+
symbols, cg, dep_map, tmp_path, include_git=False,
|
|
190
|
+
)
|
|
191
|
+
# When git is not available, churn factor should NOT appear
|
|
192
|
+
for h in report.hotspots:
|
|
193
|
+
factor_names = [f.name for f in h.factors]
|
|
194
|
+
assert "churn" not in factor_names
|
|
195
|
+
|
|
196
|
+
def test_hotspot_scores_are_sorted_desc(self, tmp_path):
|
|
197
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
198
|
+
|
|
199
|
+
_write_sample_project(tmp_path)
|
|
200
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
201
|
+
|
|
202
|
+
report = analyze_hotspots(
|
|
203
|
+
symbols, cg, dep_map, tmp_path, include_git=False,
|
|
204
|
+
)
|
|
205
|
+
scores = [h.risk_score for h in report.hotspots]
|
|
206
|
+
assert scores == sorted(scores, reverse=True)
|
|
207
|
+
|
|
208
|
+
def test_empty_project(self, tmp_path):
|
|
209
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
210
|
+
|
|
211
|
+
(tmp_path / "empty.py").write_text("", encoding="utf-8")
|
|
212
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
213
|
+
|
|
214
|
+
report = analyze_hotspots(
|
|
215
|
+
symbols, cg, dep_map, tmp_path, include_git=False,
|
|
216
|
+
)
|
|
217
|
+
assert report.symbols_analyzed == 0
|
|
218
|
+
assert report.hotspots == []
|
|
219
|
+
|
|
220
|
+
def test_report_serialisation_roundtrip(self, tmp_path):
|
|
221
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
222
|
+
|
|
223
|
+
_write_sample_project(tmp_path)
|
|
224
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
225
|
+
|
|
226
|
+
report = analyze_hotspots(
|
|
227
|
+
symbols, cg, dep_map, tmp_path, include_git=False,
|
|
228
|
+
)
|
|
229
|
+
d = report.to_dict()
|
|
230
|
+
s = json.dumps(d)
|
|
231
|
+
loaded = json.loads(s)
|
|
232
|
+
assert loaded["files_analyzed"] == report.files_analyzed
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# =========================================================================
|
|
236
|
+
# AffectedSymbol / AffectedModule / ImpactReport dataclass tests
|
|
237
|
+
# =========================================================================
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestImpactDataclasses:
|
|
241
|
+
"""Tests for impact analysis data structures."""
|
|
242
|
+
|
|
243
|
+
def test_affected_symbol_to_dict(self):
|
|
244
|
+
from semantic_code_intelligence.ci.impact import AffectedSymbol
|
|
245
|
+
|
|
246
|
+
a = AffectedSymbol("foo", "/a.py", "function", "direct_caller", 1)
|
|
247
|
+
d = a.to_dict()
|
|
248
|
+
assert d["name"] == "foo"
|
|
249
|
+
assert d["relationship"] == "direct_caller"
|
|
250
|
+
assert d["depth"] == 1
|
|
251
|
+
|
|
252
|
+
def test_affected_module_to_dict(self):
|
|
253
|
+
from semantic_code_intelligence.ci.impact import AffectedModule
|
|
254
|
+
|
|
255
|
+
m = AffectedModule("/b.py", "imports_target", 1)
|
|
256
|
+
assert m.to_dict()["file_path"] == "/b.py"
|
|
257
|
+
|
|
258
|
+
def test_dependency_chain_to_dict(self):
|
|
259
|
+
from semantic_code_intelligence.ci.impact import DependencyChain
|
|
260
|
+
|
|
261
|
+
c = DependencyChain(path=["a", "b", "c"])
|
|
262
|
+
assert c.to_dict()["path"] == ["a", "b", "c"]
|
|
263
|
+
|
|
264
|
+
def test_impact_report_total_affected(self):
|
|
265
|
+
from semantic_code_intelligence.ci.impact import AffectedSymbol, ImpactReport
|
|
266
|
+
|
|
267
|
+
r = ImpactReport(
|
|
268
|
+
target="foo", target_kind="symbol",
|
|
269
|
+
direct_symbols=[
|
|
270
|
+
AffectedSymbol("bar", "/a.py", "function", "direct_caller", 1),
|
|
271
|
+
],
|
|
272
|
+
transitive_symbols=[
|
|
273
|
+
AffectedSymbol("baz", "/b.py", "function", "transitive_caller", 2),
|
|
274
|
+
],
|
|
275
|
+
)
|
|
276
|
+
assert r.total_affected == 2
|
|
277
|
+
|
|
278
|
+
def test_impact_report_to_dict(self):
|
|
279
|
+
from semantic_code_intelligence.ci.impact import ImpactReport
|
|
280
|
+
|
|
281
|
+
r = ImpactReport(target="foo", target_kind="symbol")
|
|
282
|
+
d = r.to_dict()
|
|
283
|
+
assert d["target"] == "foo"
|
|
284
|
+
assert d["total_affected"] == 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# =========================================================================
|
|
288
|
+
# analyze_impact tests
|
|
289
|
+
# =========================================================================
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class TestAnalyzeImpact:
|
|
293
|
+
"""Tests for the impact analysis engine."""
|
|
294
|
+
|
|
295
|
+
def test_basic_impact(self, tmp_path):
|
|
296
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
297
|
+
|
|
298
|
+
_write_sample_project(tmp_path)
|
|
299
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
300
|
+
|
|
301
|
+
report = analyze_impact("helper", symbols, cg, dep_map, tmp_path)
|
|
302
|
+
assert report.target == "helper"
|
|
303
|
+
assert report.target_kind == "symbol"
|
|
304
|
+
|
|
305
|
+
def test_unknown_symbol_empty_report(self, tmp_path):
|
|
306
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
307
|
+
|
|
308
|
+
_write_sample_project(tmp_path)
|
|
309
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
310
|
+
|
|
311
|
+
report = analyze_impact("nonexistent_xyz", symbols, cg, dep_map, tmp_path)
|
|
312
|
+
assert report.total_affected == 0
|
|
313
|
+
|
|
314
|
+
def test_impact_file_target(self, tmp_path):
|
|
315
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
316
|
+
|
|
317
|
+
_write_sample_project(tmp_path)
|
|
318
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
319
|
+
|
|
320
|
+
report = analyze_impact(
|
|
321
|
+
str(tmp_path / "src" / "core.py"),
|
|
322
|
+
symbols, cg, dep_map, tmp_path,
|
|
323
|
+
)
|
|
324
|
+
assert report.target_kind == "file"
|
|
325
|
+
|
|
326
|
+
def test_impact_max_depth(self, tmp_path):
|
|
327
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
328
|
+
|
|
329
|
+
_write_sample_project(tmp_path)
|
|
330
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
331
|
+
|
|
332
|
+
report = analyze_impact(
|
|
333
|
+
"helper", symbols, cg, dep_map, tmp_path, max_depth=1,
|
|
334
|
+
)
|
|
335
|
+
# Should only get direct callers at depth 1
|
|
336
|
+
for s in report.transitive_symbols:
|
|
337
|
+
assert s.depth <= 1
|
|
338
|
+
|
|
339
|
+
def test_impact_chains(self, tmp_path):
|
|
340
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
341
|
+
|
|
342
|
+
_write_sample_project(tmp_path)
|
|
343
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
344
|
+
|
|
345
|
+
report = analyze_impact("helper", symbols, cg, dep_map, tmp_path)
|
|
346
|
+
assert isinstance(report.chains, list)
|
|
347
|
+
|
|
348
|
+
def test_impact_serialisation(self, tmp_path):
|
|
349
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
350
|
+
|
|
351
|
+
_write_sample_project(tmp_path)
|
|
352
|
+
symbols, cg, dep_map = _build_context(tmp_path)
|
|
353
|
+
|
|
354
|
+
report = analyze_impact("helper", symbols, cg, dep_map, tmp_path)
|
|
355
|
+
s = json.dumps(report.to_dict())
|
|
356
|
+
loaded = json.loads(s)
|
|
357
|
+
assert loaded["target"] == "helper"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# =========================================================================
|
|
361
|
+
# TraceNode / TraceEdge / TraceResult dataclass tests
|
|
362
|
+
# =========================================================================
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class TestTraceDataclasses:
|
|
366
|
+
"""Tests for trace data structures."""
|
|
367
|
+
|
|
368
|
+
def test_trace_node_to_dict(self):
|
|
369
|
+
from semantic_code_intelligence.ci.trace import TraceNode
|
|
370
|
+
|
|
371
|
+
n = TraceNode("func_a", "/a.py", "function", -2)
|
|
372
|
+
d = n.to_dict()
|
|
373
|
+
assert d["name"] == "func_a"
|
|
374
|
+
assert d["depth"] == -2
|
|
375
|
+
|
|
376
|
+
def test_trace_edge_to_dict(self):
|
|
377
|
+
from semantic_code_intelligence.ci.trace import TraceEdge
|
|
378
|
+
|
|
379
|
+
e = TraceEdge("a", "b", "/x.py")
|
|
380
|
+
d = e.to_dict()
|
|
381
|
+
assert d["caller"] == "a"
|
|
382
|
+
assert d["callee"] == "b"
|
|
383
|
+
|
|
384
|
+
def test_trace_result_total_nodes(self):
|
|
385
|
+
from semantic_code_intelligence.ci.trace import TraceNode, TraceResult
|
|
386
|
+
|
|
387
|
+
r = TraceResult(
|
|
388
|
+
target="f", target_file="/x.py",
|
|
389
|
+
upstream=[TraceNode("a", "/a.py", "function", -1)],
|
|
390
|
+
downstream=[TraceNode("b", "/b.py", "function", 1),
|
|
391
|
+
TraceNode("c", "/c.py", "function", 2)],
|
|
392
|
+
)
|
|
393
|
+
assert r.total_nodes == 3
|
|
394
|
+
|
|
395
|
+
def test_trace_result_to_dict(self):
|
|
396
|
+
from semantic_code_intelligence.ci.trace import TraceResult
|
|
397
|
+
|
|
398
|
+
r = TraceResult(target="f", target_file="/x.py")
|
|
399
|
+
d = r.to_dict()
|
|
400
|
+
assert d["target"] == "f"
|
|
401
|
+
assert d["total_nodes"] == 0
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# =========================================================================
|
|
405
|
+
# trace_symbol tests
|
|
406
|
+
# =========================================================================
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class TestTraceSymbol:
|
|
410
|
+
"""Tests for the symbol trace tool."""
|
|
411
|
+
|
|
412
|
+
def test_basic_trace(self, tmp_path):
|
|
413
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
414
|
+
|
|
415
|
+
_write_sample_project(tmp_path)
|
|
416
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
417
|
+
|
|
418
|
+
result = trace_symbol("compute", symbols, cg)
|
|
419
|
+
assert result.target == "compute"
|
|
420
|
+
assert result.target_file != ""
|
|
421
|
+
|
|
422
|
+
def test_trace_unknown_symbol(self, tmp_path):
|
|
423
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
424
|
+
|
|
425
|
+
_write_sample_project(tmp_path)
|
|
426
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
427
|
+
|
|
428
|
+
result = trace_symbol("nonexistent_xyz", symbols, cg)
|
|
429
|
+
assert result.target_file == ""
|
|
430
|
+
assert result.total_nodes == 0
|
|
431
|
+
|
|
432
|
+
def test_trace_has_upstream(self, tmp_path):
|
|
433
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
434
|
+
|
|
435
|
+
_write_sample_project(tmp_path)
|
|
436
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
437
|
+
|
|
438
|
+
result = trace_symbol("helper", symbols, cg)
|
|
439
|
+
# helper is called by compute, so upstream should be non-empty
|
|
440
|
+
assert len(result.upstream) >= 1 or len(result.edges) >= 0
|
|
441
|
+
|
|
442
|
+
def test_trace_has_downstream(self, tmp_path):
|
|
443
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
444
|
+
|
|
445
|
+
_write_sample_project(tmp_path)
|
|
446
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
447
|
+
|
|
448
|
+
result = trace_symbol("orchestrate", symbols, cg)
|
|
449
|
+
# orchestrate calls compute, so downstream should be non-empty
|
|
450
|
+
assert len(result.downstream) >= 0 # depends on call graph heuristic
|
|
451
|
+
|
|
452
|
+
def test_trace_max_depth(self, tmp_path):
|
|
453
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
454
|
+
|
|
455
|
+
_write_sample_project(tmp_path)
|
|
456
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
457
|
+
|
|
458
|
+
result = trace_symbol("helper", symbols, cg, max_depth=1)
|
|
459
|
+
for n in result.upstream:
|
|
460
|
+
assert abs(n.depth) <= 1
|
|
461
|
+
|
|
462
|
+
def test_trace_edges_list(self, tmp_path):
|
|
463
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
464
|
+
|
|
465
|
+
_write_sample_project(tmp_path)
|
|
466
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
467
|
+
|
|
468
|
+
result = trace_symbol("compute", symbols, cg)
|
|
469
|
+
assert isinstance(result.edges, list)
|
|
470
|
+
|
|
471
|
+
def test_trace_serialisation(self, tmp_path):
|
|
472
|
+
from semantic_code_intelligence.ci.trace import trace_symbol
|
|
473
|
+
|
|
474
|
+
_write_sample_project(tmp_path)
|
|
475
|
+
symbols, cg, _ = _build_context(tmp_path)
|
|
476
|
+
|
|
477
|
+
result = trace_symbol("compute", symbols, cg)
|
|
478
|
+
s = json.dumps(result.to_dict())
|
|
479
|
+
loaded = json.loads(s)
|
|
480
|
+
assert loaded["target"] == "compute"
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# =========================================================================
|
|
484
|
+
# _normalise helper tests
|
|
485
|
+
# =========================================================================
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class TestNormalise:
|
|
489
|
+
"""Tests for the normalisation helper."""
|
|
490
|
+
|
|
491
|
+
def test_normalise_basic(self):
|
|
492
|
+
from semantic_code_intelligence.ci.hotspots import _normalise
|
|
493
|
+
|
|
494
|
+
assert _normalise(5.0, 10.0) == 0.5
|
|
495
|
+
|
|
496
|
+
def test_normalise_zero_max(self):
|
|
497
|
+
from semantic_code_intelligence.ci.hotspots import _normalise
|
|
498
|
+
|
|
499
|
+
assert _normalise(5.0, 0.0) == 0.0
|
|
500
|
+
|
|
501
|
+
def test_normalise_clamp(self):
|
|
502
|
+
from semantic_code_intelligence.ci.hotspots import _normalise
|
|
503
|
+
|
|
504
|
+
assert _normalise(20.0, 10.0) == 1.0
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# =========================================================================
|
|
508
|
+
# _resolve_target_symbols tests
|
|
509
|
+
# =========================================================================
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class TestResolveTarget:
|
|
513
|
+
"""Tests for target resolution in impact analysis."""
|
|
514
|
+
|
|
515
|
+
def test_resolve_symbol(self, tmp_path):
|
|
516
|
+
from semantic_code_intelligence.ci.impact import _resolve_target_symbols
|
|
517
|
+
|
|
518
|
+
_write_sample_project(tmp_path)
|
|
519
|
+
symbols, _, _ = _build_context(tmp_path)
|
|
520
|
+
|
|
521
|
+
kind, matched = _resolve_target_symbols("helper", symbols, tmp_path)
|
|
522
|
+
assert kind == "symbol"
|
|
523
|
+
assert len(matched) >= 1
|
|
524
|
+
|
|
525
|
+
def test_resolve_file(self, tmp_path):
|
|
526
|
+
from semantic_code_intelligence.ci.impact import _resolve_target_symbols
|
|
527
|
+
|
|
528
|
+
_write_sample_project(tmp_path)
|
|
529
|
+
symbols, _, _ = _build_context(tmp_path)
|
|
530
|
+
|
|
531
|
+
kind, matched = _resolve_target_symbols(
|
|
532
|
+
str(tmp_path / "src" / "core.py"), symbols, tmp_path,
|
|
533
|
+
)
|
|
534
|
+
assert kind == "file"
|
|
535
|
+
|
|
536
|
+
def test_resolve_unknown(self, tmp_path):
|
|
537
|
+
from semantic_code_intelligence.ci.impact import _resolve_target_symbols
|
|
538
|
+
|
|
539
|
+
_write_sample_project(tmp_path)
|
|
540
|
+
symbols, _, _ = _build_context(tmp_path)
|
|
541
|
+
|
|
542
|
+
kind, matched = _resolve_target_symbols("not_real", symbols, tmp_path)
|
|
543
|
+
assert kind == "symbol"
|
|
544
|
+
assert matched == []
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# =========================================================================
|
|
548
|
+
# CLI: codexa hotspots
|
|
549
|
+
# =========================================================================
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class TestHotspotsCLI:
|
|
553
|
+
"""Tests for the hotspots CLI command."""
|
|
554
|
+
|
|
555
|
+
def test_hotspots_help(self):
|
|
556
|
+
from semantic_code_intelligence.cli.commands.hotspots_cmd import hotspots_cmd
|
|
557
|
+
|
|
558
|
+
runner = CliRunner()
|
|
559
|
+
result = runner.invoke(hotspots_cmd, ["--help"])
|
|
560
|
+
assert result.exit_code == 0
|
|
561
|
+
assert "hotspot" in result.output.lower()
|
|
562
|
+
|
|
563
|
+
def test_hotspots_json(self, tmp_path):
|
|
564
|
+
from semantic_code_intelligence.cli.commands.hotspots_cmd import hotspots_cmd
|
|
565
|
+
|
|
566
|
+
_write_sample_project(tmp_path)
|
|
567
|
+
runner = CliRunner()
|
|
568
|
+
result = runner.invoke(hotspots_cmd, [
|
|
569
|
+
"--path", str(tmp_path), "--json", "--no-git",
|
|
570
|
+
])
|
|
571
|
+
assert result.exit_code == 0
|
|
572
|
+
data = json.loads(result.output)
|
|
573
|
+
assert "hotspots" in data
|
|
574
|
+
|
|
575
|
+
def test_hotspots_pipe(self, tmp_path):
|
|
576
|
+
from semantic_code_intelligence.cli.commands.hotspots_cmd import hotspots_cmd
|
|
577
|
+
|
|
578
|
+
_write_sample_project(tmp_path)
|
|
579
|
+
runner = CliRunner()
|
|
580
|
+
result = runner.invoke(hotspots_cmd, [
|
|
581
|
+
"--path", str(tmp_path), "--pipe", "--no-git",
|
|
582
|
+
])
|
|
583
|
+
assert result.exit_code == 0
|
|
584
|
+
assert "files=" in result.output
|
|
585
|
+
|
|
586
|
+
def test_hotspots_top_n(self, tmp_path):
|
|
587
|
+
from semantic_code_intelligence.cli.commands.hotspots_cmd import hotspots_cmd
|
|
588
|
+
|
|
589
|
+
_write_sample_project(tmp_path)
|
|
590
|
+
runner = CliRunner()
|
|
591
|
+
result = runner.invoke(hotspots_cmd, [
|
|
592
|
+
"--path", str(tmp_path), "--json", "--no-git", "--top-n", "1",
|
|
593
|
+
])
|
|
594
|
+
assert result.exit_code == 0
|
|
595
|
+
data = json.loads(result.output)
|
|
596
|
+
assert len(data["hotspots"]) <= 1
|
|
597
|
+
|
|
598
|
+
def test_hotspots_rich_output(self, tmp_path):
|
|
599
|
+
from semantic_code_intelligence.cli.commands.hotspots_cmd import hotspots_cmd
|
|
600
|
+
|
|
601
|
+
_write_sample_project(tmp_path)
|
|
602
|
+
runner = CliRunner()
|
|
603
|
+
result = runner.invoke(hotspots_cmd, [
|
|
604
|
+
"--path", str(tmp_path), "--no-git",
|
|
605
|
+
])
|
|
606
|
+
assert result.exit_code == 0
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# =========================================================================
|
|
610
|
+
# CLI: codexa impact
|
|
611
|
+
# =========================================================================
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class TestImpactCLI:
|
|
615
|
+
"""Tests for the impact CLI command."""
|
|
616
|
+
|
|
617
|
+
def test_impact_help(self):
|
|
618
|
+
from semantic_code_intelligence.cli.commands.impact_cmd import impact_cmd
|
|
619
|
+
|
|
620
|
+
runner = CliRunner()
|
|
621
|
+
result = runner.invoke(impact_cmd, ["--help"])
|
|
622
|
+
assert result.exit_code == 0
|
|
623
|
+
assert "impact" in result.output.lower() or "blast" in result.output.lower()
|
|
624
|
+
|
|
625
|
+
def test_impact_json(self, tmp_path):
|
|
626
|
+
from semantic_code_intelligence.cli.commands.impact_cmd import impact_cmd
|
|
627
|
+
|
|
628
|
+
_write_sample_project(tmp_path)
|
|
629
|
+
runner = CliRunner()
|
|
630
|
+
result = runner.invoke(impact_cmd, [
|
|
631
|
+
"helper", "--path", str(tmp_path), "--json",
|
|
632
|
+
])
|
|
633
|
+
assert result.exit_code == 0
|
|
634
|
+
data = json.loads(result.output)
|
|
635
|
+
assert data["target"] == "helper"
|
|
636
|
+
|
|
637
|
+
def test_impact_pipe(self, tmp_path):
|
|
638
|
+
from semantic_code_intelligence.cli.commands.impact_cmd import impact_cmd
|
|
639
|
+
|
|
640
|
+
_write_sample_project(tmp_path)
|
|
641
|
+
runner = CliRunner()
|
|
642
|
+
result = runner.invoke(impact_cmd, [
|
|
643
|
+
"helper", "--path", str(tmp_path), "--pipe",
|
|
644
|
+
])
|
|
645
|
+
assert result.exit_code == 0
|
|
646
|
+
assert "target=helper" in result.output
|
|
647
|
+
|
|
648
|
+
def test_impact_unknown_target(self, tmp_path):
|
|
649
|
+
from semantic_code_intelligence.cli.commands.impact_cmd import impact_cmd
|
|
650
|
+
|
|
651
|
+
_write_sample_project(tmp_path)
|
|
652
|
+
runner = CliRunner()
|
|
653
|
+
result = runner.invoke(impact_cmd, [
|
|
654
|
+
"zzz_nonexistent", "--path", str(tmp_path), "--json",
|
|
655
|
+
])
|
|
656
|
+
assert result.exit_code == 0
|
|
657
|
+
data = json.loads(result.output)
|
|
658
|
+
assert data["total_affected"] == 0
|
|
659
|
+
|
|
660
|
+
def test_impact_rich_output(self, tmp_path):
|
|
661
|
+
from semantic_code_intelligence.cli.commands.impact_cmd import impact_cmd
|
|
662
|
+
|
|
663
|
+
_write_sample_project(tmp_path)
|
|
664
|
+
runner = CliRunner()
|
|
665
|
+
result = runner.invoke(impact_cmd, [
|
|
666
|
+
"helper", "--path", str(tmp_path),
|
|
667
|
+
])
|
|
668
|
+
assert result.exit_code == 0
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# =========================================================================
|
|
672
|
+
# CLI: codexa trace
|
|
673
|
+
# =========================================================================
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class TestTraceCLI:
|
|
677
|
+
"""Tests for the trace CLI command."""
|
|
678
|
+
|
|
679
|
+
def test_trace_help(self):
|
|
680
|
+
from semantic_code_intelligence.cli.commands.trace_cmd import trace_cmd
|
|
681
|
+
|
|
682
|
+
runner = CliRunner()
|
|
683
|
+
result = runner.invoke(trace_cmd, ["--help"])
|
|
684
|
+
assert result.exit_code == 0
|
|
685
|
+
assert "trace" in result.output.lower()
|
|
686
|
+
|
|
687
|
+
def test_trace_json(self, tmp_path):
|
|
688
|
+
from semantic_code_intelligence.cli.commands.trace_cmd import trace_cmd
|
|
689
|
+
|
|
690
|
+
_write_sample_project(tmp_path)
|
|
691
|
+
runner = CliRunner()
|
|
692
|
+
result = runner.invoke(trace_cmd, [
|
|
693
|
+
"compute", "--path", str(tmp_path), "--json",
|
|
694
|
+
])
|
|
695
|
+
assert result.exit_code == 0
|
|
696
|
+
data = json.loads(result.output)
|
|
697
|
+
assert data["target"] == "compute"
|
|
698
|
+
|
|
699
|
+
def test_trace_pipe(self, tmp_path):
|
|
700
|
+
from semantic_code_intelligence.cli.commands.trace_cmd import trace_cmd
|
|
701
|
+
|
|
702
|
+
_write_sample_project(tmp_path)
|
|
703
|
+
runner = CliRunner()
|
|
704
|
+
result = runner.invoke(trace_cmd, [
|
|
705
|
+
"compute", "--path", str(tmp_path), "--pipe",
|
|
706
|
+
])
|
|
707
|
+
assert result.exit_code == 0
|
|
708
|
+
assert "target=compute" in result.output
|
|
709
|
+
|
|
710
|
+
def test_trace_unknown_symbol(self, tmp_path):
|
|
711
|
+
from semantic_code_intelligence.cli.commands.trace_cmd import trace_cmd
|
|
712
|
+
|
|
713
|
+
_write_sample_project(tmp_path)
|
|
714
|
+
runner = CliRunner()
|
|
715
|
+
result = runner.invoke(trace_cmd, [
|
|
716
|
+
"zzz_missing", "--path", str(tmp_path), "--json",
|
|
717
|
+
])
|
|
718
|
+
assert result.exit_code == 0
|
|
719
|
+
data = json.loads(result.output)
|
|
720
|
+
assert "error" in data
|
|
721
|
+
|
|
722
|
+
def test_trace_rich_output(self, tmp_path):
|
|
723
|
+
from semantic_code_intelligence.cli.commands.trace_cmd import trace_cmd
|
|
724
|
+
|
|
725
|
+
_write_sample_project(tmp_path)
|
|
726
|
+
runner = CliRunner()
|
|
727
|
+
result = runner.invoke(trace_cmd, [
|
|
728
|
+
"compute", "--path", str(tmp_path),
|
|
729
|
+
])
|
|
730
|
+
assert result.exit_code == 0
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# =========================================================================
|
|
734
|
+
# Plugin hooks
|
|
735
|
+
# =========================================================================
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class TestPluginHooks:
|
|
739
|
+
"""Tests for Phase 18 plugin hooks."""
|
|
740
|
+
|
|
741
|
+
def test_pre_hotspot_analysis_hook(self):
|
|
742
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
743
|
+
|
|
744
|
+
assert hasattr(PluginHook, "PRE_HOTSPOT_ANALYSIS")
|
|
745
|
+
|
|
746
|
+
def test_post_hotspot_analysis_hook(self):
|
|
747
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
748
|
+
|
|
749
|
+
assert hasattr(PluginHook, "POST_HOTSPOT_ANALYSIS")
|
|
750
|
+
|
|
751
|
+
def test_pre_impact_analysis_hook(self):
|
|
752
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
753
|
+
|
|
754
|
+
assert hasattr(PluginHook, "PRE_IMPACT_ANALYSIS")
|
|
755
|
+
|
|
756
|
+
def test_post_impact_analysis_hook(self):
|
|
757
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
758
|
+
|
|
759
|
+
assert hasattr(PluginHook, "POST_IMPACT_ANALYSIS")
|
|
760
|
+
|
|
761
|
+
def test_pre_trace_hook(self):
|
|
762
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
763
|
+
|
|
764
|
+
assert hasattr(PluginHook, "PRE_TRACE")
|
|
765
|
+
|
|
766
|
+
def test_post_trace_hook(self):
|
|
767
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
768
|
+
|
|
769
|
+
assert hasattr(PluginHook, "POST_TRACE")
|
|
770
|
+
|
|
771
|
+
def test_all_hooks_registered_in_manager(self):
|
|
772
|
+
from semantic_code_intelligence.plugins import PluginHook, PluginManager
|
|
773
|
+
|
|
774
|
+
pm = PluginManager()
|
|
775
|
+
for hook in PluginHook:
|
|
776
|
+
assert hook in pm._hook_registry
|
|
777
|
+
|
|
778
|
+
def test_hook_count_is_22(self):
|
|
779
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
780
|
+
|
|
781
|
+
assert len(PluginHook) == 22
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# =========================================================================
|
|
785
|
+
# Router — 30 commands
|
|
786
|
+
# =========================================================================
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class TestRouter:
|
|
790
|
+
"""Tests for CLI router with 31 commands."""
|
|
791
|
+
|
|
792
|
+
def test_command_count_is_31(self):
|
|
793
|
+
import click
|
|
794
|
+
|
|
795
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
796
|
+
|
|
797
|
+
group = click.Group("test")
|
|
798
|
+
register_commands(group)
|
|
799
|
+
assert len(group.commands) == 39
|
|
800
|
+
|
|
801
|
+
def test_hotspots_registered(self):
|
|
802
|
+
import click
|
|
803
|
+
|
|
804
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
805
|
+
|
|
806
|
+
group = click.Group("test")
|
|
807
|
+
register_commands(group)
|
|
808
|
+
assert "hotspots" in group.commands
|
|
809
|
+
|
|
810
|
+
def test_impact_registered(self):
|
|
811
|
+
import click
|
|
812
|
+
|
|
813
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
814
|
+
|
|
815
|
+
group = click.Group("test")
|
|
816
|
+
register_commands(group)
|
|
817
|
+
assert "impact" in group.commands
|
|
818
|
+
|
|
819
|
+
def test_trace_registered(self):
|
|
820
|
+
import click
|
|
821
|
+
|
|
822
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
823
|
+
|
|
824
|
+
group = click.Group("test")
|
|
825
|
+
register_commands(group)
|
|
826
|
+
assert "trace" in group.commands
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
# =========================================================================
|
|
830
|
+
# Version
|
|
831
|
+
# =========================================================================
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class TestVersion:
|
|
835
|
+
"""Test version is 0.19.0."""
|
|
836
|
+
|
|
837
|
+
def test_version_string(self):
|
|
838
|
+
from semantic_code_intelligence import __version__
|
|
839
|
+
|
|
840
|
+
assert __version__ == "0.4.0"
|
|
841
|
+
|
|
842
|
+
def test_cli_version(self):
|
|
843
|
+
from semantic_code_intelligence.cli.main import cli
|
|
844
|
+
|
|
845
|
+
runner = CliRunner()
|
|
846
|
+
result = runner.invoke(cli, ["--version"])
|
|
847
|
+
assert result.exit_code == 0
|
|
848
|
+
assert "0.4.0" in result.output
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
# =========================================================================
|
|
852
|
+
# Docs generation
|
|
853
|
+
# =========================================================================
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
class TestDocsGeneration:
|
|
857
|
+
"""Tests for WORKFLOW_INTELLIGENCE.md generation."""
|
|
858
|
+
|
|
859
|
+
def test_generate_workflow_intelligence_reference(self):
|
|
860
|
+
from semantic_code_intelligence.docs import (
|
|
861
|
+
generate_workflow_intelligence_reference,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
md = generate_workflow_intelligence_reference()
|
|
865
|
+
assert "Hotspot Detection" in md
|
|
866
|
+
assert "Impact Analysis" in md
|
|
867
|
+
assert "Symbol Trace" in md
|
|
868
|
+
assert "codexa hotspots" in md
|
|
869
|
+
assert "codexa impact" in md
|
|
870
|
+
assert "codexa trace" in md
|
|
871
|
+
|
|
872
|
+
def test_workflow_intelligence_in_all_docs(self, tmp_path):
|
|
873
|
+
from semantic_code_intelligence.docs import generate_all_docs
|
|
874
|
+
|
|
875
|
+
generated = generate_all_docs(tmp_path)
|
|
876
|
+
assert "WORKFLOW_INTELLIGENCE.md" in generated
|
|
877
|
+
|
|
878
|
+
def test_workflow_intelligence_file_content(self, tmp_path):
|
|
879
|
+
from semantic_code_intelligence.docs import generate_all_docs
|
|
880
|
+
|
|
881
|
+
generate_all_docs(tmp_path)
|
|
882
|
+
path = tmp_path / "WORKFLOW_INTELLIGENCE.md"
|
|
883
|
+
assert path.exists()
|
|
884
|
+
content = path.read_text(encoding="utf-8")
|
|
885
|
+
assert "Workflow Intelligence Reference" in content
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
# =========================================================================
|
|
889
|
+
# Module structure
|
|
890
|
+
# =========================================================================
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
class TestModuleStructure:
|
|
894
|
+
"""Tests for Phase 18 module structure."""
|
|
895
|
+
|
|
896
|
+
def test_ci_hotspots_importable(self):
|
|
897
|
+
from semantic_code_intelligence.ci import hotspots
|
|
898
|
+
|
|
899
|
+
assert hasattr(hotspots, "analyze_hotspots")
|
|
900
|
+
assert hasattr(hotspots, "HotspotReport")
|
|
901
|
+
|
|
902
|
+
def test_ci_impact_importable(self):
|
|
903
|
+
from semantic_code_intelligence.ci import impact
|
|
904
|
+
|
|
905
|
+
assert hasattr(impact, "analyze_impact")
|
|
906
|
+
assert hasattr(impact, "ImpactReport")
|
|
907
|
+
|
|
908
|
+
def test_ci_trace_importable(self):
|
|
909
|
+
from semantic_code_intelligence.ci import trace
|
|
910
|
+
|
|
911
|
+
assert hasattr(trace, "trace_symbol")
|
|
912
|
+
assert hasattr(trace, "TraceResult")
|
|
913
|
+
|
|
914
|
+
def test_hotspots_cmd_importable(self):
|
|
915
|
+
from semantic_code_intelligence.cli.commands.hotspots_cmd import hotspots_cmd
|
|
916
|
+
|
|
917
|
+
assert hotspots_cmd is not None
|
|
918
|
+
|
|
919
|
+
def test_impact_cmd_importable(self):
|
|
920
|
+
from semantic_code_intelligence.cli.commands.impact_cmd import impact_cmd
|
|
921
|
+
|
|
922
|
+
assert impact_cmd is not None
|
|
923
|
+
|
|
924
|
+
def test_trace_cmd_importable(self):
|
|
925
|
+
from semantic_code_intelligence.cli.commands.trace_cmd import trace_cmd
|
|
926
|
+
|
|
927
|
+
assert trace_cmd is not None
|
|
928
|
+
|
|
929
|
+
def test_ci_init_docstring(self):
|
|
930
|
+
import semantic_code_intelligence.ci as ci_mod
|
|
931
|
+
|
|
932
|
+
assert "hotspots" in ci_mod.__doc__
|
|
933
|
+
assert "impact" in ci_mod.__doc__
|
|
934
|
+
assert "trace" in ci_mod.__doc__
|