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,531 @@
1
+ """Tests for v0.29.0 features: LSP Server (Phase 26) + Incremental Indexing (Phase 27)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ import numpy as np
8
+ import pytest
9
+ from pathlib import Path
10
+ from unittest.mock import MagicMock, patch
11
+ from click.testing import CliRunner
12
+
13
+ from semantic_code_intelligence import __version__
14
+ from semantic_code_intelligence.cli.main import cli
15
+
16
+
17
+ # =========================================================================
18
+ # Version
19
+ # =========================================================================
20
+
21
+ class TestVersion028:
22
+ def test_version_is_028(self):
23
+ assert __version__ == "0.4.0"
24
+
25
+
26
+ # =========================================================================
27
+ # Phase 26 — LSP Server
28
+ # =========================================================================
29
+
30
+ class TestLSPServerModule:
31
+ """Test LSP server module structure and imports."""
32
+
33
+ def test_lsp_module_importable(self):
34
+ from semantic_code_intelligence.lsp import LSPServer, run_lsp_server
35
+ assert LSPServer is not None
36
+ assert callable(run_lsp_server)
37
+
38
+ def test_lsp_message_helpers(self):
39
+ from semantic_code_intelligence.lsp import _ok, _error
40
+ ok = _ok(1, {"test": True})
41
+ assert ok["jsonrpc"] == "2.0"
42
+ assert ok["id"] == 1
43
+ assert ok["result"]["test"] is True
44
+
45
+ err = _error(2, -32601, "Not found")
46
+ assert err["id"] == 2
47
+ assert err["error"]["code"] == -32601
48
+ assert err["error"]["message"] == "Not found"
49
+
50
+ def test_lsp_path_to_uri(self):
51
+ from semantic_code_intelligence.lsp import _path_to_uri
52
+ uri = _path_to_uri("src/main.py", Path("/project"))
53
+ assert uri.startswith("file:///")
54
+ assert "main.py" in uri
55
+
56
+ def test_lsp_symbol_kind_mapping(self):
57
+ from semantic_code_intelligence.lsp import _symbol_kind_to_lsp
58
+ assert _symbol_kind_to_lsp("function") == 12
59
+ assert _symbol_kind_to_lsp("class") == 5
60
+ assert _symbol_kind_to_lsp("method") == 6
61
+ assert _symbol_kind_to_lsp("variable") == 13
62
+ assert _symbol_kind_to_lsp("unknown") == 12 # default
63
+
64
+ def test_lsp_completion_kind_mapping(self):
65
+ from semantic_code_intelligence.lsp import _symbol_kind_to_completion
66
+ assert _symbol_kind_to_completion("function") == 3
67
+ assert _symbol_kind_to_completion("class") == 7
68
+ assert _symbol_kind_to_completion("method") == 2
69
+
70
+ def test_lsp_server_capabilities(self):
71
+ from semantic_code_intelligence.lsp import _SERVER_CAPABILITIES
72
+ assert _SERVER_CAPABILITIES["hoverProvider"] is True
73
+ assert _SERVER_CAPABILITIES["completionProvider"] is not None
74
+ assert _SERVER_CAPABILITIES["definitionProvider"] is True
75
+ assert _SERVER_CAPABILITIES["referencesProvider"] is True
76
+ assert _SERVER_CAPABILITIES["workspaceSymbolProvider"] is True
77
+
78
+
79
+ class TestLSPDocumentStore:
80
+ """Test the in-memory document store."""
81
+
82
+ def test_open_and_get(self):
83
+ from semantic_code_intelligence.lsp import _DocumentStore
84
+ ds = _DocumentStore()
85
+ ds.open("file:///test.py", "hello world")
86
+ assert ds.get("file:///test.py") == "hello world"
87
+
88
+ def test_update(self):
89
+ from semantic_code_intelligence.lsp import _DocumentStore
90
+ ds = _DocumentStore()
91
+ ds.open("file:///a.py", "old")
92
+ ds.update("file:///a.py", "new")
93
+ assert ds.get("file:///a.py") == "new"
94
+
95
+ def test_close(self):
96
+ from semantic_code_intelligence.lsp import _DocumentStore
97
+ ds = _DocumentStore()
98
+ ds.open("file:///x.py", "text")
99
+ ds.close("file:///x.py")
100
+ assert ds.get("file:///x.py") is None
101
+
102
+ def test_get_word_at(self):
103
+ from semantic_code_intelligence.lsp import _DocumentStore
104
+ ds = _DocumentStore()
105
+ ds.open("file:///t.py", "def hello_world():\n pass")
106
+ # Cursor at "hello_world" (line=0, char=6)
107
+ word = ds.get_word_at("file:///t.py", 0, 6)
108
+ assert word == "hello_world"
109
+
110
+ def test_get_word_at_empty(self):
111
+ from semantic_code_intelligence.lsp import _DocumentStore
112
+ ds = _DocumentStore()
113
+ word = ds.get_word_at("file:///missing.py", 0, 0)
114
+ assert word == ""
115
+
116
+ def test_uri_to_path_unix(self):
117
+ from semantic_code_intelligence.lsp import _DocumentStore
118
+ ds = _DocumentStore()
119
+ path = ds.uri_to_path("file:///home/user/project/main.py")
120
+ assert "main.py" in path
121
+
122
+ def test_uri_to_path_windows(self):
123
+ from semantic_code_intelligence.lsp import _DocumentStore
124
+ ds = _DocumentStore()
125
+ path = ds.uri_to_path("file:///C:/Users/test/project/main.py")
126
+ assert "main.py" in path
127
+
128
+
129
+ class TestLSPServerDispatch:
130
+ """Test LSP server message dispatch without actual stdio."""
131
+
132
+ def test_initialize(self):
133
+ from semantic_code_intelligence.lsp import LSPServer
134
+ import tempfile, os
135
+ with tempfile.TemporaryDirectory() as td:
136
+ root = Path(td)
137
+ (root / ".codexa").mkdir()
138
+ server = LSPServer(root)
139
+ resp = server._handle({
140
+ "jsonrpc": "2.0",
141
+ "id": 1,
142
+ "method": "initialize",
143
+ "params": {},
144
+ })
145
+ assert resp["id"] == 1
146
+ assert "capabilities" in resp["result"]
147
+ assert resp["result"]["serverInfo"]["name"] == "codexa-lsp"
148
+
149
+ def test_shutdown(self):
150
+ from semantic_code_intelligence.lsp import LSPServer
151
+ import tempfile
152
+ with tempfile.TemporaryDirectory() as td:
153
+ root = Path(td)
154
+ (root / ".codexa").mkdir()
155
+ server = LSPServer(root)
156
+ resp = server._handle({
157
+ "jsonrpc": "2.0",
158
+ "id": 2,
159
+ "method": "shutdown",
160
+ "params": {},
161
+ })
162
+ assert resp["id"] == 2
163
+ assert resp["result"] is None
164
+ assert server._shutdown is True
165
+
166
+ def test_initialized_notification(self):
167
+ from semantic_code_intelligence.lsp import LSPServer
168
+ import tempfile
169
+ with tempfile.TemporaryDirectory() as td:
170
+ root = Path(td)
171
+ (root / ".codexa").mkdir()
172
+ server = LSPServer(root)
173
+ resp = server._handle({
174
+ "jsonrpc": "2.0",
175
+ "method": "initialized",
176
+ "params": {},
177
+ })
178
+ assert resp is None
179
+ assert server._initialized is True
180
+
181
+ def test_unknown_method_returns_error(self):
182
+ from semantic_code_intelligence.lsp import LSPServer
183
+ import tempfile
184
+ with tempfile.TemporaryDirectory() as td:
185
+ root = Path(td)
186
+ (root / ".codexa").mkdir()
187
+ server = LSPServer(root)
188
+ resp = server._handle({
189
+ "jsonrpc": "2.0",
190
+ "id": 99,
191
+ "method": "foo/bar",
192
+ "params": {},
193
+ })
194
+ assert resp["error"]["code"] == -32601
195
+
196
+ def test_unknown_notification_ignored(self):
197
+ from semantic_code_intelligence.lsp import LSPServer
198
+ import tempfile
199
+ with tempfile.TemporaryDirectory() as td:
200
+ root = Path(td)
201
+ (root / ".codexa").mkdir()
202
+ server = LSPServer(root)
203
+ resp = server._handle({
204
+ "jsonrpc": "2.0",
205
+ "method": "$/cancelRequest",
206
+ "params": {},
207
+ })
208
+ assert resp is None
209
+
210
+ def test_did_open(self):
211
+ from semantic_code_intelligence.lsp import LSPServer
212
+ import tempfile
213
+ with tempfile.TemporaryDirectory() as td:
214
+ root = Path(td)
215
+ (root / ".codexa").mkdir()
216
+ server = LSPServer(root)
217
+ server._handle({
218
+ "jsonrpc": "2.0",
219
+ "method": "textDocument/didOpen",
220
+ "params": {
221
+ "textDocument": {
222
+ "uri": "file:///test.py",
223
+ "text": "x = 1",
224
+ },
225
+ },
226
+ })
227
+ assert server._docs.get("file:///test.py") == "x = 1"
228
+
229
+ def test_codex_search_missing_query(self):
230
+ from semantic_code_intelligence.lsp import LSPServer
231
+ import tempfile
232
+ with tempfile.TemporaryDirectory() as td:
233
+ root = Path(td)
234
+ (root / ".codexa").mkdir()
235
+ server = LSPServer(root)
236
+ resp = server._handle({
237
+ "jsonrpc": "2.0",
238
+ "id": 10,
239
+ "method": "codexa/search",
240
+ "params": {},
241
+ })
242
+ assert resp["error"]["code"] == -32602
243
+
244
+ def test_codex_quality_missing_path(self):
245
+ from semantic_code_intelligence.lsp import LSPServer
246
+ import tempfile
247
+ with tempfile.TemporaryDirectory() as td:
248
+ root = Path(td)
249
+ (root / ".codexa").mkdir()
250
+ server = LSPServer(root)
251
+ resp = server._handle({
252
+ "jsonrpc": "2.0",
253
+ "id": 11,
254
+ "method": "codexa/quality",
255
+ "params": {},
256
+ })
257
+ assert resp["error"]["code"] == -32602
258
+
259
+
260
+ class TestLSPCLI:
261
+ """Test the codexa lsp CLI command."""
262
+
263
+ def test_lsp_help(self):
264
+ runner = CliRunner()
265
+ result = runner.invoke(cli, ["lsp", "--help"])
266
+ assert result.exit_code == 0
267
+ assert "Language Server Protocol" in result.output
268
+
269
+ def test_lsp_requires_init(self):
270
+ """LSP should fail if project not initialized."""
271
+ runner = CliRunner()
272
+ import tempfile
273
+ with tempfile.TemporaryDirectory() as td:
274
+ result = runner.invoke(cli, ["lsp", "--path", td])
275
+ assert result.exit_code != 0 or "not initialized" in result.output.lower()
276
+
277
+ def test_command_count_is_36(self):
278
+ """Verify we now have 36 top-level commands (35 + lsp)."""
279
+ assert len(cli.commands) == 39
280
+
281
+
282
+ # =========================================================================
283
+ # Phase 27 — Incremental Indexing
284
+ # =========================================================================
285
+
286
+ class TestIncrementalIndexingFunction:
287
+ """Test run_incremental_indexing() in isolation."""
288
+
289
+ def test_importable(self):
290
+ from semantic_code_intelligence.services.indexing_service import (
291
+ run_incremental_indexing,
292
+ )
293
+ assert callable(run_incremental_indexing)
294
+
295
+ def test_empty_changes_returns_quickly(self, tmp_path):
296
+ """No changes and no deletes → immediate return."""
297
+ from semantic_code_intelligence.services.indexing_service import (
298
+ run_incremental_indexing,
299
+ run_indexing,
300
+ )
301
+ from semantic_code_intelligence.config.settings import AppConfig
302
+
303
+ # Create a minimal codexa project
304
+ project = tmp_path / "proj"
305
+ project.mkdir()
306
+ codex_dir = project / ".codexa"
307
+ codex_dir.mkdir()
308
+ (project / "hello.py").write_text("x = 1\n", encoding="utf-8")
309
+
310
+ # First do a full index so stores exist
311
+ run_indexing(project, force=True)
312
+
313
+ # Now run incremental with nothing changed
314
+ result = run_incremental_indexing(project, changed_files=[], deleted_files=[])
315
+ assert result.files_indexed == 0
316
+ assert result.files_scanned == 0
317
+
318
+ def test_incremental_indexes_new_file(self, tmp_path):
319
+ """A new file should be chunked and embedded incrementally."""
320
+ from semantic_code_intelligence.services.indexing_service import (
321
+ run_incremental_indexing,
322
+ run_indexing,
323
+ )
324
+ from semantic_code_intelligence.storage.vector_store import VectorStore
325
+ from semantic_code_intelligence.config.settings import AppConfig
326
+
327
+ project = tmp_path / "proj"
328
+ project.mkdir()
329
+ (project / ".codexa").mkdir()
330
+ (project / "a.py").write_text("def foo():\n return 1\n", encoding="utf-8")
331
+
332
+ run_indexing(project, force=True)
333
+ index_dir = AppConfig.index_dir(project)
334
+ store_before = VectorStore.load(index_dir)
335
+ n_before = store_before.size
336
+
337
+ # Add a new file
338
+ new_file = project / "b.py"
339
+ new_file.write_text("def bar():\n return 2\n", encoding="utf-8")
340
+
341
+ result = run_incremental_indexing(project, changed_files=[str(new_file)])
342
+ assert result.files_indexed >= 1
343
+ assert result.chunks_created >= 1
344
+
345
+ store_after = VectorStore.load(index_dir)
346
+ assert store_after.size > n_before
347
+
348
+ def test_incremental_handles_deleted_file(self, tmp_path):
349
+ """Deleted file vectors should be removed from the store."""
350
+ from semantic_code_intelligence.services.indexing_service import (
351
+ run_incremental_indexing,
352
+ run_indexing,
353
+ )
354
+ from semantic_code_intelligence.storage.vector_store import VectorStore
355
+ from semantic_code_intelligence.config.settings import AppConfig
356
+
357
+ project = tmp_path / "proj"
358
+ project.mkdir()
359
+ (project / ".codexa").mkdir()
360
+ (project / "a.py").write_text("x = 1\n", encoding="utf-8")
361
+ (project / "b.py").write_text("y = 2\n", encoding="utf-8")
362
+
363
+ run_indexing(project, force=True)
364
+ index_dir = AppConfig.index_dir(project)
365
+ store_before = VectorStore.load(index_dir)
366
+ n_before = store_before.size
367
+
368
+ # Delete b.py
369
+ b_path = str(project / "b.py")
370
+ (project / "b.py").unlink()
371
+
372
+ result = run_incremental_indexing(
373
+ project, changed_files=[], deleted_files=[b_path],
374
+ )
375
+ store_after = VectorStore.load(index_dir)
376
+ assert store_after.size < n_before
377
+
378
+ def test_incremental_skips_unchanged_file(self, tmp_path):
379
+ """File with same hash should be skipped."""
380
+ from semantic_code_intelligence.services.indexing_service import (
381
+ run_incremental_indexing,
382
+ run_indexing,
383
+ )
384
+
385
+ project = tmp_path / "proj"
386
+ project.mkdir()
387
+ (project / ".codexa").mkdir()
388
+ (project / "a.py").write_text("x = 1\n", encoding="utf-8")
389
+
390
+ run_indexing(project, force=True)
391
+
392
+ # Run incremental on the same file (unchanged)
393
+ result = run_incremental_indexing(
394
+ project, changed_files=[str(project / "a.py")],
395
+ )
396
+ assert result.files_skipped == 1
397
+ assert result.files_indexed == 0
398
+
399
+ def test_fallback_to_full_when_no_index(self, tmp_path):
400
+ """With no existing index, should fall back to run_indexing."""
401
+ from semantic_code_intelligence.services.indexing_service import (
402
+ run_incremental_indexing,
403
+ )
404
+
405
+ project = tmp_path / "proj"
406
+ project.mkdir()
407
+ (project / ".codexa").mkdir()
408
+ (project / "a.py").write_text("x = 1\n", encoding="utf-8")
409
+
410
+ # No prior index exists — should fall back
411
+ result = run_incremental_indexing(
412
+ project, changed_files=[str(project / "a.py")],
413
+ )
414
+ assert result.files_indexed >= 1
415
+
416
+
417
+ class TestDaemonIncrementalWiring:
418
+ """Test that the daemon uses incremental indexing."""
419
+
420
+ def test_indexing_task_has_deleted_paths(self):
421
+ from semantic_code_intelligence.daemon.watcher import IndexingTask
422
+ task = IndexingTask(
423
+ file_paths=["a.py", "b.py"],
424
+ deleted_paths=["c.py"],
425
+ )
426
+ assert len(task.deleted_paths) == 1
427
+ assert task.deleted_paths[0] == "c.py"
428
+
429
+ def test_indexing_task_defaults(self):
430
+ from semantic_code_intelligence.daemon.watcher import IndexingTask
431
+ task = IndexingTask(file_paths=["a.py"])
432
+ assert task.deleted_paths == []
433
+ assert task.force is False
434
+
435
+ def test_enqueue_with_deleted(self):
436
+ from semantic_code_intelligence.daemon.watcher import AsyncIndexer
437
+ import tempfile
438
+ with tempfile.TemporaryDirectory() as td:
439
+ indexer = AsyncIndexer(Path(td))
440
+ indexer.enqueue(["a.py"], deleted_paths=["b.py"])
441
+ assert indexer.pending_count == 1
442
+
443
+ def test_daemon_passes_deleted_to_indexer(self):
444
+ from semantic_code_intelligence.daemon.watcher import (
445
+ IndexingDaemon,
446
+ FileChangeEvent,
447
+ )
448
+ import tempfile
449
+ with tempfile.TemporaryDirectory() as td:
450
+ root = Path(td)
451
+ (root / ".codexa").mkdir()
452
+ daemon = IndexingDaemon(root)
453
+
454
+ events = [
455
+ FileChangeEvent(
456
+ path=root / "new.py",
457
+ relative_path="new.py",
458
+ change_type="created",
459
+ timestamp=time.time(),
460
+ ),
461
+ FileChangeEvent(
462
+ path=root / "old.py",
463
+ relative_path="old.py",
464
+ change_type="deleted",
465
+ timestamp=time.time(),
466
+ ),
467
+ ]
468
+ daemon._on_file_changes(events)
469
+ assert daemon._indexer.pending_count == 1
470
+
471
+ def test_file_watcher_scan_once(self):
472
+ from semantic_code_intelligence.daemon.watcher import FileWatcher
473
+ import tempfile
474
+ with tempfile.TemporaryDirectory() as td:
475
+ root = Path(td)
476
+ (root / ".codexa").mkdir()
477
+ (root / "test.py").write_text("x = 1\n", encoding="utf-8")
478
+
479
+ watcher = FileWatcher(root, poll_interval=60)
480
+ # First scan is baseline
481
+ events = watcher.scan_once()
482
+ assert events == []
483
+
484
+ # Modify file
485
+ (root / "test.py").write_text("x = 2\n", encoding="utf-8")
486
+ events = watcher.scan_once()
487
+ assert len(events) >= 1
488
+ assert any(e.change_type == "modified" for e in events)
489
+
490
+ def test_file_watcher_detects_new_file(self):
491
+ from semantic_code_intelligence.daemon.watcher import FileWatcher
492
+ import tempfile
493
+ with tempfile.TemporaryDirectory() as td:
494
+ root = Path(td)
495
+ (root / ".codexa").mkdir()
496
+ (root / "a.py").write_text("x = 1\n", encoding="utf-8")
497
+
498
+ watcher = FileWatcher(root, poll_interval=60)
499
+ watcher.scan_once() # baseline
500
+
501
+ (root / "b.py").write_text("y = 2\n", encoding="utf-8")
502
+ events = watcher.scan_once()
503
+ assert any(e.change_type == "created" for e in events)
504
+
505
+ def test_file_watcher_detects_deletion(self):
506
+ from semantic_code_intelligence.daemon.watcher import FileWatcher
507
+ import tempfile
508
+ with tempfile.TemporaryDirectory() as td:
509
+ root = Path(td)
510
+ (root / ".codexa").mkdir()
511
+ (root / "a.py").write_text("x = 1\n", encoding="utf-8")
512
+
513
+ watcher = FileWatcher(root, poll_interval=60)
514
+ watcher.scan_once() # baseline
515
+
516
+ (root / "a.py").unlink()
517
+ events = watcher.scan_once()
518
+ assert any(e.change_type == "deleted" for e in events)
519
+
520
+
521
+ class TestIndexingResultRepr:
522
+ """Test IndexingResult representation."""
523
+
524
+ def test_repr(self):
525
+ from semantic_code_intelligence.services.indexing_service import IndexingResult
526
+ r = IndexingResult()
527
+ r.files_scanned = 10
528
+ r.files_indexed = 5
529
+ s = repr(r)
530
+ assert "scanned=10" in s
531
+ assert "indexed=5" in s