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,2753 @@
1
+ """Phase 20 — Deep Coverage & Hardening Tests.
2
+
3
+ Target: bring total tests from 1204 to 2000+.
4
+ Tests cover every under-tested module with unit-level granularity:
5
+ - ci/ (quality, metrics, pr, hooks, templates, hotspots, impact, trace)
6
+ - web/ (api, visualize)
7
+ - llm/ (safety, reasoning, conversation, investigation, streaming, providers)
8
+ - bridge/ (protocol, server, context_provider, vscode)
9
+ - context/ (engine, memory)
10
+ - tools/ (protocol, executor, registry)
11
+ - workspace/
12
+ - daemon/watcher
13
+ - docs/
14
+ - config/settings
15
+ - parsing/parser
16
+ - indexing/ (scanner, chunker, semantic_chunker)
17
+ - services/ (indexing_service, search_service)
18
+ - storage/ (vector_store, hash_store)
19
+ - embeddings/
20
+ - scalability/
21
+ - plugins/
22
+ - cli/ (router, commands)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import math
29
+ import re
30
+ import tempfile
31
+ import time
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+ from typing import Any
35
+ from unittest.mock import MagicMock, patch
36
+
37
+ import pytest
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # CI Quality
41
+ # ---------------------------------------------------------------------------
42
+ from semantic_code_intelligence.ci.quality import (
43
+ ComplexityResult,
44
+ DeadCodeResult,
45
+ DuplicateResult,
46
+ QualityReport,
47
+ _jaccard,
48
+ _normalize_body,
49
+ _rate_complexity,
50
+ _trigram_set,
51
+ analyze_complexity,
52
+ compute_complexity,
53
+ detect_dead_code,
54
+ detect_duplicates,
55
+ )
56
+ from semantic_code_intelligence.parsing.parser import Symbol
57
+
58
+
59
+ def _sym(name="foo", kind="function", body="pass", file_path="a.py",
60
+ start_line=1, end_line=2, parent="") -> Symbol:
61
+ """Helper to build stub Symbols."""
62
+ return Symbol(
63
+ name=name, kind=kind, body=body, file_path=file_path,
64
+ start_line=start_line, end_line=end_line, start_col=0, end_col=0,
65
+ parent=parent,
66
+ )
67
+
68
+
69
+ class TestRateComplexity:
70
+ def test_low(self):
71
+ assert _rate_complexity(1) == "low"
72
+ assert _rate_complexity(5) == "low"
73
+
74
+ def test_moderate(self):
75
+ assert _rate_complexity(6) == "moderate"
76
+ assert _rate_complexity(10) == "moderate"
77
+
78
+ def test_high(self):
79
+ assert _rate_complexity(11) == "high"
80
+ assert _rate_complexity(20) == "high"
81
+
82
+ def test_very_high(self):
83
+ assert _rate_complexity(21) == "very_high"
84
+ assert _rate_complexity(100) == "very_high"
85
+
86
+
87
+ class TestComputeComplexity:
88
+ def test_simple_function(self):
89
+ s = _sym(body="return 1")
90
+ cr = compute_complexity(s)
91
+ assert cr.complexity == 1
92
+ assert cr.rating == "low"
93
+
94
+ def test_with_if(self):
95
+ s = _sym(body="if x:\n return 1\nreturn 2")
96
+ cr = compute_complexity(s)
97
+ assert cr.complexity >= 2
98
+
99
+ def test_with_for_while(self):
100
+ s = _sym(body="for i in range(10):\n while True:\n break")
101
+ cr = compute_complexity(s)
102
+ assert cr.complexity >= 3
103
+
104
+ def test_with_logical_operators(self):
105
+ s = _sym(body="if a and b or c:\n pass")
106
+ cr = compute_complexity(s)
107
+ assert cr.complexity >= 4 # if + and + or
108
+
109
+ def test_comments_ignored(self):
110
+ s = _sym(body="# if something\nreturn 1")
111
+ cr = compute_complexity(s)
112
+ assert cr.complexity == 1
113
+
114
+ def test_result_fields(self):
115
+ s = _sym(name="bar", file_path="b.py", start_line=10, end_line=20, body="pass")
116
+ cr = compute_complexity(s)
117
+ assert cr.symbol_name == "bar"
118
+ assert cr.file_path == "b.py"
119
+ assert cr.start_line == 10
120
+ assert cr.end_line == 20
121
+
122
+ def test_to_dict(self):
123
+ s = _sym(body="pass")
124
+ d = compute_complexity(s).to_dict()
125
+ assert "complexity" in d
126
+ assert "rating" in d
127
+ assert "symbol_name" in d
128
+
129
+ def test_empty_body(self):
130
+ s = _sym(body="")
131
+ cr = compute_complexity(s)
132
+ assert cr.complexity == 1
133
+
134
+ def test_except_catch(self):
135
+ s = _sym(body="try:\n pass\nexcept:\n pass")
136
+ cr = compute_complexity(s)
137
+ assert cr.complexity >= 2
138
+
139
+ def test_case_switch(self):
140
+ s = _sym(body="case 1:\n break\ncase 2:\n break")
141
+ cr = compute_complexity(s)
142
+ assert cr.complexity >= 3
143
+
144
+
145
+ class TestAnalyzeComplexity:
146
+ def test_filters_by_threshold(self):
147
+ syms = [
148
+ _sym(name="simple", body="return 1"),
149
+ _sym(name="complex", body="if a:\n if b:\n if c:\n if d:\n if e:\n if f:\n if g:\n if h:\n if i:\n if j:\n pass"),
150
+ ]
151
+ results = analyze_complexity(syms, threshold=5)
152
+ names = [r.symbol_name for r in results]
153
+ assert "complex" in names
154
+
155
+ def test_skips_non_callables(self):
156
+ syms = [_sym(name="MyClass", kind="class", body="if a:\n if b:\n if c:\n if d:\n if e:\n if f:\n pass")]
157
+ results = analyze_complexity(syms, threshold=1)
158
+ assert len(results) == 0
159
+
160
+ def test_sorted_descending(self):
161
+ syms = [
162
+ _sym(name="a", body="if x:\n pass"),
163
+ _sym(name="b", body="if x:\n if y:\n if z:\n pass"),
164
+ ]
165
+ results = analyze_complexity(syms, threshold=1)
166
+ if len(results) >= 2:
167
+ assert results[0].complexity >= results[1].complexity
168
+
169
+ def test_empty_input(self):
170
+ assert analyze_complexity([]) == []
171
+
172
+
173
+ class TestNormalizeBody:
174
+ def test_strips_comments(self):
175
+ assert "# comment" not in _normalize_body("# comment\ncode")
176
+
177
+ def test_strips_blank_lines(self):
178
+ result = _normalize_body("\n\ncode\n\n")
179
+ assert result == "code"
180
+
181
+ def test_strips_js_comments(self):
182
+ assert "//" not in _normalize_body("// comment\ncode")
183
+
184
+ def test_strips_whitespace(self):
185
+ result = _normalize_body(" code ")
186
+ assert result == "code"
187
+
188
+
189
+ class TestTrigramSet:
190
+ def test_basic(self):
191
+ result = _trigram_set("abcde")
192
+ assert "abc" in result
193
+ assert "bcd" in result
194
+ assert "cde" in result
195
+
196
+ def test_short_string(self):
197
+ assert _trigram_set("ab") == {"ab"}
198
+
199
+ def test_empty_string(self):
200
+ assert _trigram_set("") == set()
201
+
202
+ def test_exactly_three(self):
203
+ result = _trigram_set("abc")
204
+ assert result == {"abc"}
205
+
206
+
207
+ class TestJaccard:
208
+ def test_identical(self):
209
+ s = {"a", "b", "c"}
210
+ assert _jaccard(s, s) == 1.0
211
+
212
+ def test_disjoint(self):
213
+ assert _jaccard({"a"}, {"b"}) == 0.0
214
+
215
+ def test_partial(self):
216
+ assert 0.0 < _jaccard({"a", "b"}, {"b", "c"}) < 1.0
217
+
218
+ def test_both_empty(self):
219
+ assert _jaccard(set(), set()) == 1.0
220
+
221
+ def test_one_empty(self):
222
+ assert _jaccard(set(), {"a"}) == 0.0
223
+
224
+
225
+ class TestDetectDuplicates:
226
+ def test_identical_bodies(self):
227
+ body = "x = 1\ny = 2\nz = 3\nw = 4\nreturn x"
228
+ syms = [
229
+ _sym(name="a", body=body, file_path="a.py"),
230
+ _sym(name="b", body=body, file_path="b.py"),
231
+ ]
232
+ results = detect_duplicates(syms, threshold=0.5)
233
+ assert len(results) >= 1
234
+ assert results[0].similarity >= 0.9
235
+
236
+ def test_different_bodies(self):
237
+ syms = [
238
+ _sym(name="a", body="x=1\ny=2\nz=3\nw=4"),
239
+ _sym(name="b", body="very different code\nnothing similar\nat all\nreally"),
240
+ ]
241
+ results = detect_duplicates(syms, threshold=0.9)
242
+ assert len(results) == 0
243
+
244
+ def test_min_lines_filter(self):
245
+ syms = [
246
+ _sym(name="a", body="short"),
247
+ _sym(name="b", body="short"),
248
+ ]
249
+ assert detect_duplicates(syms, min_lines=4) == []
250
+
251
+ def test_result_fields(self):
252
+ body = "x=1\ny=2\nz=3\nw=4\nv=5"
253
+ syms = [
254
+ _sym(name="a", body=body, file_path="f1.py", start_line=1),
255
+ _sym(name="b", body=body, file_path="f2.py", start_line=10),
256
+ ]
257
+ results = detect_duplicates(syms, threshold=0.5)
258
+ if results:
259
+ d = results[0].to_dict()
260
+ assert "symbol_a" in d
261
+ assert "similarity" in d
262
+
263
+ def test_empty_input(self):
264
+ assert detect_duplicates([]) == []
265
+
266
+
267
+ class TestDeadCodeDetection:
268
+ def test_unused_function(self):
269
+ syms = [
270
+ _sym(name="used", body="pass"),
271
+ _sym(name="orphan", body="pass"),
272
+ ]
273
+ results = detect_dead_code(syms)
274
+ names = [r.symbol_name for r in results]
275
+ assert "orphan" in names or "used" in names
276
+
277
+ def test_entry_points_excluded(self):
278
+ syms = [_sym(name="main", body="pass")]
279
+ results = detect_dead_code(syms)
280
+ assert all(r.symbol_name != "main" for r in results)
281
+
282
+ def test_test_functions_excluded(self):
283
+ syms = [_sym(name="test_something", body="pass")]
284
+ results = detect_dead_code(syms)
285
+ assert all(r.symbol_name != "test_something" for r in results)
286
+
287
+ def test_with_call_graph(self):
288
+ from semantic_code_intelligence.context.engine import CallGraph
289
+ syms = [
290
+ _sym(name="caller", body="orphan()"),
291
+ _sym(name="orphan", body="pass"),
292
+ ]
293
+ cg = CallGraph()
294
+ cg.build(syms)
295
+ results = detect_dead_code(syms, call_graph=cg)
296
+ names = [r.symbol_name for r in results]
297
+ assert "orphan" not in names # it's referenced
298
+
299
+ def test_empty_input(self):
300
+ assert detect_dead_code([]) == []
301
+
302
+ def test_imports_not_flagged(self):
303
+ syms = [_sym(name="os", kind="import", body="import os")]
304
+ results = detect_dead_code(syms)
305
+ assert len(results) == 0
306
+
307
+ def test_result_to_dict(self):
308
+ d = DeadCodeResult("foo", "function", "a.py", 1).to_dict()
309
+ assert d["symbol_name"] == "foo"
310
+ assert d["kind"] == "function"
311
+
312
+
313
+ class TestComplexityResult:
314
+ def test_to_dict(self):
315
+ cr = ComplexityResult("fn", "a.py", 1, 10, 5, "low")
316
+ d = cr.to_dict()
317
+ assert d["symbol_name"] == "fn"
318
+ assert d["complexity"] == 5
319
+ assert d["rating"] == "low"
320
+
321
+
322
+ class TestDuplicateResult:
323
+ def test_to_dict(self):
324
+ dr = DuplicateResult("a", "f1.py", 1, "b", "f2.py", 2, 0.85)
325
+ d = dr.to_dict()
326
+ assert d["similarity"] == 0.85
327
+ assert d["symbol_a"] == "a"
328
+
329
+
330
+ class TestQualityReport:
331
+ def test_issue_count(self):
332
+ r = QualityReport(
333
+ complexity_issues=[ComplexityResult("fn", "a", 1, 2, 15, "high")],
334
+ dead_code=[DeadCodeResult("x", "function", "a", 1)],
335
+ duplicates=[],
336
+ )
337
+ assert r.issue_count == 2
338
+
339
+ def test_issue_count_with_safety(self):
340
+ from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
341
+ sr = SafetyReport(safe=False, issues=[SafetyIssue("p", "d", 1)])
342
+ r = QualityReport(safety=sr)
343
+ assert r.issue_count == 1
344
+
345
+ def test_to_dict(self):
346
+ d = QualityReport().to_dict()
347
+ assert d["issue_count"] == 0
348
+ assert d["files_analyzed"] == 0
349
+
350
+ def test_empty_report(self):
351
+ r = QualityReport()
352
+ assert r.issue_count == 0
353
+ assert r.to_dict()["safety"] is None
354
+
355
+
356
+ # ---------------------------------------------------------------------------
357
+ # CI Metrics
358
+ # ---------------------------------------------------------------------------
359
+ from semantic_code_intelligence.ci.metrics import (
360
+ FileMetrics,
361
+ ProjectMetrics,
362
+ QualitySnapshot,
363
+ QualityPolicy,
364
+ TrendResult,
365
+ _compute_mi,
366
+ _count_lines,
367
+ _linear_slope,
368
+ compute_trend,
369
+ )
370
+
371
+
372
+ class TestCountLines:
373
+ def test_blank_lines(self):
374
+ code, comments, blanks = _count_lines("\n\n\n")
375
+ assert blanks == 3
376
+
377
+ def test_python_comments(self):
378
+ code, comments, blanks = _count_lines("# comment\ncode")
379
+ assert comments == 1
380
+ assert code == 1
381
+
382
+ def test_js_comments(self):
383
+ code, comments, blanks = _count_lines("// comment\ncode")
384
+ assert comments == 1
385
+
386
+ def test_block_comments(self):
387
+ code, comments, blanks = _count_lines("/* start\n * middle\n */\ncode")
388
+ assert comments == 3
389
+ assert code == 1
390
+
391
+ def test_empty_input(self):
392
+ code, comments, blanks = _count_lines("")
393
+ assert code == 0 and comments == 0 and blanks == 0
394
+
395
+ def test_mixed(self):
396
+ code, comments, blanks = _count_lines("x = 1\n\n# comment\ny = 2")
397
+ assert code == 2
398
+ assert comments == 1
399
+ assert blanks == 1
400
+
401
+
402
+ class TestComputeMI:
403
+ def test_high_mi_for_simple_code(self):
404
+ mi = _compute_mi(10.0, 1.0, 0.3)
405
+ assert 0 <= mi <= 100
406
+
407
+ def test_low_mi_for_complex_code(self):
408
+ mi = _compute_mi(10000.0, 50.0, 0.0)
409
+ assert mi < 50
410
+
411
+ def test_zero_loc(self):
412
+ mi = _compute_mi(0.0, 0.0, 0.0)
413
+ assert 0 <= mi <= 100
414
+
415
+ def test_clamped_to_100(self):
416
+ mi = _compute_mi(1.0, 0.0, 1.0)
417
+ assert mi <= 100
418
+
419
+ def test_clamped_to_0(self):
420
+ mi = _compute_mi(1e10, 100.0, 0.0)
421
+ assert mi >= 0
422
+
423
+
424
+ class TestFileMetrics:
425
+ def test_comment_ratio(self):
426
+ fm = FileMetrics(file_path="a.py", lines_of_code=70, comment_lines=20, blank_lines=10)
427
+ assert abs(fm.comment_ratio - 0.2) < 0.01
428
+
429
+ def test_comment_ratio_zero(self):
430
+ fm = FileMetrics(file_path="a.py")
431
+ assert fm.comment_ratio == 0.0
432
+
433
+ def test_to_dict(self):
434
+ d = FileMetrics(file_path="a.py", lines_of_code=10).to_dict()
435
+ assert d["file_path"] == "a.py"
436
+ assert "comment_ratio" in d
437
+ assert "maintainability_index" in d
438
+
439
+ def test_defaults(self):
440
+ fm = FileMetrics(file_path="x.py")
441
+ assert fm.lines_of_code == 0
442
+ assert fm.maintainability_index == 100.0
443
+
444
+
445
+ class TestProjectMetrics:
446
+ def test_comment_ratio(self):
447
+ pm = ProjectMetrics(total_loc=80, total_comment_lines=20, total_blank_lines=0)
448
+ assert abs(pm.comment_ratio - 0.2) < 0.01
449
+
450
+ def test_comment_ratio_zero(self):
451
+ pm = ProjectMetrics()
452
+ assert pm.comment_ratio == 0.0
453
+
454
+ def test_to_dict(self):
455
+ d = ProjectMetrics(files_analyzed=5).to_dict()
456
+ assert d["files_analyzed"] == 5
457
+ assert "file_metrics" in d
458
+
459
+ def test_with_file_metrics(self):
460
+ fm = FileMetrics(file_path="a.py", lines_of_code=10)
461
+ pm = ProjectMetrics(file_metrics=[fm])
462
+ assert len(pm.to_dict()["file_metrics"]) == 1
463
+
464
+
465
+ class TestQualitySnapshot:
466
+ def test_to_dict(self):
467
+ qs = QualitySnapshot(
468
+ timestamp=1.0, maintainability_index=80.0, total_loc=100,
469
+ total_symbols=10, issue_count=2, files_analyzed=5,
470
+ avg_complexity=3.5, comment_ratio=0.15,
471
+ )
472
+ d = qs.to_dict()
473
+ assert d["maintainability_index"] == 80.0
474
+ assert d["total_loc"] == 100
475
+
476
+ def test_from_dict(self):
477
+ d = {
478
+ "timestamp": 1.0, "maintainability_index": 80.0, "total_loc": 100,
479
+ "total_symbols": 10, "issue_count": 2, "files_analyzed": 5,
480
+ "avg_complexity": 3.5, "comment_ratio": 0.15,
481
+ }
482
+ qs = QualitySnapshot.from_dict(d)
483
+ assert qs.timestamp == 1.0
484
+ assert qs.maintainability_index == 80.0
485
+
486
+ def test_roundtrip(self):
487
+ qs = QualitySnapshot(
488
+ timestamp=2.0, maintainability_index=75.0, total_loc=200,
489
+ total_symbols=20, issue_count=5, files_analyzed=10,
490
+ avg_complexity=5.0, comment_ratio=0.1,
491
+ )
492
+ restored = QualitySnapshot.from_dict(qs.to_dict())
493
+ assert restored.timestamp == qs.timestamp
494
+ assert restored.maintainability_index == qs.maintainability_index
495
+
496
+ def test_metadata(self):
497
+ qs = QualitySnapshot(
498
+ timestamp=1.0, maintainability_index=80.0, total_loc=100,
499
+ total_symbols=10, issue_count=0, files_analyzed=5,
500
+ avg_complexity=3.5, comment_ratio=0.15,
501
+ metadata={"branch": "main"},
502
+ )
503
+ assert qs.to_dict()["metadata"]["branch"] == "main"
504
+
505
+
506
+ class TestLinearSlope:
507
+ def test_positive_slope(self):
508
+ xs = [1.0, 2.0, 3.0]
509
+ ys = [10.0, 20.0, 30.0]
510
+ assert _linear_slope(xs, ys) == pytest.approx(10.0)
511
+
512
+ def test_zero_slope(self):
513
+ xs = [1.0, 2.0, 3.0]
514
+ ys = [5.0, 5.0, 5.0]
515
+ assert _linear_slope(xs, ys) == pytest.approx(0.0)
516
+
517
+ def test_negative_slope(self):
518
+ xs = [1.0, 2.0, 3.0]
519
+ ys = [30.0, 20.0, 10.0]
520
+ assert _linear_slope(xs, ys) == pytest.approx(-10.0)
521
+
522
+ def test_single_point(self):
523
+ assert _linear_slope([1.0], [5.0]) == 0.0
524
+
525
+ def test_empty(self):
526
+ assert _linear_slope([], []) == 0.0
527
+
528
+
529
+ class TestComputeTrend:
530
+ def test_improving(self):
531
+ snaps = [
532
+ QualitySnapshot(timestamp=3.0, maintainability_index=90.0, total_loc=100,
533
+ total_symbols=10, issue_count=1, files_analyzed=5,
534
+ avg_complexity=2.0, comment_ratio=0.1),
535
+ QualitySnapshot(timestamp=2.0, maintainability_index=80.0, total_loc=100,
536
+ total_symbols=10, issue_count=2, files_analyzed=5,
537
+ avg_complexity=3.0, comment_ratio=0.1),
538
+ QualitySnapshot(timestamp=1.0, maintainability_index=70.0, total_loc=100,
539
+ total_symbols=10, issue_count=3, files_analyzed=5,
540
+ avg_complexity=4.0, comment_ratio=0.1),
541
+ ]
542
+ result = compute_trend(snaps)
543
+ assert result.direction == "improving"
544
+ assert result.delta > 0
545
+
546
+ def test_degrading(self):
547
+ snaps = [
548
+ QualitySnapshot(timestamp=3.0, maintainability_index=50.0, total_loc=100,
549
+ total_symbols=10, issue_count=5, files_analyzed=5,
550
+ avg_complexity=8.0, comment_ratio=0.1),
551
+ QualitySnapshot(timestamp=1.0, maintainability_index=90.0, total_loc=100,
552
+ total_symbols=10, issue_count=1, files_analyzed=5,
553
+ avg_complexity=2.0, comment_ratio=0.1),
554
+ ]
555
+ result = compute_trend(snaps)
556
+ assert result.direction == "degrading"
557
+
558
+ def test_stable(self):
559
+ snaps = [
560
+ QualitySnapshot(timestamp=2.0, maintainability_index=80.0, total_loc=100,
561
+ total_symbols=10, issue_count=2, files_analyzed=5,
562
+ avg_complexity=3.0, comment_ratio=0.1),
563
+ QualitySnapshot(timestamp=1.0, maintainability_index=80.0, total_loc=100,
564
+ total_symbols=10, issue_count=2, files_analyzed=5,
565
+ avg_complexity=3.0, comment_ratio=0.1),
566
+ ]
567
+ result = compute_trend(snaps)
568
+ assert result.direction == "stable"
569
+
570
+ def test_empty_snapshots(self):
571
+ result = compute_trend([])
572
+ assert result.direction == "stable"
573
+ assert result.snapshot_count == 0
574
+
575
+ def test_single_snapshot(self):
576
+ snaps = [
577
+ QualitySnapshot(timestamp=1.0, maintainability_index=80.0, total_loc=100,
578
+ total_symbols=10, issue_count=2, files_analyzed=5,
579
+ avg_complexity=3.0, comment_ratio=0.1),
580
+ ]
581
+ result = compute_trend(snaps)
582
+ assert result.direction == "stable"
583
+
584
+ def test_to_dict(self):
585
+ result = TrendResult("mi", 3, 70.0, 90.0, 20.0, 10.0, "improving")
586
+ d = result.to_dict()
587
+ assert d["metric_name"] == "mi"
588
+ assert d["direction"] == "improving"
589
+
590
+
591
+ class TestQualityPolicy:
592
+ def test_defaults(self):
593
+ p = QualityPolicy()
594
+ assert p.min_maintainability == 40.0
595
+ assert p.max_complexity == 25
596
+ assert p.require_safety_pass is True
597
+
598
+ def test_to_dict(self):
599
+ d = QualityPolicy().to_dict()
600
+ assert "min_maintainability" in d
601
+ assert "max_issues" in d
602
+
603
+ def test_from_dict(self):
604
+ d = {"min_maintainability": 60.0, "max_complexity": 15}
605
+ p = QualityPolicy.from_dict(d)
606
+ assert p.min_maintainability == 60.0
607
+ assert p.max_complexity == 15
608
+
609
+ def test_roundtrip(self):
610
+ p = QualityPolicy(min_maintainability=55.0, max_dead_code=5)
611
+ restored = QualityPolicy.from_dict(p.to_dict())
612
+ assert restored.min_maintainability == p.min_maintainability
613
+ assert restored.max_dead_code == p.max_dead_code
614
+
615
+
616
+ # ---------------------------------------------------------------------------
617
+ # CI PR
618
+ # ---------------------------------------------------------------------------
619
+ from semantic_code_intelligence.ci.pr import (
620
+ FileChange,
621
+ ChangeSummary,
622
+ ImpactResult,
623
+ RiskScore,
624
+ _risk_level,
625
+ compute_risk,
626
+ suggest_reviewers,
627
+ )
628
+
629
+
630
+ class TestFileChange:
631
+ def test_to_dict(self):
632
+ fc = FileChange(path="a.py", language="python", symbols_added=["foo"])
633
+ d = fc.to_dict()
634
+ assert d["path"] == "a.py"
635
+ assert d["symbols_added"] == ["foo"]
636
+
637
+ def test_defaults(self):
638
+ fc = FileChange(path="x.js")
639
+ assert fc.symbols_modified == []
640
+ assert fc.import_changes == []
641
+
642
+
643
+ class TestChangeSummary:
644
+ def test_to_dict(self):
645
+ cs = ChangeSummary(files_changed=3, languages=["python"])
646
+ d = cs.to_dict()
647
+ assert d["files_changed"] == 3
648
+ assert "python" in d["languages"]
649
+
650
+ def test_defaults(self):
651
+ cs = ChangeSummary()
652
+ assert cs.files_changed == 0
653
+ assert cs.total_symbols_added == 0
654
+
655
+
656
+ class TestImpactResultPR:
657
+ def test_to_dict(self):
658
+ ir = ImpactResult(
659
+ changed_symbols=["foo"],
660
+ affected_files=["a.py"],
661
+ affected_symbols=["bar"],
662
+ )
663
+ d = ir.to_dict()
664
+ assert "foo" in d["changed_symbols"]
665
+
666
+ def test_defaults(self):
667
+ ir = ImpactResult()
668
+ assert ir.changed_symbols == []
669
+
670
+
671
+ class TestRiskLevel:
672
+ def test_low(self):
673
+ assert _risk_level(10) == "low"
674
+
675
+ def test_medium(self):
676
+ assert _risk_level(30) == "medium"
677
+
678
+ def test_high(self):
679
+ assert _risk_level(60) == "high"
680
+
681
+ def test_critical(self):
682
+ assert _risk_level(80) == "critical"
683
+
684
+
685
+ class TestRiskScore:
686
+ def test_to_dict(self):
687
+ rs = RiskScore(score=45, level="medium", factors=["big changeset"])
688
+ d = rs.to_dict()
689
+ assert d["score"] == 45
690
+ assert d["level"] == "medium"
691
+
692
+
693
+ class TestComputeRisk:
694
+ def test_small_changeset(self):
695
+ cs = ChangeSummary(files_changed=2)
696
+ risk = compute_risk(cs)
697
+ assert risk.score < 25
698
+ assert risk.level == "low"
699
+
700
+ def test_large_changeset(self):
701
+ cs = ChangeSummary(files_changed=25, total_symbols_removed=15,
702
+ total_symbols_modified=15)
703
+ risk = compute_risk(cs)
704
+ assert risk.score > 25
705
+
706
+ def test_with_safety_issues(self):
707
+ from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
708
+ sr = SafetyReport(safe=False, issues=[SafetyIssue("p", "d"), SafetyIssue("p2", "d2")])
709
+ cs = ChangeSummary(files_changed=1)
710
+ risk = compute_risk(cs, safety_report=sr)
711
+ assert risk.score > 10
712
+ assert any("safety" in f for f in risk.factors)
713
+
714
+ def test_with_impact(self):
715
+ ir = ImpactResult(affected_symbols=["a", "b", "c", "d", "e", "f"])
716
+ cs = ChangeSummary(files_changed=5)
717
+ risk = compute_risk(cs, impact=ir)
718
+ assert risk.score > 10
719
+
720
+ def test_empty(self):
721
+ cs = ChangeSummary()
722
+ risk = compute_risk(cs)
723
+ assert risk.level == "low"
724
+
725
+
726
+ class TestSuggestReviewers:
727
+ def test_groups_by_domain(self):
728
+ files = ["auth/login.py", "auth/logout.py", "api/routes.py"]
729
+ reviewers = suggest_reviewers(files)
730
+ assert len(reviewers) >= 2
731
+
732
+ def test_empty_files(self):
733
+ assert suggest_reviewers([]) == []
734
+
735
+ def test_single_file(self):
736
+ result = suggest_reviewers(["app.py"])
737
+ assert len(result) >= 1
738
+
739
+
740
+ # ---------------------------------------------------------------------------
741
+ # CI Hooks
742
+ # ---------------------------------------------------------------------------
743
+ from semantic_code_intelligence.ci.hooks import HookResult, run_precommit_check
744
+
745
+
746
+ class TestHookResult:
747
+ def test_defaults(self):
748
+ hr = HookResult()
749
+ assert hr.passed is True
750
+ assert hr.files_checked == 0
751
+ assert hr.safety is None
752
+
753
+ def test_to_dict(self):
754
+ d = HookResult(passed=False, files_checked=3).to_dict()
755
+ assert d["passed"] is False
756
+ assert d["files_checked"] == 3
757
+
758
+ def test_with_safety(self):
759
+ from semantic_code_intelligence.llm.safety import SafetyReport
760
+ sr = SafetyReport(safe=True)
761
+ hr = HookResult(safety=sr)
762
+ d = hr.to_dict()
763
+ assert d["safety"]["safe"] is True
764
+
765
+
766
+ class TestRunPrecommitCheck:
767
+ def test_safe_files(self):
768
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
769
+ f.write("x = 1\ny = 2\n")
770
+ f.flush()
771
+ result = run_precommit_check([f.name], run_plugins=False)
772
+ assert result.passed is True
773
+ assert result.files_checked == 1
774
+
775
+ def test_unsafe_files(self):
776
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
777
+ f.write("import os\nos.system('rm -rf /')\n")
778
+ f.flush()
779
+ result = run_precommit_check([f.name], run_plugins=False)
780
+ assert result.passed is False
781
+
782
+ def test_empty_files(self):
783
+ result = run_precommit_check([], run_plugins=False)
784
+ assert result.passed is True
785
+ assert result.files_checked == 0
786
+
787
+ def test_nonexistent_file(self):
788
+ result = run_precommit_check(["/nonexistent/file.py"], run_plugins=False)
789
+ assert result.files_checked == 1
790
+
791
+
792
+ # ---------------------------------------------------------------------------
793
+ # CI Templates
794
+ # ---------------------------------------------------------------------------
795
+ from semantic_code_intelligence.ci.templates import (
796
+ generate_analysis_workflow,
797
+ generate_precommit_config,
798
+ generate_safety_workflow,
799
+ )
800
+
801
+
802
+ class TestGenerateAnalysisWorkflow:
803
+ def test_contains_yaml(self):
804
+ wf = generate_analysis_workflow()
805
+ assert "name: CodexA Analysis" in wf
806
+
807
+ def test_python_version(self):
808
+ wf = generate_analysis_workflow(python_version="3.13")
809
+ assert "3.13" in wf
810
+
811
+ def test_trigger(self):
812
+ wf = generate_analysis_workflow(trigger="push")
813
+ assert "push:" in wf
814
+
815
+ def test_contains_steps(self):
816
+ wf = generate_analysis_workflow()
817
+ assert "codexa init" in wf
818
+ assert "codexa quality" in wf
819
+
820
+ def test_permissions(self):
821
+ wf = generate_analysis_workflow()
822
+ assert "permissions:" in wf
823
+ assert "contents: read" in wf
824
+
825
+
826
+ class TestGeneratePrecommitConfig:
827
+ def test_contains_hooks(self):
828
+ cfg = generate_precommit_config()
829
+ assert "codexa-safety" in cfg
830
+ assert "codexa-quality" in cfg
831
+
832
+ def test_repo_local(self):
833
+ cfg = generate_precommit_config()
834
+ assert "repo: local" in cfg
835
+
836
+
837
+ class TestGenerateSafetyWorkflow:
838
+ def test_contains_yaml(self):
839
+ wf = generate_safety_workflow()
840
+ assert "name: CodexA Safety" in wf
841
+
842
+ def test_python_version(self):
843
+ wf = generate_safety_workflow(python_version="3.11")
844
+ assert "3.11" in wf
845
+
846
+ def test_permissions(self):
847
+ wf = generate_safety_workflow()
848
+ assert "contents: read" in wf
849
+
850
+
851
+ # ---------------------------------------------------------------------------
852
+ # CI Hotspots
853
+ # ---------------------------------------------------------------------------
854
+ from semantic_code_intelligence.ci.hotspots import (
855
+ HotspotFactor,
856
+ Hotspot,
857
+ HotspotReport,
858
+ _normalise,
859
+ )
860
+
861
+
862
+ class TestNormalise:
863
+ def test_zero(self):
864
+ assert _normalise(0.0, 10.0) == 0.0
865
+
866
+ def test_max(self):
867
+ assert _normalise(10.0, 10.0) == 1.0
868
+
869
+ def test_over_max(self):
870
+ assert _normalise(20.0, 10.0) == 1.0
871
+
872
+ def test_zero_max(self):
873
+ assert _normalise(5.0, 0.0) == 0.0
874
+
875
+ def test_negative_max(self):
876
+ assert _normalise(5.0, -1.0) == 0.0
877
+
878
+
879
+ class TestHotspotFactor:
880
+ def test_to_dict(self):
881
+ hf = HotspotFactor(name="complexity", raw_value=15.0, normalized=0.75, weight=0.3)
882
+ d = hf.to_dict()
883
+ assert d["name"] == "complexity"
884
+ assert d["normalized"] == 0.75
885
+
886
+ def test_rounding(self):
887
+ hf = HotspotFactor(name="x", raw_value=1.23456, normalized=0.12345, weight=0.3)
888
+ d = hf.to_dict()
889
+ assert d["raw_value"] == 1.23
890
+ assert d["normalized"] == 0.123
891
+
892
+
893
+ class TestHotspot:
894
+ def test_to_dict(self):
895
+ h = Hotspot(name="fn", file_path="a.py", kind="symbol", risk_score=85.3)
896
+ d = h.to_dict()
897
+ assert d["risk_score"] == 85.3
898
+ assert d["kind"] == "symbol"
899
+
900
+ def test_with_factors(self):
901
+ hf = HotspotFactor("x", 1.0, 0.5, 0.3)
902
+ h = Hotspot(name="fn", file_path="a.py", kind="symbol", risk_score=50.0, factors=[hf])
903
+ assert len(h.to_dict()["factors"]) == 1
904
+
905
+
906
+ class TestHotspotReport:
907
+ def test_to_dict(self):
908
+ hr = HotspotReport(files_analyzed=10, symbols_analyzed=50)
909
+ d = hr.to_dict()
910
+ assert d["files_analyzed"] == 10
911
+ assert d["hotspot_count"] == 0
912
+
913
+ def test_with_hotspots(self):
914
+ h = Hotspot(name="fn", file_path="a.py", kind="symbol", risk_score=80.0)
915
+ hr = HotspotReport(files_analyzed=5, symbols_analyzed=20, hotspots=[h])
916
+ assert hr.to_dict()["hotspot_count"] == 1
917
+
918
+
919
+ # ---------------------------------------------------------------------------
920
+ # CI Impact
921
+ # ---------------------------------------------------------------------------
922
+ from semantic_code_intelligence.ci.impact import (
923
+ AffectedSymbol,
924
+ AffectedModule,
925
+ DependencyChain,
926
+ ImpactReport as CIImpactReport,
927
+ )
928
+
929
+
930
+ class TestAffectedSymbol:
931
+ def test_to_dict(self):
932
+ s = AffectedSymbol("fn", "a.py", "function", "direct_caller", 1)
933
+ d = s.to_dict()
934
+ assert d["name"] == "fn"
935
+ assert d["depth"] == 1
936
+ assert d["relationship"] == "direct_caller"
937
+
938
+
939
+ class TestAffectedModule:
940
+ def test_to_dict(self):
941
+ m = AffectedModule("a.py", "imports_target", 1)
942
+ d = m.to_dict()
943
+ assert d["file_path"] == "a.py"
944
+
945
+
946
+ class TestDependencyChain:
947
+ def test_to_dict(self):
948
+ dc = DependencyChain(path=["a.py", "b.py", "c.py"])
949
+ d = dc.to_dict()
950
+ assert len(d["path"]) == 3
951
+
952
+
953
+ class TestCIImpactReport:
954
+ def test_total_affected(self):
955
+ r = CIImpactReport(
956
+ target="fn", target_kind="symbol",
957
+ direct_symbols=[AffectedSymbol("a", "x.py", "function", "direct_caller", 1)],
958
+ transitive_symbols=[AffectedSymbol("b", "y.py", "function", "transitive_caller", 2)],
959
+ )
960
+ assert r.total_affected == 2
961
+
962
+ def test_to_dict(self):
963
+ r = CIImpactReport(target="fn", target_kind="symbol")
964
+ d = r.to_dict()
965
+ assert d["target"] == "fn"
966
+ assert d["total_affected"] == 0
967
+
968
+ def test_empty_report(self):
969
+ r = CIImpactReport(target="x", target_kind="file")
970
+ assert r.total_affected == 0
971
+
972
+
973
+ # ---------------------------------------------------------------------------
974
+ # CI Trace
975
+ # ---------------------------------------------------------------------------
976
+ from semantic_code_intelligence.ci.trace import (
977
+ TraceNode,
978
+ TraceEdge,
979
+ TraceResult,
980
+ trace_symbol,
981
+ )
982
+
983
+
984
+ class TestTraceNode:
985
+ def test_to_dict(self):
986
+ tn = TraceNode("fn", "a.py", "function", -2)
987
+ d = tn.to_dict()
988
+ assert d["depth"] == -2
989
+ assert d["kind"] == "function"
990
+
991
+
992
+ class TestTraceEdge:
993
+ def test_to_dict(self):
994
+ te = TraceEdge("caller", "callee", "a.py")
995
+ d = te.to_dict()
996
+ assert d["caller"] == "caller"
997
+ assert d["callee"] == "callee"
998
+
999
+
1000
+ class TestTraceResult:
1001
+ def test_total_nodes(self):
1002
+ tr = TraceResult(
1003
+ target="fn", target_file="a.py",
1004
+ upstream=[TraceNode("a", "a.py", "function", -1)],
1005
+ downstream=[TraceNode("b", "b.py", "function", 1),
1006
+ TraceNode("c", "c.py", "function", 2)],
1007
+ )
1008
+ assert tr.total_nodes == 3
1009
+
1010
+ def test_to_dict(self):
1011
+ tr = TraceResult(target="fn", target_file="a.py")
1012
+ d = tr.to_dict()
1013
+ assert d["target"] == "fn"
1014
+ assert d["total_nodes"] == 0
1015
+
1016
+ def test_empty(self):
1017
+ tr = TraceResult(target="missing", target_file="")
1018
+ assert tr.total_nodes == 0
1019
+ assert tr.max_upstream_depth == 0
1020
+
1021
+
1022
+ class TestTraceSymbol:
1023
+ def test_unknown_symbol(self):
1024
+ result = trace_symbol("nonexistent", [], CallGraph())
1025
+ assert result.target_file == ""
1026
+ assert result.total_nodes == 0
1027
+
1028
+ def test_symbol_with_callers(self):
1029
+ from semantic_code_intelligence.context.engine import CallGraph
1030
+ syms = [
1031
+ _sym(name="caller", body="target()", file_path="a.py"),
1032
+ _sym(name="target", body="pass", file_path="b.py"),
1033
+ ]
1034
+ cg = CallGraph()
1035
+ cg.build(syms)
1036
+ result = trace_symbol("target", syms, cg)
1037
+ assert len(result.upstream) >= 1
1038
+
1039
+ def test_symbol_with_callees(self):
1040
+ from semantic_code_intelligence.context.engine import CallGraph
1041
+ syms = [
1042
+ _sym(name="entry", body="helper()", file_path="a.py"),
1043
+ _sym(name="helper", body="pass", file_path="a.py"),
1044
+ ]
1045
+ cg = CallGraph()
1046
+ cg.build(syms)
1047
+ result = trace_symbol("entry", syms, cg)
1048
+ assert len(result.downstream) >= 1
1049
+
1050
+
1051
+ # ---------------------------------------------------------------------------
1052
+ # LLM Safety
1053
+ # ---------------------------------------------------------------------------
1054
+ from semantic_code_intelligence.llm.safety import (
1055
+ SafetyIssue,
1056
+ SafetyReport,
1057
+ SafetyValidator,
1058
+ )
1059
+
1060
+
1061
+ class TestSafetyIssue:
1062
+ def test_to_dict(self):
1063
+ si = SafetyIssue("eval", "dangerous", line_number=5, severity="error")
1064
+ d = si.to_dict()
1065
+ assert d["pattern"] == "eval"
1066
+ assert d["line_number"] == 5
1067
+
1068
+ def test_defaults(self):
1069
+ si = SafetyIssue("p", "d")
1070
+ assert si.line_number == 0
1071
+ assert si.severity == "warning"
1072
+
1073
+
1074
+ class TestSafetyReport:
1075
+ def test_safe(self):
1076
+ sr = SafetyReport()
1077
+ assert sr.safe is True
1078
+ assert sr.to_dict()["issue_count"] == 0
1079
+
1080
+ def test_unsafe(self):
1081
+ sr = SafetyReport(safe=False, issues=[SafetyIssue("p", "d")])
1082
+ assert sr.to_dict()["issue_count"] == 1
1083
+
1084
+
1085
+ class TestSafetyValidator:
1086
+ def test_safe_code(self):
1087
+ v = SafetyValidator()
1088
+ report = v.validate("x = 1\ny = 2")
1089
+ assert report.safe is True
1090
+
1091
+ def test_os_system(self):
1092
+ v = SafetyValidator()
1093
+ report = v.validate("os.system('ls')")
1094
+ assert report.safe is False
1095
+
1096
+ def test_eval(self):
1097
+ v = SafetyValidator()
1098
+ report = v.validate("result = eval('1+1')")
1099
+ assert report.safe is False
1100
+
1101
+ def test_exec(self):
1102
+ v = SafetyValidator()
1103
+ report = v.validate("exec('print(1)')")
1104
+ assert report.safe is False
1105
+
1106
+ def test_subprocess_shell(self):
1107
+ v = SafetyValidator()
1108
+ report = v.validate("subprocess.run('cmd', shell=True)")
1109
+ assert report.safe is False
1110
+
1111
+ def test_rm_rf(self):
1112
+ v = SafetyValidator()
1113
+ report = v.validate("rm -rf /important/data")
1114
+ assert report.safe is False
1115
+
1116
+ def test_drop_table(self):
1117
+ v = SafetyValidator()
1118
+ report = v.validate("DROP TABLE users")
1119
+ assert report.safe is False
1120
+
1121
+ def test_path_traversal(self):
1122
+ v = SafetyValidator()
1123
+ report = v.validate("open('../../etc/passwd')")
1124
+ assert report.safe is False
1125
+
1126
+ def test_hardcoded_secret(self):
1127
+ v = SafetyValidator()
1128
+ report = v.validate("password = 'supersecretpassword123'")
1129
+ assert report.safe is False
1130
+
1131
+ def test_innerHTML(self):
1132
+ v = SafetyValidator()
1133
+ report = v.validate("element.innerHTML = userInput")
1134
+ assert report.safe is False
1135
+
1136
+ def test_md5(self):
1137
+ v = SafetyValidator()
1138
+ report = v.validate("hash = MD5(data)")
1139
+ assert report.safe is False
1140
+
1141
+ def test_http_url(self):
1142
+ v = SafetyValidator()
1143
+ report = v.validate("url = 'http://example.com/api'")
1144
+ assert report.safe is False
1145
+
1146
+ def test_http_localhost_ok(self):
1147
+ v = SafetyValidator()
1148
+ report = v.validate("url = 'http://localhost:8080'")
1149
+ assert report.safe is True
1150
+
1151
+ def test_verify_false(self):
1152
+ v = SafetyValidator()
1153
+ report = v.validate("requests.get(url, verify=False)")
1154
+ assert report.safe is False
1155
+
1156
+ def test_custom_patterns(self):
1157
+ v = SafetyValidator(extra_patterns=[
1158
+ (r"DANGER", "Custom danger pattern")
1159
+ ])
1160
+ report = v.validate("DANGER: doing something bad")
1161
+ assert report.safe is False
1162
+
1163
+ def test_dynamic_import(self):
1164
+ v = SafetyValidator()
1165
+ report = v.validate("mod = __import__('os')")
1166
+ assert report.safe is False
1167
+
1168
+ def test_truncate_table(self):
1169
+ v = SafetyValidator()
1170
+ report = v.validate("TRUNCATE TABLE sessions")
1171
+ assert report.safe is False
1172
+
1173
+ def test_document_write(self):
1174
+ v = SafetyValidator()
1175
+ report = v.validate("document.write(payload)")
1176
+ assert report.safe is False
1177
+
1178
+ def test_sha1(self):
1179
+ v = SafetyValidator()
1180
+ report = v.validate("hash = sha1(data)")
1181
+ assert report.safe is False
1182
+
1183
+ def test_multiple_issues(self):
1184
+ v = SafetyValidator()
1185
+ report = v.validate("eval('x')\nos.system('y')\nexec('z')")
1186
+ assert len(report.issues) >= 3
1187
+
1188
+
1189
+ # ---------------------------------------------------------------------------
1190
+ # LLM Streaming
1191
+ # ---------------------------------------------------------------------------
1192
+ from semantic_code_intelligence.llm.streaming import StreamEvent
1193
+
1194
+
1195
+ class TestStreamEvent:
1196
+ def test_create(self):
1197
+ se = StreamEvent(kind="token", content="hello")
1198
+ assert se.kind == "token"
1199
+ assert se.content == "hello"
1200
+
1201
+ def test_to_sse(self):
1202
+ se = StreamEvent(kind="token", content="hello")
1203
+ sse = se.to_sse()
1204
+ assert "data: " in sse
1205
+ assert "hello" in sse
1206
+
1207
+ def test_to_sse_multiline(self):
1208
+ se = StreamEvent(kind="chunk", content="line1\nline2")
1209
+ sse = se.to_sse()
1210
+ assert "data: " in sse
1211
+ assert "line1\\nline2" in sse
1212
+
1213
+
1214
+ # ---------------------------------------------------------------------------
1215
+ # LLM Providers
1216
+ # ---------------------------------------------------------------------------
1217
+ from semantic_code_intelligence.llm.provider import MessageRole, LLMMessage, LLMResponse
1218
+
1219
+
1220
+ class TestMessageRole:
1221
+ def test_values(self):
1222
+ assert MessageRole.SYSTEM == "system"
1223
+ assert MessageRole.USER == "user"
1224
+ assert MessageRole.ASSISTANT == "assistant"
1225
+
1226
+
1227
+ class TestLLMMessage:
1228
+ def test_create(self):
1229
+ m = LLMMessage(role=MessageRole.USER, content="Hello")
1230
+ assert m.role == "user"
1231
+ assert m.content == "Hello"
1232
+
1233
+
1234
+ class TestLLMResponse:
1235
+ def test_create(self):
1236
+ r = LLMResponse(content="answer", model="gpt-4", usage={"tokens": 100})
1237
+ assert r.content == "answer"
1238
+ assert r.model == "gpt-4"
1239
+
1240
+
1241
+ # ---------------------------------------------------------------------------
1242
+ # LLM Mock Provider
1243
+ # ---------------------------------------------------------------------------
1244
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
1245
+
1246
+
1247
+ class TestMockProvider:
1248
+ def test_chat(self):
1249
+ p = MockProvider()
1250
+ msgs = [LLMMessage(role=MessageRole.USER, content="test")]
1251
+ response = p.chat(msgs)
1252
+ assert isinstance(response, LLMResponse)
1253
+ assert len(response.content) > 0
1254
+
1255
+ def test_model_name(self):
1256
+ p = MockProvider()
1257
+ assert "mock" in p._model.lower()
1258
+
1259
+
1260
+ # ---------------------------------------------------------------------------
1261
+ # LLM Conversation
1262
+ # ---------------------------------------------------------------------------
1263
+ from semantic_code_intelligence.llm.conversation import ConversationSession
1264
+
1265
+
1266
+ class TestConversationSession:
1267
+ def test_create(self):
1268
+ cs = ConversationSession(session_id="test-1")
1269
+ assert cs.session_id == "test-1"
1270
+
1271
+ def test_add_message(self):
1272
+ cs = ConversationSession(session_id="s1")
1273
+ from semantic_code_intelligence.llm.provider import MessageRole
1274
+ cs.add_message(MessageRole.USER, "hello")
1275
+ cs.add_message(MessageRole.ASSISTANT, "hi")
1276
+ assert len(cs.messages) == 2
1277
+
1278
+ def test_messages_ordered(self):
1279
+ cs = ConversationSession(session_id="s1")
1280
+ from semantic_code_intelligence.llm.provider import MessageRole
1281
+ cs.add_message(MessageRole.USER, "first")
1282
+ cs.add_message(MessageRole.ASSISTANT, "second")
1283
+ assert cs.messages[0].content == "first"
1284
+ assert cs.messages[1].content == "second"
1285
+
1286
+ def test_to_dict(self):
1287
+ cs = ConversationSession(session_id="s1")
1288
+ from semantic_code_intelligence.llm.provider import MessageRole
1289
+ cs.add_message(MessageRole.USER, "hi")
1290
+ d = cs.to_dict()
1291
+ assert d["session_id"] == "s1"
1292
+ assert len(d["messages"]) == 1
1293
+
1294
+
1295
+ # ---------------------------------------------------------------------------
1296
+ # Context Engine
1297
+ # ---------------------------------------------------------------------------
1298
+ from semantic_code_intelligence.context.engine import (
1299
+ ContextWindow,
1300
+ ContextBuilder,
1301
+ CallEdge,
1302
+ CallGraph,
1303
+ DependencyMap,
1304
+ )
1305
+
1306
+
1307
+ class TestContextWindow:
1308
+ def test_to_dict(self):
1309
+ s = _sym(name="fn", body="pass")
1310
+ cw = ContextWindow(focal_symbol=s)
1311
+ d = cw.to_dict()
1312
+ assert d["focal_symbol"]["name"] == "fn"
1313
+
1314
+ def test_render(self):
1315
+ s = _sym(name="fn", body="pass\nreturn 1", file_path="a.py")
1316
+ cw = ContextWindow(focal_symbol=s, imports=[_sym(name="os", kind="import", body="import os")])
1317
+ text = cw.render()
1318
+ assert "fn" in text
1319
+ assert "Imports" in text
1320
+
1321
+
1322
+ class TestContextBuilder:
1323
+ def test_index_file_with_content(self):
1324
+ cb = ContextBuilder()
1325
+ syms = cb.index_file("test.py", "def foo():\n pass\n")
1326
+ assert len(syms) >= 1
1327
+
1328
+ def test_find_symbol(self):
1329
+ cb = ContextBuilder()
1330
+ cb.index_file("test.py", "def foo():\n pass\ndef bar():\n pass\n")
1331
+ results = cb.find_symbol("foo")
1332
+ assert len(results) >= 1
1333
+
1334
+ def test_get_all_symbols(self):
1335
+ cb = ContextBuilder()
1336
+ cb.index_file("a.py", "def f1(): pass\n")
1337
+ cb.index_file("b.py", "def f2(): pass\n")
1338
+ all_syms = cb.get_all_symbols()
1339
+ assert len(all_syms) >= 2
1340
+
1341
+ def test_build_context(self):
1342
+ cb = ContextBuilder()
1343
+ syms = cb.index_file("test.py", "import os\ndef foo():\n pass\ndef bar():\n pass\n")
1344
+ fn = [s for s in syms if s.name == "foo"]
1345
+ if fn:
1346
+ cw = cb.build_context(fn[0])
1347
+ assert cw.focal_symbol.name == "foo"
1348
+
1349
+ def test_build_context_for_name(self):
1350
+ cb = ContextBuilder()
1351
+ cb.index_file("test.py", "def target():\n pass\n")
1352
+ windows = cb.build_context_for_name("target")
1353
+ assert len(windows) >= 1
1354
+
1355
+ def test_get_symbols_unknown_file(self):
1356
+ cb = ContextBuilder()
1357
+ assert cb.get_symbols("nonexistent.py") == []
1358
+
1359
+
1360
+ class TestCallEdge:
1361
+ def test_to_dict(self):
1362
+ e = CallEdge("a.py:fn", "bar", "a.py", 10)
1363
+ d = e.to_dict()
1364
+ assert d["caller"] == "a.py:fn"
1365
+ assert d["callee"] == "bar"
1366
+
1367
+
1368
+ class TestCallGraphDeep:
1369
+ def test_build(self):
1370
+ syms = [
1371
+ _sym(name="caller", body="target()", file_path="a.py"),
1372
+ _sym(name="target", body="pass", file_path="a.py"),
1373
+ ]
1374
+ cg = CallGraph()
1375
+ cg.build(syms)
1376
+ assert len(cg.edges) >= 1
1377
+
1378
+ def test_callers_of(self):
1379
+ syms = [
1380
+ _sym(name="a", body="b()", file_path="x.py"),
1381
+ _sym(name="b", body="pass", file_path="x.py"),
1382
+ ]
1383
+ cg = CallGraph()
1384
+ cg.build(syms)
1385
+ callers = cg.callers_of("b")
1386
+ assert len(callers) >= 1
1387
+
1388
+ def test_callees_of(self):
1389
+ syms = [
1390
+ _sym(name="a", body="b()", file_path="x.py"),
1391
+ _sym(name="b", body="pass", file_path="x.py"),
1392
+ ]
1393
+ cg = CallGraph()
1394
+ cg.build(syms)
1395
+ callees = cg.callees_of("x.py:a")
1396
+ assert len(callees) >= 1
1397
+
1398
+ def test_no_self_reference(self):
1399
+ syms = [_sym(name="recursive", body="recursive()", file_path="a.py")]
1400
+ cg = CallGraph()
1401
+ cg.build(syms)
1402
+ # Should not have self-edge since build skips self-references
1403
+ callees = cg.callees_of("a.py:recursive")
1404
+ assert len(callees) == 0
1405
+
1406
+ def test_to_dict(self):
1407
+ cg = CallGraph()
1408
+ cg.build([])
1409
+ d = cg.to_dict()
1410
+ assert d["edge_count"] == 0
1411
+ assert d["node_count"] == 0
1412
+
1413
+ def test_empty_callers(self):
1414
+ cg = CallGraph()
1415
+ assert cg.callers_of("nonexistent") == []
1416
+
1417
+ def test_empty_callees(self):
1418
+ cg = CallGraph()
1419
+ assert cg.callees_of("nonexistent") == []
1420
+
1421
+
1422
+ # ---------------------------------------------------------------------------
1423
+ # Context Memory
1424
+ # ---------------------------------------------------------------------------
1425
+ from semantic_code_intelligence.context.memory import (
1426
+ MemoryEntry,
1427
+ ReasoningStep,
1428
+ SessionMemory,
1429
+ )
1430
+
1431
+
1432
+ class TestMemoryEntry:
1433
+ def test_to_dict(self):
1434
+ me = MemoryEntry(key="k", content="c", kind="qa")
1435
+ d = me.to_dict()
1436
+ assert d["key"] == "k"
1437
+ assert d["kind"] == "qa"
1438
+
1439
+ def test_from_dict(self):
1440
+ d = {"key": "k", "content": "c", "kind": "insight", "timestamp": 1.0, "metadata": {}}
1441
+ me = MemoryEntry.from_dict(d)
1442
+ assert me.key == "k"
1443
+ assert me.kind == "insight"
1444
+
1445
+ def test_roundtrip(self):
1446
+ me = MemoryEntry(key="test", content="data", kind="general", metadata={"x": 1})
1447
+ restored = MemoryEntry.from_dict(me.to_dict())
1448
+ assert restored.key == me.key
1449
+ assert restored.content == me.content
1450
+
1451
+
1452
+ class TestReasoningStep:
1453
+ def test_to_dict(self):
1454
+ rs = ReasoningStep(step_id=1, action="search", input_text="query", output_text="result")
1455
+ d = rs.to_dict()
1456
+ assert d["step_id"] == 1
1457
+ assert d["action"] == "search"
1458
+
1459
+
1460
+ class TestSessionMemory:
1461
+ def test_add_and_search(self):
1462
+ sm = SessionMemory()
1463
+ sm.add("key1", "authentication logic")
1464
+ results = sm.search("authentication")
1465
+ assert len(results) >= 1
1466
+
1467
+ def test_max_entries(self):
1468
+ sm = SessionMemory(max_entries=3)
1469
+ for i in range(5):
1470
+ sm.add(f"k{i}", f"content{i}")
1471
+ assert len(sm.entries) == 3
1472
+
1473
+ def test_get_recent(self):
1474
+ sm = SessionMemory()
1475
+ sm.add("k1", "first")
1476
+ sm.add("k2", "second")
1477
+ recent = sm.get_recent(1)
1478
+ assert len(recent) == 1
1479
+ assert recent[0].key == "k2"
1480
+
1481
+ def test_clear(self):
1482
+ sm = SessionMemory()
1483
+ sm.add("k", "v")
1484
+ sm.start_chain("c1")
1485
+ sm.clear()
1486
+ assert len(sm.entries) == 0
1487
+
1488
+ def test_reasoning_chain(self):
1489
+ sm = SessionMemory()
1490
+ sm.start_chain("chain1")
1491
+ sm.add_step("chain1", "search", "query", "results")
1492
+ sm.add_step("chain1", "analyze", "results", "conclusion")
1493
+ chain = sm.get_chain("chain1")
1494
+ assert len(chain) == 2
1495
+ assert chain[0].step_id == 1
1496
+ assert chain[1].step_id == 2
1497
+
1498
+ def test_to_dict(self):
1499
+ sm = SessionMemory()
1500
+ sm.add("k", "v")
1501
+ d = sm.to_dict()
1502
+ assert "entries" in d
1503
+ assert "chains" in d
1504
+
1505
+ def test_search_empty(self):
1506
+ sm = SessionMemory()
1507
+ assert sm.search("anything") == []
1508
+
1509
+ def test_get_chain_nonexistent(self):
1510
+ sm = SessionMemory()
1511
+ assert sm.get_chain("missing") == []
1512
+
1513
+
1514
+ # ---------------------------------------------------------------------------
1515
+ # Web Visualize
1516
+ # ---------------------------------------------------------------------------
1517
+ from semantic_code_intelligence.web.visualize import (
1518
+ render_call_graph,
1519
+ render_dependency_graph,
1520
+ render_workspace_graph,
1521
+ render_symbol_map,
1522
+ )
1523
+
1524
+
1525
+ class TestRenderCallGraph:
1526
+ def test_basic(self):
1527
+ edges = [{"caller": "a.py:fn1", "callee": "fn2", "file_path": "a.py"}]
1528
+ result = render_call_graph(edges)
1529
+ assert "flowchart" in result
1530
+
1531
+ def test_empty_edges(self):
1532
+ result = render_call_graph([])
1533
+ assert "No call edges found" in result
1534
+
1535
+ def test_custom_title(self):
1536
+ result = render_call_graph([], title="My Graph")
1537
+ assert "My Graph" in result
1538
+
1539
+ def test_direction(self):
1540
+ result = render_call_graph([], direction="TD")
1541
+ assert "flowchart TD" in result
1542
+
1543
+ def test_multiple_edges(self):
1544
+ edges = [
1545
+ {"caller": "a", "callee": "b", "file_path": "x.py"},
1546
+ {"caller": "b", "callee": "c", "file_path": "x.py"},
1547
+ ]
1548
+ result = render_call_graph(edges)
1549
+ assert "-->" in result
1550
+
1551
+
1552
+ class TestRenderDependencyGraph:
1553
+ def test_basic(self):
1554
+ deps = {"dependencies": [{"source_file": "a.py", "import_text": "import os"}]}
1555
+ result = render_dependency_graph(deps)
1556
+ assert "flowchart" in result
1557
+
1558
+ def test_empty(self):
1559
+ result = render_dependency_graph({})
1560
+ assert "No dependencies found" in result
1561
+
1562
+ def test_custom_title(self):
1563
+ result = render_dependency_graph({}, title="Deps")
1564
+ assert "Deps" in result
1565
+
1566
+
1567
+ class TestRenderWorkspaceGraph:
1568
+ def test_basic(self):
1569
+ repos = [{"name": "repo1", "path": "/a", "file_count": 10, "vector_count": 100}]
1570
+ result = render_workspace_graph(repos)
1571
+ assert "repo1" in result
1572
+
1573
+ def test_empty_repos(self):
1574
+ result = render_workspace_graph([])
1575
+ assert "No repositories" in result
1576
+
1577
+ def test_custom_title(self):
1578
+ result = render_workspace_graph([], title="My WS")
1579
+ assert "My WS" in result
1580
+
1581
+
1582
+ class TestRenderSymbolMap:
1583
+ def test_basic(self):
1584
+ syms = [
1585
+ {"name": "MyClass", "kind": "class", "parent": ""},
1586
+ {"name": "my_method", "kind": "method", "parent": "MyClass"},
1587
+ {"name": "standalone", "kind": "function", "parent": ""},
1588
+ ]
1589
+ result = render_symbol_map(syms)
1590
+ assert "classDiagram" in result
1591
+
1592
+ def test_empty(self):
1593
+ result = render_symbol_map([])
1594
+ assert "classDiagram" in result
1595
+
1596
+ def test_custom_title(self):
1597
+ result = render_symbol_map([], title="Symbols")
1598
+ assert "Symbols" in result
1599
+
1600
+
1601
+ # ---------------------------------------------------------------------------
1602
+ # Bridge Protocol
1603
+ # ---------------------------------------------------------------------------
1604
+ from semantic_code_intelligence.bridge.protocol import (
1605
+ RequestKind,
1606
+ AgentRequest,
1607
+ AgentResponse,
1608
+ BridgeCapabilities,
1609
+ )
1610
+
1611
+
1612
+ class TestRequestKindDeep:
1613
+ def test_all_values_are_strings(self):
1614
+ for kind in RequestKind:
1615
+ assert isinstance(kind.value, str)
1616
+
1617
+ def test_count(self):
1618
+ assert len(RequestKind) == 12
1619
+
1620
+ def test_invoke_tool(self):
1621
+ assert RequestKind.INVOKE_TOOL == "invoke_tool"
1622
+
1623
+ def test_list_tools(self):
1624
+ assert RequestKind.LIST_TOOLS == "list_tools"
1625
+
1626
+ def test_semantic_search(self):
1627
+ assert RequestKind.SEMANTIC_SEARCH == "semantic_search"
1628
+
1629
+
1630
+ class TestAgentRequestDeep:
1631
+ def test_to_json_roundtrip(self):
1632
+ req = AgentRequest(kind="semantic_search", params={"query": "test"}, request_id="r1")
1633
+ j = req.to_json()
1634
+ restored = AgentRequest.from_json(j)
1635
+ assert restored.kind == req.kind
1636
+ assert restored.params == req.params
1637
+
1638
+ def test_from_dict_defaults(self):
1639
+ req = AgentRequest.from_dict({"kind": "test", "params": {}})
1640
+ assert req.request_id == ""
1641
+ assert req.source == ""
1642
+
1643
+
1644
+ class TestAgentResponseDeep:
1645
+ def test_to_dict(self):
1646
+ resp = AgentResponse(request_id="r1", success=True, data={"key": "val"})
1647
+ d = resp.to_dict()
1648
+ assert d["success"] is True
1649
+ assert d["data"]["key"] == "val"
1650
+
1651
+ def test_error_response(self):
1652
+ resp = AgentResponse(request_id="r1", success=False, error="bad request")
1653
+ d = resp.to_dict()
1654
+ assert d["error"] == "bad request"
1655
+
1656
+ def test_from_dict(self):
1657
+ d = {"request_id": "r1", "success": True, "data": {}}
1658
+ # AgentResponse doesn't have a from_dict method, we should test the attributes instead
1659
+ resp = AgentResponse(request_id="r1", success=True)
1660
+ assert resp.request_id == "r1"
1661
+
1662
+
1663
+ class TestBridgeCapabilitiesDeep:
1664
+ def test_version(self):
1665
+ cap = BridgeCapabilities()
1666
+ assert cap.version == "0.9.0"
1667
+
1668
+ def test_name(self):
1669
+ cap = BridgeCapabilities()
1670
+ assert cap.name == "CodexA Bridge"
1671
+
1672
+ def test_supported_requests(self):
1673
+ cap = BridgeCapabilities()
1674
+ assert "semantic_search" in cap.supported_requests
1675
+ assert "invoke_tool" in cap.supported_requests
1676
+
1677
+ def test_to_json(self):
1678
+ cap = BridgeCapabilities()
1679
+ j = cap.to_json()
1680
+ data = json.loads(j)
1681
+ assert "version" in data
1682
+
1683
+
1684
+ # ---------------------------------------------------------------------------
1685
+ # Tools Protocol
1686
+ # ---------------------------------------------------------------------------
1687
+ from semantic_code_intelligence.tools.protocol import (
1688
+ ToolErrorCode,
1689
+ ToolInvocation,
1690
+ ToolError,
1691
+ ToolExecutionResult,
1692
+ )
1693
+
1694
+
1695
+ class TestToolErrorCodeDeep:
1696
+ def test_all_codes(self):
1697
+ codes = list(ToolErrorCode)
1698
+ assert len(codes) == 6
1699
+
1700
+ def test_values(self):
1701
+ assert ToolErrorCode.UNKNOWN_TOOL.value == "unknown_tool"
1702
+ assert ToolErrorCode.TIMEOUT.value == "timeout"
1703
+ assert ToolErrorCode.PERMISSION_DENIED.value == "permission_denied"
1704
+
1705
+
1706
+ class TestToolInvocationDeep:
1707
+ def test_auto_request_id(self):
1708
+ inv = ToolInvocation(tool_name="test", arguments={})
1709
+ assert inv.request_id != ""
1710
+
1711
+ def test_auto_timestamp(self):
1712
+ inv = ToolInvocation(tool_name="test", arguments={})
1713
+ assert inv.timestamp > 0
1714
+
1715
+ def test_to_json(self):
1716
+ inv = ToolInvocation(tool_name="test", arguments={"q": "hello"})
1717
+ data = json.loads(inv.to_json())
1718
+ assert data["tool_name"] == "test"
1719
+ assert data["arguments"]["q"] == "hello"
1720
+
1721
+ def test_from_dict(self):
1722
+ d = {"tool_name": "search", "arguments": {"query": "test"},
1723
+ "request_id": "r1", "timestamp": 1.0}
1724
+ inv = ToolInvocation.from_dict(d)
1725
+ assert inv.tool_name == "search"
1726
+
1727
+ def test_roundtrip(self):
1728
+ inv = ToolInvocation(tool_name="test", arguments={"a": 1})
1729
+ restored = ToolInvocation.from_dict(inv.to_dict())
1730
+ assert restored.tool_name == inv.tool_name
1731
+ assert restored.arguments == inv.arguments
1732
+
1733
+
1734
+ class TestToolErrorDeep:
1735
+ def test_to_dict(self):
1736
+ te = ToolError(tool_name="bad", error_code=ToolErrorCode.UNKNOWN_TOOL,
1737
+ error_message="not found")
1738
+ d = te.to_dict()
1739
+ assert d["error_code"] == "unknown_tool"
1740
+
1741
+ def test_from_dict(self):
1742
+ d = {"tool_name": "x", "error_code": "timeout", "error_message": "timed out",
1743
+ "request_id": "r1"}
1744
+ te = ToolError.from_dict(d)
1745
+ assert te.error_code == ToolErrorCode.TIMEOUT
1746
+
1747
+
1748
+ class TestToolExecutionResultDeep:
1749
+ def test_success(self):
1750
+ r = ToolExecutionResult(
1751
+ tool_name="test", request_id="r1", success=True,
1752
+ result_payload={"data": "value"}, execution_time_ms=10.5,
1753
+ )
1754
+ assert r.success is True
1755
+ d = r.to_dict()
1756
+ assert d["execution_time_ms"] == 10.5
1757
+
1758
+ def test_failure(self):
1759
+ err = ToolError("test", ToolErrorCode.EXECUTION_ERROR, "failed")
1760
+ r = ToolExecutionResult(
1761
+ tool_name="test", request_id="r1", success=False, error=err,
1762
+ )
1763
+ d = r.to_dict()
1764
+ assert d["success"] is False
1765
+
1766
+ def test_from_dict(self):
1767
+ d = {"tool_name": "t", "request_id": "r1", "success": True,
1768
+ "result_payload": {}, "error": None, "execution_time_ms": 5.0,
1769
+ "timestamp": 1.0}
1770
+ r = ToolExecutionResult.from_dict(d)
1771
+ assert r.success is True
1772
+
1773
+
1774
+ # ---------------------------------------------------------------------------
1775
+ # Tools Executor
1776
+ # ---------------------------------------------------------------------------
1777
+ from semantic_code_intelligence.tools.executor import ToolExecutor
1778
+
1779
+
1780
+ class TestToolExecutorDeep:
1781
+ def test_list_tool_names(self):
1782
+ ex = ToolExecutor(Path("."))
1783
+ names = ex.list_tool_names()
1784
+ assert "semantic_search" in names
1785
+ assert len(names) >= 8
1786
+
1787
+ def test_available_tools(self):
1788
+ ex = ToolExecutor(Path("."))
1789
+ tools = ex.available_tools
1790
+ assert len(tools) >= 8
1791
+ assert any(t["name"] == "semantic_search" for t in tools)
1792
+
1793
+ def test_get_tool_schema_known(self):
1794
+ ex = ToolExecutor(Path("."))
1795
+ schema = ex.get_tool_schema("semantic_search")
1796
+ assert schema is not None
1797
+ assert schema["name"] == "semantic_search"
1798
+
1799
+ def test_get_tool_schema_unknown(self):
1800
+ ex = ToolExecutor(Path("."))
1801
+ assert ex.get_tool_schema("nonexistent_tool") is None
1802
+
1803
+ def test_execute_unknown_tool(self):
1804
+ ex = ToolExecutor(Path("."))
1805
+ inv = ToolInvocation(tool_name="nonexistent", arguments={})
1806
+ result = ex.execute(inv)
1807
+ assert result.success is False
1808
+ assert result.error is not None
1809
+ assert result.error.error_code == ToolErrorCode.UNKNOWN_TOOL
1810
+
1811
+ def test_register_plugin_tool(self):
1812
+ ex = ToolExecutor(Path("."))
1813
+ ex.register_plugin_tool(
1814
+ "custom_tool", "A custom tool", {"arg1": {"type": "string"}},
1815
+ lambda args: {"result": "ok"}
1816
+ )
1817
+ assert "custom_tool" in ex.list_tool_names()
1818
+
1819
+ def test_cannot_override_builtin(self):
1820
+ ex = ToolExecutor(Path("."))
1821
+ with pytest.raises(ValueError, match="[Bb]uilt.in"):
1822
+ ex.register_plugin_tool(
1823
+ "semantic_search", "Override", {}, lambda args: {}
1824
+ )
1825
+
1826
+ def test_unregister_plugin_tool(self):
1827
+ ex = ToolExecutor(Path("."))
1828
+ ex.register_plugin_tool("temp", "Temp tool", {}, lambda a: {})
1829
+ assert "temp" in ex.list_tool_names()
1830
+ ex.unregister_plugin_tool("temp")
1831
+ assert "temp" not in ex.list_tool_names()
1832
+
1833
+ def test_execute_plugin_tool(self):
1834
+ ex = ToolExecutor(Path("."))
1835
+ ex.register_plugin_tool(
1836
+ "echo", "Echo tool", {"msg": {"type": "string", "required": True}},
1837
+ lambda msg="": {"echo": msg}
1838
+ )
1839
+ inv = ToolInvocation(tool_name="echo", arguments={"msg": "hello"})
1840
+ result = ex.execute(inv)
1841
+ assert result.success is True
1842
+ assert result.result_payload["echo"] == "hello"
1843
+
1844
+ def test_execute_batch(self):
1845
+ ex = ToolExecutor(Path("."))
1846
+ ex.register_plugin_tool("t1", "Tool 1", {}, lambda **kwargs: {"v": 1})
1847
+ ex.register_plugin_tool("t2", "Tool 2", {}, lambda **kwargs: {"v": 2})
1848
+ results = ex.execute_batch([
1849
+ ToolInvocation(tool_name="t1", arguments={}),
1850
+ ToolInvocation(tool_name="t2", arguments={}),
1851
+ ])
1852
+ assert len(results) == 2
1853
+ assert all(r.success for r in results)
1854
+
1855
+
1856
+ # ---------------------------------------------------------------------------
1857
+ # Tools Registry
1858
+ # ---------------------------------------------------------------------------
1859
+ from semantic_code_intelligence.tools import ToolResult, ToolRegistry, TOOL_DEFINITIONS
1860
+
1861
+
1862
+ class TestToolDefinitions:
1863
+ def test_count(self):
1864
+ assert len(TOOL_DEFINITIONS) == 11
1865
+
1866
+ def test_all_have_required_fields(self):
1867
+ for td in TOOL_DEFINITIONS:
1868
+ assert "name" in td
1869
+ assert "description" in td
1870
+ assert "parameters" in td
1871
+
1872
+ def test_semantic_search_params(self):
1873
+ td = next(t for t in TOOL_DEFINITIONS if t["name"] == "semantic_search")
1874
+ param_names = list(td["parameters"].keys())
1875
+ assert "query" in param_names
1876
+
1877
+ def test_explain_symbol_params(self):
1878
+ td = next(t for t in TOOL_DEFINITIONS if t["name"] == "explain_symbol")
1879
+ param_names = list(td["parameters"].keys())
1880
+ assert "symbol_name" in param_names
1881
+
1882
+ def test_summarize_repo_no_required(self):
1883
+ td = next(t for t in TOOL_DEFINITIONS if t["name"] == "summarize_repo")
1884
+ required = [name for name, p in td["parameters"].items() if p.get("required")]
1885
+ assert len(required) == 0
1886
+
1887
+
1888
+ class TestToolResult:
1889
+ def test_success(self):
1890
+ tr = ToolResult(tool_name="test", success=True, data={"key": "val"})
1891
+ assert tr.success is True
1892
+
1893
+ def test_error(self):
1894
+ tr = ToolResult(tool_name="test", success=False, error="bad")
1895
+ assert tr.success is False
1896
+ assert tr.error == "bad"
1897
+
1898
+
1899
+ class TestToolRegistryDeep:
1900
+ def test_create(self):
1901
+ tr = ToolRegistry(Path("."))
1902
+ assert tr is not None
1903
+
1904
+ def test_available_tools(self):
1905
+ tr = ToolRegistry(Path("."))
1906
+ tools = tr.tool_definitions
1907
+ assert len(tools) == 11
1908
+
1909
+ def test_invoke_unknown(self):
1910
+ tr = ToolRegistry(Path("."))
1911
+ result = tr.invoke("nonexistent", arg1="val")
1912
+ assert result.success is False
1913
+
1914
+
1915
+ # ---------------------------------------------------------------------------
1916
+ # Plugins
1917
+ # ---------------------------------------------------------------------------
1918
+ from semantic_code_intelligence.plugins import PluginHook, PluginManager, PluginBase
1919
+
1920
+
1921
+ class TestPluginHookDeep:
1922
+ def test_count(self):
1923
+ assert len(PluginHook) >= 22
1924
+
1925
+ def test_tool_hooks(self):
1926
+ names = [h.value for h in PluginHook]
1927
+ assert "register_tool" in names
1928
+ assert "pre_tool_invoke" in names
1929
+ assert "post_tool_invoke" in names
1930
+
1931
+ def test_all_string_values(self):
1932
+ for h in PluginHook:
1933
+ assert isinstance(h.value, str)
1934
+
1935
+
1936
+ class TestPluginManagerDeep:
1937
+ def test_create(self):
1938
+ pm = PluginManager()
1939
+ assert pm is not None
1940
+
1941
+ def test_register_and_activate(self):
1942
+ pm = PluginManager()
1943
+
1944
+ from semantic_code_intelligence.plugins import PluginMetadata
1945
+
1946
+ class TestPlugin20(PluginBase):
1947
+ def metadata(self) -> PluginMetadata:
1948
+ return PluginMetadata(name="test20", version="1.0", description="test")
1949
+
1950
+ def on_hook(self, hook, data=None):
1951
+ return data or {}
1952
+
1953
+ plugin = TestPlugin20()
1954
+ pm.register(plugin)
1955
+ pm.activate("test20")
1956
+ assert "test20" in pm.active_plugins
1957
+
1958
+ def test_dispatch_no_handlers(self):
1959
+ pm = PluginManager()
1960
+ result = pm.dispatch(PluginHook.PRE_SEARCH, {"query": "test"})
1961
+ assert result == {"query": "test"}
1962
+
1963
+
1964
+ # ---------------------------------------------------------------------------
1965
+ # Config Settings
1966
+ # ---------------------------------------------------------------------------
1967
+ from semantic_code_intelligence.config.settings import (
1968
+ AppConfig,
1969
+ EmbeddingConfig,
1970
+ SearchConfig,
1971
+ IndexConfig,
1972
+ LLMConfig,
1973
+ )
1974
+
1975
+
1976
+ class TestEmbeddingConfig:
1977
+ def test_defaults(self):
1978
+ ec = EmbeddingConfig()
1979
+ assert ec.model_name == "all-MiniLM-L6-v2"
1980
+ assert ec.chunk_size == 512
1981
+
1982
+ def test_custom(self):
1983
+ ec = EmbeddingConfig(model_name="custom", chunk_size=256)
1984
+ assert ec.model_name == "custom"
1985
+
1986
+
1987
+ class TestSearchConfig:
1988
+ def test_defaults(self):
1989
+ sc = SearchConfig()
1990
+ assert sc.top_k == 10
1991
+ assert sc.similarity_threshold == 0.3
1992
+
1993
+
1994
+ class TestIndexConfig:
1995
+ def test_defaults(self):
1996
+ ic = IndexConfig()
1997
+ assert ic.use_incremental is True
1998
+ assert len(ic.extensions) > 0
1999
+
2000
+ def test_ignore_dirs(self):
2001
+ ic = IndexConfig()
2002
+ assert len(ic.ignore_dirs) > 0
2003
+
2004
+
2005
+ class TestLLMConfig:
2006
+ def test_defaults(self):
2007
+ lc = LLMConfig()
2008
+ assert lc.provider == "mock"
2009
+ assert lc.temperature == 0.2
2010
+
2011
+
2012
+ class TestAppConfig:
2013
+ def test_defaults(self):
2014
+ ac = AppConfig()
2015
+ assert isinstance(ac.embedding, EmbeddingConfig)
2016
+ assert isinstance(ac.search, SearchConfig)
2017
+ assert isinstance(ac.index, IndexConfig)
2018
+ assert isinstance(ac.llm, LLMConfig)
2019
+
2020
+ def test_config_dir(self):
2021
+ d = AppConfig.config_dir(Path("/tmp/test"))
2022
+ assert ".codexa" in str(d)
2023
+
2024
+
2025
+ # ---------------------------------------------------------------------------
2026
+ # Parsing
2027
+ # ---------------------------------------------------------------------------
2028
+ from semantic_code_intelligence.parsing.parser import (
2029
+ Symbol as ParserSymbol,
2030
+ detect_language,
2031
+ parse_file as parser_parse_file,
2032
+ )
2033
+
2034
+
2035
+ class TestDetectLanguage:
2036
+ def test_python(self):
2037
+ assert detect_language("app.py") == "python"
2038
+
2039
+ def test_javascript(self):
2040
+ assert detect_language("app.js") == "javascript"
2041
+
2042
+ def test_typescript(self):
2043
+ assert detect_language("app.ts") == "typescript"
2044
+
2045
+ def test_java(self):
2046
+ assert detect_language("App.java") == "java"
2047
+
2048
+ def test_go(self):
2049
+ assert detect_language("main.go") == "go"
2050
+
2051
+ def test_rust(self):
2052
+ assert detect_language("lib.rs") == "rust"
2053
+
2054
+ def test_cpp(self):
2055
+ assert detect_language("main.cpp") == "cpp"
2056
+
2057
+ def test_csharp(self):
2058
+ assert detect_language("Program.cs") == "csharp"
2059
+
2060
+ def test_ruby(self):
2061
+ assert detect_language("app.rb") == "ruby"
2062
+
2063
+ def test_unknown(self):
2064
+ lang = detect_language("file.xyz")
2065
+ assert lang is None or lang == ""
2066
+
2067
+ def test_tsx(self):
2068
+ assert detect_language("App.tsx") == "tsx"
2069
+
2070
+ def test_php(self):
2071
+ assert detect_language("index.php") == "php"
2072
+
2073
+
2074
+ class TestParserSymbol:
2075
+ def test_to_dict(self):
2076
+ s = ParserSymbol(
2077
+ name="fn", kind="function", body="pass", file_path="a.py",
2078
+ start_line=1, end_line=2, start_col=0, end_col=0, parent="",
2079
+ )
2080
+ d = s.to_dict()
2081
+ assert d["name"] == "fn"
2082
+ assert d["kind"] == "function"
2083
+
2084
+ def test_is_dataclass(self):
2085
+ from dataclasses import fields
2086
+ f = fields(ParserSymbol)
2087
+ names = [ff.name for ff in f]
2088
+ assert "name" in names
2089
+ assert "kind" in names
2090
+
2091
+
2092
+ class TestParseFile:
2093
+ def test_python_file(self):
2094
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
2095
+ f.write("def hello():\n pass\n\nclass World:\n def method(self):\n pass\n")
2096
+ f.flush()
2097
+ syms = parser_parse_file(f.name)
2098
+ names = [s.name for s in syms]
2099
+ assert "hello" in names
2100
+
2101
+ def test_empty_file(self):
2102
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
2103
+ f.write("")
2104
+ f.flush()
2105
+ syms = parser_parse_file(f.name)
2106
+ assert isinstance(syms, list)
2107
+
2108
+
2109
+ # ---------------------------------------------------------------------------
2110
+ # Indexing Scanner
2111
+ # ---------------------------------------------------------------------------
2112
+ from semantic_code_intelligence.indexing.scanner import (
2113
+ ScannedFile,
2114
+ compute_file_hash,
2115
+ should_ignore,
2116
+ )
2117
+
2118
+
2119
+ class TestScannedFile:
2120
+ def test_fields(self):
2121
+ sf = ScannedFile(path=Path("a.py"), relative_path="a.py", extension=".py",
2122
+ size_bytes=100, content_hash="abc123")
2123
+ assert sf.relative_path == "a.py"
2124
+ assert sf.extension == ".py"
2125
+
2126
+
2127
+ class TestComputeFileHash:
2128
+ def test_deterministic(self):
2129
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
2130
+ f.write("hello world")
2131
+ f.flush()
2132
+ h1 = compute_file_hash(f.name)
2133
+ h2 = compute_file_hash(f.name)
2134
+ assert h1 == h2
2135
+ assert len(h1) > 0
2136
+
2137
+
2138
+ class TestShouldIgnore:
2139
+ def test_hidden_dirs(self):
2140
+ assert should_ignore(Path(".git/config"), Path("."), {".git"}) is True
2141
+
2142
+ def test_pycache(self):
2143
+ assert should_ignore(Path("__pycache__/module.pyc"), Path("."), {"__pycache__"}) is True
2144
+
2145
+ def test_normal_file(self):
2146
+ assert should_ignore(Path("src/app.py"), Path("."), {".git", "__pycache__"}) is False
2147
+
2148
+ def test_node_modules(self):
2149
+ assert should_ignore(Path("node_modules/pkg/index.js"), Path("."), {"node_modules"}) is True
2150
+
2151
+
2152
+ # ---------------------------------------------------------------------------
2153
+ # Indexing Chunker
2154
+ # ---------------------------------------------------------------------------
2155
+ from semantic_code_intelligence.indexing.chunker import CodeChunk, chunk_code
2156
+
2157
+
2158
+ class TestCodeChunk:
2159
+ def test_fields(self):
2160
+ cc = CodeChunk(
2161
+ content="code", file_path="a.py", start_line=1, end_line=10,
2162
+ language="python", chunk_index=0,
2163
+ )
2164
+ assert cc.content == "code"
2165
+ assert cc.language == "python"
2166
+
2167
+
2168
+ class TestChunkCode:
2169
+ def test_basic_chunking(self):
2170
+ code = "\n".join([f"line{i}" for i in range(100)])
2171
+ chunks = chunk_code(code, "test.py", chunk_size=50, chunk_overlap=10)
2172
+ assert len(chunks) >= 1
2173
+
2174
+ def test_empty_code(self):
2175
+ chunks = chunk_code("", "test.py")
2176
+ assert len(chunks) == 0
2177
+
2178
+ def test_small_code(self):
2179
+ chunks = chunk_code("single line", "test.py", chunk_size=100)
2180
+ assert len(chunks) == 1
2181
+
2182
+
2183
+ # ---------------------------------------------------------------------------
2184
+ # Storage VectorStore
2185
+ # ---------------------------------------------------------------------------
2186
+ from semantic_code_intelligence.storage.vector_store import VectorStore, ChunkMetadata
2187
+
2188
+
2189
+ class TestChunkMetadata:
2190
+ def test_fields(self):
2191
+ cm = ChunkMetadata(
2192
+ file_path="a.py", start_line=1, end_line=10,
2193
+ content="code", language="python", chunk_index=0,
2194
+ )
2195
+ assert cm.file_path == "a.py"
2196
+
2197
+
2198
+ class TestVectorStoreDeep:
2199
+ def test_create(self):
2200
+ vs = VectorStore(dimension=384)
2201
+ assert vs is not None
2202
+
2203
+ def test_add_and_search(self):
2204
+ import numpy as np
2205
+ vs = VectorStore(dimension=4)
2206
+ embedding = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
2207
+ meta = ChunkMetadata(file_path="a.py", start_line=1, end_line=2,
2208
+ chunk_index=0, content="test", language="python")
2209
+ vs.add(embedding, [meta])
2210
+ query = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
2211
+ results = vs.search(query, top_k=1)
2212
+ assert len(results) >= 1
2213
+
2214
+ def test_clear(self):
2215
+ vs = VectorStore(dimension=4)
2216
+ vs.clear()
2217
+ assert vs is not None
2218
+
2219
+
2220
+ # ---------------------------------------------------------------------------
2221
+ # Storage HashStore
2222
+ # ---------------------------------------------------------------------------
2223
+ from semantic_code_intelligence.storage.hash_store import HashStore
2224
+
2225
+
2226
+ class TestHashStoreDeep:
2227
+ def test_create(self):
2228
+ hs = HashStore()
2229
+ assert hs is not None
2230
+ assert hs.count == 0
2231
+
2232
+ def test_store_and_check(self):
2233
+ hs = HashStore()
2234
+ hs.set("a.py", "hash1")
2235
+ assert hs.has_changed("a.py", "hash1") is False
2236
+ assert hs.has_changed("a.py", "hash2") is True
2237
+
2238
+ def test_unknown_file(self):
2239
+ hs = HashStore()
2240
+ assert hs.has_changed("missing.py", "hash1") is True
2241
+
2242
+
2243
+ # ---------------------------------------------------------------------------
2244
+ # Docs
2245
+ # ---------------------------------------------------------------------------
2246
+ from semantic_code_intelligence.docs import generate_all_docs
2247
+
2248
+
2249
+ class TestDocsGeneration:
2250
+ def test_generate_all_docs(self):
2251
+ with tempfile.TemporaryDirectory() as tmp:
2252
+ docs = generate_all_docs(Path(tmp))
2253
+ assert isinstance(docs, list)
2254
+
2255
+ def test_all_docs_are_strings(self):
2256
+ with tempfile.TemporaryDirectory() as tmp:
2257
+ docs = generate_all_docs(Path(tmp))
2258
+ for name in docs:
2259
+ assert isinstance(name, str)
2260
+
2261
+ def test_cli_reference_exists(self):
2262
+ with tempfile.TemporaryDirectory() as tmp:
2263
+ docs = generate_all_docs(Path(tmp))
2264
+ assert any("CLI" in k or "cli" in k for k in docs)
2265
+
2266
+ def test_architecture_exists(self):
2267
+ with tempfile.TemporaryDirectory() as tmp:
2268
+ docs = generate_all_docs(Path(tmp))
2269
+ # May generate PLUGINS, BRIDGE, WEB, CI, etc.
2270
+ assert isinstance(docs, list)
2271
+
2272
+ def test_tool_protocol_exists(self):
2273
+ with tempfile.TemporaryDirectory() as tmp:
2274
+ docs = generate_all_docs(Path(tmp))
2275
+ assert any("TOOL" in k or "tool" in k.lower() for k in docs)
2276
+
2277
+
2278
+ # ---------------------------------------------------------------------------
2279
+ # Scalability
2280
+ # ---------------------------------------------------------------------------
2281
+ from semantic_code_intelligence.scalability import (
2282
+ BatchProcessor,
2283
+ MemoryAwareEmbedder,
2284
+ ParallelScanner,
2285
+ )
2286
+
2287
+
2288
+ class TestBatchProcessorDeep:
2289
+ def test_create(self):
2290
+ bp = BatchProcessor(batch_size=10)
2291
+ assert bp is not None
2292
+ assert bp.batch_size == 10
2293
+
2294
+ def test_process(self):
2295
+ bp = BatchProcessor(batch_size=3)
2296
+ items = list(range(10))
2297
+ results, stats = bp.process(items, lambda batch: batch)
2298
+ assert results == items
2299
+ assert stats.total_items == 10
2300
+
2301
+ def test_single_batch(self):
2302
+ bp = BatchProcessor(batch_size=100)
2303
+ items = [1, 2, 3]
2304
+ results, stats = bp.process(items, lambda batch: batch)
2305
+ assert stats.batches_processed == 1
2306
+
2307
+ def test_empty(self):
2308
+ bp = BatchProcessor(batch_size=5)
2309
+ results, stats = bp.process([], lambda batch: batch)
2310
+ assert len(results) == 0
2311
+ assert stats.total_items == 0
2312
+
2313
+
2314
+ class TestMemoryAwareEmbedder:
2315
+ def test_create(self):
2316
+ mae = MemoryAwareEmbedder(model_name="all-MiniLM-L6-v2", batch_size=32)
2317
+ assert mae is not None
2318
+
2319
+
2320
+ class TestParallelScanner:
2321
+ def test_create(self):
2322
+ ps = ParallelScanner(max_workers=2)
2323
+ assert ps is not None
2324
+
2325
+ def test_scan_empty(self):
2326
+ ps = ParallelScanner(max_workers=2)
2327
+ results, errors = ps.scan_and_process([], lambda fp: str(fp))
2328
+ assert results == []
2329
+ assert errors == []
2330
+
2331
+
2332
+ # ---------------------------------------------------------------------------
2333
+ # Workspace
2334
+ # ---------------------------------------------------------------------------
2335
+ from semantic_code_intelligence.workspace import RepoEntry, WorkspaceManifest, Workspace
2336
+
2337
+
2338
+ class TestRepoEntry:
2339
+ def test_to_dict(self):
2340
+ re = RepoEntry(name="myrepo", path="/path/to/repo")
2341
+ d = re.to_dict()
2342
+ assert d["name"] == "myrepo"
2343
+ assert d["path"] == "/path/to/repo"
2344
+
2345
+ def test_defaults(self):
2346
+ re = RepoEntry(name="r", path="/r")
2347
+ assert re.file_count == 0
2348
+
2349
+
2350
+ class TestWorkspaceManifest:
2351
+ def test_create(self):
2352
+ wm = WorkspaceManifest()
2353
+ assert wm is not None
2354
+ assert wm.version == "1.0.0"
2355
+
2356
+ def test_to_dict(self):
2357
+ wm = WorkspaceManifest()
2358
+ d = wm.to_dict()
2359
+ assert isinstance(d, dict)
2360
+ assert "repos" in d
2361
+
2362
+
2363
+ class TestWorkspace:
2364
+ def test_create(self):
2365
+ with tempfile.TemporaryDirectory() as tmp:
2366
+ ws = Workspace(Path(tmp))
2367
+ assert ws is not None
2368
+
2369
+ def test_repos_empty(self):
2370
+ with tempfile.TemporaryDirectory() as tmp:
2371
+ ws = Workspace(Path(tmp))
2372
+ assert isinstance(ws.repos, list)
2373
+ assert len(ws.repos) == 0
2374
+
2375
+
2376
+ # ---------------------------------------------------------------------------
2377
+ # Daemon Watcher
2378
+ # ---------------------------------------------------------------------------
2379
+ from semantic_code_intelligence.daemon.watcher import FileChangeEvent, FileWatcher
2380
+
2381
+
2382
+ class TestFileChangeEvent:
2383
+ def test_create(self):
2384
+ ev = FileChangeEvent(path=Path("a.py"), relative_path="a.py", change_type="modified")
2385
+ assert ev.path == Path("a.py")
2386
+ assert ev.change_type == "modified"
2387
+
2388
+ def test_to_dict(self):
2389
+ ev = FileChangeEvent(path=Path("b.py"), relative_path="b.py", change_type="created")
2390
+ d = ev.to_dict()
2391
+ assert d["change_type"] == "created"
2392
+ assert d["relative_path"] == "b.py"
2393
+
2394
+
2395
+ class TestFileWatcherDeep:
2396
+ def test_create(self):
2397
+ with tempfile.TemporaryDirectory() as tmp:
2398
+ fw = FileWatcher(Path(tmp))
2399
+ assert fw is not None
2400
+
2401
+ def test_has_start_stop(self):
2402
+ with tempfile.TemporaryDirectory() as tmp:
2403
+ fw = FileWatcher(Path(tmp))
2404
+ assert hasattr(fw, "start")
2405
+ assert hasattr(fw, "stop")
2406
+
2407
+
2408
+ # ---------------------------------------------------------------------------
2409
+ # Version & Meta
2410
+ # ---------------------------------------------------------------------------
2411
+ from semantic_code_intelligence import __version__, __app_name__
2412
+
2413
+
2414
+ class TestVersionMeta:
2415
+ def test_version_format(self):
2416
+ parts = __version__.split(".")
2417
+ assert len(parts) == 3
2418
+
2419
+ def test_app_name(self):
2420
+ assert __app_name__ == "codexa"
2421
+
2422
+
2423
+ # ---------------------------------------------------------------------------
2424
+ # CLI Router
2425
+ # ---------------------------------------------------------------------------
2426
+ from semantic_code_intelligence.cli.router import register_commands
2427
+ from semantic_code_intelligence.cli.main import cli
2428
+
2429
+
2430
+ class TestCLIRouterDeep:
2431
+ def test_command_count(self):
2432
+ register_commands(cli)
2433
+ # 31 commands
2434
+ assert len(cli.commands) >= 31
2435
+
2436
+ def test_tool_command_registered(self):
2437
+ register_commands(cli)
2438
+ assert "tool" in cli.commands
2439
+
2440
+ def test_serve_command_registered(self):
2441
+ register_commands(cli)
2442
+ assert "serve" in cli.commands
2443
+
2444
+ def test_quality_command_registered(self):
2445
+ register_commands(cli)
2446
+ assert "quality" in cli.commands
2447
+
2448
+ def test_impact_command_registered(self):
2449
+ register_commands(cli)
2450
+ assert "impact" in cli.commands
2451
+
2452
+
2453
+ # ---------------------------------------------------------------------------
2454
+ # Cross-cutting: README and copilot-instructions.md
2455
+ # ---------------------------------------------------------------------------
2456
+
2457
+ _PROJECT_ROOT = Path(__file__).resolve().parents[2]
2458
+
2459
+
2460
+ class TestReadmeExists:
2461
+ def test_readme_exists(self):
2462
+ assert (_PROJECT_ROOT / "README.md").exists()
2463
+
2464
+
2465
+ class TestCopilotInstructionsExists:
2466
+ _ci_path = _PROJECT_ROOT / ".github" / "copilot-instructions.md"
2467
+
2468
+ def test_file_exists(self):
2469
+ assert self._ci_path.exists()
2470
+
2471
+ def test_contains_codex_commands(self):
2472
+ content = self._ci_path.read_text(encoding="utf-8")
2473
+ assert "codexa search" in content
2474
+ assert "codexa tool run" in content
2475
+
2476
+ def test_contains_rules(self):
2477
+ content = self._ci_path.read_text(encoding="utf-8")
2478
+ assert "--json" in content
2479
+
2480
+ def test_contains_project_structure(self):
2481
+ content = self._ci_path.read_text(encoding="utf-8")
2482
+ assert "cli/" in content
2483
+ assert "tools/" in content
2484
+ assert "bridge/" in content
2485
+
2486
+
2487
+ # ---------------------------------------------------------------------------
2488
+ # Embeddings (basic import & structure tests)
2489
+ # ---------------------------------------------------------------------------
2490
+ from semantic_code_intelligence.embeddings.enhanced import (
2491
+ preprocess_code_for_embedding,
2492
+ prepare_semantic_texts,
2493
+ )
2494
+
2495
+
2496
+ class TestPreprocessCodeForEmbedding:
2497
+ def test_basic(self):
2498
+ result = preprocess_code_for_embedding("def foo():\n pass")
2499
+ assert isinstance(result, str)
2500
+ assert len(result) > 0
2501
+
2502
+ def test_empty(self):
2503
+ result = preprocess_code_for_embedding("")
2504
+ assert isinstance(result, str)
2505
+
2506
+
2507
+ class TestPrepareSemanticTexts:
2508
+ def test_basic(self):
2509
+ from semantic_code_intelligence.indexing.semantic_chunker import SemanticChunk
2510
+ chunks = [SemanticChunk(content="def foo(): pass", file_path="a.py",
2511
+ start_line=1, end_line=1, language="python",
2512
+ chunk_index=0, semantic_label="function foo")]
2513
+ texts = prepare_semantic_texts(chunks)
2514
+ assert len(texts) == 1
2515
+
2516
+
2517
+ # ---------------------------------------------------------------------------
2518
+ # Search Formatter
2519
+ # ---------------------------------------------------------------------------
2520
+ from semantic_code_intelligence.search.formatter import format_results_json
2521
+
2522
+
2523
+ class TestFormatResultsJson:
2524
+ def test_basic(self):
2525
+ from semantic_code_intelligence.services.search_service import SearchResult
2526
+ results = [SearchResult(file_path="a.py", content="code", score=0.9,
2527
+ start_line=1, end_line=1, language="python", chunk_index=0)]
2528
+ output = format_results_json("test query", results, top_k=5)
2529
+ parsed = json.loads(output)
2530
+ assert isinstance(parsed, (list, dict))
2531
+
2532
+ def test_empty(self):
2533
+ output = format_results_json("empty", [], top_k=5)
2534
+ parsed = json.loads(output)
2535
+ assert isinstance(parsed, (list, dict))
2536
+
2537
+
2538
+ # ---------------------------------------------------------------------------
2539
+ # Bridge VSCode
2540
+ # ---------------------------------------------------------------------------
2541
+ from semantic_code_intelligence.bridge.vscode import VSCodeBridge
2542
+
2543
+
2544
+ class TestVSCodeBridge:
2545
+ def test_create(self):
2546
+ vsb = VSCodeBridge(Path("."))
2547
+ assert vsb is not None
2548
+
2549
+ def test_has_methods(self):
2550
+ vsb = VSCodeBridge(Path("."))
2551
+ assert hasattr(vsb, "hover")
2552
+ assert hasattr(vsb, "diagnostics")
2553
+ assert hasattr(vsb, "completions")
2554
+
2555
+
2556
+ # ---------------------------------------------------------------------------
2557
+ # Logging
2558
+ # ---------------------------------------------------------------------------
2559
+ from semantic_code_intelligence.utils.logging import get_logger, setup_logging
2560
+
2561
+
2562
+ class TestLogging:
2563
+ def test_get_logger(self):
2564
+ log = get_logger("test")
2565
+ assert log is not None
2566
+
2567
+ def test_setup_logging(self):
2568
+ setup_logging(verbose=False)
2569
+ setup_logging(verbose=True)
2570
+
2571
+
2572
+ # ---------------------------------------------------------------------------
2573
+ # Additional edge cases and integration-like tests
2574
+ # ---------------------------------------------------------------------------
2575
+
2576
+
2577
+ class TestToolRegistryInvocations:
2578
+ """Test ToolRegistry invoke for each built-in tool (error paths)."""
2579
+
2580
+ def test_invoke_semantic_search_no_index(self):
2581
+ tr = ToolRegistry(Path("."))
2582
+ result = tr.invoke("semantic_search", query="test")
2583
+ # Should return ToolResult (might not have index)
2584
+ assert isinstance(result, ToolResult)
2585
+
2586
+ def test_invoke_explain_symbol(self):
2587
+ tr = ToolRegistry(Path("."))
2588
+ result = tr.invoke("explain_symbol", symbol_name="nonexistent")
2589
+ assert isinstance(result, ToolResult)
2590
+
2591
+ def test_invoke_explain_file(self):
2592
+ tr = ToolRegistry(Path("."))
2593
+ result = tr.invoke("explain_file", file_path="nonexistent.py")
2594
+ assert isinstance(result, ToolResult)
2595
+
2596
+ def test_invoke_summarize_repo(self):
2597
+ tr = ToolRegistry(Path("."))
2598
+ result = tr.invoke("summarize_repo")
2599
+ assert isinstance(result, ToolResult)
2600
+
2601
+ def test_invoke_find_references(self):
2602
+ tr = ToolRegistry(Path("."))
2603
+ result = tr.invoke("find_references", symbol_name="nonexistent")
2604
+ assert isinstance(result, ToolResult)
2605
+
2606
+ def test_invoke_get_dependencies(self):
2607
+ tr = ToolRegistry(Path("."))
2608
+ result = tr.invoke("get_dependencies", file_path="nonexistent.py")
2609
+ assert isinstance(result, ToolResult)
2610
+
2611
+ def test_invoke_get_call_graph(self):
2612
+ tr = ToolRegistry(Path("."))
2613
+ result = tr.invoke("get_call_graph", symbol_name="nonexistent")
2614
+ assert isinstance(result, ToolResult)
2615
+
2616
+ def test_invoke_get_context(self):
2617
+ tr = ToolRegistry(Path("."))
2618
+ result = tr.invoke("get_context", symbol_name="nonexistent")
2619
+ assert isinstance(result, ToolResult)
2620
+
2621
+
2622
+ class TestDependencyMapBasic:
2623
+ def test_create(self):
2624
+ dm = DependencyMap()
2625
+ assert dm is not None
2626
+
2627
+ def test_add_file(self):
2628
+ dm = DependencyMap()
2629
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
2630
+ f.write("import os\nimport sys\n")
2631
+ f.flush()
2632
+ dm.add_file(f.name)
2633
+ deps = dm.get_dependencies(f.name)
2634
+ assert isinstance(deps, list)
2635
+
2636
+ def test_get_all_files(self):
2637
+ dm = DependencyMap()
2638
+ assert isinstance(dm.get_all_files(), (list, set))
2639
+
2640
+
2641
+ class TestBuildChangeSummary:
2642
+ """Test build_change_summary from ci.pr."""
2643
+
2644
+ def test_with_python_file(self):
2645
+ from semantic_code_intelligence.ci.pr import build_change_summary
2646
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
2647
+ f.write("def hello():\n pass\n")
2648
+ f.flush()
2649
+ summary = build_change_summary([f.name])
2650
+ assert summary.files_changed == 1
2651
+
2652
+ def test_with_nonexistent_file(self):
2653
+ from semantic_code_intelligence.ci.pr import build_change_summary
2654
+ summary = build_change_summary(["/nonexistent/file.py"])
2655
+ assert summary.files_changed == 1
2656
+
2657
+ def test_empty(self):
2658
+ from semantic_code_intelligence.ci.pr import build_change_summary
2659
+ summary = build_change_summary([])
2660
+ assert summary.files_changed == 0
2661
+
2662
+
2663
+ class TestGateViolation:
2664
+ """Test GateViolation dataclass from ci.metrics."""
2665
+
2666
+ def test_create(self):
2667
+ from semantic_code_intelligence.ci.metrics import GateViolation
2668
+ gv = GateViolation(rule="min_mi", message="MI too low", actual=30.0, threshold=40.0)
2669
+ d = gv.to_dict()
2670
+ assert d["rule"] == "min_mi"
2671
+ assert d["actual"] == 30.0
2672
+
2673
+
2674
+ class TestStreamEventSSE:
2675
+ """Additional SSE formatting tests."""
2676
+
2677
+ def test_empty_data(self):
2678
+ se = StreamEvent(kind="heartbeat", content="")
2679
+ sse = se.to_sse()
2680
+ assert "heartbeat" in sse
2681
+
2682
+ def test_json_data(self):
2683
+ data = json.dumps({"key": "value"})
2684
+ se = StreamEvent(kind="data", content=data)
2685
+ sse = se.to_sse()
2686
+ assert "key" in sse
2687
+
2688
+
2689
+ class TestContextWindowRender:
2690
+ """More context window rendering tests."""
2691
+
2692
+ def test_render_with_max_lines(self):
2693
+ s = _sym(body="\n".join(f"line{i}" for i in range(100)))
2694
+ cw = ContextWindow(focal_symbol=s)
2695
+ text = cw.render(max_lines=5)
2696
+ assert "more lines" in text
2697
+
2698
+ def test_render_with_related(self):
2699
+ s = _sym(name="main", body="pass")
2700
+ related = _sym(name="helper", body="pass")
2701
+ cw = ContextWindow(focal_symbol=s, related_symbols=[related])
2702
+ text = cw.render()
2703
+ assert "helper" in text
2704
+
2705
+
2706
+ class TestCallGraphMultiFile:
2707
+ """Test call graph across multiple files."""
2708
+
2709
+ def test_cross_file_calls(self):
2710
+ syms = [
2711
+ _sym(name="fn_a", body="fn_b()", file_path="a.py"),
2712
+ _sym(name="fn_b", body="fn_c()", file_path="b.py"),
2713
+ _sym(name="fn_c", body="pass", file_path="c.py"),
2714
+ ]
2715
+ cg = CallGraph()
2716
+ cg.build(syms)
2717
+ assert len(cg.edges) >= 2
2718
+
2719
+ def test_multiple_callers(self):
2720
+ syms = [
2721
+ _sym(name="caller1", body="target()", file_path="a.py"),
2722
+ _sym(name="caller2", body="target()", file_path="b.py"),
2723
+ _sym(name="target", body="pass", file_path="c.py"),
2724
+ ]
2725
+ cg = CallGraph()
2726
+ cg.build(syms)
2727
+ callers = cg.callers_of("target")
2728
+ assert len(callers) >= 2
2729
+
2730
+
2731
+ class TestQualityReportAggregation:
2732
+ """Test QualityReport with mixed issues."""
2733
+
2734
+ def test_mixed_issues(self):
2735
+ from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
2736
+ r = QualityReport(
2737
+ files_analyzed=5,
2738
+ symbol_count=20,
2739
+ complexity_issues=[ComplexityResult("fn", "a.py", 1, 10, 15, "high")],
2740
+ dead_code=[DeadCodeResult("x", "function", "a.py", 1),
2741
+ DeadCodeResult("y", "function", "b.py", 5)],
2742
+ duplicates=[DuplicateResult("a", "f1.py", 1, "b", "f2.py", 2, 0.9)],
2743
+ safety=SafetyReport(safe=False, issues=[SafetyIssue("eval", "bad")]),
2744
+ )
2745
+ assert r.issue_count == 5 # 1 complexity + 2 dead + 1 dup + 1 safety
2746
+
2747
+ def test_to_dict_completeness(self):
2748
+ r = QualityReport(files_analyzed=3, symbol_count=15)
2749
+ d = r.to_dict()
2750
+ assert "complexity_issues" in d
2751
+ assert "dead_code" in d
2752
+ assert "duplicates" in d
2753
+ assert "safety" in d