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,592 @@
1
+ """Tests for LLM provider abstraction, reasoning engine, safety validator,
2
+ context memory, and CLI commands introduced in Phase 8.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import textwrap
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from unittest.mock import MagicMock, patch
13
+
14
+ import pytest
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # LLM Provider core types
18
+ # ---------------------------------------------------------------------------
19
+ from semantic_code_intelligence.llm.provider import (
20
+ LLMMessage,
21
+ LLMProvider,
22
+ LLMResponse,
23
+ MessageRole,
24
+ )
25
+
26
+
27
+ class TestLLMMessage:
28
+ def test_to_dict(self):
29
+ msg = LLMMessage(role=MessageRole.USER, content="hello")
30
+ assert msg.to_dict() == {"role": "user", "content": "hello"}
31
+
32
+ def test_roles(self):
33
+ assert MessageRole.SYSTEM.value == "system"
34
+ assert MessageRole.ASSISTANT.value == "assistant"
35
+
36
+
37
+ class TestLLMResponse:
38
+ def test_to_dict(self):
39
+ resp = LLMResponse(content="answer", model="gpt-4", provider="openai", usage={"total_tokens": 10})
40
+ d = resp.to_dict()
41
+ assert d["content"] == "answer"
42
+ assert d["model"] == "gpt-4"
43
+ assert d["provider"] == "openai"
44
+ assert d["usage"]["total_tokens"] == 10
45
+
46
+ def test_defaults(self):
47
+ resp = LLMResponse(content="hi")
48
+ assert resp.model == ""
49
+ assert resp.provider == ""
50
+ assert resp.usage == {}
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Mock Provider
55
+ # ---------------------------------------------------------------------------
56
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
57
+
58
+
59
+ class TestMockProvider:
60
+ def test_name(self):
61
+ p = MockProvider()
62
+ assert p.name == "mock"
63
+
64
+ def test_complete_default(self):
65
+ p = MockProvider(default_response="test response")
66
+ resp = p.complete("prompt")
67
+ assert resp.content == "test response"
68
+ assert resp.provider == "mock"
69
+
70
+ def test_chat(self):
71
+ p = MockProvider()
72
+ msgs = [LLMMessage(role=MessageRole.USER, content="hi")]
73
+ resp = p.chat(msgs)
74
+ assert resp.content == "This is a mock LLM response."
75
+ assert len(p.call_history) == 1
76
+ assert p.call_history[0]["method"] == "chat"
77
+
78
+ def test_enqueue_response(self):
79
+ p = MockProvider()
80
+ p.enqueue_response("first")
81
+ p.enqueue_response("second")
82
+ assert p.complete("a").content == "first"
83
+ assert p.complete("b").content == "second"
84
+ assert p.complete("c").content == "This is a mock LLM response."
85
+
86
+ def test_call_history(self):
87
+ p = MockProvider()
88
+ p.complete("p1")
89
+ p.chat([LLMMessage(role=MessageRole.USER, content="p2")])
90
+ assert len(p.call_history) == 2
91
+
92
+ def test_is_available(self):
93
+ # LLMProvider default is True; mock inherits
94
+ p = MockProvider()
95
+ assert p.is_available() is True
96
+
97
+ def test_usage_tokens(self):
98
+ p = MockProvider(default_response="short")
99
+ resp = p.complete("a longer prompt text here")
100
+ assert "prompt_tokens" in resp.usage
101
+ assert "completion_tokens" in resp.usage
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # OpenAI Provider (only initialization, no API calls)
106
+ # ---------------------------------------------------------------------------
107
+ from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
108
+
109
+
110
+ class TestOpenAIProvider:
111
+ def test_name(self):
112
+ p = OpenAIProvider(api_key="test-key")
113
+ assert p.name == "openai"
114
+
115
+ def test_is_available(self):
116
+ p = OpenAIProvider(api_key="key")
117
+ assert p.is_available() is True
118
+
119
+ def test_not_available_without_key(self):
120
+ p = OpenAIProvider(api_key="")
121
+ assert p.is_available() is False
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Ollama Provider (initialization only)
126
+ # ---------------------------------------------------------------------------
127
+ from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
128
+
129
+
130
+ class TestOllamaProvider:
131
+ def test_name(self):
132
+ p = OllamaProvider()
133
+ assert p.name == "ollama"
134
+
135
+ def test_is_available_offline(self):
136
+ # Ollama is unlikely to be running in CI, so expect False
137
+ p = OllamaProvider(base_url="http://127.0.0.1:99999")
138
+ assert p.is_available() is False
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Safety Validator
143
+ # ---------------------------------------------------------------------------
144
+ from semantic_code_intelligence.llm.safety import SafetyIssue, SafetyReport, SafetyValidator
145
+
146
+
147
+ class TestSafetyValidator:
148
+ def test_safe_code(self):
149
+ v = SafetyValidator()
150
+ report = v.validate("x = 1 + 2\nprint(x)")
151
+ assert report.safe is True
152
+ assert len(report.issues) == 0
153
+
154
+ def test_detects_eval(self):
155
+ v = SafetyValidator()
156
+ report = v.validate("result = eval(user_input)")
157
+ assert report.safe is False
158
+ assert any("eval" in i.description for i in report.issues)
159
+
160
+ def test_detects_exec(self):
161
+ v = SafetyValidator()
162
+ report = v.validate("exec(code)")
163
+ assert report.safe is False
164
+
165
+ def test_detects_os_system(self):
166
+ v = SafetyValidator()
167
+ report = v.validate('os.system("rm -rf /")')
168
+ assert report.safe is False
169
+
170
+ def test_detects_shell_true(self):
171
+ v = SafetyValidator()
172
+ report = v.validate('subprocess.run(cmd, shell=True)')
173
+ assert report.safe is False
174
+
175
+ def test_detects_sql_drop(self):
176
+ v = SafetyValidator()
177
+ report = v.validate("DROP TABLE users;")
178
+ assert report.safe is False
179
+
180
+ def test_is_safe_shortcut(self):
181
+ v = SafetyValidator()
182
+ assert v.is_safe("x = 1") is True
183
+ assert v.is_safe("eval('code')") is False
184
+
185
+ def test_custom_patterns(self):
186
+ v = SafetyValidator(extra_patterns=[(r"DANGER", "custom danger pattern")])
187
+ report = v.validate("DANGER zone")
188
+ assert report.safe is False
189
+ assert report.issues[0].description == "custom danger pattern"
190
+
191
+ def test_report_to_dict(self):
192
+ report = SafetyReport(safe=False, issues=[
193
+ SafetyIssue(pattern="test", description="desc", line_number=5),
194
+ ])
195
+ d = report.to_dict()
196
+ assert d["safe"] is False
197
+ assert d["issue_count"] == 1
198
+ assert d["issues"][0]["line_number"] == 5
199
+
200
+ def test_line_numbers(self):
201
+ v = SafetyValidator()
202
+ code = "x = 1\ny = 2\nresult = eval(z)"
203
+ report = v.validate(code)
204
+ assert report.issues[0].line_number == 3
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Context Memory
209
+ # ---------------------------------------------------------------------------
210
+ from semantic_code_intelligence.context.memory import (
211
+ MemoryEntry,
212
+ ReasoningStep,
213
+ SessionMemory,
214
+ WorkspaceMemory,
215
+ )
216
+
217
+
218
+ class TestMemoryEntry:
219
+ def test_to_dict_roundtrip(self):
220
+ entry = MemoryEntry(key="q1", content="answer", kind="qa", metadata={"score": 0.9})
221
+ d = entry.to_dict()
222
+ restored = MemoryEntry.from_dict(d)
223
+ assert restored.key == "q1"
224
+ assert restored.content == "answer"
225
+ assert restored.kind == "qa"
226
+
227
+ def test_defaults(self):
228
+ entry = MemoryEntry(key="k", content="c")
229
+ assert entry.kind == "general"
230
+ assert entry.timestamp > 0
231
+
232
+
233
+ class TestReasoningStep:
234
+ def test_to_dict(self):
235
+ step = ReasoningStep(step_id=1, action="search", input_text="query", output_text="results")
236
+ d = step.to_dict()
237
+ assert d["step_id"] == 1
238
+ assert d["action"] == "search"
239
+
240
+
241
+ class TestSessionMemory:
242
+ def test_add_and_search(self):
243
+ mem = SessionMemory()
244
+ mem.add("q1", "How does auth work?", kind="qa")
245
+ results = mem.search("auth")
246
+ assert len(results) == 1
247
+ assert results[0].key == "q1"
248
+
249
+ def test_get_recent(self):
250
+ mem = SessionMemory()
251
+ for i in range(15):
252
+ mem.add(f"key{i}", f"content{i}")
253
+ recent = mem.get_recent(5)
254
+ assert len(recent) == 5
255
+ assert recent[-1].key == "key14"
256
+
257
+ def test_max_entries(self):
258
+ mem = SessionMemory(max_entries=5)
259
+ for i in range(10):
260
+ mem.add(f"k{i}", f"c{i}")
261
+ assert len(mem.entries) == 5
262
+
263
+ def test_clear(self):
264
+ mem = SessionMemory()
265
+ mem.add("k", "v")
266
+ mem.start_chain("chain1")
267
+ mem.clear()
268
+ assert len(mem.entries) == 0
269
+
270
+ def test_reasoning_chain(self):
271
+ mem = SessionMemory()
272
+ mem.start_chain("c1")
273
+ mem.add_step("c1", "search", "query", "results")
274
+ mem.add_step("c1", "analyze", "results", "insights")
275
+ chain = mem.get_chain("c1")
276
+ assert len(chain) == 2
277
+ assert chain[0].step_id == 1
278
+ assert chain[1].action == "analyze"
279
+
280
+ def test_to_dict(self):
281
+ mem = SessionMemory()
282
+ mem.add("k", "v")
283
+ mem.start_chain("c1")
284
+ mem.add_step("c1", "search", "q", "r")
285
+ d = mem.to_dict()
286
+ assert "entries" in d
287
+ assert "chains" in d
288
+ assert len(d["chains"]["c1"]) == 1
289
+
290
+
291
+ class TestWorkspaceMemory:
292
+ def test_add_and_get(self, tmp_path):
293
+ # Set up a .codexa dir structure
294
+ codex_dir = tmp_path / ".codexa"
295
+ codex_dir.mkdir()
296
+ with patch(
297
+ "semantic_code_intelligence.config.settings.AppConfig.config_dir",
298
+ return_value=codex_dir,
299
+ ):
300
+ mem = WorkspaceMemory(tmp_path)
301
+ mem.add("test_key", "test_value", kind="insight")
302
+ entry = mem.get("test_key")
303
+ assert entry is not None
304
+ assert entry.content == "test_value"
305
+
306
+ def test_persistence(self, tmp_path):
307
+ codex_dir = tmp_path / ".codexa"
308
+ codex_dir.mkdir()
309
+ with patch(
310
+ "semantic_code_intelligence.config.settings.AppConfig.config_dir",
311
+ return_value=codex_dir,
312
+ ):
313
+ mem1 = WorkspaceMemory(tmp_path)
314
+ mem1.add("k1", "v1")
315
+
316
+ # Create a new instance — should load from disk
317
+ mem2 = WorkspaceMemory(tmp_path)
318
+ entry = mem2.get("k1")
319
+ assert entry is not None
320
+ assert entry.content == "v1"
321
+
322
+ def test_remove(self, tmp_path):
323
+ codex_dir = tmp_path / ".codexa"
324
+ codex_dir.mkdir()
325
+ with patch(
326
+ "semantic_code_intelligence.config.settings.AppConfig.config_dir",
327
+ return_value=codex_dir,
328
+ ):
329
+ mem = WorkspaceMemory(tmp_path)
330
+ mem.add("k", "v")
331
+ assert mem.remove("k") is True
332
+ assert mem.get("k") is None
333
+
334
+ def test_search(self, tmp_path):
335
+ codex_dir = tmp_path / ".codexa"
336
+ codex_dir.mkdir()
337
+ with patch(
338
+ "semantic_code_intelligence.config.settings.AppConfig.config_dir",
339
+ return_value=codex_dir,
340
+ ):
341
+ mem = WorkspaceMemory(tmp_path)
342
+ mem.add("auth", "JWT token validation")
343
+ mem.add("db", "Database connection pooling")
344
+ results = mem.search("JWT")
345
+ assert len(results) == 1
346
+
347
+ def test_clear(self, tmp_path):
348
+ codex_dir = tmp_path / ".codexa"
349
+ codex_dir.mkdir()
350
+ with patch(
351
+ "semantic_code_intelligence.config.settings.AppConfig.config_dir",
352
+ return_value=codex_dir,
353
+ ):
354
+ mem = WorkspaceMemory(tmp_path)
355
+ mem.add("k", "v")
356
+ mem.clear()
357
+ assert len(mem.entries) == 0
358
+
359
+
360
+ # ---------------------------------------------------------------------------
361
+ # Reasoning Engine
362
+ # ---------------------------------------------------------------------------
363
+ from semantic_code_intelligence.llm.reasoning import (
364
+ AskResult,
365
+ RefactorResult,
366
+ ReasoningEngine,
367
+ ReviewResult,
368
+ SuggestResult,
369
+ )
370
+
371
+
372
+ class TestAskResult:
373
+ def test_to_dict(self):
374
+ r = AskResult(question="q", answer="a", context_snippets=[{"file": "x.py"}])
375
+ d = r.to_dict()
376
+ assert d["question"] == "q"
377
+ assert d["answer"] == "a"
378
+ assert len(d["context_snippets"]) == 1
379
+
380
+
381
+ class TestReviewResult:
382
+ def test_to_dict(self):
383
+ r = ReviewResult(file_path="x.py", summary="looks good", issues=[])
384
+ d = r.to_dict()
385
+ assert d["file_path"] == "x.py"
386
+
387
+
388
+ class TestRefactorResult:
389
+ def test_to_dict(self):
390
+ r = RefactorResult(file_path="x.py", explanation="improved")
391
+ d = r.to_dict()
392
+ assert d["explanation"] == "improved"
393
+
394
+
395
+ class TestSuggestResult:
396
+ def test_to_dict(self):
397
+ r = SuggestResult(target="func", suggestions=[{"title": "s1"}])
398
+ d = r.to_dict()
399
+ assert d["target"] == "func"
400
+ assert len(d["suggestions"]) == 1
401
+
402
+
403
+ class TestReasoningEngine:
404
+ def _make_engine(self, tmp_path, mock_response="mock answer"):
405
+ """Helper: create an engine with a mock provider and a dummy project."""
406
+ provider = MockProvider(default_response=mock_response)
407
+ # Create a minimal project structure
408
+ codex_dir = tmp_path / ".codexa"
409
+ codex_dir.mkdir(parents=True)
410
+ index_dir = codex_dir / "index"
411
+ index_dir.mkdir()
412
+ # Minimal config
413
+ config = {
414
+ "project_root": str(tmp_path),
415
+ "embedding": {},
416
+ "search": {},
417
+ "index": {"ignore_dirs": [], "extensions": [".py"]},
418
+ "llm": {"provider": "mock"},
419
+ }
420
+ (codex_dir / "config.json").write_text(json.dumps(config))
421
+
422
+ # Create a sample source file
423
+ src = tmp_path / "sample.py"
424
+ src.write_text("def hello():\n return 'world'\n")
425
+
426
+ engine = ReasoningEngine(provider, tmp_path)
427
+ return engine, provider
428
+
429
+ def test_ask(self, tmp_path):
430
+ engine, provider = self._make_engine(tmp_path, "The answer is 42.")
431
+ result = engine.ask("What is the meaning?")
432
+ assert result.answer == "The answer is 42."
433
+ assert result.question == "What is the meaning?"
434
+
435
+ def test_review(self, tmp_path):
436
+ engine, provider = self._make_engine(
437
+ tmp_path,
438
+ json.dumps({"issues": [{"severity": "warning", "line": 1, "message": "Missing docstring"}], "summary": "Needs docs"}),
439
+ )
440
+ src = tmp_path / "sample.py"
441
+ result = engine.review(str(src))
442
+ assert result.file_path == str(src)
443
+ assert len(result.issues) == 1
444
+ assert result.summary == "Needs docs"
445
+
446
+ def test_review_file_not_found(self, tmp_path):
447
+ engine, _ = self._make_engine(tmp_path)
448
+ result = engine.review(str(tmp_path / "nonexistent.py"))
449
+ assert "not found" in result.summary.lower() or "empty" in result.summary.lower()
450
+
451
+ def test_refactor(self, tmp_path):
452
+ engine, provider = self._make_engine(
453
+ tmp_path,
454
+ json.dumps({"refactored_code": "def hello():\n '''Say hello.'''\n return 'world'\n", "explanation": "Added docstring"}),
455
+ )
456
+ src = tmp_path / "sample.py"
457
+ result = engine.refactor(str(src), "Add docstrings")
458
+ assert result.refactored_code != ""
459
+ assert "docstring" in result.explanation.lower()
460
+
461
+ def test_suggest(self, tmp_path):
462
+ engine, provider = self._make_engine(
463
+ tmp_path,
464
+ json.dumps({"suggestions": [{"title": "Add type hints", "description": "Use type annotations", "reason": "Better IDE support", "priority": "medium"}]}),
465
+ )
466
+ result = engine.suggest("hello")
467
+ assert len(result.suggestions) == 1
468
+ assert result.suggestions[0]["title"] == "Add type hints"
469
+
470
+ def test_suggest_raw_fallback(self, tmp_path):
471
+ engine, _ = self._make_engine(tmp_path, "Just some plain text")
472
+ result = engine.suggest("hello")
473
+ assert len(result.suggestions) == 1
474
+ assert result.suggestions[0]["title"] == "Raw response"
475
+
476
+
477
+ # ---------------------------------------------------------------------------
478
+ # LLMConfig in settings
479
+ # ---------------------------------------------------------------------------
480
+ from semantic_code_intelligence.config.settings import AppConfig, LLMConfig, load_config
481
+
482
+
483
+ class TestLLMConfig:
484
+ def test_defaults(self):
485
+ cfg = LLMConfig()
486
+ assert cfg.provider == "mock"
487
+ assert cfg.model == "gpt-3.5-turbo"
488
+ assert cfg.temperature == 0.2
489
+ assert cfg.max_tokens == 2048
490
+
491
+ def test_app_config_has_llm(self):
492
+ app = AppConfig()
493
+ assert isinstance(app.llm, LLMConfig)
494
+ assert app.llm.provider == "mock"
495
+
496
+ def test_serialisation_roundtrip(self, tmp_path):
497
+ from semantic_code_intelligence.config.settings import save_config
498
+
499
+ app = AppConfig(project_root=str(tmp_path))
500
+ app.llm.provider = "openai"
501
+ app.llm.model = "gpt-4"
502
+
503
+ codex_dir = tmp_path / ".codexa"
504
+ codex_dir.mkdir()
505
+ save_config(app, tmp_path)
506
+
507
+ loaded = load_config(tmp_path)
508
+ assert loaded.llm.provider == "openai"
509
+ assert loaded.llm.model == "gpt-4"
510
+
511
+
512
+ # ---------------------------------------------------------------------------
513
+ # Plugin AI hooks
514
+ # ---------------------------------------------------------------------------
515
+ from semantic_code_intelligence.plugins import PluginHook
516
+
517
+
518
+ class TestPluginAIHooks:
519
+ def test_pre_ai_hook_exists(self):
520
+ assert PluginHook.PRE_AI.value == "pre_ai"
521
+
522
+ def test_post_ai_hook_exists(self):
523
+ assert PluginHook.POST_AI.value == "post_ai"
524
+
525
+ def test_all_hooks_count(self):
526
+ # Expect 11 hooks: 3 indexing + 2 search + 2 analysis + 2 AI + 1 file + 1 custom
527
+ assert len(PluginHook) == 22
528
+
529
+
530
+ # ---------------------------------------------------------------------------
531
+ # CLI commands (smoke tests via Click testing)
532
+ # ---------------------------------------------------------------------------
533
+ from click.testing import CliRunner
534
+
535
+ from semantic_code_intelligence.cli.main import cli
536
+
537
+
538
+ class TestCLICommands:
539
+ """Smoke tests to verify the 4 new commands are registered and invocable."""
540
+
541
+ def test_ask_help(self):
542
+ runner = CliRunner()
543
+ result = runner.invoke(cli, ["ask", "--help"])
544
+ assert result.exit_code == 0
545
+ assert "Ask a natural-language question" in result.output
546
+
547
+ def test_review_help(self):
548
+ runner = CliRunner()
549
+ result = runner.invoke(cli, ["review", "--help"])
550
+ assert result.exit_code == 0
551
+ assert "Review a source file" in result.output
552
+
553
+ def test_refactor_help(self):
554
+ runner = CliRunner()
555
+ result = runner.invoke(cli, ["refactor", "--help"])
556
+ assert result.exit_code == 0
557
+ assert "Suggest refactored code" in result.output
558
+
559
+ def test_suggest_help(self):
560
+ runner = CliRunner()
561
+ result = runner.invoke(cli, ["suggest", "--help"])
562
+ assert result.exit_code == 0
563
+ assert "intelligent suggestions" in result.output.lower() or "suggestions" in result.output.lower()
564
+
565
+ def test_total_commands(self):
566
+ """Ensure 11 commands are registered (7 original + 4 new)."""
567
+ runner = CliRunner()
568
+ result = runner.invoke(cli, ["--help"])
569
+ assert result.exit_code == 0
570
+ # Count command names listed in help
571
+ commands = [
572
+ "init", "index", "search", "explain", "summary", "watch", "deps",
573
+ "ask", "review", "refactor", "suggest",
574
+ ]
575
+ for cmd in commands:
576
+ assert cmd in result.output, f"Command '{cmd}' not found in help output"
577
+
578
+
579
+ # ---------------------------------------------------------------------------
580
+ # Router test (updated count)
581
+ # ---------------------------------------------------------------------------
582
+ from semantic_code_intelligence.cli.router import register_commands
583
+
584
+
585
+ class TestRouterPhase8:
586
+ def test_register_commands_count(self):
587
+ """Router should register exactly 17 commands."""
588
+ import click
589
+
590
+ group = click.Group()
591
+ register_commands(group)
592
+ assert len(group.commands) == 39