codexa 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,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