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,2058 @@
|
|
|
1
|
+
"""Phase 20b — Extended deep coverage tests (targeting 2000+ total).
|
|
2
|
+
|
|
3
|
+
Covers configuration, services, protocol, reasoning, investigation,
|
|
4
|
+
analysis, context engine, memory, conversation, streaming, storage,
|
|
5
|
+
chunking, scanning, parsing, and CLI helpers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import tempfile
|
|
12
|
+
from dataclasses import fields, is_dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from unittest.mock import MagicMock, patch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ==========================================================================
|
|
19
|
+
# Config / Settings
|
|
20
|
+
# ==========================================================================
|
|
21
|
+
|
|
22
|
+
from semantic_code_intelligence.config.settings import (
|
|
23
|
+
AppConfig,
|
|
24
|
+
EmbeddingConfig,
|
|
25
|
+
IndexConfig,
|
|
26
|
+
LLMConfig,
|
|
27
|
+
QualityConfig,
|
|
28
|
+
SearchConfig,
|
|
29
|
+
DEFAULT_EXTENSIONS,
|
|
30
|
+
DEFAULT_IGNORE_DIRS,
|
|
31
|
+
load_config,
|
|
32
|
+
save_config,
|
|
33
|
+
init_project,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestEmbeddingConfig:
|
|
38
|
+
def test_defaults(self):
|
|
39
|
+
ec = EmbeddingConfig()
|
|
40
|
+
assert ec.model_name == "all-MiniLM-L6-v2"
|
|
41
|
+
assert ec.chunk_size == 512
|
|
42
|
+
assert ec.chunk_overlap == 64
|
|
43
|
+
|
|
44
|
+
def test_custom(self):
|
|
45
|
+
ec = EmbeddingConfig(model_name="custom", chunk_size=256, chunk_overlap=32)
|
|
46
|
+
assert ec.model_name == "custom"
|
|
47
|
+
assert ec.chunk_size == 256
|
|
48
|
+
|
|
49
|
+
def test_model_dump(self):
|
|
50
|
+
ec = EmbeddingConfig()
|
|
51
|
+
d = ec.model_dump()
|
|
52
|
+
assert "model_name" in d
|
|
53
|
+
assert "chunk_size" in d
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestSearchConfig:
|
|
57
|
+
def test_defaults(self):
|
|
58
|
+
sc = SearchConfig()
|
|
59
|
+
assert sc.top_k == 10
|
|
60
|
+
assert sc.similarity_threshold == 0.3
|
|
61
|
+
|
|
62
|
+
def test_custom(self):
|
|
63
|
+
sc = SearchConfig(top_k=5, similarity_threshold=0.5)
|
|
64
|
+
assert sc.top_k == 5
|
|
65
|
+
assert sc.similarity_threshold == 0.5
|
|
66
|
+
|
|
67
|
+
def test_model_dump(self):
|
|
68
|
+
d = SearchConfig().model_dump()
|
|
69
|
+
assert "top_k" in d
|
|
70
|
+
assert "similarity_threshold" in d
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestIndexConfig:
|
|
74
|
+
def test_defaults(self):
|
|
75
|
+
ic = IndexConfig()
|
|
76
|
+
assert ".git" in ic.ignore_dirs
|
|
77
|
+
assert ".py" in ic.extensions
|
|
78
|
+
assert ic.use_incremental is True
|
|
79
|
+
|
|
80
|
+
def test_custom_ignore(self):
|
|
81
|
+
ic = IndexConfig(ignore_dirs={"custom_dir"})
|
|
82
|
+
assert "custom_dir" in ic.ignore_dirs
|
|
83
|
+
|
|
84
|
+
def test_custom_extensions(self):
|
|
85
|
+
ic = IndexConfig(extensions={".xyz"})
|
|
86
|
+
assert ".xyz" in ic.extensions
|
|
87
|
+
|
|
88
|
+
def test_model_dump(self):
|
|
89
|
+
d = IndexConfig().model_dump()
|
|
90
|
+
assert "ignore_dirs" in d
|
|
91
|
+
assert "extensions" in d
|
|
92
|
+
assert "use_incremental" in d
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestLLMConfig:
|
|
96
|
+
def test_defaults(self):
|
|
97
|
+
lc = LLMConfig()
|
|
98
|
+
assert lc.provider == "mock"
|
|
99
|
+
assert lc.model == "gpt-3.5-turbo"
|
|
100
|
+
assert lc.temperature == 0.2
|
|
101
|
+
assert lc.max_tokens == 2048
|
|
102
|
+
|
|
103
|
+
def test_custom(self):
|
|
104
|
+
lc = LLMConfig(provider="openai", api_key="key123", temperature=0.8)
|
|
105
|
+
assert lc.provider == "openai"
|
|
106
|
+
assert lc.api_key == "key123"
|
|
107
|
+
assert lc.temperature == 0.8
|
|
108
|
+
|
|
109
|
+
def test_model_dump(self):
|
|
110
|
+
d = LLMConfig().model_dump()
|
|
111
|
+
assert "provider" in d
|
|
112
|
+
assert "model" in d
|
|
113
|
+
assert "api_key" in d
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestQualityConfig:
|
|
117
|
+
def test_defaults(self):
|
|
118
|
+
qc = QualityConfig()
|
|
119
|
+
assert qc.complexity_threshold == 10
|
|
120
|
+
assert qc.min_maintainability == 40.0
|
|
121
|
+
assert qc.max_issues == 20
|
|
122
|
+
assert qc.snapshot_on_index is False
|
|
123
|
+
assert qc.history_limit == 50
|
|
124
|
+
|
|
125
|
+
def test_custom(self):
|
|
126
|
+
qc = QualityConfig(complexity_threshold=15, min_maintainability=50.0)
|
|
127
|
+
assert qc.complexity_threshold == 15
|
|
128
|
+
|
|
129
|
+
def test_model_dump(self):
|
|
130
|
+
d = QualityConfig().model_dump()
|
|
131
|
+
assert "complexity_threshold" in d
|
|
132
|
+
assert "history_limit" in d
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestAppConfig:
|
|
136
|
+
def test_defaults(self):
|
|
137
|
+
ac = AppConfig()
|
|
138
|
+
assert ac.project_root == "."
|
|
139
|
+
assert ac.verbose is False
|
|
140
|
+
assert isinstance(ac.embedding, EmbeddingConfig)
|
|
141
|
+
assert isinstance(ac.search, SearchConfig)
|
|
142
|
+
assert isinstance(ac.index, IndexConfig)
|
|
143
|
+
assert isinstance(ac.llm, LLMConfig)
|
|
144
|
+
assert isinstance(ac.quality, QualityConfig)
|
|
145
|
+
|
|
146
|
+
def test_config_dir(self):
|
|
147
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
148
|
+
cd = AppConfig.config_dir(tmp)
|
|
149
|
+
assert cd.name == ".codexa"
|
|
150
|
+
assert cd.parent == Path(tmp).resolve()
|
|
151
|
+
|
|
152
|
+
def test_config_path(self):
|
|
153
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
154
|
+
cp = AppConfig.config_path(tmp)
|
|
155
|
+
assert cp.name == "config.json"
|
|
156
|
+
|
|
157
|
+
def test_index_dir(self):
|
|
158
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
159
|
+
idx = AppConfig.index_dir(tmp)
|
|
160
|
+
assert idx.name == "index"
|
|
161
|
+
|
|
162
|
+
def test_model_dump_roundtrip(self):
|
|
163
|
+
ac = AppConfig()
|
|
164
|
+
d = ac.model_dump()
|
|
165
|
+
ac2 = AppConfig.model_validate(d)
|
|
166
|
+
assert ac2.project_root == ac.project_root
|
|
167
|
+
|
|
168
|
+
def test_nested_configs(self):
|
|
169
|
+
ac = AppConfig()
|
|
170
|
+
d = ac.model_dump()
|
|
171
|
+
assert "embedding" in d
|
|
172
|
+
assert "search" in d
|
|
173
|
+
assert "index" in d
|
|
174
|
+
assert "llm" in d
|
|
175
|
+
assert "quality" in d
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestLoadConfig:
|
|
179
|
+
def test_load_default(self):
|
|
180
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
181
|
+
cfg = load_config(tmp)
|
|
182
|
+
assert isinstance(cfg, AppConfig)
|
|
183
|
+
|
|
184
|
+
def test_load_from_file(self):
|
|
185
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
186
|
+
cfg, path = init_project(tmp)
|
|
187
|
+
loaded = load_config(tmp)
|
|
188
|
+
assert loaded.project_root == cfg.project_root
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestSaveConfig:
|
|
192
|
+
def test_save_creates_file(self):
|
|
193
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
194
|
+
cfg = AppConfig(project_root=str(Path(tmp).resolve()))
|
|
195
|
+
path = save_config(cfg, tmp)
|
|
196
|
+
assert path.exists()
|
|
197
|
+
|
|
198
|
+
def test_save_json_parseable(self):
|
|
199
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
200
|
+
cfg = AppConfig(project_root=str(Path(tmp).resolve()))
|
|
201
|
+
path = save_config(cfg, tmp)
|
|
202
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
203
|
+
assert "project_root" in data
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TestInitProject:
|
|
207
|
+
def test_creates_dirs(self):
|
|
208
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
209
|
+
cfg, path = init_project(tmp)
|
|
210
|
+
assert path.exists()
|
|
211
|
+
assert AppConfig.config_dir(tmp).exists()
|
|
212
|
+
assert AppConfig.index_dir(tmp).exists()
|
|
213
|
+
|
|
214
|
+
def test_returns_config(self):
|
|
215
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
216
|
+
cfg, _ = init_project(tmp)
|
|
217
|
+
assert isinstance(cfg, AppConfig)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestDefaultSets:
|
|
221
|
+
def test_default_ignore_dirs(self):
|
|
222
|
+
assert ".git" in DEFAULT_IGNORE_DIRS
|
|
223
|
+
assert "__pycache__" in DEFAULT_IGNORE_DIRS
|
|
224
|
+
assert "node_modules" in DEFAULT_IGNORE_DIRS
|
|
225
|
+
|
|
226
|
+
def test_default_extensions(self):
|
|
227
|
+
assert ".py" in DEFAULT_EXTENSIONS
|
|
228
|
+
assert ".js" in DEFAULT_EXTENSIONS
|
|
229
|
+
assert ".ts" in DEFAULT_EXTENSIONS
|
|
230
|
+
assert ".java" in DEFAULT_EXTENSIONS
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ==========================================================================
|
|
234
|
+
# Bridge Protocol
|
|
235
|
+
# ==========================================================================
|
|
236
|
+
|
|
237
|
+
from semantic_code_intelligence.bridge.protocol import (
|
|
238
|
+
RequestKind,
|
|
239
|
+
AgentRequest,
|
|
240
|
+
AgentResponse,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestRequestKind:
|
|
245
|
+
def test_has_semantic_search(self):
|
|
246
|
+
assert RequestKind.SEMANTIC_SEARCH.value == "semantic_search"
|
|
247
|
+
|
|
248
|
+
def test_has_explain_symbol(self):
|
|
249
|
+
assert RequestKind.EXPLAIN_SYMBOL.value == "explain_symbol"
|
|
250
|
+
|
|
251
|
+
def test_has_explain_file(self):
|
|
252
|
+
assert RequestKind.EXPLAIN_FILE.value == "explain_file"
|
|
253
|
+
|
|
254
|
+
def test_has_get_context(self):
|
|
255
|
+
assert RequestKind.GET_CONTEXT.value == "get_context"
|
|
256
|
+
|
|
257
|
+
def test_has_get_dependencies(self):
|
|
258
|
+
assert RequestKind.GET_DEPENDENCIES.value == "get_dependencies"
|
|
259
|
+
|
|
260
|
+
def test_has_get_call_graph(self):
|
|
261
|
+
assert RequestKind.GET_CALL_GRAPH.value == "get_call_graph"
|
|
262
|
+
|
|
263
|
+
def test_has_summarize_repo(self):
|
|
264
|
+
assert RequestKind.SUMMARIZE_REPO.value == "summarize_repo"
|
|
265
|
+
|
|
266
|
+
def test_has_find_references(self):
|
|
267
|
+
assert RequestKind.FIND_REFERENCES.value == "find_references"
|
|
268
|
+
|
|
269
|
+
def test_has_validate_code(self):
|
|
270
|
+
assert RequestKind.VALIDATE_CODE.value == "validate_code"
|
|
271
|
+
|
|
272
|
+
def test_has_list_capabilities(self):
|
|
273
|
+
assert RequestKind.LIST_CAPABILITIES.value == "list_capabilities"
|
|
274
|
+
|
|
275
|
+
def test_has_invoke_tool(self):
|
|
276
|
+
assert RequestKind.INVOKE_TOOL.value == "invoke_tool"
|
|
277
|
+
|
|
278
|
+
def test_has_list_tools(self):
|
|
279
|
+
assert RequestKind.LIST_TOOLS.value == "list_tools"
|
|
280
|
+
|
|
281
|
+
def test_count(self):
|
|
282
|
+
assert len(RequestKind) == 12
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestAgentRequestProtocol:
|
|
286
|
+
def test_create_minimal(self):
|
|
287
|
+
r = AgentRequest(kind="semantic_search")
|
|
288
|
+
assert r.kind == "semantic_search"
|
|
289
|
+
assert r.params == {}
|
|
290
|
+
|
|
291
|
+
def test_create_full(self):
|
|
292
|
+
r = AgentRequest(kind="explain_symbol", params={"name": "foo"},
|
|
293
|
+
request_id="r1", source="copilot")
|
|
294
|
+
assert r.params["name"] == "foo"
|
|
295
|
+
assert r.request_id == "r1"
|
|
296
|
+
assert r.source == "copilot"
|
|
297
|
+
|
|
298
|
+
def test_to_dict(self):
|
|
299
|
+
r = AgentRequest(kind="test", params={"a": 1})
|
|
300
|
+
d = r.to_dict()
|
|
301
|
+
assert d["kind"] == "test"
|
|
302
|
+
assert d["params"]["a"] == 1
|
|
303
|
+
assert "request_id" in d
|
|
304
|
+
assert "source" in d
|
|
305
|
+
|
|
306
|
+
def test_to_json(self):
|
|
307
|
+
r = AgentRequest(kind="test")
|
|
308
|
+
j = r.to_json()
|
|
309
|
+
parsed = json.loads(j)
|
|
310
|
+
assert parsed["kind"] == "test"
|
|
311
|
+
|
|
312
|
+
def test_from_dict(self):
|
|
313
|
+
data = {"kind": "semantic_search", "params": {"q": "hello"}, "request_id": "x"}
|
|
314
|
+
r = AgentRequest.from_dict(data)
|
|
315
|
+
assert r.kind == "semantic_search"
|
|
316
|
+
assert r.params["q"] == "hello"
|
|
317
|
+
|
|
318
|
+
def test_from_json(self):
|
|
319
|
+
j = '{"kind":"explain_symbol","params":{"name":"foo"}}'
|
|
320
|
+
r = AgentRequest.from_json(j)
|
|
321
|
+
assert r.kind == "explain_symbol"
|
|
322
|
+
assert r.params["name"] == "foo"
|
|
323
|
+
|
|
324
|
+
def test_roundtrip(self):
|
|
325
|
+
r = AgentRequest(kind="test", params={"x": 42}, request_id="abc")
|
|
326
|
+
r2 = AgentRequest.from_json(r.to_json())
|
|
327
|
+
assert r2.kind == r.kind
|
|
328
|
+
assert r2.params == r.params
|
|
329
|
+
assert r2.request_id == r.request_id
|
|
330
|
+
|
|
331
|
+
def test_from_dict_missing_fields(self):
|
|
332
|
+
r = AgentRequest.from_dict({})
|
|
333
|
+
assert r.kind == ""
|
|
334
|
+
assert r.params == {}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class TestAgentResponseProtocol:
|
|
338
|
+
def test_success(self):
|
|
339
|
+
r = AgentResponse(success=True, data={"result": "ok"})
|
|
340
|
+
assert r.success is True
|
|
341
|
+
assert r.data["result"] == "ok"
|
|
342
|
+
|
|
343
|
+
def test_error(self):
|
|
344
|
+
r = AgentResponse(success=False, error="not found")
|
|
345
|
+
assert r.success is False
|
|
346
|
+
assert r.error == "not found"
|
|
347
|
+
|
|
348
|
+
def test_to_dict_success(self):
|
|
349
|
+
r = AgentResponse(success=True, data={"a": 1}, request_id="r1")
|
|
350
|
+
d = r.to_dict()
|
|
351
|
+
assert d["success"] is True
|
|
352
|
+
assert "data" in d
|
|
353
|
+
assert d["request_id"] == "r1"
|
|
354
|
+
|
|
355
|
+
def test_to_dict_error(self):
|
|
356
|
+
r = AgentResponse(success=False, error="boom")
|
|
357
|
+
d = r.to_dict()
|
|
358
|
+
assert "error" in d
|
|
359
|
+
assert d["error"] == "boom"
|
|
360
|
+
|
|
361
|
+
def test_to_json(self):
|
|
362
|
+
r = AgentResponse(success=True, data={})
|
|
363
|
+
j = r.to_json()
|
|
364
|
+
parsed = json.loads(j)
|
|
365
|
+
assert parsed["success"] is True
|
|
366
|
+
|
|
367
|
+
def test_elapsed_ms(self):
|
|
368
|
+
r = AgentResponse(success=True, data={}, elapsed_ms=12.345)
|
|
369
|
+
d = r.to_dict()
|
|
370
|
+
assert d["elapsed_ms"] == 12.35 # rounded to 2 decimal places
|
|
371
|
+
|
|
372
|
+
def test_to_json_with_indent(self):
|
|
373
|
+
r = AgentResponse(success=True, data={"x": 1})
|
|
374
|
+
j = r.to_json(indent=2)
|
|
375
|
+
assert "\n" in j
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ==========================================================================
|
|
379
|
+
# LLM Provider Base & Message
|
|
380
|
+
# ==========================================================================
|
|
381
|
+
|
|
382
|
+
from semantic_code_intelligence.llm.provider import (
|
|
383
|
+
LLMMessage,
|
|
384
|
+
LLMResponse,
|
|
385
|
+
MessageRole,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class TestMessageRole:
|
|
390
|
+
def test_system(self):
|
|
391
|
+
assert MessageRole.SYSTEM.value == "system"
|
|
392
|
+
|
|
393
|
+
def test_user(self):
|
|
394
|
+
assert MessageRole.USER.value == "user"
|
|
395
|
+
|
|
396
|
+
def test_assistant(self):
|
|
397
|
+
assert MessageRole.ASSISTANT.value == "assistant"
|
|
398
|
+
|
|
399
|
+
def test_count(self):
|
|
400
|
+
assert len(MessageRole) == 3
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestLLMMessage:
|
|
404
|
+
def test_create(self):
|
|
405
|
+
m = LLMMessage(role=MessageRole.USER, content="hello")
|
|
406
|
+
assert m.role == MessageRole.USER
|
|
407
|
+
assert m.content == "hello"
|
|
408
|
+
|
|
409
|
+
def test_to_dict(self):
|
|
410
|
+
m = LLMMessage(role=MessageRole.SYSTEM, content="prompt")
|
|
411
|
+
d = m.to_dict()
|
|
412
|
+
assert d["role"] == "system"
|
|
413
|
+
assert d["content"] == "prompt"
|
|
414
|
+
|
|
415
|
+
def test_is_dataclass(self):
|
|
416
|
+
assert is_dataclass(LLMMessage)
|
|
417
|
+
|
|
418
|
+
def test_equality(self):
|
|
419
|
+
m1 = LLMMessage(role=MessageRole.USER, content="hi")
|
|
420
|
+
m2 = LLMMessage(role=MessageRole.USER, content="hi")
|
|
421
|
+
assert m1 == m2
|
|
422
|
+
|
|
423
|
+
def test_different(self):
|
|
424
|
+
m1 = LLMMessage(role=MessageRole.USER, content="hi")
|
|
425
|
+
m2 = LLMMessage(role=MessageRole.ASSISTANT, content="hi")
|
|
426
|
+
assert m1 != m2
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class TestLLMResponse:
|
|
430
|
+
def test_create(self):
|
|
431
|
+
r = LLMResponse(content="answer")
|
|
432
|
+
assert r.content == "answer"
|
|
433
|
+
assert r.model == ""
|
|
434
|
+
assert r.provider == ""
|
|
435
|
+
|
|
436
|
+
def test_to_dict(self):
|
|
437
|
+
r = LLMResponse(content="ans", model="gpt-4", provider="openai",
|
|
438
|
+
usage={"prompt_tokens": 10, "completion_tokens": 20})
|
|
439
|
+
d = r.to_dict()
|
|
440
|
+
assert d["content"] == "ans"
|
|
441
|
+
assert d["model"] == "gpt-4"
|
|
442
|
+
assert d["provider"] == "openai"
|
|
443
|
+
assert d["usage"]["prompt_tokens"] == 10
|
|
444
|
+
|
|
445
|
+
def test_defaults(self):
|
|
446
|
+
r = LLMResponse(content="x")
|
|
447
|
+
assert r.usage == {}
|
|
448
|
+
assert r.raw == {}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ==========================================================================
|
|
452
|
+
# LLM Safety
|
|
453
|
+
# ==========================================================================
|
|
454
|
+
|
|
455
|
+
from semantic_code_intelligence.llm.safety import SafetyIssue, SafetyReport, SafetyValidator
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class TestSafetyIssueDeep:
|
|
459
|
+
def test_create(self):
|
|
460
|
+
si = SafetyIssue(pattern="eval", description="Dangerous eval", line_number=5)
|
|
461
|
+
assert si.pattern == "eval"
|
|
462
|
+
assert si.line_number == 5
|
|
463
|
+
|
|
464
|
+
def test_to_dict(self):
|
|
465
|
+
si = SafetyIssue(pattern="exec", description="exec call", severity="error")
|
|
466
|
+
d = si.to_dict()
|
|
467
|
+
assert d["pattern"] == "exec"
|
|
468
|
+
assert d["severity"] == "error"
|
|
469
|
+
|
|
470
|
+
def test_default_severity(self):
|
|
471
|
+
si = SafetyIssue(pattern="x", description="y")
|
|
472
|
+
assert si.severity == "warning"
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class TestSafetyReportDeep:
|
|
476
|
+
def test_safe(self):
|
|
477
|
+
sr = SafetyReport()
|
|
478
|
+
assert sr.safe is True
|
|
479
|
+
assert sr.issues == []
|
|
480
|
+
|
|
481
|
+
def test_unsafe(self):
|
|
482
|
+
sr = SafetyReport(safe=False, issues=[
|
|
483
|
+
SafetyIssue(pattern="eval", description="eval call")
|
|
484
|
+
])
|
|
485
|
+
assert sr.safe is False
|
|
486
|
+
assert len(sr.issues) == 1
|
|
487
|
+
|
|
488
|
+
def test_to_dict(self):
|
|
489
|
+
sr = SafetyReport(safe=True)
|
|
490
|
+
d = sr.to_dict()
|
|
491
|
+
assert d["safe"] is True
|
|
492
|
+
assert d["issues"] == []
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class TestSafetyValidatorDeep:
|
|
496
|
+
def test_safe_code(self):
|
|
497
|
+
sv = SafetyValidator()
|
|
498
|
+
report = sv.validate("x = 1 + 2")
|
|
499
|
+
assert report.safe is True
|
|
500
|
+
|
|
501
|
+
def test_unsafe_eval(self):
|
|
502
|
+
sv = SafetyValidator()
|
|
503
|
+
report = sv.validate("result = eval(user_input)")
|
|
504
|
+
assert report.safe is False
|
|
505
|
+
|
|
506
|
+
def test_unsafe_exec(self):
|
|
507
|
+
sv = SafetyValidator()
|
|
508
|
+
report = sv.validate("exec(code)")
|
|
509
|
+
assert report.safe is False
|
|
510
|
+
|
|
511
|
+
def test_is_safe_shorthand(self):
|
|
512
|
+
sv = SafetyValidator()
|
|
513
|
+
assert sv.is_safe("x = 1") is True
|
|
514
|
+
|
|
515
|
+
def test_is_safe_unsafe(self):
|
|
516
|
+
sv = SafetyValidator()
|
|
517
|
+
assert sv.is_safe("eval('code')") is False
|
|
518
|
+
|
|
519
|
+
def test_extra_patterns(self):
|
|
520
|
+
sv = SafetyValidator(extra_patterns=[("DANGER", "custom danger")])
|
|
521
|
+
report = sv.validate("DANGER this is bad")
|
|
522
|
+
assert report.safe is False
|
|
523
|
+
assert any("DANGER" in i.pattern for i in report.issues)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ==========================================================================
|
|
527
|
+
# LLM Reasoning Data Types
|
|
528
|
+
# ==========================================================================
|
|
529
|
+
|
|
530
|
+
from semantic_code_intelligence.llm.reasoning import (
|
|
531
|
+
AskResult,
|
|
532
|
+
ReviewResult,
|
|
533
|
+
RefactorResult,
|
|
534
|
+
SuggestResult,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class TestAskResult:
|
|
539
|
+
def test_create(self):
|
|
540
|
+
ar = AskResult(question="what?", answer="this")
|
|
541
|
+
assert ar.question == "what?"
|
|
542
|
+
assert ar.answer == "this"
|
|
543
|
+
|
|
544
|
+
def test_to_dict(self):
|
|
545
|
+
ar = AskResult(question="q", answer="a")
|
|
546
|
+
d = ar.to_dict()
|
|
547
|
+
assert d["question"] == "q"
|
|
548
|
+
assert d["answer"] == "a"
|
|
549
|
+
assert d["context_snippets"] == []
|
|
550
|
+
assert d["usage"] == {}
|
|
551
|
+
|
|
552
|
+
def test_with_context(self):
|
|
553
|
+
ar = AskResult(question="q", answer="a",
|
|
554
|
+
context_snippets=[{"file": "x.py", "content": "code"}])
|
|
555
|
+
d = ar.to_dict()
|
|
556
|
+
assert len(d["context_snippets"]) == 1
|
|
557
|
+
|
|
558
|
+
def test_with_llm_response(self):
|
|
559
|
+
resp = LLMResponse(content="ans", usage={"tokens": 100})
|
|
560
|
+
ar = AskResult(question="q", answer="a", llm_response=resp)
|
|
561
|
+
d = ar.to_dict()
|
|
562
|
+
assert d["usage"]["tokens"] == 100
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class TestReviewResult:
|
|
566
|
+
def test_create(self):
|
|
567
|
+
rr = ReviewResult(file_path="test.py")
|
|
568
|
+
assert rr.file_path == "test.py"
|
|
569
|
+
assert rr.issues == []
|
|
570
|
+
assert rr.summary == ""
|
|
571
|
+
|
|
572
|
+
def test_to_dict(self):
|
|
573
|
+
rr = ReviewResult(file_path="a.py", summary="looks good",
|
|
574
|
+
issues=[{"type": "style", "msg": "long line"}])
|
|
575
|
+
d = rr.to_dict()
|
|
576
|
+
assert d["file_path"] == "a.py"
|
|
577
|
+
assert len(d["issues"]) == 1
|
|
578
|
+
assert d["summary"] == "looks good"
|
|
579
|
+
|
|
580
|
+
def test_empty_usage(self):
|
|
581
|
+
d = ReviewResult(file_path="x").to_dict()
|
|
582
|
+
assert d["usage"] == {}
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class TestRefactorResult:
|
|
586
|
+
def test_create(self):
|
|
587
|
+
rr = RefactorResult(file_path="a.py", original_code="x=1",
|
|
588
|
+
refactored_code="x = 1", explanation="add spacing")
|
|
589
|
+
assert rr.original_code == "x=1"
|
|
590
|
+
assert rr.refactored_code == "x = 1"
|
|
591
|
+
|
|
592
|
+
def test_to_dict(self):
|
|
593
|
+
rr = RefactorResult(file_path="a.py")
|
|
594
|
+
d = rr.to_dict()
|
|
595
|
+
assert "file_path" in d
|
|
596
|
+
assert "original_code" in d
|
|
597
|
+
assert "refactored_code" in d
|
|
598
|
+
assert "explanation" in d
|
|
599
|
+
|
|
600
|
+
def test_defaults(self):
|
|
601
|
+
rr = RefactorResult(file_path="b.py")
|
|
602
|
+
assert rr.original_code == ""
|
|
603
|
+
assert rr.refactored_code == ""
|
|
604
|
+
assert rr.explanation == ""
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class TestSuggestResult:
|
|
608
|
+
def test_create(self):
|
|
609
|
+
sr = SuggestResult(target="function_name")
|
|
610
|
+
assert sr.target == "function_name"
|
|
611
|
+
assert sr.suggestions == []
|
|
612
|
+
|
|
613
|
+
def test_to_dict(self):
|
|
614
|
+
sr = SuggestResult(target="x", suggestions=[{"text": "use typing"}])
|
|
615
|
+
d = sr.to_dict()
|
|
616
|
+
assert d["target"] == "x"
|
|
617
|
+
assert len(d["suggestions"]) == 1
|
|
618
|
+
|
|
619
|
+
def test_defaults(self):
|
|
620
|
+
sr = SuggestResult(target="t")
|
|
621
|
+
assert sr.llm_response is None
|
|
622
|
+
assert sr.explainability == {}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
# ==========================================================================
|
|
626
|
+
# LLM Investigation Data Types
|
|
627
|
+
# ==========================================================================
|
|
628
|
+
|
|
629
|
+
from semantic_code_intelligence.llm.investigation import InvestigationResult
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class TestInvestigationResult:
|
|
633
|
+
def test_create(self):
|
|
634
|
+
ir = InvestigationResult(question="why?", conclusion="because")
|
|
635
|
+
assert ir.question == "why?"
|
|
636
|
+
assert ir.conclusion == "because"
|
|
637
|
+
|
|
638
|
+
def test_to_dict(self):
|
|
639
|
+
ir = InvestigationResult(question="q", conclusion="c",
|
|
640
|
+
chain_id="ch1", total_steps=3,
|
|
641
|
+
steps=[{"action": "search", "result": "found"}])
|
|
642
|
+
d = ir.to_dict()
|
|
643
|
+
assert d["question"] == "q"
|
|
644
|
+
assert d["conclusion"] == "c"
|
|
645
|
+
assert d["chain_id"] == "ch1"
|
|
646
|
+
assert d["total_steps"] == 3
|
|
647
|
+
assert len(d["steps"]) == 1
|
|
648
|
+
|
|
649
|
+
def test_defaults(self):
|
|
650
|
+
ir = InvestigationResult(question="q", conclusion="c")
|
|
651
|
+
assert ir.steps == []
|
|
652
|
+
assert ir.chain_id == ""
|
|
653
|
+
assert ir.total_steps == 0
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ==========================================================================
|
|
657
|
+
# LLM Streaming
|
|
658
|
+
# ==========================================================================
|
|
659
|
+
|
|
660
|
+
from semantic_code_intelligence.llm.streaming import StreamEvent
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class TestStreamEventDeep:
|
|
664
|
+
def test_to_dict(self):
|
|
665
|
+
se = StreamEvent(kind="token", content="hello")
|
|
666
|
+
d = se.to_dict()
|
|
667
|
+
assert d["kind"] == "token"
|
|
668
|
+
assert d["content"] == "hello"
|
|
669
|
+
|
|
670
|
+
def test_to_sse(self):
|
|
671
|
+
se = StreamEvent(kind="token", content="hi")
|
|
672
|
+
sse = se.to_sse()
|
|
673
|
+
assert isinstance(sse, str)
|
|
674
|
+
|
|
675
|
+
def test_metadata(self):
|
|
676
|
+
se = StreamEvent(kind="done", metadata={"model": "gpt-4"})
|
|
677
|
+
d = se.to_dict()
|
|
678
|
+
assert d["metadata"]["model"] == "gpt-4"
|
|
679
|
+
|
|
680
|
+
def test_default_content(self):
|
|
681
|
+
se = StreamEvent(kind="start")
|
|
682
|
+
assert se.content == ""
|
|
683
|
+
|
|
684
|
+
def test_default_metadata(self):
|
|
685
|
+
se = StreamEvent(kind="end")
|
|
686
|
+
assert se.metadata == {}
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# ==========================================================================
|
|
690
|
+
# LLM Conversation
|
|
691
|
+
# ==========================================================================
|
|
692
|
+
|
|
693
|
+
from semantic_code_intelligence.llm.conversation import ConversationSession
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class TestConversationSessionDeep:
|
|
697
|
+
def test_create(self):
|
|
698
|
+
cs = ConversationSession()
|
|
699
|
+
assert cs.session_id != ""
|
|
700
|
+
assert cs.messages == []
|
|
701
|
+
|
|
702
|
+
def test_add_user(self):
|
|
703
|
+
cs = ConversationSession()
|
|
704
|
+
cs.add_user("hello")
|
|
705
|
+
assert len(cs.messages) == 1
|
|
706
|
+
assert cs.messages[0].role == MessageRole.USER
|
|
707
|
+
|
|
708
|
+
def test_add_assistant(self):
|
|
709
|
+
cs = ConversationSession()
|
|
710
|
+
cs.add_assistant("response")
|
|
711
|
+
assert cs.messages[0].role == MessageRole.ASSISTANT
|
|
712
|
+
|
|
713
|
+
def test_add_system(self):
|
|
714
|
+
cs = ConversationSession()
|
|
715
|
+
cs.add_system("system prompt")
|
|
716
|
+
assert cs.messages[0].role == MessageRole.SYSTEM
|
|
717
|
+
|
|
718
|
+
def test_turn_count(self):
|
|
719
|
+
cs = ConversationSession()
|
|
720
|
+
assert cs.turn_count == 0
|
|
721
|
+
cs.add_user("q1")
|
|
722
|
+
cs.add_assistant("a1")
|
|
723
|
+
assert cs.turn_count == 2 # counts individual user+assistant messages
|
|
724
|
+
|
|
725
|
+
def test_last_message(self):
|
|
726
|
+
cs = ConversationSession()
|
|
727
|
+
cs.add_user("first")
|
|
728
|
+
cs.add_assistant("second")
|
|
729
|
+
assert cs.last_message.content == "second"
|
|
730
|
+
|
|
731
|
+
def test_last_message_none(self):
|
|
732
|
+
cs = ConversationSession()
|
|
733
|
+
assert cs.last_message is None
|
|
734
|
+
|
|
735
|
+
def test_get_messages_for_llm(self):
|
|
736
|
+
cs = ConversationSession()
|
|
737
|
+
cs.add_user("q1")
|
|
738
|
+
cs.add_assistant("a1")
|
|
739
|
+
cs.add_user("q2")
|
|
740
|
+
msgs = cs.get_messages_for_llm()
|
|
741
|
+
assert len(msgs) == 3
|
|
742
|
+
|
|
743
|
+
def test_get_messages_for_llm_max_turns(self):
|
|
744
|
+
cs = ConversationSession()
|
|
745
|
+
for i in range(10):
|
|
746
|
+
cs.add_user(f"q{i}")
|
|
747
|
+
cs.add_assistant(f"a{i}")
|
|
748
|
+
msgs = cs.get_messages_for_llm(max_turns=2)
|
|
749
|
+
assert len(msgs) <= 4
|
|
750
|
+
|
|
751
|
+
def test_to_dict(self):
|
|
752
|
+
cs = ConversationSession()
|
|
753
|
+
cs.add_user("test")
|
|
754
|
+
d = cs.to_dict()
|
|
755
|
+
assert "session_id" in d
|
|
756
|
+
assert "messages" in d
|
|
757
|
+
assert len(d["messages"]) == 1
|
|
758
|
+
|
|
759
|
+
def test_from_dict_roundtrip(self):
|
|
760
|
+
cs = ConversationSession(title="test session")
|
|
761
|
+
cs.add_user("hello")
|
|
762
|
+
cs.add_assistant("world")
|
|
763
|
+
d = cs.to_dict()
|
|
764
|
+
cs2 = ConversationSession.from_dict(d)
|
|
765
|
+
assert cs2.title == "test session"
|
|
766
|
+
assert len(cs2.messages) == 2
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
# ==========================================================================
|
|
770
|
+
# Context Engine
|
|
771
|
+
# ==========================================================================
|
|
772
|
+
|
|
773
|
+
from semantic_code_intelligence.context.engine import (
|
|
774
|
+
ContextBuilder,
|
|
775
|
+
ContextWindow,
|
|
776
|
+
)
|
|
777
|
+
from semantic_code_intelligence.parsing.parser import Symbol
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _sym(name="test_fn", kind="function", file_path="test.py",
|
|
781
|
+
start_line=1, end_line=5, body="def test_fn(): pass"):
|
|
782
|
+
return Symbol(
|
|
783
|
+
name=name, kind=kind, file_path=file_path,
|
|
784
|
+
start_line=start_line, end_line=end_line,
|
|
785
|
+
start_col=0, end_col=0, body=body,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class TestContextWindow:
|
|
790
|
+
def test_create(self):
|
|
791
|
+
s = _sym()
|
|
792
|
+
cw = ContextWindow(focal_symbol=s)
|
|
793
|
+
assert cw.focal_symbol.name == "test_fn"
|
|
794
|
+
|
|
795
|
+
def test_to_dict(self):
|
|
796
|
+
cw = ContextWindow(focal_symbol=_sym(), related_symbols=[_sym("helper")])
|
|
797
|
+
d = cw.to_dict()
|
|
798
|
+
assert "focal_symbol" in d
|
|
799
|
+
|
|
800
|
+
def test_render(self):
|
|
801
|
+
cw = ContextWindow(focal_symbol=_sym(), file_content="def test_fn(): pass")
|
|
802
|
+
text = cw.render()
|
|
803
|
+
assert isinstance(text, str)
|
|
804
|
+
|
|
805
|
+
def test_defaults(self):
|
|
806
|
+
cw = ContextWindow(focal_symbol=_sym())
|
|
807
|
+
assert cw.related_symbols == []
|
|
808
|
+
assert cw.imports == []
|
|
809
|
+
assert cw.file_content == ""
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
class TestContextBuilder:
|
|
813
|
+
def test_create(self):
|
|
814
|
+
cb = ContextBuilder()
|
|
815
|
+
assert cb is not None
|
|
816
|
+
|
|
817
|
+
def test_get_symbols_empty(self):
|
|
818
|
+
cb = ContextBuilder()
|
|
819
|
+
syms = cb.get_symbols("nonexistent.py")
|
|
820
|
+
assert syms == []
|
|
821
|
+
|
|
822
|
+
def test_get_all_symbols_empty(self):
|
|
823
|
+
cb = ContextBuilder()
|
|
824
|
+
syms = cb.get_all_symbols()
|
|
825
|
+
assert syms == []
|
|
826
|
+
|
|
827
|
+
def test_find_symbol_empty(self):
|
|
828
|
+
cb = ContextBuilder()
|
|
829
|
+
matches = cb.find_symbol("nonexistent")
|
|
830
|
+
assert matches == []
|
|
831
|
+
|
|
832
|
+
def test_index_file(self):
|
|
833
|
+
cb = ContextBuilder()
|
|
834
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py",
|
|
835
|
+
delete=False, encoding="utf-8") as f:
|
|
836
|
+
f.write("def hello():\n pass\n\nclass World:\n pass\n")
|
|
837
|
+
f.flush()
|
|
838
|
+
syms = cb.index_file(f.name)
|
|
839
|
+
assert isinstance(syms, list)
|
|
840
|
+
|
|
841
|
+
def test_find_after_index(self):
|
|
842
|
+
cb = ContextBuilder()
|
|
843
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py",
|
|
844
|
+
delete=False, encoding="utf-8") as f:
|
|
845
|
+
f.write("def my_unique_fn():\n return 42\n")
|
|
846
|
+
f.flush()
|
|
847
|
+
cb.index_file(f.name)
|
|
848
|
+
matches = cb.find_symbol("my_unique_fn")
|
|
849
|
+
assert len(matches) >= 1
|
|
850
|
+
assert matches[0].name == "my_unique_fn"
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
# ==========================================================================
|
|
854
|
+
# Context Memory
|
|
855
|
+
# ==========================================================================
|
|
856
|
+
|
|
857
|
+
from semantic_code_intelligence.context.memory import (
|
|
858
|
+
MemoryEntry,
|
|
859
|
+
ReasoningStep,
|
|
860
|
+
SessionMemory,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
class TestMemoryEntry:
|
|
865
|
+
def test_create(self):
|
|
866
|
+
me = MemoryEntry(key="k1", content="stuff")
|
|
867
|
+
assert me.key == "k1"
|
|
868
|
+
assert me.content == "stuff"
|
|
869
|
+
|
|
870
|
+
def test_to_dict(self):
|
|
871
|
+
me = MemoryEntry(key="k", content="v", kind="fact")
|
|
872
|
+
d = me.to_dict()
|
|
873
|
+
assert d["key"] == "k"
|
|
874
|
+
assert d["kind"] == "fact"
|
|
875
|
+
|
|
876
|
+
def test_from_dict(self):
|
|
877
|
+
d = {"key": "a", "content": "b", "kind": "general", "timestamp": 0.0, "metadata": {}}
|
|
878
|
+
me = MemoryEntry.from_dict(d)
|
|
879
|
+
assert me.key == "a"
|
|
880
|
+
|
|
881
|
+
def test_default_kind(self):
|
|
882
|
+
me = MemoryEntry(key="x", content="y")
|
|
883
|
+
assert me.kind == "general"
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
class TestReasoningStep:
|
|
887
|
+
def test_create(self):
|
|
888
|
+
rs = ReasoningStep(step_id=1, action="search", input_text="query",
|
|
889
|
+
output_text="results")
|
|
890
|
+
assert rs.step_id == 1
|
|
891
|
+
assert rs.action == "search"
|
|
892
|
+
|
|
893
|
+
def test_to_dict(self):
|
|
894
|
+
rs = ReasoningStep(step_id=0, action="analyze", input_text="i",
|
|
895
|
+
output_text="o")
|
|
896
|
+
d = rs.to_dict()
|
|
897
|
+
assert d["step_id"] == 0
|
|
898
|
+
assert d["action"] == "analyze"
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
class TestSessionMemory:
|
|
902
|
+
def test_create(self):
|
|
903
|
+
sm = SessionMemory()
|
|
904
|
+
assert len(sm.entries) == 0
|
|
905
|
+
|
|
906
|
+
def test_add(self):
|
|
907
|
+
sm = SessionMemory()
|
|
908
|
+
entry = sm.add("k1", "content1")
|
|
909
|
+
assert isinstance(entry, MemoryEntry)
|
|
910
|
+
assert len(sm.entries) == 1
|
|
911
|
+
|
|
912
|
+
def test_search(self):
|
|
913
|
+
sm = SessionMemory()
|
|
914
|
+
sm.add("python", "Python is a programming language")
|
|
915
|
+
sm.add("java", "Java is a compiled language")
|
|
916
|
+
results = sm.search("python", limit=1)
|
|
917
|
+
assert isinstance(results, list)
|
|
918
|
+
|
|
919
|
+
def test_get_recent(self):
|
|
920
|
+
sm = SessionMemory()
|
|
921
|
+
for i in range(5):
|
|
922
|
+
sm.add(f"k{i}", f"content{i}")
|
|
923
|
+
recent = sm.get_recent(limit=3)
|
|
924
|
+
assert len(recent) == 3
|
|
925
|
+
|
|
926
|
+
def test_clear(self):
|
|
927
|
+
sm = SessionMemory()
|
|
928
|
+
sm.add("k", "v")
|
|
929
|
+
sm.clear()
|
|
930
|
+
assert len(sm.entries) == 0
|
|
931
|
+
|
|
932
|
+
def test_start_chain(self):
|
|
933
|
+
sm = SessionMemory()
|
|
934
|
+
sm.start_chain("ch1")
|
|
935
|
+
# No error means success
|
|
936
|
+
|
|
937
|
+
def test_add_step(self):
|
|
938
|
+
sm = SessionMemory()
|
|
939
|
+
sm.start_chain("ch1")
|
|
940
|
+
step = sm.add_step("ch1", "search", "query", "results")
|
|
941
|
+
assert isinstance(step, ReasoningStep)
|
|
942
|
+
|
|
943
|
+
def test_get_chain(self):
|
|
944
|
+
sm = SessionMemory()
|
|
945
|
+
sm.start_chain("ch1")
|
|
946
|
+
sm.add_step("ch1", "search", "q", "r")
|
|
947
|
+
sm.add_step("ch1", "analyze", "sym", "ctx")
|
|
948
|
+
chain = sm.get_chain("ch1")
|
|
949
|
+
assert len(chain) == 2
|
|
950
|
+
|
|
951
|
+
def test_to_dict(self):
|
|
952
|
+
sm = SessionMemory()
|
|
953
|
+
sm.add("k", "v")
|
|
954
|
+
d = sm.to_dict()
|
|
955
|
+
assert isinstance(d, dict)
|
|
956
|
+
|
|
957
|
+
def test_max_entries(self):
|
|
958
|
+
sm = SessionMemory(max_entries=3)
|
|
959
|
+
for i in range(5):
|
|
960
|
+
sm.add(f"k{i}", f"v{i}")
|
|
961
|
+
assert len(sm.entries) <= 3
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
# ==========================================================================
|
|
965
|
+
# Analysis / AI Features
|
|
966
|
+
# ==========================================================================
|
|
967
|
+
|
|
968
|
+
from semantic_code_intelligence.analysis.ai_features import (
|
|
969
|
+
LanguageStats,
|
|
970
|
+
RepoSummary,
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
class TestLanguageStats:
|
|
975
|
+
def test_create(self):
|
|
976
|
+
ls = LanguageStats(language="python")
|
|
977
|
+
assert ls.language == "python"
|
|
978
|
+
assert ls.file_count == 0
|
|
979
|
+
|
|
980
|
+
def test_to_dict(self):
|
|
981
|
+
ls = LanguageStats(language="javascript", file_count=10, function_count=50)
|
|
982
|
+
d = ls.to_dict()
|
|
983
|
+
assert d["language"] == "javascript"
|
|
984
|
+
assert d["file_count"] == 10
|
|
985
|
+
assert d["function_count"] == 50
|
|
986
|
+
|
|
987
|
+
def test_defaults(self):
|
|
988
|
+
ls = LanguageStats(language="go")
|
|
989
|
+
assert ls.class_count == 0
|
|
990
|
+
assert ls.method_count == 0
|
|
991
|
+
assert ls.import_count == 0
|
|
992
|
+
assert ls.total_lines == 0
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
class TestRepoSummary:
|
|
996
|
+
def test_create(self):
|
|
997
|
+
rs = RepoSummary()
|
|
998
|
+
assert rs.total_files == 0
|
|
999
|
+
assert rs.total_symbols == 0
|
|
1000
|
+
|
|
1001
|
+
def test_to_dict(self):
|
|
1002
|
+
rs = RepoSummary(total_files=5, total_functions=20)
|
|
1003
|
+
d = rs.to_dict()
|
|
1004
|
+
assert d["total_files"] == 5
|
|
1005
|
+
assert d["total_functions"] == 20
|
|
1006
|
+
|
|
1007
|
+
def test_to_json(self):
|
|
1008
|
+
rs = RepoSummary()
|
|
1009
|
+
j = rs.to_json()
|
|
1010
|
+
parsed = json.loads(j)
|
|
1011
|
+
assert "total_files" in parsed
|
|
1012
|
+
|
|
1013
|
+
def test_render(self):
|
|
1014
|
+
rs = RepoSummary(total_files=3, total_functions=10)
|
|
1015
|
+
text = rs.render()
|
|
1016
|
+
assert "3" in text
|
|
1017
|
+
assert "Repository Summary" in text
|
|
1018
|
+
|
|
1019
|
+
def test_render_with_languages(self):
|
|
1020
|
+
ls = LanguageStats(language="python", file_count=5, function_count=20, class_count=3)
|
|
1021
|
+
rs = RepoSummary(total_files=5, languages=[ls])
|
|
1022
|
+
text = rs.render()
|
|
1023
|
+
assert "python" in text
|
|
1024
|
+
|
|
1025
|
+
def test_defaults(self):
|
|
1026
|
+
rs = RepoSummary()
|
|
1027
|
+
assert rs.languages == []
|
|
1028
|
+
assert rs.top_functions == []
|
|
1029
|
+
assert rs.top_classes == []
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
# ==========================================================================
|
|
1033
|
+
# Indexing - Chunker
|
|
1034
|
+
# ==========================================================================
|
|
1035
|
+
|
|
1036
|
+
from semantic_code_intelligence.indexing.chunker import (
|
|
1037
|
+
CodeChunk,
|
|
1038
|
+
chunk_code,
|
|
1039
|
+
detect_language as chunker_detect_language,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
class TestCodeChunk:
|
|
1044
|
+
def test_create(self):
|
|
1045
|
+
cc = CodeChunk(file_path="a.py", content="code", start_line=1,
|
|
1046
|
+
end_line=10, chunk_index=0, language="python")
|
|
1047
|
+
assert cc.file_path == "a.py"
|
|
1048
|
+
assert cc.language == "python"
|
|
1049
|
+
|
|
1050
|
+
def test_is_dataclass(self):
|
|
1051
|
+
assert is_dataclass(CodeChunk)
|
|
1052
|
+
|
|
1053
|
+
def test_fields(self):
|
|
1054
|
+
names = {f.name for f in fields(CodeChunk)}
|
|
1055
|
+
assert "file_path" in names
|
|
1056
|
+
assert "content" in names
|
|
1057
|
+
assert "start_line" in names
|
|
1058
|
+
assert "end_line" in names
|
|
1059
|
+
assert "chunk_index" in names
|
|
1060
|
+
assert "language" in names
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
class TestChunkCode:
|
|
1064
|
+
def test_basic(self):
|
|
1065
|
+
code = "line1\nline2\nline3\nline4\nline5\n" * 50
|
|
1066
|
+
chunks = chunk_code(code, "test.py", chunk_size=100, chunk_overlap=10)
|
|
1067
|
+
assert len(chunks) >= 1
|
|
1068
|
+
assert all(isinstance(c, CodeChunk) for c in chunks)
|
|
1069
|
+
|
|
1070
|
+
def test_empty(self):
|
|
1071
|
+
chunks = chunk_code("", "empty.py")
|
|
1072
|
+
assert isinstance(chunks, list)
|
|
1073
|
+
|
|
1074
|
+
def test_small_file(self):
|
|
1075
|
+
chunks = chunk_code("x = 1\n", "small.py")
|
|
1076
|
+
assert len(chunks) >= 1
|
|
1077
|
+
|
|
1078
|
+
def test_chunk_index_sequential(self):
|
|
1079
|
+
code = "x = 1\n" * 200
|
|
1080
|
+
chunks = chunk_code(code, "test.py", chunk_size=50)
|
|
1081
|
+
if len(chunks) > 1:
|
|
1082
|
+
for i, c in enumerate(chunks):
|
|
1083
|
+
assert c.chunk_index == i
|
|
1084
|
+
|
|
1085
|
+
def test_language_detection(self):
|
|
1086
|
+
chunks = chunk_code("def foo(): pass", "test.py")
|
|
1087
|
+
if chunks:
|
|
1088
|
+
assert chunks[0].language == "python"
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
class TestChunkerDetectLanguage:
|
|
1092
|
+
def test_python(self):
|
|
1093
|
+
assert chunker_detect_language("test.py") == "python"
|
|
1094
|
+
|
|
1095
|
+
def test_javascript(self):
|
|
1096
|
+
assert chunker_detect_language("app.js") == "javascript"
|
|
1097
|
+
|
|
1098
|
+
def test_typescript(self):
|
|
1099
|
+
assert chunker_detect_language("main.ts") == "typescript"
|
|
1100
|
+
|
|
1101
|
+
def test_java(self):
|
|
1102
|
+
assert chunker_detect_language("Main.java") == "java"
|
|
1103
|
+
|
|
1104
|
+
def test_go(self):
|
|
1105
|
+
assert chunker_detect_language("main.go") == "go"
|
|
1106
|
+
|
|
1107
|
+
def test_rust(self):
|
|
1108
|
+
assert chunker_detect_language("lib.rs") == "rust"
|
|
1109
|
+
|
|
1110
|
+
def test_c(self):
|
|
1111
|
+
assert chunker_detect_language("main.c") == "c"
|
|
1112
|
+
|
|
1113
|
+
def test_cpp(self):
|
|
1114
|
+
assert chunker_detect_language("main.cpp") == "cpp"
|
|
1115
|
+
|
|
1116
|
+
def test_ruby(self):
|
|
1117
|
+
assert chunker_detect_language("app.rb") == "ruby"
|
|
1118
|
+
|
|
1119
|
+
def test_php(self):
|
|
1120
|
+
assert chunker_detect_language("index.php") == "php"
|
|
1121
|
+
|
|
1122
|
+
def test_csharp(self):
|
|
1123
|
+
assert chunker_detect_language("Program.cs") == "csharp"
|
|
1124
|
+
|
|
1125
|
+
def test_swift(self):
|
|
1126
|
+
assert chunker_detect_language("vc.swift") == "swift"
|
|
1127
|
+
|
|
1128
|
+
def test_kotlin(self):
|
|
1129
|
+
assert chunker_detect_language("Main.kt") == "kotlin"
|
|
1130
|
+
|
|
1131
|
+
def test_scala(self):
|
|
1132
|
+
assert chunker_detect_language("App.scala") == "scala"
|
|
1133
|
+
|
|
1134
|
+
def test_unknown(self):
|
|
1135
|
+
result = chunker_detect_language("readme.md")
|
|
1136
|
+
assert result == "unknown" or result is None
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
# ==========================================================================
|
|
1140
|
+
# Indexing - Scanner
|
|
1141
|
+
# ==========================================================================
|
|
1142
|
+
|
|
1143
|
+
from semantic_code_intelligence.indexing.scanner import (
|
|
1144
|
+
ScannedFile,
|
|
1145
|
+
compute_file_hash,
|
|
1146
|
+
should_ignore,
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class TestComputeFileHash:
|
|
1151
|
+
def test_basic(self):
|
|
1152
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py",
|
|
1153
|
+
delete=False, encoding="utf-8") as f:
|
|
1154
|
+
f.write("x = 1\n")
|
|
1155
|
+
f.flush()
|
|
1156
|
+
h = compute_file_hash(Path(f.name))
|
|
1157
|
+
assert isinstance(h, str)
|
|
1158
|
+
assert len(h) > 0
|
|
1159
|
+
|
|
1160
|
+
def test_deterministic(self):
|
|
1161
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py",
|
|
1162
|
+
delete=False, encoding="utf-8") as f:
|
|
1163
|
+
f.write("deterministic content\n")
|
|
1164
|
+
f.flush()
|
|
1165
|
+
h1 = compute_file_hash(Path(f.name))
|
|
1166
|
+
h2 = compute_file_hash(Path(f.name))
|
|
1167
|
+
assert h1 == h2
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
class TestShouldIgnoreDeep:
|
|
1171
|
+
def test_git_dir(self):
|
|
1172
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1173
|
+
root = Path(tmp)
|
|
1174
|
+
git_file = root / ".git" / "config"
|
|
1175
|
+
git_file.parent.mkdir()
|
|
1176
|
+
git_file.touch()
|
|
1177
|
+
assert should_ignore(git_file, root, {".git"}) is True
|
|
1178
|
+
|
|
1179
|
+
def test_normal_file(self):
|
|
1180
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1181
|
+
root = Path(tmp)
|
|
1182
|
+
f = root / "src" / "main.py"
|
|
1183
|
+
f.parent.mkdir()
|
|
1184
|
+
f.touch()
|
|
1185
|
+
assert should_ignore(f, root, {".git"}) is False
|
|
1186
|
+
|
|
1187
|
+
def test_node_modules(self):
|
|
1188
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1189
|
+
root = Path(tmp)
|
|
1190
|
+
f = root / "node_modules" / "pkg" / "index.js"
|
|
1191
|
+
f.parent.mkdir(parents=True)
|
|
1192
|
+
f.touch()
|
|
1193
|
+
assert should_ignore(f, root, {"node_modules"}) is True
|
|
1194
|
+
|
|
1195
|
+
def test_pycache(self):
|
|
1196
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1197
|
+
root = Path(tmp)
|
|
1198
|
+
f = root / "__pycache__" / "mod.pyc"
|
|
1199
|
+
f.parent.mkdir()
|
|
1200
|
+
f.touch()
|
|
1201
|
+
assert should_ignore(f, root, {"__pycache__"}) is True
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
class TestScannedFileDeep:
|
|
1205
|
+
def test_create(self):
|
|
1206
|
+
sf = ScannedFile(path=Path("a.py"), relative_path="a.py",
|
|
1207
|
+
extension=".py", size_bytes=100, content_hash="abc")
|
|
1208
|
+
assert sf.relative_path == "a.py"
|
|
1209
|
+
assert sf.extension == ".py"
|
|
1210
|
+
assert sf.size_bytes == 100
|
|
1211
|
+
|
|
1212
|
+
def test_is_dataclass(self):
|
|
1213
|
+
assert is_dataclass(ScannedFile)
|
|
1214
|
+
|
|
1215
|
+
def test_fields(self):
|
|
1216
|
+
names = {f.name for f in fields(ScannedFile)}
|
|
1217
|
+
assert "path" in names
|
|
1218
|
+
assert "relative_path" in names
|
|
1219
|
+
assert "extension" in names
|
|
1220
|
+
assert "size_bytes" in names
|
|
1221
|
+
assert "content_hash" in names
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
# ==========================================================================
|
|
1225
|
+
# Parsing - Symbol
|
|
1226
|
+
# ==========================================================================
|
|
1227
|
+
|
|
1228
|
+
from semantic_code_intelligence.parsing.parser import (
|
|
1229
|
+
Symbol as ParserSymbol,
|
|
1230
|
+
parse_file,
|
|
1231
|
+
detect_language as parser_detect_language,
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class TestParserSymbol:
|
|
1236
|
+
def test_create(self):
|
|
1237
|
+
s = _sym()
|
|
1238
|
+
assert s.name == "test_fn"
|
|
1239
|
+
|
|
1240
|
+
def test_to_dict(self):
|
|
1241
|
+
s = _sym(name="foo", kind="class")
|
|
1242
|
+
d = s.to_dict()
|
|
1243
|
+
assert d["name"] == "foo"
|
|
1244
|
+
assert d["kind"] == "class"
|
|
1245
|
+
|
|
1246
|
+
def test_parent(self):
|
|
1247
|
+
s = _sym()
|
|
1248
|
+
assert s.parent is None
|
|
1249
|
+
|
|
1250
|
+
def test_with_parent(self):
|
|
1251
|
+
s = Symbol(name="method", kind="method", file_path="a.py",
|
|
1252
|
+
start_line=5, end_line=10, start_col=4, end_col=0,
|
|
1253
|
+
body="def method(): pass", parent="MyClass")
|
|
1254
|
+
assert s.parent == "MyClass"
|
|
1255
|
+
|
|
1256
|
+
def test_decorators(self):
|
|
1257
|
+
s = Symbol(name="fn", kind="function", file_path="a.py",
|
|
1258
|
+
start_line=1, end_line=3, start_col=0, end_col=0,
|
|
1259
|
+
body="@staticmethod\ndef fn(): pass",
|
|
1260
|
+
decorators=["staticmethod"])
|
|
1261
|
+
assert "staticmethod" in s.decorators
|
|
1262
|
+
|
|
1263
|
+
def test_parameters(self):
|
|
1264
|
+
s = Symbol(name="fn", kind="function", file_path="a.py",
|
|
1265
|
+
start_line=1, end_line=2, start_col=0, end_col=0,
|
|
1266
|
+
body="def fn(x, y): pass",
|
|
1267
|
+
parameters=["x", "y"])
|
|
1268
|
+
assert s.parameters == ["x", "y"]
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
class TestParseFile:
|
|
1272
|
+
def test_python_file(self):
|
|
1273
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py",
|
|
1274
|
+
delete=False, encoding="utf-8") as f:
|
|
1275
|
+
f.write("def greet(name):\n print(f'Hello {name}')\n\nclass Greeter:\n pass\n")
|
|
1276
|
+
f.flush()
|
|
1277
|
+
symbols = parse_file(f.name)
|
|
1278
|
+
assert len(symbols) >= 2
|
|
1279
|
+
names = [s.name for s in symbols]
|
|
1280
|
+
assert "greet" in names
|
|
1281
|
+
assert "Greeter" in names
|
|
1282
|
+
|
|
1283
|
+
def test_empty_file(self):
|
|
1284
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py",
|
|
1285
|
+
delete=False, encoding="utf-8") as f:
|
|
1286
|
+
f.write("")
|
|
1287
|
+
f.flush()
|
|
1288
|
+
symbols = parse_file(f.name)
|
|
1289
|
+
assert isinstance(symbols, list)
|
|
1290
|
+
|
|
1291
|
+
def test_with_content(self):
|
|
1292
|
+
symbols = parse_file("virtual.py", content="class Foo:\n pass\n")
|
|
1293
|
+
assert any(s.name == "Foo" for s in symbols)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
class TestParserDetectLanguage:
|
|
1297
|
+
def test_python(self):
|
|
1298
|
+
assert parser_detect_language("test.py") == "python"
|
|
1299
|
+
|
|
1300
|
+
def test_javascript(self):
|
|
1301
|
+
assert parser_detect_language("app.js") == "javascript"
|
|
1302
|
+
|
|
1303
|
+
def test_none_for_unknown(self):
|
|
1304
|
+
result = parser_detect_language("file.xyz123")
|
|
1305
|
+
assert result is None or result == "unknown"
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
# ==========================================================================
|
|
1309
|
+
# Storage - Hash Store
|
|
1310
|
+
# ==========================================================================
|
|
1311
|
+
|
|
1312
|
+
from semantic_code_intelligence.storage.hash_store import HashStore
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
class TestHashStoreExtended:
|
|
1316
|
+
def test_get_existing(self):
|
|
1317
|
+
hs = HashStore()
|
|
1318
|
+
hs.set("a.py", "hash1")
|
|
1319
|
+
assert hs.get("a.py") == "hash1"
|
|
1320
|
+
|
|
1321
|
+
def test_get_missing(self):
|
|
1322
|
+
hs = HashStore()
|
|
1323
|
+
assert hs.get("nonexistent.py") is None
|
|
1324
|
+
|
|
1325
|
+
def test_remove(self):
|
|
1326
|
+
hs = HashStore()
|
|
1327
|
+
hs.set("a.py", "h1")
|
|
1328
|
+
hs.remove("a.py")
|
|
1329
|
+
assert hs.get("a.py") is None
|
|
1330
|
+
assert hs.count == 0
|
|
1331
|
+
|
|
1332
|
+
def test_remove_nonexistent(self):
|
|
1333
|
+
hs = HashStore()
|
|
1334
|
+
hs.remove("nope") # Should not raise
|
|
1335
|
+
|
|
1336
|
+
def test_overwrite(self):
|
|
1337
|
+
hs = HashStore()
|
|
1338
|
+
hs.set("a.py", "h1")
|
|
1339
|
+
hs.set("a.py", "h2")
|
|
1340
|
+
assert hs.get("a.py") == "h2"
|
|
1341
|
+
assert hs.count == 1
|
|
1342
|
+
|
|
1343
|
+
def test_count(self):
|
|
1344
|
+
hs = HashStore()
|
|
1345
|
+
assert hs.count == 0
|
|
1346
|
+
hs.set("a.py", "h1")
|
|
1347
|
+
hs.set("b.py", "h2")
|
|
1348
|
+
assert hs.count == 2
|
|
1349
|
+
|
|
1350
|
+
def test_has_changed_new_file(self):
|
|
1351
|
+
hs = HashStore()
|
|
1352
|
+
assert hs.has_changed("new.py", "anyhash") is True
|
|
1353
|
+
|
|
1354
|
+
def test_has_changed_same_hash(self):
|
|
1355
|
+
hs = HashStore()
|
|
1356
|
+
hs.set("a.py", "h1")
|
|
1357
|
+
assert hs.has_changed("a.py", "h1") is False
|
|
1358
|
+
|
|
1359
|
+
def test_has_changed_different_hash(self):
|
|
1360
|
+
hs = HashStore()
|
|
1361
|
+
hs.set("a.py", "h1")
|
|
1362
|
+
assert hs.has_changed("a.py", "h2") is True
|
|
1363
|
+
|
|
1364
|
+
def test_save_load_roundtrip(self):
|
|
1365
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1366
|
+
hs = HashStore()
|
|
1367
|
+
hs.set("a.py", "h1")
|
|
1368
|
+
hs.set("b.py", "h2")
|
|
1369
|
+
hs.save(Path(tmp))
|
|
1370
|
+
|
|
1371
|
+
hs2 = HashStore.load(Path(tmp))
|
|
1372
|
+
assert hs2.get("a.py") == "h1"
|
|
1373
|
+
assert hs2.get("b.py") == "h2"
|
|
1374
|
+
assert hs2.count == 2
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
# ==========================================================================
|
|
1378
|
+
# Storage - Vector Store
|
|
1379
|
+
# ==========================================================================
|
|
1380
|
+
|
|
1381
|
+
from semantic_code_intelligence.storage.vector_store import VectorStore
|
|
1382
|
+
from semantic_code_intelligence.storage.vector_store import ChunkMetadata
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
class TestVectorStoreExtended:
|
|
1386
|
+
def test_create(self):
|
|
1387
|
+
vs = VectorStore(dimension=8)
|
|
1388
|
+
assert vs.size == 0
|
|
1389
|
+
assert vs.dimension == 8
|
|
1390
|
+
|
|
1391
|
+
def test_add_and_size(self):
|
|
1392
|
+
import numpy as np
|
|
1393
|
+
vs = VectorStore(dimension=4)
|
|
1394
|
+
emb = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
1395
|
+
meta = ChunkMetadata(file_path="a.py", start_line=1, end_line=2,
|
|
1396
|
+
chunk_index=0, content="test", language="python")
|
|
1397
|
+
vs.add(emb, [meta])
|
|
1398
|
+
assert vs.size == 1
|
|
1399
|
+
|
|
1400
|
+
def test_search_returns_list(self):
|
|
1401
|
+
import numpy as np
|
|
1402
|
+
vs = VectorStore(dimension=4)
|
|
1403
|
+
emb = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
1404
|
+
meta = ChunkMetadata(file_path="a.py", start_line=1, end_line=2,
|
|
1405
|
+
chunk_index=0, content="code", language="python")
|
|
1406
|
+
vs.add(emb, [meta])
|
|
1407
|
+
query = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
1408
|
+
results = vs.search(query, top_k=1)
|
|
1409
|
+
assert isinstance(results, list)
|
|
1410
|
+
assert len(results) >= 1
|
|
1411
|
+
|
|
1412
|
+
def test_search_empty(self):
|
|
1413
|
+
import numpy as np
|
|
1414
|
+
vs = VectorStore(dimension=4)
|
|
1415
|
+
query = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
1416
|
+
results = vs.search(query, top_k=5)
|
|
1417
|
+
assert results == []
|
|
1418
|
+
|
|
1419
|
+
def test_save_load(self):
|
|
1420
|
+
import numpy as np
|
|
1421
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1422
|
+
vs = VectorStore(dimension=4)
|
|
1423
|
+
emb = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
1424
|
+
meta = ChunkMetadata(file_path="x.py", start_line=1, end_line=5,
|
|
1425
|
+
chunk_index=0, content="code", language="python")
|
|
1426
|
+
vs.add(emb, [meta])
|
|
1427
|
+
vs.save(Path(tmp))
|
|
1428
|
+
|
|
1429
|
+
vs2 = VectorStore.load(Path(tmp))
|
|
1430
|
+
assert vs2.size == 1
|
|
1431
|
+
assert vs2.dimension == 4
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
# ==========================================================================
|
|
1435
|
+
# Workspace
|
|
1436
|
+
# ==========================================================================
|
|
1437
|
+
|
|
1438
|
+
from semantic_code_intelligence.workspace import (
|
|
1439
|
+
RepoEntry,
|
|
1440
|
+
WorkspaceManifest,
|
|
1441
|
+
Workspace,
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
class TestRepoEntryExtended:
|
|
1446
|
+
def test_from_dict(self):
|
|
1447
|
+
d = {"name": "myrepo", "path": "/path"}
|
|
1448
|
+
re = RepoEntry.from_dict(d)
|
|
1449
|
+
assert re.name == "myrepo"
|
|
1450
|
+
assert re.path == "/path"
|
|
1451
|
+
|
|
1452
|
+
def test_from_dict_with_extras(self):
|
|
1453
|
+
d = {"name": "r", "path": "/r", "last_indexed": 1.0, "file_count": 5, "vector_count": 10}
|
|
1454
|
+
re = RepoEntry.from_dict(d)
|
|
1455
|
+
assert re.last_indexed == 1.0
|
|
1456
|
+
assert re.file_count == 5
|
|
1457
|
+
assert re.vector_count == 10
|
|
1458
|
+
|
|
1459
|
+
def test_roundtrip(self):
|
|
1460
|
+
re = RepoEntry(name="test", path="/test", file_count=3)
|
|
1461
|
+
d = re.to_dict()
|
|
1462
|
+
re2 = RepoEntry.from_dict(d)
|
|
1463
|
+
assert re2.name == re.name
|
|
1464
|
+
assert re2.file_count == re.file_count
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
class TestWorkspaceManifestExtended:
|
|
1468
|
+
def test_from_dict(self):
|
|
1469
|
+
d = {"version": "2.0.0", "repos": [{"name": "r1", "path": "/r1"}]}
|
|
1470
|
+
wm = WorkspaceManifest.from_dict(d)
|
|
1471
|
+
assert wm.version == "2.0.0"
|
|
1472
|
+
assert len(wm.repos) == 1
|
|
1473
|
+
|
|
1474
|
+
def test_roundtrip(self):
|
|
1475
|
+
wm = WorkspaceManifest(repos=[RepoEntry(name="a", path="/a")])
|
|
1476
|
+
d = wm.to_dict()
|
|
1477
|
+
wm2 = WorkspaceManifest.from_dict(d)
|
|
1478
|
+
assert len(wm2.repos) == 1
|
|
1479
|
+
assert wm2.repos[0].name == "a"
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
class TestWorkspaceExtended:
|
|
1483
|
+
def test_properties(self):
|
|
1484
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1485
|
+
ws = Workspace(Path(tmp))
|
|
1486
|
+
assert ws.root == Path(tmp).resolve()
|
|
1487
|
+
assert ws.config_dir.name == ".codexa"
|
|
1488
|
+
assert ws.repos_dir.name == "repos"
|
|
1489
|
+
|
|
1490
|
+
def test_add_repo(self):
|
|
1491
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1492
|
+
ws = Workspace(Path(tmp))
|
|
1493
|
+
entry = ws.add_repo("myrepo", Path(tmp))
|
|
1494
|
+
assert entry.name == "myrepo"
|
|
1495
|
+
assert len(ws.repos) == 1
|
|
1496
|
+
|
|
1497
|
+
def test_get_repo(self):
|
|
1498
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1499
|
+
ws = Workspace(Path(tmp))
|
|
1500
|
+
ws.add_repo("r1", Path(tmp))
|
|
1501
|
+
found = ws.get_repo("r1")
|
|
1502
|
+
assert found is not None
|
|
1503
|
+
assert found.name == "r1"
|
|
1504
|
+
|
|
1505
|
+
def test_get_repo_missing(self):
|
|
1506
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1507
|
+
ws = Workspace(Path(tmp))
|
|
1508
|
+
assert ws.get_repo("nonexistent") is None
|
|
1509
|
+
|
|
1510
|
+
def test_add_duplicate_raises(self):
|
|
1511
|
+
import pytest
|
|
1512
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1513
|
+
ws = Workspace(Path(tmp))
|
|
1514
|
+
ws.add_repo("r1", Path(tmp))
|
|
1515
|
+
with pytest.raises(ValueError):
|
|
1516
|
+
ws.add_repo("r1", Path(tmp))
|
|
1517
|
+
|
|
1518
|
+
def test_save_and_load(self):
|
|
1519
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1520
|
+
ws = Workspace(Path(tmp))
|
|
1521
|
+
ws.add_repo("myrepo", Path(tmp))
|
|
1522
|
+
ws.save()
|
|
1523
|
+
|
|
1524
|
+
ws2 = Workspace.load(Path(tmp))
|
|
1525
|
+
assert len(ws2.repos) == 1
|
|
1526
|
+
assert ws2.repos[0].name == "myrepo"
|
|
1527
|
+
|
|
1528
|
+
def test_load_or_create(self):
|
|
1529
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1530
|
+
ws = Workspace.load_or_create(Path(tmp))
|
|
1531
|
+
assert ws is not None
|
|
1532
|
+
assert ws.root == Path(tmp).resolve()
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
# ==========================================================================
|
|
1536
|
+
# Daemon / Watcher
|
|
1537
|
+
# ==========================================================================
|
|
1538
|
+
|
|
1539
|
+
from semantic_code_intelligence.daemon.watcher import FileChangeEvent, FileWatcher
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class TestFileChangeEventExtended:
|
|
1543
|
+
def test_default_timestamp(self):
|
|
1544
|
+
ev = FileChangeEvent(path=Path("a.py"), relative_path="a.py",
|
|
1545
|
+
change_type="created")
|
|
1546
|
+
assert ev.timestamp == 0.0
|
|
1547
|
+
|
|
1548
|
+
def test_custom_timestamp(self):
|
|
1549
|
+
ev = FileChangeEvent(path=Path("b.py"), relative_path="b.py",
|
|
1550
|
+
change_type="modified", timestamp=123.456)
|
|
1551
|
+
assert ev.timestamp == 123.456
|
|
1552
|
+
|
|
1553
|
+
def test_change_types(self):
|
|
1554
|
+
for ct in ("created", "modified", "deleted"):
|
|
1555
|
+
ev = FileChangeEvent(path=Path("x"), relative_path="x", change_type=ct)
|
|
1556
|
+
assert ev.change_type == ct
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
class TestFileWatcherExtended:
|
|
1560
|
+
def test_not_running(self):
|
|
1561
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1562
|
+
fw = FileWatcher(Path(tmp))
|
|
1563
|
+
assert fw.is_running is False
|
|
1564
|
+
|
|
1565
|
+
def test_on_change_callback(self):
|
|
1566
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1567
|
+
fw = FileWatcher(Path(tmp))
|
|
1568
|
+
callback = MagicMock()
|
|
1569
|
+
fw.on_change(callback)
|
|
1570
|
+
# Callback registered, no error
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
# ==========================================================================
|
|
1574
|
+
# Semantic Chunker
|
|
1575
|
+
# ==========================================================================
|
|
1576
|
+
|
|
1577
|
+
from semantic_code_intelligence.indexing.semantic_chunker import SemanticChunk
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
class TestSemanticChunk:
|
|
1581
|
+
def test_create(self):
|
|
1582
|
+
sc = SemanticChunk(file_path="a.py", content="def foo(): pass",
|
|
1583
|
+
start_line=1, end_line=1, chunk_index=0,
|
|
1584
|
+
language="python", symbol_name="foo",
|
|
1585
|
+
symbol_kind="function", semantic_label="function foo")
|
|
1586
|
+
assert sc.symbol_name == "foo"
|
|
1587
|
+
assert sc.symbol_kind == "function"
|
|
1588
|
+
assert sc.semantic_label == "function foo"
|
|
1589
|
+
|
|
1590
|
+
def test_to_dict(self):
|
|
1591
|
+
sc = SemanticChunk(file_path="b.py", content="class Bar: pass",
|
|
1592
|
+
start_line=1, end_line=1, chunk_index=0,
|
|
1593
|
+
language="python", symbol_name="Bar",
|
|
1594
|
+
symbol_kind="class", semantic_label="class Bar")
|
|
1595
|
+
d = sc.to_dict()
|
|
1596
|
+
assert d["symbol_name"] == "Bar"
|
|
1597
|
+
assert d["symbol_kind"] == "class"
|
|
1598
|
+
assert d["semantic_label"] == "class Bar"
|
|
1599
|
+
|
|
1600
|
+
def test_inherits_from_code_chunk(self):
|
|
1601
|
+
assert issubclass(SemanticChunk, CodeChunk)
|
|
1602
|
+
|
|
1603
|
+
def test_defaults(self):
|
|
1604
|
+
sc = SemanticChunk(file_path="c.py", content="x", start_line=1,
|
|
1605
|
+
end_line=1, chunk_index=0, language="python")
|
|
1606
|
+
assert sc.symbol_name == ""
|
|
1607
|
+
assert sc.symbol_kind == ""
|
|
1608
|
+
assert sc.parent_symbol == ""
|
|
1609
|
+
assert sc.parameters == []
|
|
1610
|
+
assert sc.semantic_label == ""
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
# ==========================================================================
|
|
1614
|
+
# Quality Module Deep Tests
|
|
1615
|
+
# ==========================================================================
|
|
1616
|
+
|
|
1617
|
+
from semantic_code_intelligence.ci.quality import (
|
|
1618
|
+
ComplexityResult,
|
|
1619
|
+
compute_complexity,
|
|
1620
|
+
)
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
class TestComplexityResultDeep:
|
|
1624
|
+
def test_create(self):
|
|
1625
|
+
cr = ComplexityResult(symbol_name="fn", file_path="a.py",
|
|
1626
|
+
start_line=1, end_line=5, complexity=3, rating="A")
|
|
1627
|
+
assert cr.symbol_name == "fn"
|
|
1628
|
+
assert cr.complexity == 3
|
|
1629
|
+
assert cr.rating == "A"
|
|
1630
|
+
|
|
1631
|
+
def test_to_dict(self):
|
|
1632
|
+
cr = ComplexityResult(symbol_name="fn", file_path="a.py",
|
|
1633
|
+
start_line=1, end_line=5, complexity=15, rating="C")
|
|
1634
|
+
d = cr.to_dict()
|
|
1635
|
+
assert d["symbol_name"] == "fn"
|
|
1636
|
+
assert d["complexity"] == 15
|
|
1637
|
+
assert d["rating"] == "C"
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
class TestComputeComplexity:
|
|
1641
|
+
def test_simple_function(self):
|
|
1642
|
+
s = _sym(name="simple", body="def simple():\n return 1\n")
|
|
1643
|
+
cr = compute_complexity(s)
|
|
1644
|
+
assert isinstance(cr, ComplexityResult)
|
|
1645
|
+
assert cr.complexity >= 1
|
|
1646
|
+
|
|
1647
|
+
def test_complex_function(self):
|
|
1648
|
+
body = "def complex():\n"
|
|
1649
|
+
body += " if True:\n pass\n"
|
|
1650
|
+
body += " for i in range(10):\n pass\n"
|
|
1651
|
+
body += " while True:\n break\n"
|
|
1652
|
+
s = _sym(name="complex", body=body)
|
|
1653
|
+
cr = compute_complexity(s)
|
|
1654
|
+
assert cr.complexity >= 3
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
# ==========================================================================
|
|
1658
|
+
# CI Metrics
|
|
1659
|
+
# ==========================================================================
|
|
1660
|
+
|
|
1661
|
+
from semantic_code_intelligence.ci.metrics import QualitySnapshot
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
class TestQualitySnapshotExtended:
|
|
1665
|
+
def test_to_dict(self):
|
|
1666
|
+
qs = QualitySnapshot(
|
|
1667
|
+
timestamp=1000.0,
|
|
1668
|
+
maintainability_index=75.0,
|
|
1669
|
+
total_loc=500,
|
|
1670
|
+
total_symbols=50,
|
|
1671
|
+
issue_count=0,
|
|
1672
|
+
files_analyzed=10,
|
|
1673
|
+
avg_complexity=5.0,
|
|
1674
|
+
comment_ratio=0.2,
|
|
1675
|
+
)
|
|
1676
|
+
d = qs.to_dict()
|
|
1677
|
+
assert d["maintainability_index"] == 75.0
|
|
1678
|
+
assert d["avg_complexity"] == 5.0
|
|
1679
|
+
assert d["total_loc"] == 500
|
|
1680
|
+
|
|
1681
|
+
def test_is_dataclass(self):
|
|
1682
|
+
assert is_dataclass(QualitySnapshot)
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
# ==========================================================================
|
|
1686
|
+
# Mock Provider
|
|
1687
|
+
# ==========================================================================
|
|
1688
|
+
|
|
1689
|
+
from semantic_code_intelligence.llm.mock_provider import MockProvider
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
class TestMockProviderExtended:
|
|
1693
|
+
def test_name(self):
|
|
1694
|
+
p = MockProvider()
|
|
1695
|
+
assert p.name == "mock"
|
|
1696
|
+
|
|
1697
|
+
def test_is_available(self):
|
|
1698
|
+
p = MockProvider()
|
|
1699
|
+
assert p.is_available() is True
|
|
1700
|
+
|
|
1701
|
+
def test_complete(self):
|
|
1702
|
+
p = MockProvider()
|
|
1703
|
+
resp = p.complete("Hello")
|
|
1704
|
+
assert isinstance(resp, LLMResponse)
|
|
1705
|
+
assert len(resp.content) > 0
|
|
1706
|
+
|
|
1707
|
+
def test_chat(self):
|
|
1708
|
+
p = MockProvider()
|
|
1709
|
+
messages = [LLMMessage(role=MessageRole.USER, content="Hi")]
|
|
1710
|
+
resp = p.chat(messages)
|
|
1711
|
+
assert isinstance(resp, LLMResponse)
|
|
1712
|
+
assert len(resp.content) > 0
|
|
1713
|
+
|
|
1714
|
+
def test_response_has_provider(self):
|
|
1715
|
+
p = MockProvider()
|
|
1716
|
+
resp = p.complete("test")
|
|
1717
|
+
assert resp.provider == "mock"
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
# ==========================================================================
|
|
1721
|
+
# Scalability
|
|
1722
|
+
# ==========================================================================
|
|
1723
|
+
|
|
1724
|
+
from semantic_code_intelligence.scalability import BatchProcessor, ParallelScanner
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
class TestBatchProcessorExtended:
|
|
1728
|
+
def test_batch_counting(self):
|
|
1729
|
+
bp = BatchProcessor(batch_size=3)
|
|
1730
|
+
items = list(range(10))
|
|
1731
|
+
_, stats = bp.process(items, lambda batch: batch)
|
|
1732
|
+
assert stats.batches_processed >= 4 # ceil(10/3) = 4
|
|
1733
|
+
|
|
1734
|
+
def test_callback(self):
|
|
1735
|
+
bp = BatchProcessor(batch_size=2)
|
|
1736
|
+
callbacks = []
|
|
1737
|
+
items = list(range(5))
|
|
1738
|
+
_, stats = bp.process(items, lambda b: b,
|
|
1739
|
+
on_batch=lambda cur, tot: callbacks.append((cur, tot)))
|
|
1740
|
+
assert len(callbacks) >= 1
|
|
1741
|
+
|
|
1742
|
+
def test_processor_failure(self):
|
|
1743
|
+
bp = BatchProcessor(batch_size=2)
|
|
1744
|
+
def failing(batch):
|
|
1745
|
+
raise ValueError("boom")
|
|
1746
|
+
results, stats = bp.process([1, 2, 3], failing)
|
|
1747
|
+
assert stats.items_failed >= 1
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
class TestParallelScannerExtended:
|
|
1751
|
+
def test_process_files(self):
|
|
1752
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1753
|
+
for i in range(3):
|
|
1754
|
+
(Path(tmp) / f"file{i}.txt").write_text(f"content{i}")
|
|
1755
|
+
files = list(Path(tmp).glob("*.txt"))
|
|
1756
|
+
ps = ParallelScanner(max_workers=2)
|
|
1757
|
+
results, errors = ps.scan_and_process(files, lambda fp: fp.name)
|
|
1758
|
+
assert len(results) == 3
|
|
1759
|
+
assert errors == []
|
|
1760
|
+
|
|
1761
|
+
def test_error_handling(self):
|
|
1762
|
+
ps = ParallelScanner(max_workers=1)
|
|
1763
|
+
def fail(fp):
|
|
1764
|
+
raise ValueError("error")
|
|
1765
|
+
results, errors = ps.scan_and_process([Path("fake.txt")], fail)
|
|
1766
|
+
assert len(errors) >= 1
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
# ==========================================================================
|
|
1770
|
+
# Plugins
|
|
1771
|
+
# ==========================================================================
|
|
1772
|
+
|
|
1773
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
class TestPluginHookValues:
|
|
1777
|
+
def test_pre_index(self):
|
|
1778
|
+
assert PluginHook.PRE_INDEX.value == "pre_index"
|
|
1779
|
+
|
|
1780
|
+
def test_post_index(self):
|
|
1781
|
+
assert PluginHook.POST_INDEX.value == "post_index"
|
|
1782
|
+
|
|
1783
|
+
def test_pre_search(self):
|
|
1784
|
+
assert PluginHook.PRE_SEARCH.value == "pre_search"
|
|
1785
|
+
|
|
1786
|
+
def test_post_search(self):
|
|
1787
|
+
assert PluginHook.POST_SEARCH.value == "post_search"
|
|
1788
|
+
|
|
1789
|
+
def test_on_chunk(self):
|
|
1790
|
+
assert PluginHook.ON_CHUNK.value == "on_chunk"
|
|
1791
|
+
|
|
1792
|
+
def test_count(self):
|
|
1793
|
+
assert len(PluginHook) >= 20
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
# ==========================================================================
|
|
1797
|
+
# Tools
|
|
1798
|
+
# ==========================================================================
|
|
1799
|
+
|
|
1800
|
+
from semantic_code_intelligence.tools import (
|
|
1801
|
+
TOOL_DEFINITIONS,
|
|
1802
|
+
ToolResult,
|
|
1803
|
+
ToolRegistry,
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
class TestToolDefinitionsExtended:
|
|
1808
|
+
_names = [t["name"] for t in TOOL_DEFINITIONS]
|
|
1809
|
+
|
|
1810
|
+
def test_explain_symbol(self):
|
|
1811
|
+
assert "explain_symbol" in self._names
|
|
1812
|
+
|
|
1813
|
+
def test_get_call_graph(self):
|
|
1814
|
+
assert "get_call_graph" in self._names
|
|
1815
|
+
|
|
1816
|
+
def test_get_dependencies(self):
|
|
1817
|
+
assert "get_dependencies" in self._names
|
|
1818
|
+
|
|
1819
|
+
def test_find_references(self):
|
|
1820
|
+
assert "find_references" in self._names
|
|
1821
|
+
|
|
1822
|
+
def test_get_context(self):
|
|
1823
|
+
assert "get_context" in self._names
|
|
1824
|
+
|
|
1825
|
+
def test_summarize_repo(self):
|
|
1826
|
+
assert "summarize_repo" in self._names
|
|
1827
|
+
|
|
1828
|
+
def test_explain_file(self):
|
|
1829
|
+
assert "explain_file" in self._names
|
|
1830
|
+
|
|
1831
|
+
def test_search(self):
|
|
1832
|
+
assert "semantic_search" in self._names
|
|
1833
|
+
|
|
1834
|
+
def test_each_has_description(self):
|
|
1835
|
+
for defn in TOOL_DEFINITIONS:
|
|
1836
|
+
assert "description" in defn, f"Tool {defn['name']} missing description"
|
|
1837
|
+
|
|
1838
|
+
def test_each_has_parameters(self):
|
|
1839
|
+
for defn in TOOL_DEFINITIONS:
|
|
1840
|
+
assert "parameters" in defn, f"Tool {defn['name']} missing parameters"
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
class TestToolResultExtended:
|
|
1844
|
+
def test_success(self):
|
|
1845
|
+
tr = ToolResult(tool_name="search", success=True, data={"results": []})
|
|
1846
|
+
assert tr.tool_name == "search"
|
|
1847
|
+
assert tr.success is True
|
|
1848
|
+
|
|
1849
|
+
def test_failure(self):
|
|
1850
|
+
tr = ToolResult(tool_name="explain", success=False, error="not found")
|
|
1851
|
+
assert tr.success is False
|
|
1852
|
+
assert tr.error == "not found"
|
|
1853
|
+
|
|
1854
|
+
def test_to_dict(self):
|
|
1855
|
+
tr = ToolResult(tool_name="test", success=True, data={"x": 1})
|
|
1856
|
+
d = tr.to_dict()
|
|
1857
|
+
assert d["tool"] == "test"
|
|
1858
|
+
assert d["success"] is True
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
class TestToolRegistryExtended:
|
|
1862
|
+
def test_create(self):
|
|
1863
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1864
|
+
tr = ToolRegistry(Path(tmp))
|
|
1865
|
+
assert tr is not None
|
|
1866
|
+
|
|
1867
|
+
def test_tool_definitions(self):
|
|
1868
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1869
|
+
tr = ToolRegistry(Path(tmp))
|
|
1870
|
+
defns = tr.tool_definitions
|
|
1871
|
+
assert isinstance(defns, list)
|
|
1872
|
+
assert len(defns) >= 8
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
# ==========================================================================
|
|
1876
|
+
# Version
|
|
1877
|
+
# ==========================================================================
|
|
1878
|
+
|
|
1879
|
+
from semantic_code_intelligence import __version__, __app_name__
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
class TestVersionExtended:
|
|
1883
|
+
def test_semver(self):
|
|
1884
|
+
parts = __version__.split(".")
|
|
1885
|
+
assert len(parts) == 3
|
|
1886
|
+
for p in parts:
|
|
1887
|
+
assert p.isdigit()
|
|
1888
|
+
|
|
1889
|
+
def test_app_name_value(self):
|
|
1890
|
+
assert __app_name__ == "codexa"
|
|
1891
|
+
|
|
1892
|
+
def test_version_not_empty(self):
|
|
1893
|
+
assert len(__version__) >= 5
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
# ==========================================================================
|
|
1897
|
+
# Bridge Context Provider
|
|
1898
|
+
# ==========================================================================
|
|
1899
|
+
|
|
1900
|
+
from semantic_code_intelligence.bridge.context_provider import ContextProvider
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
class TestContextProvider:
|
|
1904
|
+
def test_create(self):
|
|
1905
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1906
|
+
cp = ContextProvider(Path(tmp))
|
|
1907
|
+
assert cp is not None
|
|
1908
|
+
|
|
1909
|
+
def test_repo_summary(self):
|
|
1910
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1911
|
+
cp = ContextProvider(Path(tmp))
|
|
1912
|
+
summary = cp.context_for_repo()
|
|
1913
|
+
assert isinstance(summary, dict)
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
# ==========================================================================
|
|
1917
|
+
# Services - Indexing
|
|
1918
|
+
# ==========================================================================
|
|
1919
|
+
|
|
1920
|
+
from semantic_code_intelligence.services.indexing_service import IndexingResult
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
class TestIndexingResult:
|
|
1924
|
+
def test_create(self):
|
|
1925
|
+
ir = IndexingResult()
|
|
1926
|
+
assert ir is not None
|
|
1927
|
+
|
|
1928
|
+
def test_repr(self):
|
|
1929
|
+
ir = IndexingResult()
|
|
1930
|
+
r = repr(ir)
|
|
1931
|
+
assert isinstance(r, str)
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
# ==========================================================================
|
|
1935
|
+
# Services - Search
|
|
1936
|
+
# ==========================================================================
|
|
1937
|
+
|
|
1938
|
+
from semantic_code_intelligence.services.search_service import SearchResult
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
class TestSearchResultExtended:
|
|
1942
|
+
def test_create(self):
|
|
1943
|
+
sr = SearchResult(file_path="a.py", start_line=1, end_line=5,
|
|
1944
|
+
language="python", content="code", score=0.95,
|
|
1945
|
+
chunk_index=0)
|
|
1946
|
+
assert sr.file_path == "a.py"
|
|
1947
|
+
assert sr.score == 0.95
|
|
1948
|
+
|
|
1949
|
+
def test_to_dict(self):
|
|
1950
|
+
sr = SearchResult(file_path="b.py", start_line=10, end_line=20,
|
|
1951
|
+
language="js", content="function()", score=0.8,
|
|
1952
|
+
chunk_index=1)
|
|
1953
|
+
d = sr.to_dict()
|
|
1954
|
+
assert d["file_path"] == "b.py"
|
|
1955
|
+
assert d["start_line"] == 10
|
|
1956
|
+
assert d["score"] == 0.8
|
|
1957
|
+
|
|
1958
|
+
def test_is_dataclass(self):
|
|
1959
|
+
assert is_dataclass(SearchResult)
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
# ==========================================================================
|
|
1963
|
+
# CI Hooks
|
|
1964
|
+
# ==========================================================================
|
|
1965
|
+
|
|
1966
|
+
from semantic_code_intelligence.ci.hooks import run_precommit_check
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
class TestPrecommitCheck:
|
|
1970
|
+
def test_no_git_dir(self):
|
|
1971
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1972
|
+
# No .git dir — just verify it's callable
|
|
1973
|
+
assert callable(run_precommit_check)
|
|
1974
|
+
|
|
1975
|
+
|
|
1976
|
+
# ==========================================================================
|
|
1977
|
+
# CI Templates
|
|
1978
|
+
# ==========================================================================
|
|
1979
|
+
|
|
1980
|
+
from semantic_code_intelligence.ci.templates import get_template, generate_precommit_config
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
class TestCITemplates:
|
|
1984
|
+
def test_get_analysis_template(self):
|
|
1985
|
+
tmpl = get_template("analysis")
|
|
1986
|
+
assert isinstance(tmpl, str)
|
|
1987
|
+
assert len(tmpl) > 0
|
|
1988
|
+
|
|
1989
|
+
def test_get_safety_template(self):
|
|
1990
|
+
tmpl = get_template("safety")
|
|
1991
|
+
assert isinstance(tmpl, str)
|
|
1992
|
+
assert len(tmpl) > 0
|
|
1993
|
+
|
|
1994
|
+
def test_get_precommit_template(self):
|
|
1995
|
+
tmpl = get_template("precommit")
|
|
1996
|
+
assert isinstance(tmpl, str)
|
|
1997
|
+
|
|
1998
|
+
def test_get_template_invalid(self):
|
|
1999
|
+
import pytest
|
|
2000
|
+
with pytest.raises(KeyError):
|
|
2001
|
+
get_template("nonexistent_template_xyz")
|
|
2002
|
+
|
|
2003
|
+
def test_generate_precommit_config(self):
|
|
2004
|
+
config = generate_precommit_config()
|
|
2005
|
+
assert isinstance(config, str)
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
# ==========================================================================
|
|
2009
|
+
# CI PR Review
|
|
2010
|
+
# ==========================================================================
|
|
2011
|
+
|
|
2012
|
+
from semantic_code_intelligence.ci.pr import FileChange, ChangeSummary, RiskScore, PRReport
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
class TestFileChange:
|
|
2016
|
+
def test_is_dataclass(self):
|
|
2017
|
+
assert is_dataclass(FileChange)
|
|
2018
|
+
|
|
2019
|
+
|
|
2020
|
+
class TestChangeSummary:
|
|
2021
|
+
def test_is_dataclass(self):
|
|
2022
|
+
assert is_dataclass(ChangeSummary)
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
class TestRiskScore:
|
|
2026
|
+
def test_is_dataclass(self):
|
|
2027
|
+
assert is_dataclass(RiskScore)
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
class TestPRReport:
|
|
2031
|
+
def test_is_dataclass(self):
|
|
2032
|
+
assert is_dataclass(PRReport)
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
# ==========================================================================
|
|
2036
|
+
# Docs generators
|
|
2037
|
+
# ==========================================================================
|
|
2038
|
+
|
|
2039
|
+
from semantic_code_intelligence.docs import generate_all_docs
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
class TestDocsGenerationExtended:
|
|
2043
|
+
def test_returns_list(self):
|
|
2044
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2045
|
+
result = generate_all_docs(Path(tmp))
|
|
2046
|
+
assert isinstance(result, list)
|
|
2047
|
+
|
|
2048
|
+
def test_files_created(self):
|
|
2049
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2050
|
+
result = generate_all_docs(Path(tmp))
|
|
2051
|
+
for name in result:
|
|
2052
|
+
assert (Path(tmp) / name).exists()
|
|
2053
|
+
|
|
2054
|
+
def test_files_are_markdown(self):
|
|
2055
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2056
|
+
result = generate_all_docs(Path(tmp))
|
|
2057
|
+
for name in result:
|
|
2058
|
+
assert name.endswith(".md")
|