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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- 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
|