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