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,715 @@
1
+ """Phase 24 — Self-Improving Development Loop.
2
+
3
+ Tests verify:
4
+ 1. BudgetGuard — start, can_continue, record_tokens/iteration, stop_reason, summary
5
+ 2. TestRunner — parse_summary, TestResult
6
+ 3. CommitManager — git operations with mocked subprocess
7
+ 4. TaskSelector — priority selection, task builders
8
+ 5. ContextBuilder — system prompt, build sections, truncation, estimate_tokens
9
+ 6. PatchGenerator — diff extraction, diff parsing, safety limits
10
+ 7. EvolutionEngine — orchestrated loop with mocked components
11
+ 8. CLI command — evolve command exists, help text, options
12
+ 9. Module imports and version
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import textwrap
19
+ import time
20
+ from pathlib import Path
21
+ from unittest.mock import MagicMock, patch
22
+
23
+ import pytest
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Imports under test
27
+ # ---------------------------------------------------------------------------
28
+
29
+ from semantic_code_intelligence.evolution.budget_guard import BudgetGuard
30
+ from semantic_code_intelligence.evolution.test_runner import TestResult, TestRunner, _parse_summary
31
+ from semantic_code_intelligence.evolution.commit_manager import CommitManager
32
+ from semantic_code_intelligence.evolution.task_selector import (
33
+ TASK_ERROR_HANDLING,
34
+ TASK_FIX_TESTS,
35
+ TASK_REDUCE_DUPLICATION,
36
+ TASK_SMALL_OPTIMISATION,
37
+ TASK_TYPE_HINTS,
38
+ EvolutionTask,
39
+ TaskSelector,
40
+ )
41
+ from semantic_code_intelligence.evolution.context_builder import (
42
+ SYSTEM_PROMPT,
43
+ ContextBuilder,
44
+ )
45
+ from semantic_code_intelligence.evolution.patch_generator import (
46
+ PatchGenerator,
47
+ PatchResult,
48
+ _diff_files,
49
+ _diff_line_count,
50
+ _extract_diff,
51
+ )
52
+ from semantic_code_intelligence.evolution.engine import (
53
+ EvolutionEngine,
54
+ EvolutionResult,
55
+ IterationRecord,
56
+ )
57
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
58
+ from semantic_code_intelligence.llm.provider import LLMMessage, LLMResponse, MessageRole
59
+
60
+ _PROJECT_ROOT = Path(__file__).resolve().parents[2]
61
+ _SRC = _PROJECT_ROOT / "semantic_code_intelligence"
62
+
63
+
64
+ # ═══════════════════════════════════════════════════════════════════════════
65
+ # 1 — BudgetGuard
66
+ # ═══════════════════════════════════════════════════════════════════════════
67
+
68
+
69
+ class TestBudgetGuard:
70
+ """Tests for the BudgetGuard resource tracker."""
71
+
72
+ def test_defaults(self):
73
+ g = BudgetGuard()
74
+ assert g.max_tokens == 20_000
75
+ assert g.max_iterations == 5
76
+ assert g.max_seconds == 600.0
77
+ assert g.tokens_used == 0
78
+ assert g.iterations_done == 0
79
+
80
+ def test_can_continue_fresh(self):
81
+ g = BudgetGuard()
82
+ assert g.can_continue() is True
83
+
84
+ def test_record_tokens(self):
85
+ g = BudgetGuard(max_tokens=100)
86
+ g.record_tokens(40)
87
+ assert g.tokens_used == 40
88
+ assert g.tokens_remaining == 60
89
+
90
+ def test_record_iteration(self):
91
+ g = BudgetGuard(max_iterations=3)
92
+ g.record_iteration()
93
+ g.record_iteration()
94
+ assert g.iterations_done == 2
95
+ assert g.iterations_remaining == 1
96
+
97
+ def test_stop_on_token_limit(self):
98
+ g = BudgetGuard(max_tokens=50)
99
+ g.record_tokens(50)
100
+ assert g.can_continue() is False
101
+ assert "token" in g.stop_reason().lower()
102
+
103
+ def test_stop_on_iteration_limit(self):
104
+ g = BudgetGuard(max_iterations=2)
105
+ g.record_iteration()
106
+ g.record_iteration()
107
+ assert g.can_continue() is False
108
+ assert "iteration" in g.stop_reason().lower()
109
+
110
+ def test_stop_on_time_limit(self):
111
+ g = BudgetGuard(max_seconds=0.01)
112
+ g.start()
113
+ time.sleep(0.02)
114
+ assert g.can_continue() is False
115
+ assert "time" in g.stop_reason().lower()
116
+
117
+ def test_stop_reason_none_when_ok(self):
118
+ g = BudgetGuard()
119
+ assert g.stop_reason() is None
120
+
121
+ def test_elapsed_without_start(self):
122
+ g = BudgetGuard()
123
+ assert g.elapsed_seconds == 0.0
124
+
125
+ def test_summary(self):
126
+ g = BudgetGuard(max_tokens=1000, max_iterations=3, max_seconds=300)
127
+ g.start()
128
+ g.record_tokens(150)
129
+ g.record_iteration()
130
+ s = g.summary()
131
+ assert s["tokens_used"] == 150
132
+ assert s["tokens_max"] == 1000
133
+ assert s["iterations_done"] == 1
134
+ assert s["iterations_max"] == 3
135
+ assert isinstance(s["elapsed_seconds"], float)
136
+
137
+ def test_tokens_remaining_never_negative(self):
138
+ g = BudgetGuard(max_tokens=10)
139
+ g.record_tokens(100)
140
+ assert g.tokens_remaining == 0
141
+
142
+
143
+ # ═══════════════════════════════════════════════════════════════════════════
144
+ # 2 — TestRunner
145
+ # ═══════════════════════════════════════════════════════════════════════════
146
+
147
+
148
+ class TestTestResult:
149
+ """Tests for the TestResult dataclass."""
150
+
151
+ def test_summary_line_pass(self):
152
+ r = TestResult(passed=True, total=10, failures=0, errors=0)
153
+ line = r.summary_line()
154
+ assert "PASS" in line
155
+ assert "10 tests" in line
156
+
157
+ def test_summary_line_fail(self):
158
+ r = TestResult(passed=False, total=10, failures=2, errors=0)
159
+ line = r.summary_line()
160
+ assert "FAIL" in line
161
+ assert "2 failures" in line
162
+
163
+ def test_summary_line_errors(self):
164
+ r = TestResult(passed=False, total=10, failures=1, errors=2)
165
+ line = r.summary_line()
166
+ assert "2 errors" in line
167
+
168
+
169
+ class TestTestRunnerParsing:
170
+ """Tests for TestRunner._parse_summary regex parsing."""
171
+
172
+ def test_parse_all_passed(self):
173
+ total, failures, errors = _parse_summary("10 passed in 2.34s")
174
+ assert total == 10
175
+ assert failures == 0
176
+ assert errors == 0
177
+
178
+ def test_parse_mixed(self):
179
+ total, failures, errors = _parse_summary("8 passed, 2 failed in 5.67s")
180
+ assert total == 10
181
+ assert failures == 2
182
+ assert errors == 0
183
+
184
+ def test_parse_errors(self):
185
+ total, failures, errors = _parse_summary("5 passed, 1 failed, 2 errors in 3.0s")
186
+ assert total == 8
187
+ assert failures == 1
188
+ assert errors == 2
189
+
190
+ def test_parse_empty(self):
191
+ total, failures, errors = _parse_summary("")
192
+ assert total == 0
193
+ assert failures == 0
194
+
195
+
196
+ # ═══════════════════════════════════════════════════════════════════════════
197
+ # 3 — CommitManager
198
+ # ═══════════════════════════════════════════════════════════════════════════
199
+
200
+
201
+ class TestCommitManager:
202
+ """Tests for CommitManager git operations (mocked subprocess)."""
203
+
204
+ def test_has_changes_true(self):
205
+ cm = CommitManager(project_root=_PROJECT_ROOT)
206
+ with patch("subprocess.run") as mock_run:
207
+ mock_run.return_value = MagicMock(
208
+ stdout="M foo.py\n", returncode=0
209
+ )
210
+ assert cm.has_changes() is True
211
+
212
+ def test_has_changes_false(self):
213
+ cm = CommitManager(project_root=_PROJECT_ROOT)
214
+ with patch("subprocess.run") as mock_run:
215
+ mock_run.return_value = MagicMock(stdout="", returncode=0)
216
+ assert cm.has_changes() is False
217
+
218
+ def test_git_diff(self):
219
+ cm = CommitManager(project_root=_PROJECT_ROOT)
220
+ with patch("subprocess.run") as mock_run:
221
+ mock_run.return_value = MagicMock(
222
+ stdout="+added line\n-removed line\n", returncode=0
223
+ )
224
+ diff = cm.git_diff()
225
+ assert "+added line" in diff
226
+
227
+ def test_commit_returns_sha(self):
228
+ cm = CommitManager(project_root=_PROJECT_ROOT)
229
+ with patch("subprocess.run") as mock_run:
230
+ mock_run.return_value = MagicMock(
231
+ stdout="abc1234\n", returncode=0
232
+ )
233
+ sha = cm.commit("test message")
234
+ assert sha == "abc1234"
235
+
236
+ def test_stage_files(self):
237
+ cm = CommitManager(project_root=_PROJECT_ROOT)
238
+ with patch("subprocess.run") as mock_run:
239
+ mock_run.return_value = MagicMock(returncode=0)
240
+ cm.stage_files(["a.py", "b.py"])
241
+ args = mock_run.call_args[0][0]
242
+ assert "add" in args
243
+ assert "a.py" in args
244
+ assert "b.py" in args
245
+
246
+ def test_revert_files(self):
247
+ cm = CommitManager(project_root=_PROJECT_ROOT)
248
+ with patch("subprocess.run") as mock_run:
249
+ mock_run.return_value = MagicMock(returncode=0)
250
+ cm.revert_files(["c.py"])
251
+ args = mock_run.call_args[0][0]
252
+ assert "checkout" in args
253
+ assert "c.py" in args
254
+
255
+
256
+ # ═══════════════════════════════════════════════════════════════════════════
257
+ # 4 — TaskSelector
258
+ # ═══════════════════════════════════════════════════════════════════════════
259
+
260
+
261
+ class TestEvolutionTask:
262
+ """Tests for the EvolutionTask dataclass."""
263
+
264
+ def test_basic_construction(self):
265
+ t = EvolutionTask(
266
+ category=TASK_TYPE_HINTS,
267
+ description="Add return types to foo.py",
268
+ target_files=["foo.py"],
269
+ )
270
+ assert t.category == TASK_TYPE_HINTS
271
+ assert len(t.target_files) == 1
272
+ assert t.context_hint == ""
273
+
274
+
275
+ class TestTaskSelector:
276
+ """Tests for priority-based task selection."""
277
+
278
+ def test_fix_tests_has_highest_priority(self):
279
+ runner = MagicMock()
280
+ cm = MagicMock()
281
+ sel = TaskSelector(
282
+ project_root=_PROJECT_ROOT,
283
+ test_runner=runner,
284
+ commit_manager=cm,
285
+ )
286
+ failed_result = TestResult(
287
+ passed=False, total=7, failures=2, errors=0,
288
+ output="FAILED tests/test_foo.py::test_bar - AssertionError",
289
+ return_code=1,
290
+ )
291
+ task = sel.select(failed_result)
292
+ assert task.category == TASK_FIX_TESTS
293
+
294
+ def test_fallback_to_generic(self):
295
+ """With no test failures and no code issues -> small optimisation."""
296
+ runner = MagicMock()
297
+ cm = MagicMock()
298
+ sel = TaskSelector(
299
+ project_root=_PROJECT_ROOT,
300
+ test_runner=runner,
301
+ commit_manager=cm,
302
+ )
303
+ passing = TestResult(passed=10, total=10, failures=0, errors=0, return_code=0)
304
+ with patch.object(sel, "_find_type_hint_task", return_value=None), \
305
+ patch.object(sel, "_find_error_handling_task", return_value=None), \
306
+ patch.object(sel, "_find_duplication_task", return_value=None):
307
+ task = sel.select(passing)
308
+ assert task.category == TASK_SMALL_OPTIMISATION
309
+
310
+ def test_task_categories_are_strings(self):
311
+ assert isinstance(TASK_FIX_TESTS, str)
312
+ assert isinstance(TASK_TYPE_HINTS, str)
313
+ assert isinstance(TASK_ERROR_HANDLING, str)
314
+ assert isinstance(TASK_REDUCE_DUPLICATION, str)
315
+ assert isinstance(TASK_SMALL_OPTIMISATION, str)
316
+
317
+
318
+ # ═══════════════════════════════════════════════════════════════════════════
319
+ # 5 — ContextBuilder
320
+ # ═══════════════════════════════════════════════════════════════════════════
321
+
322
+
323
+ class TestContextBuilder:
324
+ """Tests for context assembly."""
325
+
326
+ def test_system_prompt_exists(self):
327
+ assert len(SYSTEM_PROMPT) > 100
328
+ assert "diff" in SYSTEM_PROMPT.lower()
329
+
330
+ def test_estimate_tokens(self):
331
+ cm = MagicMock()
332
+ cb = ContextBuilder(project_root=_PROJECT_ROOT, commit_manager=cm)
333
+ # 11 chars // 4 = 2 (floor division), min 1
334
+ assert cb.estimate_tokens("hello world") == 2
335
+ assert cb.estimate_tokens("hi") == 1 # min clamp
336
+
337
+ def test_build_includes_task(self):
338
+ cm = MagicMock()
339
+ cm.git_diff.return_value = ""
340
+ cb = ContextBuilder(project_root=_PROJECT_ROOT, commit_manager=cm)
341
+ task = EvolutionTask(
342
+ category="test_cat",
343
+ description="Do something helpful",
344
+ target_files=[],
345
+ )
346
+ ctx = cb.build(task)
347
+ assert "Do something helpful" in ctx
348
+ assert "test_cat" in ctx
349
+
350
+ def test_build_truncates_large_content(self):
351
+ cm = MagicMock()
352
+ cm.git_diff.return_value = ""
353
+ cb = ContextBuilder(
354
+ project_root=_PROJECT_ROOT,
355
+ commit_manager=cm,
356
+ max_context_tokens=50,
357
+ )
358
+ task = EvolutionTask(
359
+ category="demo",
360
+ description="truncation test",
361
+ target_files=[],
362
+ context_hint="A" * 10000,
363
+ )
364
+ ctx = cb.build(task)
365
+ assert len(ctx) < 10000
366
+
367
+
368
+ # ═══════════════════════════════════════════════════════════════════════════
369
+ # 6 — PatchGenerator — diff helpers
370
+ # ═══════════════════════════════════════════════════════════════════════════
371
+
372
+
373
+ class TestDiffExtraction:
374
+ """Tests for _extract_diff from LLM output."""
375
+
376
+ def test_fenced_diff_block(self):
377
+ text = textwrap.dedent("""\
378
+ Here is the patch:
379
+
380
+ ```diff
381
+ --- a/foo.py
382
+ +++ b/foo.py
383
+ @@ -1,3 +1,3 @@
384
+ -old line
385
+ +new line
386
+ context
387
+ ```
388
+ """)
389
+ diff = _extract_diff(text)
390
+ assert "--- a/foo.py" in diff
391
+ assert "+new line" in diff
392
+
393
+ def test_raw_diff_without_fence(self):
394
+ text = textwrap.dedent("""\
395
+ --- a/bar.py
396
+ +++ b/bar.py
397
+ @@ -10,4 +10,5 @@
398
+ context
399
+ -removed
400
+ +added
401
+ +another
402
+ """)
403
+ diff = _extract_diff(text)
404
+ assert "--- a/bar.py" in diff
405
+ assert "+added" in diff
406
+
407
+ def test_no_diff_returns_empty(self):
408
+ text = "Just some regular text with no diff."
409
+ diff = _extract_diff(text)
410
+ assert diff == ""
411
+
412
+
413
+ class TestDiffFiles:
414
+ """Tests for _diff_files extraction."""
415
+
416
+ def test_single_file(self):
417
+ diff = "--- a/foo.py\n+++ b/foo.py\n@@ -1 +1 @@\n-x\n+y"
418
+ files = _diff_files(diff)
419
+ assert files == ["foo.py"]
420
+
421
+ def test_multiple_files(self):
422
+ diff = (
423
+ "--- a/a.py\n+++ b/a.py\n@@ -1 +1 @@\n-x\n+y\n"
424
+ "--- a/b.py\n+++ b/b.py\n@@ -1 +1 @@\n-x\n+y"
425
+ )
426
+ files = _diff_files(diff)
427
+ assert "a.py" in files
428
+ assert "b.py" in files
429
+
430
+ def test_dev_null_excluded(self):
431
+ diff = "--- /dev/null\n+++ b/new.py\n@@ -0,0 +1 @@\n+new"
432
+ files = _diff_files(diff)
433
+ assert files == ["new.py"]
434
+
435
+
436
+ class TestDiffLineCount:
437
+ """Tests for _diff_line_count."""
438
+
439
+ def test_counts_adds_and_removes(self):
440
+ diff = (
441
+ "--- a/foo.py\n+++ b/foo.py\n@@ -1,3 +1,3 @@\n"
442
+ " ctx\n-removed\n+added\n ctx\n"
443
+ )
444
+ assert _diff_line_count(diff) == 2
445
+
446
+ def test_ignores_header_lines(self):
447
+ diff = "--- a/foo.py\n+++ b/foo.py\n@@ -1 +1 @@\n-x\n+y"
448
+ assert _diff_line_count(diff) == 2
449
+
450
+
451
+ class TestPatchResult:
452
+ """Tests for the PatchResult dataclass."""
453
+
454
+ def test_default_is_failure(self):
455
+ r = PatchResult()
456
+ assert r.success is False
457
+ assert r.files_changed == []
458
+
459
+ def test_to_dict(self):
460
+ r = PatchResult(success=True, files_changed=["a.py"], lines_changed=5)
461
+ d = r.to_dict()
462
+ assert d["success"] is True
463
+ assert d["files_changed"] == ["a.py"]
464
+
465
+
466
+ class TestPatchGeneratorSafety:
467
+ """Tests for PatchGenerator safety limits with mock LLM."""
468
+
469
+ def test_rejects_too_many_files(self):
470
+ provider = MockProvider()
471
+ lines = []
472
+ for i in range(5):
473
+ lines.append(f"--- a/f{i}.py")
474
+ lines.append(f"+++ b/f{i}.py")
475
+ lines.append("@@ -1 +1 @@")
476
+ lines.append(f"-old{i}")
477
+ lines.append(f"+new{i}")
478
+ diff_text = "\n".join(lines)
479
+ provider.enqueue_response(f"```diff\n{diff_text}\n```")
480
+
481
+ budget = BudgetGuard(max_tokens=50000)
482
+ cm = MagicMock()
483
+ cm.git_diff.return_value = ""
484
+ cb = ContextBuilder(project_root=_PROJECT_ROOT, commit_manager=cm)
485
+ pg = PatchGenerator(
486
+ project_root=_PROJECT_ROOT,
487
+ provider=provider,
488
+ context_builder=cb,
489
+ budget=budget,
490
+ )
491
+ task = EvolutionTask(category="test", description="test", target_files=[])
492
+ result = pg.generate_and_apply(task)
493
+ assert result.success is False
494
+ assert "files" in result.error.lower()
495
+
496
+ def test_rejects_when_budget_exceeded(self):
497
+ provider = MockProvider()
498
+ budget = BudgetGuard(max_tokens=10)
499
+ budget.record_tokens(10)
500
+
501
+ cm = MagicMock()
502
+ cm.git_diff.return_value = ""
503
+ cb = ContextBuilder(project_root=_PROJECT_ROOT, commit_manager=cm)
504
+ pg = PatchGenerator(
505
+ project_root=_PROJECT_ROOT,
506
+ provider=provider,
507
+ context_builder=cb,
508
+ budget=budget,
509
+ )
510
+ task = EvolutionTask(category="test", description="test", target_files=[])
511
+ result = pg.generate_and_apply(task)
512
+ assert result.success is False
513
+ assert "budget" in result.error.lower()
514
+
515
+ def test_handles_llm_returning_no_diff(self):
516
+ provider = MockProvider()
517
+ provider.enqueue_response("Sorry, I cannot generate a diff for this.")
518
+
519
+ budget = BudgetGuard(max_tokens=50000)
520
+ cm = MagicMock()
521
+ cm.git_diff.return_value = ""
522
+ cb = ContextBuilder(project_root=_PROJECT_ROOT, commit_manager=cm)
523
+ pg = PatchGenerator(
524
+ project_root=_PROJECT_ROOT,
525
+ provider=provider,
526
+ context_builder=cb,
527
+ budget=budget,
528
+ )
529
+ task = EvolutionTask(category="test", description="test", target_files=[])
530
+ result = pg.generate_and_apply(task)
531
+ assert result.success is False
532
+ assert "valid" in result.error.lower() or "diff" in result.error.lower()
533
+
534
+
535
+ # ═══════════════════════════════════════════════════════════════════════════
536
+ # 7 — Engine — IterationRecord and EvolutionResult
537
+ # ═══════════════════════════════════════════════════════════════════════════
538
+
539
+
540
+ class TestIterationRecord:
541
+ """Tests for the IterationRecord dataclass."""
542
+
543
+ def test_to_dict(self):
544
+ r = IterationRecord(
545
+ iteration=1,
546
+ task_category="type_hints",
547
+ task_description="Add types",
548
+ committed=True,
549
+ commit_sha="abc123",
550
+ )
551
+ d = r.to_dict()
552
+ assert d["iteration"] == 1
553
+ assert d["committed"] is True
554
+ assert d["commit_sha"] == "abc123"
555
+
556
+
557
+ class TestEvolutionResult:
558
+ """Tests for the EvolutionResult aggregate."""
559
+
560
+ def test_to_dict(self):
561
+ r = EvolutionResult(
562
+ iterations_completed=2,
563
+ commits=["abc", "def"],
564
+ reverts=1,
565
+ stop_reason="iteration limit reached (2)",
566
+ )
567
+ d = r.to_dict()
568
+ assert d["iterations_completed"] == 2
569
+ assert len(d["commits"]) == 2
570
+ assert d["reverts"] == 1
571
+
572
+
573
+ class TestEvolutionEngineOrchestration:
574
+ """Tests for the EvolutionEngine loop with mocked components."""
575
+
576
+ def test_engine_runs_iterations(self, tmp_path):
577
+ """Engine should run iterations and respect budget."""
578
+ provider = MockProvider()
579
+ provider.enqueue_response("no diff here")
580
+ provider.enqueue_response("no diff here")
581
+
582
+ budget = BudgetGuard(max_tokens=50000, max_iterations=2, max_seconds=60)
583
+
584
+ with patch.object(TestRunner, "run") as mock_test_run, \
585
+ patch.object(CommitManager, "has_changes", return_value=False):
586
+ mock_test_run.return_value = TestResult(
587
+ passed=10, total=10, failures=0, errors=0, return_code=0,
588
+ )
589
+
590
+ engine = EvolutionEngine(
591
+ project_root=tmp_path,
592
+ provider=provider,
593
+ budget=budget,
594
+ )
595
+ result = engine.run()
596
+
597
+ assert result.iterations_completed == 2
598
+ assert result.stop_reason is not None
599
+ assert len(result.history) == 2
600
+
601
+ def test_engine_writes_history(self, tmp_path):
602
+ """Engine should persist evolution_history.json."""
603
+ provider = MockProvider()
604
+ provider.enqueue_response("no diff")
605
+ budget = BudgetGuard(max_tokens=50000, max_iterations=1, max_seconds=60)
606
+
607
+ with patch.object(TestRunner, "run") as mock_test_run, \
608
+ patch.object(CommitManager, "has_changes", return_value=False):
609
+ mock_test_run.return_value = TestResult(
610
+ passed=5, total=5, failures=0, errors=0, return_code=0,
611
+ )
612
+
613
+ engine = EvolutionEngine(
614
+ project_root=tmp_path,
615
+ provider=provider,
616
+ budget=budget,
617
+ )
618
+ engine.run()
619
+
620
+ history_file = tmp_path / ".codexa" / "evolution_history.json"
621
+ assert history_file.exists()
622
+ data = json.loads(history_file.read_text(encoding="utf-8"))
623
+ assert isinstance(data, list)
624
+ assert len(data) == 1
625
+
626
+
627
+ # ═══════════════════════════════════════════════════════════════════════════
628
+ # 8 — CLI command
629
+ # ═══════════════════════════════════════════════════════════════════════════
630
+
631
+
632
+ class TestEvolveCLI:
633
+ """Tests for the evolve CLI command."""
634
+
635
+ def test_command_exists(self):
636
+ from semantic_code_intelligence.cli.commands.evolve_cmd import evolve_cmd
637
+ assert evolve_cmd.name == "evolve"
638
+
639
+ def test_command_has_options(self):
640
+ from semantic_code_intelligence.cli.commands.evolve_cmd import evolve_cmd
641
+ param_names = [p.name for p in evolve_cmd.params]
642
+ assert "iterations" in param_names
643
+ assert "budget" in param_names
644
+ assert "timeout" in param_names
645
+ assert "path" in param_names
646
+
647
+ def test_command_registered_in_router(self):
648
+ from semantic_code_intelligence.cli.router import register_commands
649
+ group = MagicMock(spec=["add_command"])
650
+ register_commands(group)
651
+ added_names = [call.args[0].name for call in group.add_command.call_args_list]
652
+ assert "evolve" in added_names
653
+
654
+ def test_help_mentions_evolve(self):
655
+ from click.testing import CliRunner
656
+ from semantic_code_intelligence.cli.commands.evolve_cmd import evolve_cmd
657
+ runner = CliRunner()
658
+ result = runner.invoke(evolve_cmd, ["--help"])
659
+ assert result.exit_code == 0
660
+ assert "self-improving" in result.output.lower() or "evolve" in result.output.lower()
661
+
662
+
663
+ # ═══════════════════════════════════════════════════════════════════════════
664
+ # 9 — Module imports and version
665
+ # ═══════════════════════════════════════════════════════════════════════════
666
+
667
+
668
+ class TestModuleImports:
669
+ """Verify that all Phase 24 modules are importable."""
670
+
671
+ def test_import_budget_guard(self):
672
+ from semantic_code_intelligence.evolution import budget_guard
673
+ assert hasattr(budget_guard, "BudgetGuard")
674
+
675
+ def test_import_test_runner(self):
676
+ from semantic_code_intelligence.evolution import test_runner
677
+ assert hasattr(test_runner, "TestRunner")
678
+ assert hasattr(test_runner, "TestResult")
679
+
680
+ def test_import_commit_manager(self):
681
+ from semantic_code_intelligence.evolution import commit_manager
682
+ assert hasattr(commit_manager, "CommitManager")
683
+
684
+ def test_import_task_selector(self):
685
+ from semantic_code_intelligence.evolution import task_selector
686
+ assert hasattr(task_selector, "TaskSelector")
687
+ assert hasattr(task_selector, "EvolutionTask")
688
+
689
+ def test_import_context_builder(self):
690
+ from semantic_code_intelligence.evolution import context_builder
691
+ assert hasattr(context_builder, "ContextBuilder")
692
+ assert hasattr(context_builder, "SYSTEM_PROMPT")
693
+
694
+ def test_import_patch_generator(self):
695
+ from semantic_code_intelligence.evolution import patch_generator
696
+ assert hasattr(patch_generator, "PatchGenerator")
697
+ assert hasattr(patch_generator, "PatchResult")
698
+
699
+ def test_import_engine(self):
700
+ from semantic_code_intelligence.evolution import engine
701
+ assert hasattr(engine, "EvolutionEngine")
702
+ assert hasattr(engine, "EvolutionResult")
703
+ assert hasattr(engine, "IterationRecord")
704
+
705
+ def test_import_evolve_cmd(self):
706
+ from semantic_code_intelligence.cli.commands import evolve_cmd
707
+ assert hasattr(evolve_cmd, "evolve_cmd")
708
+
709
+
710
+ class TestVersion:
711
+ """Verify the project version reflects Phase 24."""
712
+
713
+ def test_version_is_0_24_0(self):
714
+ from semantic_code_intelligence import __version__
715
+ assert __version__ == "0.4.0"