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,351 @@
|
|
|
1
|
+
"""Tests for AI features — repository summary, AI context, code explanations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from semantic_code_intelligence.analysis.ai_features import (
|
|
8
|
+
CodeExplanation,
|
|
9
|
+
LanguageStats,
|
|
10
|
+
RepoSummary,
|
|
11
|
+
explain_file,
|
|
12
|
+
explain_symbol,
|
|
13
|
+
generate_ai_context,
|
|
14
|
+
summarize_repository,
|
|
15
|
+
)
|
|
16
|
+
from semantic_code_intelligence.context.engine import ContextBuilder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Sample code
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
PYTHON_SAMPLE = '''\
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
def helper():
|
|
28
|
+
return 42
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
result = helper()
|
|
32
|
+
print(result)
|
|
33
|
+
|
|
34
|
+
class Worker:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.data = []
|
|
37
|
+
|
|
38
|
+
def process(self):
|
|
39
|
+
result = helper()
|
|
40
|
+
return result
|
|
41
|
+
'''
|
|
42
|
+
|
|
43
|
+
JS_SAMPLE = '''\
|
|
44
|
+
import { readFile } from 'fs';
|
|
45
|
+
|
|
46
|
+
function parse(data) {
|
|
47
|
+
return JSON.parse(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function load(path) {
|
|
51
|
+
const data = readFile(path);
|
|
52
|
+
return parse(data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class DataLoader {
|
|
56
|
+
constructor(path) {
|
|
57
|
+
this.path = path;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
load() {
|
|
61
|
+
return load(this.path);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# RepoSummary
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
class TestRepoSummary:
|
|
72
|
+
@pytest.fixture(autouse=True)
|
|
73
|
+
def setup(self):
|
|
74
|
+
self.builder = ContextBuilder()
|
|
75
|
+
self.builder.index_file("app.py", PYTHON_SAMPLE)
|
|
76
|
+
self.builder.index_file("app.js", JS_SAMPLE)
|
|
77
|
+
self.summary = summarize_repository(self.builder)
|
|
78
|
+
|
|
79
|
+
def test_total_files(self):
|
|
80
|
+
assert self.summary.total_files == 2
|
|
81
|
+
|
|
82
|
+
def test_total_symbols(self):
|
|
83
|
+
assert self.summary.total_symbols > 0
|
|
84
|
+
|
|
85
|
+
def test_total_functions(self):
|
|
86
|
+
assert self.summary.total_functions >= 2 # helper, main, parse, load
|
|
87
|
+
|
|
88
|
+
def test_total_classes(self):
|
|
89
|
+
assert self.summary.total_classes >= 2 # Worker, DataLoader
|
|
90
|
+
|
|
91
|
+
def test_total_methods(self):
|
|
92
|
+
assert self.summary.total_methods >= 2
|
|
93
|
+
|
|
94
|
+
def test_total_imports(self):
|
|
95
|
+
assert self.summary.total_imports >= 2
|
|
96
|
+
|
|
97
|
+
def test_languages_listed(self):
|
|
98
|
+
lang_names = {l.language for l in self.summary.languages}
|
|
99
|
+
assert "python" in lang_names
|
|
100
|
+
assert "javascript" in lang_names
|
|
101
|
+
|
|
102
|
+
def test_top_functions(self):
|
|
103
|
+
assert len(self.summary.top_functions) > 0
|
|
104
|
+
assert "name" in self.summary.top_functions[0]
|
|
105
|
+
|
|
106
|
+
def test_top_classes(self):
|
|
107
|
+
assert len(self.summary.top_classes) > 0
|
|
108
|
+
assert "name" in self.summary.top_classes[0]
|
|
109
|
+
|
|
110
|
+
def test_to_dict(self):
|
|
111
|
+
d = self.summary.to_dict()
|
|
112
|
+
assert "total_files" in d
|
|
113
|
+
assert "languages" in d
|
|
114
|
+
assert "top_functions" in d
|
|
115
|
+
|
|
116
|
+
def test_to_json(self):
|
|
117
|
+
j = self.summary.to_json()
|
|
118
|
+
parsed = json.loads(j)
|
|
119
|
+
assert parsed["total_files"] == 2
|
|
120
|
+
|
|
121
|
+
def test_render(self):
|
|
122
|
+
text = self.summary.render()
|
|
123
|
+
assert "Repository Summary" in text
|
|
124
|
+
assert "Files:" in text
|
|
125
|
+
assert "Languages" in text
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestLanguageStats:
|
|
129
|
+
def test_to_dict(self):
|
|
130
|
+
stats = LanguageStats(
|
|
131
|
+
language="python",
|
|
132
|
+
file_count=5,
|
|
133
|
+
function_count=10,
|
|
134
|
+
class_count=3,
|
|
135
|
+
)
|
|
136
|
+
d = stats.to_dict()
|
|
137
|
+
assert d["language"] == "python"
|
|
138
|
+
assert d["file_count"] == 5
|
|
139
|
+
assert d["function_count"] == 10
|
|
140
|
+
assert d["class_count"] == 3
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestRepoSummaryEmpty:
|
|
144
|
+
def test_empty_builder(self):
|
|
145
|
+
builder = ContextBuilder()
|
|
146
|
+
summary = summarize_repository(builder)
|
|
147
|
+
assert summary.total_files == 0
|
|
148
|
+
assert summary.total_symbols == 0
|
|
149
|
+
assert summary.languages == []
|
|
150
|
+
|
|
151
|
+
def test_single_file(self):
|
|
152
|
+
builder = ContextBuilder()
|
|
153
|
+
builder.index_file("app.py", PYTHON_SAMPLE)
|
|
154
|
+
summary = summarize_repository(builder)
|
|
155
|
+
assert summary.total_files == 1
|
|
156
|
+
lang_names = {l.language for l in summary.languages}
|
|
157
|
+
assert "python" in lang_names
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# AI Context Generation
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
class TestGenerateAIContext:
|
|
165
|
+
@pytest.fixture(autouse=True)
|
|
166
|
+
def setup(self):
|
|
167
|
+
self.builder = ContextBuilder()
|
|
168
|
+
self.builder.index_file("app.py", PYTHON_SAMPLE)
|
|
169
|
+
self.builder.index_file("app.js", JS_SAMPLE)
|
|
170
|
+
|
|
171
|
+
def test_basic_context(self):
|
|
172
|
+
ctx = generate_ai_context(self.builder)
|
|
173
|
+
assert "summary" in ctx
|
|
174
|
+
assert "call_graph" in ctx
|
|
175
|
+
assert "dependencies" in ctx
|
|
176
|
+
|
|
177
|
+
def test_context_with_symbol_focus(self):
|
|
178
|
+
ctx = generate_ai_context(self.builder, symbol_name="helper")
|
|
179
|
+
assert "focused_contexts" in ctx
|
|
180
|
+
assert len(ctx["focused_contexts"]) >= 1
|
|
181
|
+
|
|
182
|
+
def test_context_with_file_focus(self):
|
|
183
|
+
ctx = generate_ai_context(self.builder, file_path="app.py")
|
|
184
|
+
assert "file_symbols" in ctx
|
|
185
|
+
assert len(ctx["file_symbols"]) > 0
|
|
186
|
+
|
|
187
|
+
def test_context_without_call_graph(self):
|
|
188
|
+
ctx = generate_ai_context(self.builder, include_call_graph=False)
|
|
189
|
+
assert "call_graph" not in ctx
|
|
190
|
+
|
|
191
|
+
def test_context_without_dependencies(self):
|
|
192
|
+
ctx = generate_ai_context(self.builder, include_dependencies=False)
|
|
193
|
+
assert "dependencies" not in ctx
|
|
194
|
+
|
|
195
|
+
def test_context_is_json_serializable(self):
|
|
196
|
+
ctx = generate_ai_context(self.builder)
|
|
197
|
+
j = json.dumps(ctx)
|
|
198
|
+
assert len(j) > 0
|
|
199
|
+
|
|
200
|
+
def test_call_graph_has_edges(self):
|
|
201
|
+
ctx = generate_ai_context(self.builder)
|
|
202
|
+
assert ctx["call_graph"]["edge_count"] > 0
|
|
203
|
+
|
|
204
|
+
def test_dependencies_has_files(self):
|
|
205
|
+
ctx = generate_ai_context(self.builder)
|
|
206
|
+
assert "app.py" in ctx["dependencies"]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# Code Explanation
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
class TestExplainSymbol:
|
|
214
|
+
@pytest.fixture(autouse=True)
|
|
215
|
+
def setup(self):
|
|
216
|
+
self.builder = ContextBuilder()
|
|
217
|
+
self.builder.index_file("app.py", PYTHON_SAMPLE)
|
|
218
|
+
self.symbols = self.builder.get_all_symbols()
|
|
219
|
+
|
|
220
|
+
def test_explain_function(self):
|
|
221
|
+
func = next(s for s in self.symbols if s.name == "helper")
|
|
222
|
+
explanation = explain_symbol(func)
|
|
223
|
+
assert explanation.symbol_name == "helper"
|
|
224
|
+
assert explanation.symbol_kind == "function"
|
|
225
|
+
assert "Function" in explanation.summary
|
|
226
|
+
assert "helper" in explanation.summary
|
|
227
|
+
|
|
228
|
+
def test_explain_class(self):
|
|
229
|
+
cls = next(s for s in self.symbols if s.name == "Worker")
|
|
230
|
+
explanation = explain_symbol(cls)
|
|
231
|
+
assert explanation.symbol_name == "Worker"
|
|
232
|
+
assert "Class" in explanation.summary
|
|
233
|
+
|
|
234
|
+
def test_explain_method(self):
|
|
235
|
+
method = next(s for s in self.symbols if s.name == "process")
|
|
236
|
+
explanation = explain_symbol(method)
|
|
237
|
+
assert explanation.symbol_name == "process"
|
|
238
|
+
assert "Method" in explanation.summary
|
|
239
|
+
assert "Worker" in explanation.summary
|
|
240
|
+
|
|
241
|
+
def test_explain_import(self):
|
|
242
|
+
imp = next(s for s in self.symbols if s.kind == "import")
|
|
243
|
+
explanation = explain_symbol(imp)
|
|
244
|
+
assert "Import" in explanation.summary
|
|
245
|
+
|
|
246
|
+
def test_explain_with_builder_context(self):
|
|
247
|
+
func = next(s for s in self.symbols if s.name == "main")
|
|
248
|
+
explanation = explain_symbol(func, self.builder)
|
|
249
|
+
assert "related_symbols" in explanation.details or "file_imports" in explanation.details
|
|
250
|
+
|
|
251
|
+
def test_explanation_to_dict(self):
|
|
252
|
+
func = next(s for s in self.symbols if s.name == "helper")
|
|
253
|
+
explanation = explain_symbol(func)
|
|
254
|
+
d = explanation.to_dict()
|
|
255
|
+
assert "symbol_name" in d
|
|
256
|
+
assert "summary" in d
|
|
257
|
+
assert "details" in d
|
|
258
|
+
|
|
259
|
+
def test_explanation_render(self):
|
|
260
|
+
func = next(s for s in self.symbols if s.name == "helper")
|
|
261
|
+
explanation = explain_symbol(func)
|
|
262
|
+
text = explanation.render()
|
|
263
|
+
assert "helper" in text
|
|
264
|
+
assert "File:" in text
|
|
265
|
+
|
|
266
|
+
def test_explanation_render_with_details(self):
|
|
267
|
+
func = next(s for s in self.symbols if s.name == "main")
|
|
268
|
+
explanation = explain_symbol(func, self.builder)
|
|
269
|
+
text = explanation.render()
|
|
270
|
+
assert "main" in text
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class TestExplainFile:
|
|
274
|
+
def test_explain_python_file(self):
|
|
275
|
+
explanations = explain_file("app.py", PYTHON_SAMPLE)
|
|
276
|
+
assert len(explanations) > 0
|
|
277
|
+
# Should not include imports
|
|
278
|
+
for e in explanations:
|
|
279
|
+
assert e.symbol_kind != "import"
|
|
280
|
+
|
|
281
|
+
def test_explain_js_file(self):
|
|
282
|
+
explanations = explain_file("app.js", JS_SAMPLE)
|
|
283
|
+
assert len(explanations) > 0
|
|
284
|
+
|
|
285
|
+
def test_explain_empty_file(self):
|
|
286
|
+
explanations = explain_file("empty.py", "")
|
|
287
|
+
assert explanations == []
|
|
288
|
+
|
|
289
|
+
def test_explain_unsupported_file(self):
|
|
290
|
+
explanations = explain_file("style.css", "body { color: red; }")
|
|
291
|
+
assert explanations == []
|
|
292
|
+
|
|
293
|
+
def test_each_explanation_has_name(self):
|
|
294
|
+
explanations = explain_file("app.py", PYTHON_SAMPLE)
|
|
295
|
+
for e in explanations:
|
|
296
|
+
assert e.symbol_name
|
|
297
|
+
assert e.file_path == "app.py"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class TestCodeExplanation:
|
|
301
|
+
def test_dataclass_fields(self):
|
|
302
|
+
exp = CodeExplanation(
|
|
303
|
+
symbol_name="foo",
|
|
304
|
+
symbol_kind="function",
|
|
305
|
+
file_path="test.py",
|
|
306
|
+
summary="A test function.",
|
|
307
|
+
details={"parameters": "a, b"},
|
|
308
|
+
)
|
|
309
|
+
assert exp.symbol_name == "foo"
|
|
310
|
+
assert exp.details["parameters"] == "a, b"
|
|
311
|
+
|
|
312
|
+
def test_render_empty_details(self):
|
|
313
|
+
exp = CodeExplanation(
|
|
314
|
+
symbol_name="foo",
|
|
315
|
+
symbol_kind="function",
|
|
316
|
+
file_path="test.py",
|
|
317
|
+
summary="A test function.",
|
|
318
|
+
)
|
|
319
|
+
text = exp.render()
|
|
320
|
+
assert "foo" in text
|
|
321
|
+
assert "Function" in text
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Edge cases
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
class TestAIFeaturesEdgeCases:
|
|
329
|
+
def test_summary_single_symbol(self):
|
|
330
|
+
builder = ContextBuilder()
|
|
331
|
+
builder.index_file("one.py", "def single(): pass\n")
|
|
332
|
+
summary = summarize_repository(builder)
|
|
333
|
+
assert summary.total_functions == 1
|
|
334
|
+
assert summary.total_files == 1
|
|
335
|
+
|
|
336
|
+
def test_ai_context_empty_builder(self):
|
|
337
|
+
builder = ContextBuilder()
|
|
338
|
+
ctx = generate_ai_context(builder)
|
|
339
|
+
assert ctx["summary"]["total_files"] == 0
|
|
340
|
+
|
|
341
|
+
def test_ai_context_nonexistent_symbol(self):
|
|
342
|
+
builder = ContextBuilder()
|
|
343
|
+
builder.index_file("app.py", PYTHON_SAMPLE)
|
|
344
|
+
ctx = generate_ai_context(builder, symbol_name="nonexistent")
|
|
345
|
+
assert ctx["focused_contexts"] == []
|
|
346
|
+
|
|
347
|
+
def test_ai_context_nonexistent_file(self):
|
|
348
|
+
builder = ContextBuilder()
|
|
349
|
+
builder.index_file("app.py", PYTHON_SAMPLE)
|
|
350
|
+
ctx = generate_ai_context(builder, file_path="nonexistent.py")
|
|
351
|
+
assert ctx["file_symbols"] == []
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Tests for the code chunker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from semantic_code_intelligence.indexing.chunker import (
|
|
10
|
+
CodeChunk,
|
|
11
|
+
chunk_code,
|
|
12
|
+
chunk_file,
|
|
13
|
+
detect_language,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestDetectLanguage:
|
|
18
|
+
"""Tests for language detection."""
|
|
19
|
+
|
|
20
|
+
def test_python(self):
|
|
21
|
+
assert detect_language("main.py") == "python"
|
|
22
|
+
|
|
23
|
+
def test_javascript(self):
|
|
24
|
+
assert detect_language("app.js") == "javascript"
|
|
25
|
+
|
|
26
|
+
def test_typescript(self):
|
|
27
|
+
assert detect_language("component.tsx") == "typescript"
|
|
28
|
+
|
|
29
|
+
def test_java(self):
|
|
30
|
+
assert detect_language("Main.java") == "java"
|
|
31
|
+
|
|
32
|
+
def test_unknown(self):
|
|
33
|
+
assert detect_language("data.xyz") == "unknown"
|
|
34
|
+
|
|
35
|
+
def test_path_with_directory(self):
|
|
36
|
+
assert detect_language("/some/path/file.py") == "python"
|
|
37
|
+
assert detect_language("C:\\code\\file.js") == "javascript"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestChunkCode:
|
|
41
|
+
"""Tests for code chunking logic."""
|
|
42
|
+
|
|
43
|
+
def test_empty_content(self):
|
|
44
|
+
chunks = chunk_code("", "test.py")
|
|
45
|
+
assert chunks == []
|
|
46
|
+
|
|
47
|
+
def test_whitespace_only(self):
|
|
48
|
+
chunks = chunk_code(" \n \n ", "test.py")
|
|
49
|
+
assert chunks == []
|
|
50
|
+
|
|
51
|
+
def test_small_file_single_chunk(self):
|
|
52
|
+
code = "def hello():\n return 'world'\n"
|
|
53
|
+
chunks = chunk_code(code, "test.py", chunk_size=1000)
|
|
54
|
+
assert len(chunks) == 1
|
|
55
|
+
assert chunks[0].content == code
|
|
56
|
+
assert chunks[0].start_line == 1
|
|
57
|
+
assert chunks[0].end_line == 2
|
|
58
|
+
assert chunks[0].language == "python"
|
|
59
|
+
assert chunks[0].chunk_index == 0
|
|
60
|
+
|
|
61
|
+
def test_large_file_multiple_chunks(self):
|
|
62
|
+
lines = [f"line_{i} = {i}\n" for i in range(100)]
|
|
63
|
+
code = "".join(lines)
|
|
64
|
+
chunks = chunk_code(code, "test.py", chunk_size=200, chunk_overlap=50)
|
|
65
|
+
assert len(chunks) > 1
|
|
66
|
+
|
|
67
|
+
def test_chunks_cover_all_content(self):
|
|
68
|
+
lines = [f"x_{i} = {i}\n" for i in range(50)]
|
|
69
|
+
code = "".join(lines)
|
|
70
|
+
chunks = chunk_code(code, "test.py", chunk_size=100, chunk_overlap=20)
|
|
71
|
+
# Every line should appear in at least one chunk
|
|
72
|
+
all_chunk_text = "".join(c.content for c in chunks)
|
|
73
|
+
for line in lines:
|
|
74
|
+
assert line in all_chunk_text
|
|
75
|
+
|
|
76
|
+
def test_chunk_index_sequential(self):
|
|
77
|
+
lines = [f"var_{i} = {i}\n" for i in range(100)]
|
|
78
|
+
code = "".join(lines)
|
|
79
|
+
chunks = chunk_code(code, "test.py", chunk_size=150, chunk_overlap=30)
|
|
80
|
+
for i, chunk in enumerate(chunks):
|
|
81
|
+
assert chunk.chunk_index == i
|
|
82
|
+
|
|
83
|
+
def test_chunk_metadata(self):
|
|
84
|
+
code = "function hello() { return 1; }\n"
|
|
85
|
+
chunks = chunk_code(code, "app.js", chunk_size=1000)
|
|
86
|
+
assert chunks[0].file_path == "app.js"
|
|
87
|
+
assert chunks[0].language == "javascript"
|
|
88
|
+
|
|
89
|
+
def test_overlap_between_chunks(self):
|
|
90
|
+
lines = [f"line_{i:03d} = {i}\n" for i in range(100)]
|
|
91
|
+
code = "".join(lines)
|
|
92
|
+
chunks = chunk_code(code, "test.py", chunk_size=200, chunk_overlap=50)
|
|
93
|
+
if len(chunks) >= 2:
|
|
94
|
+
# Last lines of chunk N should appear in chunk N+1
|
|
95
|
+
chunk0_lines = set(chunks[0].content.splitlines())
|
|
96
|
+
chunk1_lines = set(chunks[1].content.splitlines())
|
|
97
|
+
overlap = chunk0_lines & chunk1_lines
|
|
98
|
+
assert len(overlap) > 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestChunkFile:
|
|
102
|
+
"""Tests for file-based chunking."""
|
|
103
|
+
|
|
104
|
+
def test_chunk_existing_file(self, tmp_path: Path):
|
|
105
|
+
f = tmp_path / "test.py"
|
|
106
|
+
f.write_text("def hello():\n pass\n", encoding="utf-8")
|
|
107
|
+
chunks = chunk_file(f, chunk_size=1000)
|
|
108
|
+
assert len(chunks) == 1
|
|
109
|
+
|
|
110
|
+
def test_chunk_nonexistent_file(self, tmp_path: Path):
|
|
111
|
+
f = tmp_path / "missing.py"
|
|
112
|
+
chunks = chunk_file(f)
|
|
113
|
+
assert chunks == []
|
|
114
|
+
|
|
115
|
+
def test_chunk_empty_file(self, tmp_path: Path):
|
|
116
|
+
f = tmp_path / "empty.py"
|
|
117
|
+
f.write_text("", encoding="utf-8")
|
|
118
|
+
chunks = chunk_file(f)
|
|
119
|
+
assert chunks == []
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Tests for CLI commands and routing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from click.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
from semantic_code_intelligence.cli.main import cli
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def runner() -> CliRunner:
|
|
16
|
+
"""Provide a Click test runner."""
|
|
17
|
+
return CliRunner()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def initialized_project(tmp_path: Path) -> Path:
|
|
22
|
+
"""Create an initialized project directory."""
|
|
23
|
+
runner = CliRunner()
|
|
24
|
+
with runner.isolated_filesystem(temp_dir=tmp_path) as td:
|
|
25
|
+
project = Path(td)
|
|
26
|
+
runner.invoke(cli, ["init", str(project)])
|
|
27
|
+
yield project
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestCLIMain:
|
|
31
|
+
"""Tests for the main CLI group."""
|
|
32
|
+
|
|
33
|
+
def test_cli_help(self, runner: CliRunner):
|
|
34
|
+
result = runner.invoke(cli, ["--help"])
|
|
35
|
+
assert result.exit_code == 0
|
|
36
|
+
assert "Codex" in result.output
|
|
37
|
+
|
|
38
|
+
def test_cli_version(self, runner: CliRunner):
|
|
39
|
+
result = runner.invoke(cli, ["--version"])
|
|
40
|
+
assert result.exit_code == 0
|
|
41
|
+
assert "0.4.0" in result.output
|
|
42
|
+
|
|
43
|
+
def test_cli_verbose_flag(self, runner: CliRunner):
|
|
44
|
+
result = runner.invoke(cli, ["--verbose", "--help"])
|
|
45
|
+
assert result.exit_code == 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestInitCommand:
|
|
49
|
+
"""Tests for the init command."""
|
|
50
|
+
|
|
51
|
+
def test_init_creates_project(self, runner: CliRunner, tmp_path: Path):
|
|
52
|
+
result = runner.invoke(cli, ["init", str(tmp_path)])
|
|
53
|
+
assert result.exit_code == 0
|
|
54
|
+
assert (tmp_path / ".codexa").is_dir()
|
|
55
|
+
assert (tmp_path / ".codexa" / "config.json").exists()
|
|
56
|
+
assert (tmp_path / ".codexa" / "index").is_dir()
|
|
57
|
+
|
|
58
|
+
def test_init_already_initialized(self, runner: CliRunner, tmp_path: Path):
|
|
59
|
+
# First init
|
|
60
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
61
|
+
# Second init should detect existing
|
|
62
|
+
result = runner.invoke(cli, ["init", str(tmp_path)])
|
|
63
|
+
assert result.exit_code == 0
|
|
64
|
+
assert "already initialized" in result.output
|
|
65
|
+
|
|
66
|
+
def test_init_default_path(self, runner: CliRunner):
|
|
67
|
+
with runner.isolated_filesystem() as td:
|
|
68
|
+
result = runner.invoke(cli, ["init"])
|
|
69
|
+
assert result.exit_code == 0
|
|
70
|
+
assert Path(td, ".codexa").is_dir()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestIndexCommand:
|
|
74
|
+
"""Tests for the index command."""
|
|
75
|
+
|
|
76
|
+
def test_index_without_init_fails(self, runner: CliRunner, tmp_path: Path):
|
|
77
|
+
result = runner.invoke(cli, ["index", str(tmp_path)])
|
|
78
|
+
assert result.exit_code != 0 or "not initialized" in result.output.lower()
|
|
79
|
+
|
|
80
|
+
def test_index_initialized_project(self, runner: CliRunner, tmp_path: Path):
|
|
81
|
+
# Initialize first
|
|
82
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
83
|
+
result = runner.invoke(cli, ["index", str(tmp_path)])
|
|
84
|
+
assert result.exit_code == 0
|
|
85
|
+
assert "Indexing" in result.output or "index" in result.output.lower()
|
|
86
|
+
|
|
87
|
+
def test_index_with_python_files(self, runner: CliRunner, tmp_path: Path):
|
|
88
|
+
# Create some Python files
|
|
89
|
+
(tmp_path / "main.py").write_text("def hello(): pass", encoding="utf-8")
|
|
90
|
+
(tmp_path / "utils.py").write_text("def helper(): pass", encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
93
|
+
result = runner.invoke(cli, ["index", str(tmp_path)])
|
|
94
|
+
assert result.exit_code == 0
|
|
95
|
+
# Should find 2 py files (not counting files in .codexa)
|
|
96
|
+
assert "2 files" in result.output
|
|
97
|
+
|
|
98
|
+
def test_index_ignores_excluded_dirs(self, runner: CliRunner, tmp_path: Path):
|
|
99
|
+
# Create files in ignored directories
|
|
100
|
+
(tmp_path / "main.py").write_text("def hello(): pass", encoding="utf-8")
|
|
101
|
+
node_modules = tmp_path / "node_modules"
|
|
102
|
+
node_modules.mkdir()
|
|
103
|
+
(node_modules / "pkg.js").write_text("function f(){}", encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
106
|
+
result = runner.invoke(cli, ["index", str(tmp_path)])
|
|
107
|
+
assert result.exit_code == 0
|
|
108
|
+
assert "1 files" in result.output
|
|
109
|
+
|
|
110
|
+
def test_index_force_flag(self, runner: CliRunner, tmp_path: Path):
|
|
111
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
112
|
+
result = runner.invoke(cli, ["index", str(tmp_path), "--force"])
|
|
113
|
+
assert result.exit_code == 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestSearchCommand:
|
|
117
|
+
"""Tests for the search command."""
|
|
118
|
+
|
|
119
|
+
def test_search_without_init_fails(self, runner: CliRunner, tmp_path: Path):
|
|
120
|
+
result = runner.invoke(cli, ["search", "test query", "--path", str(tmp_path)])
|
|
121
|
+
assert result.exit_code != 0 or "not initialized" in result.output.lower()
|
|
122
|
+
|
|
123
|
+
def test_search_human_readable(self, runner: CliRunner, tmp_path: Path):
|
|
124
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
125
|
+
result = runner.invoke(
|
|
126
|
+
cli, ["search", "test query", "--path", str(tmp_path)]
|
|
127
|
+
)
|
|
128
|
+
assert result.exit_code == 0
|
|
129
|
+
# Without an index, shows empty index warning
|
|
130
|
+
assert "empty" in result.output.lower() or "no results" in result.output.lower()
|
|
131
|
+
|
|
132
|
+
def test_search_json_output(self, runner: CliRunner, tmp_path: Path):
|
|
133
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
134
|
+
result = runner.invoke(
|
|
135
|
+
cli, ["search", "jwt verification", "--json", "--no-auto-index", "--path", str(tmp_path)]
|
|
136
|
+
)
|
|
137
|
+
assert result.exit_code == 0
|
|
138
|
+
data = json.loads(result.output)
|
|
139
|
+
assert data["query"] == "jwt verification"
|
|
140
|
+
assert "results" in data
|
|
141
|
+
assert isinstance(data["results"], list)
|
|
142
|
+
|
|
143
|
+
def test_search_custom_top_k(self, runner: CliRunner, tmp_path: Path):
|
|
144
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
145
|
+
result = runner.invoke(
|
|
146
|
+
cli,
|
|
147
|
+
["search", "query", "-k", "5", "--json", "--no-auto-index", "--path", str(tmp_path)],
|
|
148
|
+
)
|
|
149
|
+
assert result.exit_code == 0
|
|
150
|
+
data = json.loads(result.output)
|
|
151
|
+
assert data["top_k"] == 5
|
|
152
|
+
|
|
153
|
+
def test_search_default_top_k_from_config(self, runner: CliRunner, tmp_path: Path):
|
|
154
|
+
runner.invoke(cli, ["init", str(tmp_path)])
|
|
155
|
+
result = runner.invoke(
|
|
156
|
+
cli,
|
|
157
|
+
["search", "query", "--json", "--no-auto-index", "--path", str(tmp_path)],
|
|
158
|
+
)
|
|
159
|
+
assert result.exit_code == 0
|
|
160
|
+
data = json.loads(result.output)
|
|
161
|
+
assert data["top_k"] == 10 # default from config
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestCommandRouting:
|
|
165
|
+
"""Tests that all commands are properly registered and routable."""
|
|
166
|
+
|
|
167
|
+
def test_all_commands_registered(self, runner: CliRunner):
|
|
168
|
+
result = runner.invoke(cli, ["--help"])
|
|
169
|
+
assert result.exit_code == 0
|
|
170
|
+
# All Phase 1 commands should appear in help
|
|
171
|
+
for cmd_name in ["init", "index", "search"]:
|
|
172
|
+
assert cmd_name in result.output
|
|
173
|
+
|
|
174
|
+
def test_init_command_accessible(self, runner: CliRunner):
|
|
175
|
+
result = runner.invoke(cli, ["init", "--help"])
|
|
176
|
+
assert result.exit_code == 0
|
|
177
|
+
|
|
178
|
+
def test_index_command_accessible(self, runner: CliRunner):
|
|
179
|
+
result = runner.invoke(cli, ["index", "--help"])
|
|
180
|
+
assert result.exit_code == 0
|
|
181
|
+
|
|
182
|
+
def test_search_command_accessible(self, runner: CliRunner):
|
|
183
|
+
result = runner.invoke(cli, ["search", "--help"])
|
|
184
|
+
assert result.exit_code == 0
|
|
185
|
+
|
|
186
|
+
def test_unknown_command_fails(self, runner: CliRunner):
|
|
187
|
+
result = runner.invoke(cli, ["nonexistent"])
|
|
188
|
+
assert result.exit_code != 0
|