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,154 @@
|
|
|
1
|
+
"""Tests for config/settings module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from semantic_code_intelligence.config.settings import (
|
|
11
|
+
AppConfig,
|
|
12
|
+
DEFAULT_EXTENSIONS,
|
|
13
|
+
DEFAULT_IGNORE_DIRS,
|
|
14
|
+
EmbeddingConfig,
|
|
15
|
+
IndexConfig,
|
|
16
|
+
SearchConfig,
|
|
17
|
+
init_project,
|
|
18
|
+
load_config,
|
|
19
|
+
save_config,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestDefaultConfigs:
|
|
24
|
+
"""Tests for default configuration values."""
|
|
25
|
+
|
|
26
|
+
def test_embedding_config_defaults(self):
|
|
27
|
+
cfg = EmbeddingConfig()
|
|
28
|
+
assert cfg.model_name == "all-MiniLM-L6-v2"
|
|
29
|
+
assert cfg.chunk_size == 512
|
|
30
|
+
assert cfg.chunk_overlap == 64
|
|
31
|
+
|
|
32
|
+
def test_search_config_defaults(self):
|
|
33
|
+
cfg = SearchConfig()
|
|
34
|
+
assert cfg.top_k == 10
|
|
35
|
+
assert cfg.similarity_threshold == 0.3
|
|
36
|
+
|
|
37
|
+
def test_index_config_defaults(self):
|
|
38
|
+
cfg = IndexConfig()
|
|
39
|
+
assert cfg.ignore_dirs == DEFAULT_IGNORE_DIRS
|
|
40
|
+
assert cfg.extensions == DEFAULT_EXTENSIONS
|
|
41
|
+
assert cfg.use_incremental is True
|
|
42
|
+
|
|
43
|
+
def test_app_config_defaults(self):
|
|
44
|
+
cfg = AppConfig()
|
|
45
|
+
assert cfg.verbose is False
|
|
46
|
+
assert isinstance(cfg.embedding, EmbeddingConfig)
|
|
47
|
+
assert isinstance(cfg.search, SearchConfig)
|
|
48
|
+
assert isinstance(cfg.index, IndexConfig)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestConfigPaths:
|
|
52
|
+
"""Tests for config path resolution."""
|
|
53
|
+
|
|
54
|
+
def test_config_dir(self, tmp_path: Path):
|
|
55
|
+
config_dir = AppConfig.config_dir(tmp_path)
|
|
56
|
+
assert config_dir == tmp_path / ".codexa"
|
|
57
|
+
|
|
58
|
+
def test_config_path(self, tmp_path: Path):
|
|
59
|
+
config_path = AppConfig.config_path(tmp_path)
|
|
60
|
+
assert config_path == tmp_path / ".codexa" / "config.json"
|
|
61
|
+
|
|
62
|
+
def test_index_dir(self, tmp_path: Path):
|
|
63
|
+
index_dir = AppConfig.index_dir(tmp_path)
|
|
64
|
+
assert index_dir == tmp_path / ".codexa" / "index"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestLoadConfig:
|
|
68
|
+
"""Tests for loading config from disk."""
|
|
69
|
+
|
|
70
|
+
def test_load_config_no_file_returns_defaults(self, tmp_path: Path):
|
|
71
|
+
cfg = load_config(tmp_path)
|
|
72
|
+
assert cfg.project_root == str(tmp_path.resolve())
|
|
73
|
+
assert cfg.embedding.model_name == "all-MiniLM-L6-v2"
|
|
74
|
+
|
|
75
|
+
def test_load_config_from_file(self, tmp_path: Path):
|
|
76
|
+
config_dir = tmp_path / ".codexa"
|
|
77
|
+
config_dir.mkdir()
|
|
78
|
+
config_data = {
|
|
79
|
+
"project_root": str(tmp_path),
|
|
80
|
+
"verbose": True,
|
|
81
|
+
"embedding": {"model_name": "custom-model", "chunk_size": 256},
|
|
82
|
+
"search": {"top_k": 5},
|
|
83
|
+
"index": {"use_incremental": False},
|
|
84
|
+
}
|
|
85
|
+
(config_dir / "config.json").write_text(
|
|
86
|
+
json.dumps(config_data), encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
cfg = load_config(tmp_path)
|
|
90
|
+
assert cfg.verbose is True
|
|
91
|
+
assert cfg.embedding.model_name == "custom-model"
|
|
92
|
+
assert cfg.embedding.chunk_size == 256
|
|
93
|
+
assert cfg.search.top_k == 5
|
|
94
|
+
assert cfg.index.use_incremental is False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestSaveConfig:
|
|
98
|
+
"""Tests for saving config to disk."""
|
|
99
|
+
|
|
100
|
+
def test_save_config_creates_file(self, tmp_path: Path):
|
|
101
|
+
cfg = AppConfig(project_root=str(tmp_path))
|
|
102
|
+
config_path = save_config(cfg, tmp_path)
|
|
103
|
+
|
|
104
|
+
assert config_path.exists()
|
|
105
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
106
|
+
assert data["project_root"] == str(tmp_path)
|
|
107
|
+
|
|
108
|
+
def test_save_config_creates_directory(self, tmp_path: Path):
|
|
109
|
+
cfg = AppConfig(project_root=str(tmp_path))
|
|
110
|
+
save_config(cfg, tmp_path)
|
|
111
|
+
|
|
112
|
+
assert (tmp_path / ".codexa").is_dir()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestInitProject:
|
|
116
|
+
"""Tests for project initialization."""
|
|
117
|
+
|
|
118
|
+
def test_init_creates_config_dir(self, tmp_path: Path):
|
|
119
|
+
config, config_path = init_project(tmp_path)
|
|
120
|
+
assert (tmp_path / ".codexa").is_dir()
|
|
121
|
+
|
|
122
|
+
def test_init_creates_index_dir(self, tmp_path: Path):
|
|
123
|
+
config, config_path = init_project(tmp_path)
|
|
124
|
+
assert (tmp_path / ".codexa" / "index").is_dir()
|
|
125
|
+
|
|
126
|
+
def test_init_creates_config_file(self, tmp_path: Path):
|
|
127
|
+
config, config_path = init_project(tmp_path)
|
|
128
|
+
assert config_path.exists()
|
|
129
|
+
assert config_path.name == "config.json"
|
|
130
|
+
|
|
131
|
+
def test_init_returns_valid_config(self, tmp_path: Path):
|
|
132
|
+
config, _ = init_project(tmp_path)
|
|
133
|
+
assert config.project_root == str(tmp_path.resolve())
|
|
134
|
+
assert isinstance(config, AppConfig)
|
|
135
|
+
|
|
136
|
+
def test_init_config_is_loadable(self, tmp_path: Path):
|
|
137
|
+
init_project(tmp_path)
|
|
138
|
+
loaded = load_config(tmp_path)
|
|
139
|
+
assert loaded.project_root == str(tmp_path.resolve())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestDefaultIgnoreDirs:
|
|
143
|
+
"""Tests for default ignore directories."""
|
|
144
|
+
|
|
145
|
+
def test_common_dirs_ignored(self):
|
|
146
|
+
for dirname in [".git", "node_modules", "build", "dist", "venv", "__pycache__"]:
|
|
147
|
+
assert dirname in DEFAULT_IGNORE_DIRS
|
|
148
|
+
|
|
149
|
+
def test_default_extensions_include_python(self):
|
|
150
|
+
assert ".py" in DEFAULT_EXTENSIONS
|
|
151
|
+
|
|
152
|
+
def test_default_extensions_include_common_languages(self):
|
|
153
|
+
for ext in [".js", ".ts", ".java", ".go", ".rs", ".cpp"]:
|
|
154
|
+
assert ext in DEFAULT_EXTENSIONS
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Tests for the context engine — ContextBuilder, CallGraph, DependencyMap."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from semantic_code_intelligence.context.engine import (
|
|
6
|
+
CallGraph,
|
|
7
|
+
ContextBuilder,
|
|
8
|
+
ContextWindow,
|
|
9
|
+
DependencyMap,
|
|
10
|
+
FileDependency,
|
|
11
|
+
)
|
|
12
|
+
from semantic_code_intelligence.parsing.parser import Symbol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Sample code
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
PYTHON_SAMPLE = '''\
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
def helper():
|
|
24
|
+
return 42
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
result = helper()
|
|
28
|
+
print(result)
|
|
29
|
+
|
|
30
|
+
class Worker:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.data = []
|
|
33
|
+
|
|
34
|
+
def process(self):
|
|
35
|
+
result = helper()
|
|
36
|
+
return result
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
JS_SAMPLE = '''\
|
|
40
|
+
import { readFile } from 'fs';
|
|
41
|
+
|
|
42
|
+
function parse(data) {
|
|
43
|
+
return JSON.parse(data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function load(path) {
|
|
47
|
+
const data = readFile(path);
|
|
48
|
+
return parse(data);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class DataLoader {
|
|
52
|
+
constructor(path) {
|
|
53
|
+
this.path = path;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
load() {
|
|
57
|
+
return load(this.path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# ContextBuilder
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
class TestContextBuilder:
|
|
68
|
+
@pytest.fixture(autouse=True)
|
|
69
|
+
def setup(self):
|
|
70
|
+
self.builder = ContextBuilder()
|
|
71
|
+
self.builder.index_file("app.py", PYTHON_SAMPLE)
|
|
72
|
+
|
|
73
|
+
def test_index_file_returns_symbols(self):
|
|
74
|
+
symbols = self.builder.get_symbols("app.py")
|
|
75
|
+
assert len(symbols) > 0
|
|
76
|
+
|
|
77
|
+
def test_get_all_symbols(self):
|
|
78
|
+
self.builder.index_file("app.js", JS_SAMPLE)
|
|
79
|
+
all_syms = self.builder.get_all_symbols()
|
|
80
|
+
assert len(all_syms) > 5 # should have symbols from both files
|
|
81
|
+
|
|
82
|
+
def test_find_symbol_by_name(self):
|
|
83
|
+
results = self.builder.find_symbol("helper")
|
|
84
|
+
assert len(results) >= 1
|
|
85
|
+
assert results[0].name == "helper"
|
|
86
|
+
|
|
87
|
+
def test_find_symbol_by_name_and_kind(self):
|
|
88
|
+
results = self.builder.find_symbol("Worker", kind="class")
|
|
89
|
+
assert len(results) == 1
|
|
90
|
+
assert results[0].kind == "class"
|
|
91
|
+
|
|
92
|
+
def test_find_symbol_not_found(self):
|
|
93
|
+
results = self.builder.find_symbol("nonexistent")
|
|
94
|
+
assert results == []
|
|
95
|
+
|
|
96
|
+
def test_build_context(self):
|
|
97
|
+
symbols = self.builder.get_symbols("app.py")
|
|
98
|
+
main_sym = next(s for s in symbols if s.name == "main")
|
|
99
|
+
ctx = self.builder.build_context(main_sym)
|
|
100
|
+
assert isinstance(ctx, ContextWindow)
|
|
101
|
+
assert ctx.focal_symbol.name == "main"
|
|
102
|
+
|
|
103
|
+
def test_context_has_imports(self):
|
|
104
|
+
symbols = self.builder.get_symbols("app.py")
|
|
105
|
+
main_sym = next(s for s in symbols if s.name == "main")
|
|
106
|
+
ctx = self.builder.build_context(main_sym)
|
|
107
|
+
assert len(ctx.imports) >= 2
|
|
108
|
+
|
|
109
|
+
def test_context_has_related_symbols(self):
|
|
110
|
+
symbols = self.builder.get_symbols("app.py")
|
|
111
|
+
main_sym = next(s for s in symbols if s.name == "main")
|
|
112
|
+
ctx = self.builder.build_context(main_sym)
|
|
113
|
+
related_names = {s.name for s in ctx.related_symbols}
|
|
114
|
+
assert "helper" in related_names
|
|
115
|
+
|
|
116
|
+
def test_context_to_dict(self):
|
|
117
|
+
symbols = self.builder.get_symbols("app.py")
|
|
118
|
+
main_sym = next(s for s in symbols if s.name == "main")
|
|
119
|
+
ctx = self.builder.build_context(main_sym)
|
|
120
|
+
d = ctx.to_dict()
|
|
121
|
+
assert "focal_symbol" in d
|
|
122
|
+
assert "related_symbols" in d
|
|
123
|
+
assert "imports" in d
|
|
124
|
+
assert d["focal_symbol"]["name"] == "main"
|
|
125
|
+
|
|
126
|
+
def test_context_render(self):
|
|
127
|
+
symbols = self.builder.get_symbols("app.py")
|
|
128
|
+
main_sym = next(s for s in symbols if s.name == "main")
|
|
129
|
+
ctx = self.builder.build_context(main_sym)
|
|
130
|
+
text = ctx.render()
|
|
131
|
+
assert "main" in text
|
|
132
|
+
assert "File:" in text
|
|
133
|
+
assert "Lines:" in text
|
|
134
|
+
|
|
135
|
+
def test_context_render_with_max_lines(self):
|
|
136
|
+
symbols = self.builder.get_symbols("app.py")
|
|
137
|
+
main_sym = next(s for s in symbols if s.name == "main")
|
|
138
|
+
ctx = self.builder.build_context(main_sym)
|
|
139
|
+
text = ctx.render(max_lines=1)
|
|
140
|
+
assert "main" in text
|
|
141
|
+
|
|
142
|
+
def test_build_context_for_name(self):
|
|
143
|
+
contexts = self.builder.build_context_for_name("helper")
|
|
144
|
+
assert len(contexts) >= 1
|
|
145
|
+
assert contexts[0].focal_symbol.name == "helper"
|
|
146
|
+
|
|
147
|
+
def test_build_context_for_name_not_found(self):
|
|
148
|
+
contexts = self.builder.build_context_for_name("nonexistent")
|
|
149
|
+
assert contexts == []
|
|
150
|
+
|
|
151
|
+
def test_get_symbols_unknown_file(self):
|
|
152
|
+
assert self.builder.get_symbols("unknown.py") == []
|
|
153
|
+
|
|
154
|
+
def test_index_multiple_files(self):
|
|
155
|
+
self.builder.index_file("app.js", JS_SAMPLE)
|
|
156
|
+
py_symbols = self.builder.get_symbols("app.py")
|
|
157
|
+
js_symbols = self.builder.get_symbols("app.js")
|
|
158
|
+
assert len(py_symbols) > 0
|
|
159
|
+
assert len(js_symbols) > 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# CallGraph
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
class TestCallGraph:
|
|
167
|
+
@pytest.fixture(autouse=True)
|
|
168
|
+
def setup(self):
|
|
169
|
+
self.builder = ContextBuilder()
|
|
170
|
+
self.builder.index_file("app.py", PYTHON_SAMPLE)
|
|
171
|
+
self.symbols = self.builder.get_all_symbols()
|
|
172
|
+
self.graph = CallGraph()
|
|
173
|
+
self.graph.build(self.symbols)
|
|
174
|
+
|
|
175
|
+
def test_edges_found(self):
|
|
176
|
+
assert len(self.graph.edges) > 0
|
|
177
|
+
|
|
178
|
+
def test_main_calls_helper(self):
|
|
179
|
+
callers = self.graph.callers_of("helper")
|
|
180
|
+
caller_names = {e.caller for e in callers}
|
|
181
|
+
assert any("main" in c for c in caller_names)
|
|
182
|
+
|
|
183
|
+
def test_process_calls_helper(self):
|
|
184
|
+
callers = self.graph.callers_of("helper")
|
|
185
|
+
caller_names = {e.caller for e in callers}
|
|
186
|
+
assert any("process" in c for c in caller_names)
|
|
187
|
+
|
|
188
|
+
def test_callees_of_main(self):
|
|
189
|
+
callees = self.graph.callees_of("app.py:main")
|
|
190
|
+
callee_names = {e.callee for e in callees}
|
|
191
|
+
assert "helper" in callee_names
|
|
192
|
+
|
|
193
|
+
def test_no_self_references(self):
|
|
194
|
+
for edge in self.graph.edges:
|
|
195
|
+
# Caller key includes file path, callee is just name
|
|
196
|
+
assert edge.callee not in edge.caller or edge.callee != edge.caller.split(":")[-1]
|
|
197
|
+
|
|
198
|
+
def test_callers_of_unknown(self):
|
|
199
|
+
assert self.graph.callers_of("nonexistent") == []
|
|
200
|
+
|
|
201
|
+
def test_callees_of_unknown(self):
|
|
202
|
+
assert self.graph.callees_of("nonexistent") == []
|
|
203
|
+
|
|
204
|
+
def test_to_dict(self):
|
|
205
|
+
d = self.graph.to_dict()
|
|
206
|
+
assert "edges" in d
|
|
207
|
+
assert "node_count" in d
|
|
208
|
+
assert "edge_count" in d
|
|
209
|
+
assert d["edge_count"] == len(self.graph.edges)
|
|
210
|
+
|
|
211
|
+
def test_build_clears_previous(self):
|
|
212
|
+
"""Build should reset the graph."""
|
|
213
|
+
initial_count = len(self.graph.edges)
|
|
214
|
+
self.graph.build([]) # rebuild with no symbols
|
|
215
|
+
assert len(self.graph.edges) == 0
|
|
216
|
+
|
|
217
|
+
def test_edge_to_dict(self):
|
|
218
|
+
if self.graph.edges:
|
|
219
|
+
d = self.graph.edges[0].to_dict()
|
|
220
|
+
assert "caller" in d
|
|
221
|
+
assert "callee" in d
|
|
222
|
+
assert "file_path" in d
|
|
223
|
+
assert "line" in d
|
|
224
|
+
|
|
225
|
+
def test_js_call_graph(self):
|
|
226
|
+
builder = ContextBuilder()
|
|
227
|
+
builder.index_file("app.js", JS_SAMPLE)
|
|
228
|
+
symbols = builder.get_all_symbols()
|
|
229
|
+
graph = CallGraph()
|
|
230
|
+
graph.build(symbols)
|
|
231
|
+
callers = graph.callers_of("parse")
|
|
232
|
+
assert any("load" in e.caller for e in callers)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# DependencyMap
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
class TestDependencyMap:
|
|
240
|
+
@pytest.fixture(autouse=True)
|
|
241
|
+
def setup(self):
|
|
242
|
+
self.dep_map = DependencyMap()
|
|
243
|
+
|
|
244
|
+
def test_add_python_file(self):
|
|
245
|
+
deps = self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
246
|
+
assert len(deps) >= 2 # import os, from pathlib import Path
|
|
247
|
+
|
|
248
|
+
def test_add_js_file(self):
|
|
249
|
+
deps = self.dep_map.add_file("app.js", JS_SAMPLE)
|
|
250
|
+
assert len(deps) >= 1
|
|
251
|
+
|
|
252
|
+
def test_get_dependencies(self):
|
|
253
|
+
self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
254
|
+
deps = self.dep_map.get_dependencies("app.py")
|
|
255
|
+
assert len(deps) >= 2
|
|
256
|
+
|
|
257
|
+
def test_get_dependencies_unknown(self):
|
|
258
|
+
assert self.dep_map.get_dependencies("unknown.py") == []
|
|
259
|
+
|
|
260
|
+
def test_get_all_files(self):
|
|
261
|
+
self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
262
|
+
self.dep_map.add_file("app.js", JS_SAMPLE)
|
|
263
|
+
files = self.dep_map.get_all_files()
|
|
264
|
+
assert "app.py" in files
|
|
265
|
+
assert "app.js" in files
|
|
266
|
+
|
|
267
|
+
def test_get_dependents(self):
|
|
268
|
+
self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
269
|
+
dependents = self.dep_map.get_dependents("os")
|
|
270
|
+
assert len(dependents) >= 1
|
|
271
|
+
assert dependents[0].source_file == "app.py"
|
|
272
|
+
|
|
273
|
+
def test_get_dependents_pathlib(self):
|
|
274
|
+
self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
275
|
+
dependents = self.dep_map.get_dependents("pathlib")
|
|
276
|
+
assert len(dependents) >= 1
|
|
277
|
+
|
|
278
|
+
def test_get_dependents_not_found(self):
|
|
279
|
+
self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
280
|
+
assert self.dep_map.get_dependents("nonexistent_module") == []
|
|
281
|
+
|
|
282
|
+
def test_dependency_to_dict(self):
|
|
283
|
+
deps = self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
284
|
+
if deps:
|
|
285
|
+
d = deps[0].to_dict()
|
|
286
|
+
assert "source_file" in d
|
|
287
|
+
assert "import_text" in d
|
|
288
|
+
assert "line" in d
|
|
289
|
+
|
|
290
|
+
def test_to_dict(self):
|
|
291
|
+
self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
292
|
+
d = self.dep_map.to_dict()
|
|
293
|
+
assert "app.py" in d
|
|
294
|
+
assert isinstance(d["app.py"], list)
|
|
295
|
+
|
|
296
|
+
def test_dependency_line_numbers(self):
|
|
297
|
+
deps = self.dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
298
|
+
lines = [d.line for d in deps]
|
|
299
|
+
assert 1 in lines # 'import os' is line 1
|
|
300
|
+
assert 2 in lines # 'from pathlib...' is line 2
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# Edge cases
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
class TestContextEdgeCases:
|
|
308
|
+
def test_builder_nonexistent_file(self, tmp_path):
|
|
309
|
+
builder = ContextBuilder()
|
|
310
|
+
syms = builder.index_file(str(tmp_path / "nope.py"))
|
|
311
|
+
assert syms == []
|
|
312
|
+
|
|
313
|
+
def test_builder_empty_content(self):
|
|
314
|
+
builder = ContextBuilder()
|
|
315
|
+
syms = builder.index_file("empty.py", "")
|
|
316
|
+
assert syms == []
|
|
317
|
+
|
|
318
|
+
def test_builder_unsupported_extension(self):
|
|
319
|
+
builder = ContextBuilder()
|
|
320
|
+
syms = builder.index_file("style.css", "body { color: red; }")
|
|
321
|
+
assert syms == []
|
|
322
|
+
|
|
323
|
+
def test_call_graph_empty(self):
|
|
324
|
+
graph = CallGraph()
|
|
325
|
+
graph.build([])
|
|
326
|
+
assert graph.edges == []
|
|
327
|
+
|
|
328
|
+
def test_dep_map_empty_file(self):
|
|
329
|
+
dep_map = DependencyMap()
|
|
330
|
+
deps = dep_map.add_file("empty.py", "")
|
|
331
|
+
assert deps == []
|
|
332
|
+
|
|
333
|
+
def test_dep_map_unsupported_extension(self):
|
|
334
|
+
dep_map = DependencyMap()
|
|
335
|
+
deps = dep_map.add_file("style.css", "body { color: red; }")
|
|
336
|
+
assert deps == []
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# Integration: builder -> call graph -> dependency map
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
class TestContextIntegration:
|
|
344
|
+
def test_full_pipeline(self):
|
|
345
|
+
builder = ContextBuilder()
|
|
346
|
+
builder.index_file("app.py", PYTHON_SAMPLE)
|
|
347
|
+
builder.index_file("app.js", JS_SAMPLE)
|
|
348
|
+
|
|
349
|
+
# Build call graph from all symbols
|
|
350
|
+
all_symbols = builder.get_all_symbols()
|
|
351
|
+
graph = CallGraph()
|
|
352
|
+
graph.build(all_symbols)
|
|
353
|
+
assert len(graph.edges) > 0
|
|
354
|
+
|
|
355
|
+
# Build dependency map
|
|
356
|
+
dep_map = DependencyMap()
|
|
357
|
+
dep_map.add_file("app.py", PYTHON_SAMPLE)
|
|
358
|
+
dep_map.add_file("app.js", JS_SAMPLE)
|
|
359
|
+
assert len(dep_map.get_all_files()) == 2
|
|
360
|
+
|
|
361
|
+
# Build context for a symbol
|
|
362
|
+
helpers = builder.find_symbol("helper")
|
|
363
|
+
assert len(helpers) >= 1
|
|
364
|
+
ctx = builder.build_context(helpers[0])
|
|
365
|
+
assert ctx.focal_symbol.name == "helper"
|
|
366
|
+
assert len(ctx.imports) >= 2
|
|
367
|
+
|
|
368
|
+
def test_cross_file_symbol_search(self):
|
|
369
|
+
builder = ContextBuilder()
|
|
370
|
+
builder.index_file("app.py", PYTHON_SAMPLE)
|
|
371
|
+
builder.index_file("app.js", JS_SAMPLE)
|
|
372
|
+
|
|
373
|
+
# "parse" exists in the JS file
|
|
374
|
+
results = builder.find_symbol("parse")
|
|
375
|
+
assert len(results) >= 1
|
|
376
|
+
assert any(r.file_path == "app.js" for r in results)
|
|
377
|
+
|
|
378
|
+
# "helper" exists in the Python file
|
|
379
|
+
results = builder.find_symbol("helper")
|
|
380
|
+
assert len(results) >= 1
|
|
381
|
+
assert any(r.file_path == "app.py" for r in results)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tests for the embedding generator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from semantic_code_intelligence.embeddings.generator import (
|
|
9
|
+
generate_embeddings,
|
|
10
|
+
get_embedding_dimension,
|
|
11
|
+
get_model,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestGetModel:
|
|
16
|
+
"""Tests for model loading."""
|
|
17
|
+
|
|
18
|
+
def test_loads_model(self):
|
|
19
|
+
model = get_model("all-MiniLM-L6-v2")
|
|
20
|
+
assert model is not None
|
|
21
|
+
|
|
22
|
+
def test_model_cached(self):
|
|
23
|
+
m1 = get_model("all-MiniLM-L6-v2")
|
|
24
|
+
m2 = get_model("all-MiniLM-L6-v2")
|
|
25
|
+
assert m1 is m2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestGenerateEmbeddings:
|
|
29
|
+
"""Tests for embedding generation."""
|
|
30
|
+
|
|
31
|
+
def test_returns_numpy_array(self):
|
|
32
|
+
emb = generate_embeddings(["hello world"])
|
|
33
|
+
assert isinstance(emb, np.ndarray)
|
|
34
|
+
|
|
35
|
+
def test_correct_shape(self):
|
|
36
|
+
texts = ["hello", "world", "foo"]
|
|
37
|
+
emb = generate_embeddings(texts)
|
|
38
|
+
assert emb.shape[0] == 3
|
|
39
|
+
assert emb.shape[1] > 0
|
|
40
|
+
|
|
41
|
+
def test_empty_input(self):
|
|
42
|
+
emb = generate_embeddings([])
|
|
43
|
+
assert emb.shape == (0, 0)
|
|
44
|
+
|
|
45
|
+
def test_embeddings_normalized(self):
|
|
46
|
+
emb = generate_embeddings(["test string"])
|
|
47
|
+
norm = np.linalg.norm(emb[0])
|
|
48
|
+
assert abs(norm - 1.0) < 0.01
|
|
49
|
+
|
|
50
|
+
def test_similar_texts_close(self):
|
|
51
|
+
emb = generate_embeddings([
|
|
52
|
+
"def authenticate_user(username, password):",
|
|
53
|
+
"def verify_user_credentials(user, pwd):",
|
|
54
|
+
"import random; x = random.randint(0, 100)",
|
|
55
|
+
])
|
|
56
|
+
# Cosine similarity (already normalized, so dot product)
|
|
57
|
+
sim_related = np.dot(emb[0], emb[1])
|
|
58
|
+
sim_unrelated = np.dot(emb[0], emb[2])
|
|
59
|
+
assert sim_related > sim_unrelated
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestGetEmbeddingDimension:
|
|
63
|
+
"""Tests for embedding dimension retrieval."""
|
|
64
|
+
|
|
65
|
+
def test_returns_positive_int(self):
|
|
66
|
+
dim = get_embedding_dimension()
|
|
67
|
+
assert isinstance(dim, int)
|
|
68
|
+
assert dim > 0
|
|
69
|
+
|
|
70
|
+
def test_matches_actual_embedding(self):
|
|
71
|
+
dim = get_embedding_dimension()
|
|
72
|
+
emb = generate_embeddings(["test"])
|
|
73
|
+
assert emb.shape[1] == dim
|