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,792 @@
1
+ """Tests for Phase 16 — Advanced AI Workflows.
2
+
3
+ Covers: conversation sessions, session store, investigation chains,
4
+ cross-repo refactoring, streaming LLM, CLI commands, router, version, docs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ from pathlib import Path
12
+ from unittest.mock import patch
13
+
14
+ import pytest
15
+ from click.testing import CliRunner
16
+
17
+ from semantic_code_intelligence.llm.provider import LLMMessage, LLMResponse, MessageRole
18
+
19
+
20
+ # =========================================================================
21
+ # Conversation session tests
22
+ # =========================================================================
23
+
24
+
25
+ class TestConversationSession:
26
+ """Tests for ConversationSession data model."""
27
+
28
+ def test_create_session(self):
29
+ from semantic_code_intelligence.llm.conversation import ConversationSession
30
+
31
+ session = ConversationSession()
32
+ assert len(session.session_id) == 12
33
+ assert session.messages == []
34
+ assert session.turn_count == 0
35
+
36
+ def test_add_messages(self):
37
+ from semantic_code_intelligence.llm.conversation import ConversationSession
38
+
39
+ session = ConversationSession()
40
+ session.add_system("You are a helper.")
41
+ session.add_user("Hello")
42
+ session.add_assistant("Hi!")
43
+
44
+ assert len(session.messages) == 3
45
+ assert session.messages[0].role == MessageRole.SYSTEM
46
+ assert session.messages[1].role == MessageRole.USER
47
+ assert session.messages[2].role == MessageRole.ASSISTANT
48
+ assert session.turn_count == 2 # user + assistant
49
+
50
+ def test_last_message(self):
51
+ from semantic_code_intelligence.llm.conversation import ConversationSession
52
+
53
+ session = ConversationSession()
54
+ assert session.last_message is None
55
+ session.add_user("Hello")
56
+ assert session.last_message.content == "Hello"
57
+
58
+ def test_get_messages_for_llm_no_limit(self):
59
+ from semantic_code_intelligence.llm.conversation import ConversationSession
60
+
61
+ session = ConversationSession()
62
+ session.add_system("sys")
63
+ session.add_user("Q1")
64
+ session.add_assistant("A1")
65
+ session.add_user("Q2")
66
+
67
+ msgs = session.get_messages_for_llm()
68
+ assert len(msgs) == 4
69
+ assert msgs[0].role == MessageRole.SYSTEM
70
+
71
+ def test_get_messages_for_llm_with_limit(self):
72
+ from semantic_code_intelligence.llm.conversation import ConversationSession
73
+
74
+ session = ConversationSession()
75
+ session.add_system("sys")
76
+ for i in range(10):
77
+ session.add_user(f"Q{i}")
78
+ session.add_assistant(f"A{i}")
79
+
80
+ msgs = session.get_messages_for_llm(max_turns=2)
81
+ # 1 system + 4 recent messages (2 turns × 2)
82
+ assert len(msgs) == 5
83
+ assert msgs[0].role == MessageRole.SYSTEM
84
+
85
+ def test_serialization_roundtrip(self):
86
+ from semantic_code_intelligence.llm.conversation import ConversationSession
87
+
88
+ session = ConversationSession(title="Test")
89
+ session.add_system("sys")
90
+ session.add_user("Hello")
91
+ session.add_assistant("Hi!")
92
+
93
+ data = session.to_dict()
94
+ restored = ConversationSession.from_dict(data)
95
+
96
+ assert restored.session_id == session.session_id
97
+ assert restored.title == "Test"
98
+ assert len(restored.messages) == 3
99
+ assert restored.messages[1].content == "Hello"
100
+
101
+ def test_title_setting(self):
102
+ from semantic_code_intelligence.llm.conversation import ConversationSession
103
+
104
+ session = ConversationSession(title="My Chat")
105
+ assert session.title == "My Chat"
106
+
107
+
108
+ # =========================================================================
109
+ # Session store tests
110
+ # =========================================================================
111
+
112
+
113
+ class TestSessionStore:
114
+ """Tests for persistent SessionStore."""
115
+
116
+ def test_create_store(self, tmp_path):
117
+ from semantic_code_intelligence.llm.conversation import SessionStore
118
+
119
+ store = SessionStore(tmp_path)
120
+ assert (tmp_path / ".codexa" / "sessions").is_dir()
121
+
122
+ def test_save_and_load(self, tmp_path):
123
+ from semantic_code_intelligence.llm.conversation import (
124
+ ConversationSession,
125
+ SessionStore,
126
+ )
127
+
128
+ store = SessionStore(tmp_path)
129
+ session = ConversationSession(title="Test Chat")
130
+ session.add_user("Hello")
131
+ session.add_assistant("Hi!")
132
+
133
+ store.save(session)
134
+ loaded = store.load(session.session_id)
135
+
136
+ assert loaded is not None
137
+ assert loaded.session_id == session.session_id
138
+ assert loaded.title == "Test Chat"
139
+ assert len(loaded.messages) == 2
140
+
141
+ def test_load_nonexistent(self, tmp_path):
142
+ from semantic_code_intelligence.llm.conversation import SessionStore
143
+
144
+ store = SessionStore(tmp_path)
145
+ assert store.load("nonexistent") is None
146
+
147
+ def test_list_sessions(self, tmp_path):
148
+ from semantic_code_intelligence.llm.conversation import (
149
+ ConversationSession,
150
+ SessionStore,
151
+ )
152
+
153
+ store = SessionStore(tmp_path)
154
+ s1 = ConversationSession(title="Chat 1")
155
+ s1.add_user("Hello")
156
+ s2 = ConversationSession(title="Chat 2")
157
+ s2.add_user("Hey")
158
+
159
+ store.save(s1)
160
+ store.save(s2)
161
+
162
+ sessions = store.list_sessions()
163
+ assert len(sessions) == 2
164
+ ids = {s["session_id"] for s in sessions}
165
+ assert s1.session_id in ids
166
+ assert s2.session_id in ids
167
+
168
+ def test_delete_session(self, tmp_path):
169
+ from semantic_code_intelligence.llm.conversation import (
170
+ ConversationSession,
171
+ SessionStore,
172
+ )
173
+
174
+ store = SessionStore(tmp_path)
175
+ session = ConversationSession()
176
+ store.save(session)
177
+ assert store.delete(session.session_id) is True
178
+ assert store.load(session.session_id) is None
179
+ assert store.delete(session.session_id) is False
180
+
181
+ def test_get_or_create_new(self, tmp_path):
182
+ from semantic_code_intelligence.llm.conversation import SessionStore
183
+
184
+ store = SessionStore(tmp_path)
185
+ session = store.get_or_create()
186
+ assert session is not None
187
+ assert len(session.messages) == 0
188
+
189
+ def test_get_or_create_existing(self, tmp_path):
190
+ from semantic_code_intelligence.llm.conversation import (
191
+ ConversationSession,
192
+ SessionStore,
193
+ )
194
+
195
+ store = SessionStore(tmp_path)
196
+ original = ConversationSession(title="Existing")
197
+ original.add_user("Test")
198
+ store.save(original)
199
+
200
+ resumed = store.get_or_create(original.session_id)
201
+ assert resumed.title == "Existing"
202
+ assert len(resumed.messages) == 1
203
+
204
+ def test_path_traversal_prevention(self, tmp_path):
205
+ from semantic_code_intelligence.llm.conversation import SessionStore
206
+
207
+ store = SessionStore(tmp_path)
208
+ # Attempt path traversal — should be sanitised
209
+ path = store._session_path("../../../etc/passwd")
210
+ assert ".." not in str(path.name)
211
+ assert "passwd" in str(path.name)
212
+
213
+
214
+ # =========================================================================
215
+ # Investigation chain tests
216
+ # =========================================================================
217
+
218
+
219
+ class TestInvestigationChain:
220
+ """Tests for autonomous investigation chains."""
221
+
222
+ def test_simple_conclude(self, tmp_path):
223
+ from semantic_code_intelligence.llm.investigation import InvestigationChain
224
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
225
+
226
+ provider = MockProvider()
227
+ provider.enqueue_response(json.dumps({
228
+ "thought": "I can answer directly.",
229
+ "action": "conclude",
230
+ "action_input": "The answer is 42.",
231
+ }))
232
+
233
+ chain = InvestigationChain(provider, tmp_path, max_steps=3)
234
+ result = chain.investigate("What is the answer?")
235
+
236
+ assert result.conclusion == "The answer is 42."
237
+ assert result.total_steps == 1
238
+ assert result.chain_id != ""
239
+
240
+ def test_search_then_conclude(self, tmp_path):
241
+ from semantic_code_intelligence.llm.investigation import InvestigationChain
242
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
243
+
244
+ provider = MockProvider()
245
+ # Step 1: search
246
+ provider.enqueue_response(json.dumps({
247
+ "thought": "Need to search for context.",
248
+ "action": "search",
249
+ "action_input": "authentication logic",
250
+ }))
251
+ # Step 2: conclude
252
+ provider.enqueue_response(json.dumps({
253
+ "thought": "Found it.",
254
+ "action": "conclude",
255
+ "action_input": "Auth logic is in auth.py",
256
+ }))
257
+
258
+ chain = InvestigationChain(provider, tmp_path, max_steps=5)
259
+ result = chain.investigate("Where is authentication?")
260
+
261
+ assert result.total_steps == 2
262
+ assert "auth.py" in result.conclusion
263
+
264
+ def test_max_steps_forces_conclusion(self, tmp_path):
265
+ from semantic_code_intelligence.llm.investigation import InvestigationChain
266
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
267
+
268
+ provider = MockProvider()
269
+ # Always want to search — never conclude
270
+ for _ in range(5):
271
+ provider.enqueue_response(json.dumps({
272
+ "thought": "Keep searching.",
273
+ "action": "search",
274
+ "action_input": "something",
275
+ }))
276
+ # Forced conclusion response
277
+ provider.enqueue_response("Forced final answer.")
278
+
279
+ chain = InvestigationChain(provider, tmp_path, max_steps=3)
280
+ result = chain.investigate("Tell me everything")
281
+
282
+ # Should have 3 search steps, then forced conclusion
283
+ assert result.total_steps == 3
284
+
285
+ def test_result_to_dict(self, tmp_path):
286
+ from semantic_code_intelligence.llm.investigation import InvestigationResult
287
+
288
+ result = InvestigationResult(
289
+ question="Why?",
290
+ conclusion="Because.",
291
+ chain_id="abc",
292
+ total_steps=2,
293
+ steps=[{"step": 1, "action": "search"}],
294
+ )
295
+ d = result.to_dict()
296
+ assert d["question"] == "Why?"
297
+ assert d["total_steps"] == 2
298
+
299
+ def test_parse_fallback(self, tmp_path):
300
+ from semantic_code_intelligence.llm.investigation import InvestigationChain
301
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
302
+
303
+ provider = MockProvider()
304
+ # Non-JSON response — should fallback to conclude
305
+ provider.enqueue_response("Just a plain text answer.")
306
+
307
+ chain = InvestigationChain(provider, tmp_path, max_steps=3)
308
+ result = chain.investigate("Question?")
309
+
310
+ assert result.conclusion == "Just a plain text answer."
311
+ assert result.total_steps == 1
312
+
313
+
314
+ # =========================================================================
315
+ # Cross-repo refactoring tests
316
+ # =========================================================================
317
+
318
+
319
+ class TestCrossRefactor:
320
+ """Tests for cross-repo refactoring analysis."""
321
+
322
+ def test_empty_workspace(self, tmp_path):
323
+ from semantic_code_intelligence.llm.cross_refactor import analyze_cross_repo
324
+
325
+ result = analyze_cross_repo(tmp_path)
326
+ assert result.repos_analyzed == []
327
+ assert result.total_symbols == 0
328
+ assert result.matches == []
329
+
330
+ def test_result_to_dict(self):
331
+ from semantic_code_intelligence.llm.cross_refactor import (
332
+ CrossRefactorResult,
333
+ CrossRepoMatch,
334
+ )
335
+
336
+ match = CrossRepoMatch(
337
+ repo_a="backend",
338
+ symbol_a="validate",
339
+ file_a="auth.py",
340
+ repo_b="frontend",
341
+ symbol_b="validate",
342
+ file_b="auth.ts",
343
+ similarity_note="Jaccard: 0.85",
344
+ )
345
+ result = CrossRefactorResult(
346
+ repos_analyzed=["backend", "frontend"],
347
+ total_symbols=10,
348
+ matches=[match],
349
+ )
350
+ d = result.to_dict()
351
+ assert d["repos_analyzed"] == ["backend", "frontend"]
352
+ assert len(d["matches"]) == 1
353
+ assert d["matches"][0]["symbol_a"] == "validate"
354
+
355
+ def test_cross_repo_match_to_dict(self):
356
+ from semantic_code_intelligence.llm.cross_refactor import CrossRepoMatch
357
+
358
+ m = CrossRepoMatch(
359
+ repo_a="a", symbol_a="foo", file_a="a.py",
360
+ repo_b="b", symbol_b="bar", file_b="b.py",
361
+ )
362
+ d = m.to_dict()
363
+ assert d["repo_a"] == "a"
364
+ assert d["file_b"] == "b.py"
365
+
366
+
367
+ class TestCrossRepoDuplicates:
368
+ """Tests for cross-repo duplicate detection internals."""
369
+
370
+ def test_find_cross_duplicates_same_repo_excluded(self):
371
+ from semantic_code_intelligence.llm.cross_refactor import _find_cross_duplicates
372
+
373
+ body = "def f(x):\n y = x + 1\n z = y * 2\n return z\n # pad\n"
374
+ repo_symbols = {
375
+ "repoA": [
376
+ {"name": "f1", "kind": "function", "file": "a.py", "lines": 5, "body": body},
377
+ {"name": "f2", "kind": "function", "file": "b.py", "lines": 5, "body": body},
378
+ ],
379
+ }
380
+ matches = _find_cross_duplicates(repo_symbols)
381
+ assert len(matches) == 0 # Same repo — excluded
382
+
383
+ def test_find_cross_duplicates_across_repos(self):
384
+ from semantic_code_intelligence.llm.cross_refactor import _find_cross_duplicates
385
+
386
+ body = "def f(x):\n y = x + 1\n z = y * 2\n return z\n # extra\n"
387
+ repo_symbols = {
388
+ "repoA": [{"name": "compute", "kind": "function", "file": "a.py", "lines": 5, "body": body}],
389
+ "repoB": [{"name": "calc", "kind": "function", "file": "b.py", "lines": 5, "body": body}],
390
+ }
391
+ matches = _find_cross_duplicates(repo_symbols, threshold=0.5)
392
+ assert len(matches) == 1
393
+ assert matches[0].repo_a != matches[0].repo_b
394
+
395
+
396
+ # =========================================================================
397
+ # Streaming LLM tests
398
+ # =========================================================================
399
+
400
+
401
+ class TestStreaming:
402
+ """Tests for streaming LLM support."""
403
+
404
+ def test_stream_mock(self):
405
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
406
+ from semantic_code_intelligence.llm.streaming import stream_chat, StreamEvent
407
+
408
+ provider = MockProvider(default_response="Hello world test")
409
+ messages = [LLMMessage(role=MessageRole.USER, content="Hi")]
410
+
411
+ events = list(stream_chat(provider, messages))
412
+ kinds = [e.kind for e in events]
413
+
414
+ assert "start" in kinds
415
+ assert "token" in kinds
416
+ assert "done" in kinds
417
+
418
+ # Accumulate tokens
419
+ text = "".join(e.content for e in events if e.kind == "token")
420
+ assert text == "Hello world test"
421
+
422
+ def test_stream_mock_with_custom_response(self):
423
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
424
+ from semantic_code_intelligence.llm.streaming import stream_chat
425
+
426
+ provider = MockProvider()
427
+ provider.enqueue_response("Custom streaming response")
428
+ messages = [LLMMessage(role=MessageRole.USER, content="Test")]
429
+
430
+ events = list(stream_chat(provider, messages))
431
+ text = "".join(e.content for e in events if e.kind == "token")
432
+ assert text == "Custom streaming response"
433
+
434
+ def test_stream_event_to_dict(self):
435
+ from semantic_code_intelligence.llm.streaming import StreamEvent
436
+
437
+ event = StreamEvent(kind="token", content="hello", metadata={"pos": 1})
438
+ d = event.to_dict()
439
+ assert d["kind"] == "token"
440
+ assert d["content"] == "hello"
441
+
442
+ def test_stream_event_to_sse(self):
443
+ from semantic_code_intelligence.llm.streaming import StreamEvent
444
+
445
+ event = StreamEvent(kind="token", content="hi")
446
+ sse = event.to_sse()
447
+ assert sse.startswith("data: ")
448
+ assert sse.endswith("\n\n")
449
+ payload = json.loads(sse[len("data: "):])
450
+ assert payload["kind"] == "token"
451
+
452
+ def test_stream_with_plugin_manager(self):
453
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
454
+ from semantic_code_intelligence.llm.streaming import stream_chat
455
+ from semantic_code_intelligence.plugins import PluginManager
456
+
457
+ provider = MockProvider(default_response="Token test")
458
+ pm = PluginManager()
459
+ messages = [LLMMessage(role=MessageRole.USER, content="Test")]
460
+
461
+ events = list(stream_chat(provider, messages, plugin_manager=pm))
462
+ assert any(e.kind == "token" for e in events)
463
+
464
+ def test_fallback_non_standard_provider(self):
465
+ """Test that a non-standard provider falls back to single-token emit."""
466
+ from semantic_code_intelligence.llm.streaming import stream_chat
467
+
468
+ class CustomProvider:
469
+ name = "custom"
470
+
471
+ def chat(self, messages, **kwargs):
472
+ return LLMResponse(content="Custom response", model="custom", provider="custom")
473
+
474
+ provider = CustomProvider()
475
+ messages = [LLMMessage(role=MessageRole.USER, content="Test")]
476
+
477
+ events = list(stream_chat(provider, messages))
478
+ kinds = [e.kind for e in events]
479
+ assert "start" in kinds
480
+ assert "token" in kinds
481
+ assert "done" in kinds
482
+ assert events[1].content == "Custom response"
483
+
484
+
485
+ # =========================================================================
486
+ # CLI command tests
487
+ # =========================================================================
488
+
489
+
490
+ class TestChatCLI:
491
+ """Tests for the `codexa chat` command."""
492
+
493
+ @pytest.fixture
494
+ def runner(self):
495
+ return CliRunner()
496
+
497
+ def _extract_json(self, output: str) -> dict:
498
+ """Extract JSON object from CLI output, skipping any log noise."""
499
+ # Find the first '{' and parse from there
500
+ start = output.index("{")
501
+ return json.loads(output[start:])
502
+
503
+ def test_help(self, runner):
504
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
505
+
506
+ result = runner.invoke(chat_cmd, ["--help"])
507
+ assert result.exit_code == 0
508
+ assert "chat" in result.output.lower() or "conversation" in result.output.lower()
509
+
510
+ def test_has_session_option(self, runner):
511
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
512
+
513
+ result = runner.invoke(chat_cmd, ["--help"])
514
+ assert "--session" in result.output
515
+
516
+ def test_has_list_sessions(self, runner):
517
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
518
+
519
+ result = runner.invoke(chat_cmd, ["--help"])
520
+ assert "--list-sessions" in result.output
521
+
522
+ def test_has_max_turns(self, runner):
523
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
524
+
525
+ result = runner.invoke(chat_cmd, ["--help"])
526
+ assert "--max-turns" in result.output
527
+
528
+ def test_json_output(self, runner, tmp_path):
529
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
530
+
531
+ result = runner.invoke(chat_cmd, [
532
+ "Hello", "--json", "--path", str(tmp_path)
533
+ ], obj={"pipe": False})
534
+ assert result.exit_code == 0
535
+ data = self._extract_json(result.output)
536
+ assert "session_id" in data
537
+ assert "answer" in data
538
+
539
+ def test_pipe_mode(self, runner, tmp_path):
540
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
541
+
542
+ result = runner.invoke(chat_cmd, [
543
+ "Hello", "--pipe", "--path", str(tmp_path)
544
+ ], obj={"pipe": False})
545
+ assert result.exit_code == 0
546
+ assert len(result.output.strip()) > 0
547
+
548
+ def test_list_sessions_json(self, runner, tmp_path):
549
+ from semantic_code_intelligence.cli.commands.chat_cmd import chat_cmd
550
+
551
+ result = runner.invoke(chat_cmd, [
552
+ "x", "--list-sessions", "--json", "--path", str(tmp_path)
553
+ ], obj={"pipe": False})
554
+ assert result.exit_code == 0
555
+ data = json.loads(result.output)
556
+ assert isinstance(data, list)
557
+
558
+
559
+ class TestInvestigateCLI:
560
+ """Tests for the `codexa investigate` command."""
561
+
562
+ @pytest.fixture
563
+ def runner(self):
564
+ return CliRunner()
565
+
566
+ def test_help(self, runner):
567
+ from semantic_code_intelligence.cli.commands.investigate_cmd import investigate_cmd
568
+
569
+ result = runner.invoke(investigate_cmd, ["--help"])
570
+ assert result.exit_code == 0
571
+ assert "investigate" in result.output.lower() or "investigation" in result.output.lower()
572
+
573
+ def test_has_max_steps(self, runner):
574
+ from semantic_code_intelligence.cli.commands.investigate_cmd import investigate_cmd
575
+
576
+ result = runner.invoke(investigate_cmd, ["--help"])
577
+ assert "--max-steps" in result.output
578
+
579
+ def test_json_output(self, runner, tmp_path):
580
+ from semantic_code_intelligence.cli.commands.investigate_cmd import investigate_cmd
581
+
582
+ result = runner.invoke(investigate_cmd, [
583
+ "What is this?", "--json", "--path", str(tmp_path)
584
+ ], obj={"pipe": False})
585
+ assert result.exit_code == 0
586
+ data = json.loads(result.output)
587
+ assert "question" in data
588
+ assert "conclusion" in data
589
+ assert "steps" in data
590
+
591
+ def test_pipe_mode(self, runner, tmp_path):
592
+ from semantic_code_intelligence.cli.commands.investigate_cmd import investigate_cmd
593
+
594
+ result = runner.invoke(investigate_cmd, [
595
+ "What is this?", "--pipe", "--path", str(tmp_path)
596
+ ], obj={"pipe": False})
597
+ assert result.exit_code == 0
598
+ assert "Conclusion:" in result.output
599
+
600
+
601
+ class TestCrossRefactorCLI:
602
+ """Tests for the `codexa cross-refactor` command."""
603
+
604
+ @pytest.fixture
605
+ def runner(self):
606
+ return CliRunner()
607
+
608
+ def test_help(self, runner):
609
+ from semantic_code_intelligence.cli.commands.cross_refactor_cmd import cross_refactor_cmd
610
+
611
+ result = runner.invoke(cross_refactor_cmd, ["--help"])
612
+ assert result.exit_code == 0
613
+ assert "cross-refactor" in result.output.lower() or "refactor" in result.output.lower()
614
+
615
+ def test_has_threshold(self, runner):
616
+ from semantic_code_intelligence.cli.commands.cross_refactor_cmd import cross_refactor_cmd
617
+
618
+ result = runner.invoke(cross_refactor_cmd, ["--help"])
619
+ assert "--threshold" in result.output
620
+
621
+ def test_json_empty_workspace(self, runner, tmp_path):
622
+ from semantic_code_intelligence.cli.commands.cross_refactor_cmd import cross_refactor_cmd
623
+
624
+ result = runner.invoke(cross_refactor_cmd, [
625
+ "--json", "--path", str(tmp_path)
626
+ ], obj={"pipe": False})
627
+ assert result.exit_code == 0
628
+ data = json.loads(result.output)
629
+ assert data["repos_analyzed"] == []
630
+
631
+ def test_pipe_mode(self, runner, tmp_path):
632
+ from semantic_code_intelligence.cli.commands.cross_refactor_cmd import cross_refactor_cmd
633
+
634
+ result = runner.invoke(cross_refactor_cmd, [
635
+ "--pipe", "--path", str(tmp_path)
636
+ ], obj={"pipe": False})
637
+ assert result.exit_code == 0
638
+ assert "Repos:" in result.output
639
+
640
+
641
+ # =========================================================================
642
+ # Router, version, and module structure tests
643
+ # =========================================================================
644
+
645
+
646
+ class TestRouterPhase16:
647
+ """Tests for CLI router registration."""
648
+
649
+ def test_register_commands_count(self):
650
+ import click
651
+ from semantic_code_intelligence.cli.router import register_commands
652
+
653
+ group = click.Group("test")
654
+ register_commands(group)
655
+ assert len(group.commands) == 39
656
+
657
+ def test_chat_command_registered(self):
658
+ from semantic_code_intelligence.cli.main import cli
659
+
660
+ assert "chat" in cli.commands
661
+
662
+ def test_investigate_command_registered(self):
663
+ from semantic_code_intelligence.cli.main import cli
664
+
665
+ assert "investigate" in cli.commands
666
+
667
+ def test_cross_refactor_command_registered(self):
668
+ from semantic_code_intelligence.cli.main import cli
669
+
670
+ assert "cross-refactor" in cli.commands
671
+
672
+
673
+ class TestVersionBump:
674
+ """Test version is 0.19.0."""
675
+
676
+ def test_version_is_016(self):
677
+ from semantic_code_intelligence import __version__
678
+
679
+ assert __version__ == "0.4.0"
680
+
681
+
682
+ class TestPhase16ModuleStructure:
683
+ """Tests for module import structure."""
684
+
685
+ def test_import_conversation(self):
686
+ from semantic_code_intelligence.llm.conversation import (
687
+ ConversationSession,
688
+ SessionStore,
689
+ )
690
+
691
+ def test_import_investigation(self):
692
+ from semantic_code_intelligence.llm.investigation import (
693
+ InvestigationChain,
694
+ InvestigationResult,
695
+ )
696
+
697
+ def test_import_streaming(self):
698
+ from semantic_code_intelligence.llm.streaming import (
699
+ stream_chat,
700
+ StreamEvent,
701
+ )
702
+
703
+ def test_import_cross_refactor(self):
704
+ from semantic_code_intelligence.llm.cross_refactor import (
705
+ analyze_cross_repo,
706
+ CrossRefactorResult,
707
+ CrossRepoMatch,
708
+ )
709
+
710
+ def test_llm_package_exports(self):
711
+ from semantic_code_intelligence.llm import (
712
+ ConversationSession,
713
+ SessionStore,
714
+ InvestigationChain,
715
+ InvestigationResult,
716
+ stream_chat,
717
+ StreamEvent,
718
+ analyze_cross_repo,
719
+ CrossRefactorResult,
720
+ )
721
+
722
+
723
+ class TestDocsGenerator:
724
+ """Tests for AI workflows doc generation."""
725
+
726
+ def test_generate_ai_workflows_reference(self):
727
+ from semantic_code_intelligence.docs import generate_ai_workflows_reference
728
+
729
+ md = generate_ai_workflows_reference()
730
+ assert "AI Workflows" in md
731
+ assert "codexa chat" in md
732
+ assert "codexa investigate" in md
733
+ assert "codexa cross-refactor" in md
734
+ assert "stream_chat" in md
735
+ assert "ON_STREAM" in md
736
+
737
+ def test_generate_all_docs_includes_ai(self, tmp_path):
738
+ from semantic_code_intelligence.docs import generate_all_docs
739
+
740
+ generated = generate_all_docs(tmp_path)
741
+ assert "AI_WORKFLOWS.md" in generated
742
+
743
+
744
+ # =========================================================================
745
+ # Backward compatibility tests
746
+ # =========================================================================
747
+
748
+
749
+ class TestBackwardCompatibility:
750
+ """Ensure Phase 14 and Phase 15 modules still work."""
751
+
752
+ def test_ci_module_imports(self):
753
+ from semantic_code_intelligence.ci.quality import analyze_project, QualityReport
754
+ from semantic_code_intelligence.ci.pr import generate_pr_report, PRReport
755
+ from semantic_code_intelligence.ci.templates import get_template
756
+ from semantic_code_intelligence.ci.hooks import run_precommit_check
757
+
758
+ def test_web_module_imports(self):
759
+ from semantic_code_intelligence.web.api import APIHandler
760
+ from semantic_code_intelligence.web.ui import page_search
761
+ from semantic_code_intelligence.web.visualize import render_call_graph
762
+
763
+ def test_reasoning_engine_intact(self):
764
+ from semantic_code_intelligence.llm.reasoning import (
765
+ ReasoningEngine,
766
+ AskResult,
767
+ ReviewResult,
768
+ RefactorResult,
769
+ SuggestResult,
770
+ )
771
+
772
+ def test_plugin_hooks_intact(self):
773
+ from semantic_code_intelligence.plugins import PluginHook
774
+
775
+ assert PluginHook.ON_STREAM.value == "on_stream"
776
+ assert PluginHook.CUSTOM_VALIDATION.value == "custom_validation"
777
+
778
+ def test_safety_validator_intact(self):
779
+ from semantic_code_intelligence.llm.safety import SafetyValidator
780
+
781
+ v = SafetyValidator()
782
+ assert v.validate("x = 1\n").safe is True
783
+
784
+ def test_session_memory_intact(self):
785
+ from semantic_code_intelligence.context.memory import SessionMemory, ReasoningStep
786
+
787
+ mem = SessionMemory()
788
+ mem.start_chain("test")
789
+ mem.add_step("test", "search", "query", "results")
790
+ chain = mem.get_chain("test")
791
+ assert len(chain) == 1
792
+ assert chain[0].action == "search"