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,2753 @@
|
|
|
1
|
+
"""Phase 20 — Deep Coverage & Hardening Tests.
|
|
2
|
+
|
|
3
|
+
Target: bring total tests from 1204 to 2000+.
|
|
4
|
+
Tests cover every under-tested module with unit-level granularity:
|
|
5
|
+
- ci/ (quality, metrics, pr, hooks, templates, hotspots, impact, trace)
|
|
6
|
+
- web/ (api, visualize)
|
|
7
|
+
- llm/ (safety, reasoning, conversation, investigation, streaming, providers)
|
|
8
|
+
- bridge/ (protocol, server, context_provider, vscode)
|
|
9
|
+
- context/ (engine, memory)
|
|
10
|
+
- tools/ (protocol, executor, registry)
|
|
11
|
+
- workspace/
|
|
12
|
+
- daemon/watcher
|
|
13
|
+
- docs/
|
|
14
|
+
- config/settings
|
|
15
|
+
- parsing/parser
|
|
16
|
+
- indexing/ (scanner, chunker, semantic_chunker)
|
|
17
|
+
- services/ (indexing_service, search_service)
|
|
18
|
+
- storage/ (vector_store, hash_store)
|
|
19
|
+
- embeddings/
|
|
20
|
+
- scalability/
|
|
21
|
+
- plugins/
|
|
22
|
+
- cli/ (router, commands)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
import re
|
|
30
|
+
import tempfile
|
|
31
|
+
import time
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
from unittest.mock import MagicMock, patch
|
|
36
|
+
|
|
37
|
+
import pytest
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# CI Quality
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
from semantic_code_intelligence.ci.quality import (
|
|
43
|
+
ComplexityResult,
|
|
44
|
+
DeadCodeResult,
|
|
45
|
+
DuplicateResult,
|
|
46
|
+
QualityReport,
|
|
47
|
+
_jaccard,
|
|
48
|
+
_normalize_body,
|
|
49
|
+
_rate_complexity,
|
|
50
|
+
_trigram_set,
|
|
51
|
+
analyze_complexity,
|
|
52
|
+
compute_complexity,
|
|
53
|
+
detect_dead_code,
|
|
54
|
+
detect_duplicates,
|
|
55
|
+
)
|
|
56
|
+
from semantic_code_intelligence.parsing.parser import Symbol
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sym(name="foo", kind="function", body="pass", file_path="a.py",
|
|
60
|
+
start_line=1, end_line=2, parent="") -> Symbol:
|
|
61
|
+
"""Helper to build stub Symbols."""
|
|
62
|
+
return Symbol(
|
|
63
|
+
name=name, kind=kind, body=body, file_path=file_path,
|
|
64
|
+
start_line=start_line, end_line=end_line, start_col=0, end_col=0,
|
|
65
|
+
parent=parent,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestRateComplexity:
|
|
70
|
+
def test_low(self):
|
|
71
|
+
assert _rate_complexity(1) == "low"
|
|
72
|
+
assert _rate_complexity(5) == "low"
|
|
73
|
+
|
|
74
|
+
def test_moderate(self):
|
|
75
|
+
assert _rate_complexity(6) == "moderate"
|
|
76
|
+
assert _rate_complexity(10) == "moderate"
|
|
77
|
+
|
|
78
|
+
def test_high(self):
|
|
79
|
+
assert _rate_complexity(11) == "high"
|
|
80
|
+
assert _rate_complexity(20) == "high"
|
|
81
|
+
|
|
82
|
+
def test_very_high(self):
|
|
83
|
+
assert _rate_complexity(21) == "very_high"
|
|
84
|
+
assert _rate_complexity(100) == "very_high"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestComputeComplexity:
|
|
88
|
+
def test_simple_function(self):
|
|
89
|
+
s = _sym(body="return 1")
|
|
90
|
+
cr = compute_complexity(s)
|
|
91
|
+
assert cr.complexity == 1
|
|
92
|
+
assert cr.rating == "low"
|
|
93
|
+
|
|
94
|
+
def test_with_if(self):
|
|
95
|
+
s = _sym(body="if x:\n return 1\nreturn 2")
|
|
96
|
+
cr = compute_complexity(s)
|
|
97
|
+
assert cr.complexity >= 2
|
|
98
|
+
|
|
99
|
+
def test_with_for_while(self):
|
|
100
|
+
s = _sym(body="for i in range(10):\n while True:\n break")
|
|
101
|
+
cr = compute_complexity(s)
|
|
102
|
+
assert cr.complexity >= 3
|
|
103
|
+
|
|
104
|
+
def test_with_logical_operators(self):
|
|
105
|
+
s = _sym(body="if a and b or c:\n pass")
|
|
106
|
+
cr = compute_complexity(s)
|
|
107
|
+
assert cr.complexity >= 4 # if + and + or
|
|
108
|
+
|
|
109
|
+
def test_comments_ignored(self):
|
|
110
|
+
s = _sym(body="# if something\nreturn 1")
|
|
111
|
+
cr = compute_complexity(s)
|
|
112
|
+
assert cr.complexity == 1
|
|
113
|
+
|
|
114
|
+
def test_result_fields(self):
|
|
115
|
+
s = _sym(name="bar", file_path="b.py", start_line=10, end_line=20, body="pass")
|
|
116
|
+
cr = compute_complexity(s)
|
|
117
|
+
assert cr.symbol_name == "bar"
|
|
118
|
+
assert cr.file_path == "b.py"
|
|
119
|
+
assert cr.start_line == 10
|
|
120
|
+
assert cr.end_line == 20
|
|
121
|
+
|
|
122
|
+
def test_to_dict(self):
|
|
123
|
+
s = _sym(body="pass")
|
|
124
|
+
d = compute_complexity(s).to_dict()
|
|
125
|
+
assert "complexity" in d
|
|
126
|
+
assert "rating" in d
|
|
127
|
+
assert "symbol_name" in d
|
|
128
|
+
|
|
129
|
+
def test_empty_body(self):
|
|
130
|
+
s = _sym(body="")
|
|
131
|
+
cr = compute_complexity(s)
|
|
132
|
+
assert cr.complexity == 1
|
|
133
|
+
|
|
134
|
+
def test_except_catch(self):
|
|
135
|
+
s = _sym(body="try:\n pass\nexcept:\n pass")
|
|
136
|
+
cr = compute_complexity(s)
|
|
137
|
+
assert cr.complexity >= 2
|
|
138
|
+
|
|
139
|
+
def test_case_switch(self):
|
|
140
|
+
s = _sym(body="case 1:\n break\ncase 2:\n break")
|
|
141
|
+
cr = compute_complexity(s)
|
|
142
|
+
assert cr.complexity >= 3
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestAnalyzeComplexity:
|
|
146
|
+
def test_filters_by_threshold(self):
|
|
147
|
+
syms = [
|
|
148
|
+
_sym(name="simple", body="return 1"),
|
|
149
|
+
_sym(name="complex", body="if a:\n if b:\n if c:\n if d:\n if e:\n if f:\n if g:\n if h:\n if i:\n if j:\n pass"),
|
|
150
|
+
]
|
|
151
|
+
results = analyze_complexity(syms, threshold=5)
|
|
152
|
+
names = [r.symbol_name for r in results]
|
|
153
|
+
assert "complex" in names
|
|
154
|
+
|
|
155
|
+
def test_skips_non_callables(self):
|
|
156
|
+
syms = [_sym(name="MyClass", kind="class", body="if a:\n if b:\n if c:\n if d:\n if e:\n if f:\n pass")]
|
|
157
|
+
results = analyze_complexity(syms, threshold=1)
|
|
158
|
+
assert len(results) == 0
|
|
159
|
+
|
|
160
|
+
def test_sorted_descending(self):
|
|
161
|
+
syms = [
|
|
162
|
+
_sym(name="a", body="if x:\n pass"),
|
|
163
|
+
_sym(name="b", body="if x:\n if y:\n if z:\n pass"),
|
|
164
|
+
]
|
|
165
|
+
results = analyze_complexity(syms, threshold=1)
|
|
166
|
+
if len(results) >= 2:
|
|
167
|
+
assert results[0].complexity >= results[1].complexity
|
|
168
|
+
|
|
169
|
+
def test_empty_input(self):
|
|
170
|
+
assert analyze_complexity([]) == []
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TestNormalizeBody:
|
|
174
|
+
def test_strips_comments(self):
|
|
175
|
+
assert "# comment" not in _normalize_body("# comment\ncode")
|
|
176
|
+
|
|
177
|
+
def test_strips_blank_lines(self):
|
|
178
|
+
result = _normalize_body("\n\ncode\n\n")
|
|
179
|
+
assert result == "code"
|
|
180
|
+
|
|
181
|
+
def test_strips_js_comments(self):
|
|
182
|
+
assert "//" not in _normalize_body("// comment\ncode")
|
|
183
|
+
|
|
184
|
+
def test_strips_whitespace(self):
|
|
185
|
+
result = _normalize_body(" code ")
|
|
186
|
+
assert result == "code"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestTrigramSet:
|
|
190
|
+
def test_basic(self):
|
|
191
|
+
result = _trigram_set("abcde")
|
|
192
|
+
assert "abc" in result
|
|
193
|
+
assert "bcd" in result
|
|
194
|
+
assert "cde" in result
|
|
195
|
+
|
|
196
|
+
def test_short_string(self):
|
|
197
|
+
assert _trigram_set("ab") == {"ab"}
|
|
198
|
+
|
|
199
|
+
def test_empty_string(self):
|
|
200
|
+
assert _trigram_set("") == set()
|
|
201
|
+
|
|
202
|
+
def test_exactly_three(self):
|
|
203
|
+
result = _trigram_set("abc")
|
|
204
|
+
assert result == {"abc"}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestJaccard:
|
|
208
|
+
def test_identical(self):
|
|
209
|
+
s = {"a", "b", "c"}
|
|
210
|
+
assert _jaccard(s, s) == 1.0
|
|
211
|
+
|
|
212
|
+
def test_disjoint(self):
|
|
213
|
+
assert _jaccard({"a"}, {"b"}) == 0.0
|
|
214
|
+
|
|
215
|
+
def test_partial(self):
|
|
216
|
+
assert 0.0 < _jaccard({"a", "b"}, {"b", "c"}) < 1.0
|
|
217
|
+
|
|
218
|
+
def test_both_empty(self):
|
|
219
|
+
assert _jaccard(set(), set()) == 1.0
|
|
220
|
+
|
|
221
|
+
def test_one_empty(self):
|
|
222
|
+
assert _jaccard(set(), {"a"}) == 0.0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestDetectDuplicates:
|
|
226
|
+
def test_identical_bodies(self):
|
|
227
|
+
body = "x = 1\ny = 2\nz = 3\nw = 4\nreturn x"
|
|
228
|
+
syms = [
|
|
229
|
+
_sym(name="a", body=body, file_path="a.py"),
|
|
230
|
+
_sym(name="b", body=body, file_path="b.py"),
|
|
231
|
+
]
|
|
232
|
+
results = detect_duplicates(syms, threshold=0.5)
|
|
233
|
+
assert len(results) >= 1
|
|
234
|
+
assert results[0].similarity >= 0.9
|
|
235
|
+
|
|
236
|
+
def test_different_bodies(self):
|
|
237
|
+
syms = [
|
|
238
|
+
_sym(name="a", body="x=1\ny=2\nz=3\nw=4"),
|
|
239
|
+
_sym(name="b", body="very different code\nnothing similar\nat all\nreally"),
|
|
240
|
+
]
|
|
241
|
+
results = detect_duplicates(syms, threshold=0.9)
|
|
242
|
+
assert len(results) == 0
|
|
243
|
+
|
|
244
|
+
def test_min_lines_filter(self):
|
|
245
|
+
syms = [
|
|
246
|
+
_sym(name="a", body="short"),
|
|
247
|
+
_sym(name="b", body="short"),
|
|
248
|
+
]
|
|
249
|
+
assert detect_duplicates(syms, min_lines=4) == []
|
|
250
|
+
|
|
251
|
+
def test_result_fields(self):
|
|
252
|
+
body = "x=1\ny=2\nz=3\nw=4\nv=5"
|
|
253
|
+
syms = [
|
|
254
|
+
_sym(name="a", body=body, file_path="f1.py", start_line=1),
|
|
255
|
+
_sym(name="b", body=body, file_path="f2.py", start_line=10),
|
|
256
|
+
]
|
|
257
|
+
results = detect_duplicates(syms, threshold=0.5)
|
|
258
|
+
if results:
|
|
259
|
+
d = results[0].to_dict()
|
|
260
|
+
assert "symbol_a" in d
|
|
261
|
+
assert "similarity" in d
|
|
262
|
+
|
|
263
|
+
def test_empty_input(self):
|
|
264
|
+
assert detect_duplicates([]) == []
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestDeadCodeDetection:
|
|
268
|
+
def test_unused_function(self):
|
|
269
|
+
syms = [
|
|
270
|
+
_sym(name="used", body="pass"),
|
|
271
|
+
_sym(name="orphan", body="pass"),
|
|
272
|
+
]
|
|
273
|
+
results = detect_dead_code(syms)
|
|
274
|
+
names = [r.symbol_name for r in results]
|
|
275
|
+
assert "orphan" in names or "used" in names
|
|
276
|
+
|
|
277
|
+
def test_entry_points_excluded(self):
|
|
278
|
+
syms = [_sym(name="main", body="pass")]
|
|
279
|
+
results = detect_dead_code(syms)
|
|
280
|
+
assert all(r.symbol_name != "main" for r in results)
|
|
281
|
+
|
|
282
|
+
def test_test_functions_excluded(self):
|
|
283
|
+
syms = [_sym(name="test_something", body="pass")]
|
|
284
|
+
results = detect_dead_code(syms)
|
|
285
|
+
assert all(r.symbol_name != "test_something" for r in results)
|
|
286
|
+
|
|
287
|
+
def test_with_call_graph(self):
|
|
288
|
+
from semantic_code_intelligence.context.engine import CallGraph
|
|
289
|
+
syms = [
|
|
290
|
+
_sym(name="caller", body="orphan()"),
|
|
291
|
+
_sym(name="orphan", body="pass"),
|
|
292
|
+
]
|
|
293
|
+
cg = CallGraph()
|
|
294
|
+
cg.build(syms)
|
|
295
|
+
results = detect_dead_code(syms, call_graph=cg)
|
|
296
|
+
names = [r.symbol_name for r in results]
|
|
297
|
+
assert "orphan" not in names # it's referenced
|
|
298
|
+
|
|
299
|
+
def test_empty_input(self):
|
|
300
|
+
assert detect_dead_code([]) == []
|
|
301
|
+
|
|
302
|
+
def test_imports_not_flagged(self):
|
|
303
|
+
syms = [_sym(name="os", kind="import", body="import os")]
|
|
304
|
+
results = detect_dead_code(syms)
|
|
305
|
+
assert len(results) == 0
|
|
306
|
+
|
|
307
|
+
def test_result_to_dict(self):
|
|
308
|
+
d = DeadCodeResult("foo", "function", "a.py", 1).to_dict()
|
|
309
|
+
assert d["symbol_name"] == "foo"
|
|
310
|
+
assert d["kind"] == "function"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class TestComplexityResult:
|
|
314
|
+
def test_to_dict(self):
|
|
315
|
+
cr = ComplexityResult("fn", "a.py", 1, 10, 5, "low")
|
|
316
|
+
d = cr.to_dict()
|
|
317
|
+
assert d["symbol_name"] == "fn"
|
|
318
|
+
assert d["complexity"] == 5
|
|
319
|
+
assert d["rating"] == "low"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class TestDuplicateResult:
|
|
323
|
+
def test_to_dict(self):
|
|
324
|
+
dr = DuplicateResult("a", "f1.py", 1, "b", "f2.py", 2, 0.85)
|
|
325
|
+
d = dr.to_dict()
|
|
326
|
+
assert d["similarity"] == 0.85
|
|
327
|
+
assert d["symbol_a"] == "a"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class TestQualityReport:
|
|
331
|
+
def test_issue_count(self):
|
|
332
|
+
r = QualityReport(
|
|
333
|
+
complexity_issues=[ComplexityResult("fn", "a", 1, 2, 15, "high")],
|
|
334
|
+
dead_code=[DeadCodeResult("x", "function", "a", 1)],
|
|
335
|
+
duplicates=[],
|
|
336
|
+
)
|
|
337
|
+
assert r.issue_count == 2
|
|
338
|
+
|
|
339
|
+
def test_issue_count_with_safety(self):
|
|
340
|
+
from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
|
|
341
|
+
sr = SafetyReport(safe=False, issues=[SafetyIssue("p", "d", 1)])
|
|
342
|
+
r = QualityReport(safety=sr)
|
|
343
|
+
assert r.issue_count == 1
|
|
344
|
+
|
|
345
|
+
def test_to_dict(self):
|
|
346
|
+
d = QualityReport().to_dict()
|
|
347
|
+
assert d["issue_count"] == 0
|
|
348
|
+
assert d["files_analyzed"] == 0
|
|
349
|
+
|
|
350
|
+
def test_empty_report(self):
|
|
351
|
+
r = QualityReport()
|
|
352
|
+
assert r.issue_count == 0
|
|
353
|
+
assert r.to_dict()["safety"] is None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
# CI Metrics
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
from semantic_code_intelligence.ci.metrics import (
|
|
360
|
+
FileMetrics,
|
|
361
|
+
ProjectMetrics,
|
|
362
|
+
QualitySnapshot,
|
|
363
|
+
QualityPolicy,
|
|
364
|
+
TrendResult,
|
|
365
|
+
_compute_mi,
|
|
366
|
+
_count_lines,
|
|
367
|
+
_linear_slope,
|
|
368
|
+
compute_trend,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class TestCountLines:
|
|
373
|
+
def test_blank_lines(self):
|
|
374
|
+
code, comments, blanks = _count_lines("\n\n\n")
|
|
375
|
+
assert blanks == 3
|
|
376
|
+
|
|
377
|
+
def test_python_comments(self):
|
|
378
|
+
code, comments, blanks = _count_lines("# comment\ncode")
|
|
379
|
+
assert comments == 1
|
|
380
|
+
assert code == 1
|
|
381
|
+
|
|
382
|
+
def test_js_comments(self):
|
|
383
|
+
code, comments, blanks = _count_lines("// comment\ncode")
|
|
384
|
+
assert comments == 1
|
|
385
|
+
|
|
386
|
+
def test_block_comments(self):
|
|
387
|
+
code, comments, blanks = _count_lines("/* start\n * middle\n */\ncode")
|
|
388
|
+
assert comments == 3
|
|
389
|
+
assert code == 1
|
|
390
|
+
|
|
391
|
+
def test_empty_input(self):
|
|
392
|
+
code, comments, blanks = _count_lines("")
|
|
393
|
+
assert code == 0 and comments == 0 and blanks == 0
|
|
394
|
+
|
|
395
|
+
def test_mixed(self):
|
|
396
|
+
code, comments, blanks = _count_lines("x = 1\n\n# comment\ny = 2")
|
|
397
|
+
assert code == 2
|
|
398
|
+
assert comments == 1
|
|
399
|
+
assert blanks == 1
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class TestComputeMI:
|
|
403
|
+
def test_high_mi_for_simple_code(self):
|
|
404
|
+
mi = _compute_mi(10.0, 1.0, 0.3)
|
|
405
|
+
assert 0 <= mi <= 100
|
|
406
|
+
|
|
407
|
+
def test_low_mi_for_complex_code(self):
|
|
408
|
+
mi = _compute_mi(10000.0, 50.0, 0.0)
|
|
409
|
+
assert mi < 50
|
|
410
|
+
|
|
411
|
+
def test_zero_loc(self):
|
|
412
|
+
mi = _compute_mi(0.0, 0.0, 0.0)
|
|
413
|
+
assert 0 <= mi <= 100
|
|
414
|
+
|
|
415
|
+
def test_clamped_to_100(self):
|
|
416
|
+
mi = _compute_mi(1.0, 0.0, 1.0)
|
|
417
|
+
assert mi <= 100
|
|
418
|
+
|
|
419
|
+
def test_clamped_to_0(self):
|
|
420
|
+
mi = _compute_mi(1e10, 100.0, 0.0)
|
|
421
|
+
assert mi >= 0
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class TestFileMetrics:
|
|
425
|
+
def test_comment_ratio(self):
|
|
426
|
+
fm = FileMetrics(file_path="a.py", lines_of_code=70, comment_lines=20, blank_lines=10)
|
|
427
|
+
assert abs(fm.comment_ratio - 0.2) < 0.01
|
|
428
|
+
|
|
429
|
+
def test_comment_ratio_zero(self):
|
|
430
|
+
fm = FileMetrics(file_path="a.py")
|
|
431
|
+
assert fm.comment_ratio == 0.0
|
|
432
|
+
|
|
433
|
+
def test_to_dict(self):
|
|
434
|
+
d = FileMetrics(file_path="a.py", lines_of_code=10).to_dict()
|
|
435
|
+
assert d["file_path"] == "a.py"
|
|
436
|
+
assert "comment_ratio" in d
|
|
437
|
+
assert "maintainability_index" in d
|
|
438
|
+
|
|
439
|
+
def test_defaults(self):
|
|
440
|
+
fm = FileMetrics(file_path="x.py")
|
|
441
|
+
assert fm.lines_of_code == 0
|
|
442
|
+
assert fm.maintainability_index == 100.0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class TestProjectMetrics:
|
|
446
|
+
def test_comment_ratio(self):
|
|
447
|
+
pm = ProjectMetrics(total_loc=80, total_comment_lines=20, total_blank_lines=0)
|
|
448
|
+
assert abs(pm.comment_ratio - 0.2) < 0.01
|
|
449
|
+
|
|
450
|
+
def test_comment_ratio_zero(self):
|
|
451
|
+
pm = ProjectMetrics()
|
|
452
|
+
assert pm.comment_ratio == 0.0
|
|
453
|
+
|
|
454
|
+
def test_to_dict(self):
|
|
455
|
+
d = ProjectMetrics(files_analyzed=5).to_dict()
|
|
456
|
+
assert d["files_analyzed"] == 5
|
|
457
|
+
assert "file_metrics" in d
|
|
458
|
+
|
|
459
|
+
def test_with_file_metrics(self):
|
|
460
|
+
fm = FileMetrics(file_path="a.py", lines_of_code=10)
|
|
461
|
+
pm = ProjectMetrics(file_metrics=[fm])
|
|
462
|
+
assert len(pm.to_dict()["file_metrics"]) == 1
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class TestQualitySnapshot:
|
|
466
|
+
def test_to_dict(self):
|
|
467
|
+
qs = QualitySnapshot(
|
|
468
|
+
timestamp=1.0, maintainability_index=80.0, total_loc=100,
|
|
469
|
+
total_symbols=10, issue_count=2, files_analyzed=5,
|
|
470
|
+
avg_complexity=3.5, comment_ratio=0.15,
|
|
471
|
+
)
|
|
472
|
+
d = qs.to_dict()
|
|
473
|
+
assert d["maintainability_index"] == 80.0
|
|
474
|
+
assert d["total_loc"] == 100
|
|
475
|
+
|
|
476
|
+
def test_from_dict(self):
|
|
477
|
+
d = {
|
|
478
|
+
"timestamp": 1.0, "maintainability_index": 80.0, "total_loc": 100,
|
|
479
|
+
"total_symbols": 10, "issue_count": 2, "files_analyzed": 5,
|
|
480
|
+
"avg_complexity": 3.5, "comment_ratio": 0.15,
|
|
481
|
+
}
|
|
482
|
+
qs = QualitySnapshot.from_dict(d)
|
|
483
|
+
assert qs.timestamp == 1.0
|
|
484
|
+
assert qs.maintainability_index == 80.0
|
|
485
|
+
|
|
486
|
+
def test_roundtrip(self):
|
|
487
|
+
qs = QualitySnapshot(
|
|
488
|
+
timestamp=2.0, maintainability_index=75.0, total_loc=200,
|
|
489
|
+
total_symbols=20, issue_count=5, files_analyzed=10,
|
|
490
|
+
avg_complexity=5.0, comment_ratio=0.1,
|
|
491
|
+
)
|
|
492
|
+
restored = QualitySnapshot.from_dict(qs.to_dict())
|
|
493
|
+
assert restored.timestamp == qs.timestamp
|
|
494
|
+
assert restored.maintainability_index == qs.maintainability_index
|
|
495
|
+
|
|
496
|
+
def test_metadata(self):
|
|
497
|
+
qs = QualitySnapshot(
|
|
498
|
+
timestamp=1.0, maintainability_index=80.0, total_loc=100,
|
|
499
|
+
total_symbols=10, issue_count=0, files_analyzed=5,
|
|
500
|
+
avg_complexity=3.5, comment_ratio=0.15,
|
|
501
|
+
metadata={"branch": "main"},
|
|
502
|
+
)
|
|
503
|
+
assert qs.to_dict()["metadata"]["branch"] == "main"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class TestLinearSlope:
|
|
507
|
+
def test_positive_slope(self):
|
|
508
|
+
xs = [1.0, 2.0, 3.0]
|
|
509
|
+
ys = [10.0, 20.0, 30.0]
|
|
510
|
+
assert _linear_slope(xs, ys) == pytest.approx(10.0)
|
|
511
|
+
|
|
512
|
+
def test_zero_slope(self):
|
|
513
|
+
xs = [1.0, 2.0, 3.0]
|
|
514
|
+
ys = [5.0, 5.0, 5.0]
|
|
515
|
+
assert _linear_slope(xs, ys) == pytest.approx(0.0)
|
|
516
|
+
|
|
517
|
+
def test_negative_slope(self):
|
|
518
|
+
xs = [1.0, 2.0, 3.0]
|
|
519
|
+
ys = [30.0, 20.0, 10.0]
|
|
520
|
+
assert _linear_slope(xs, ys) == pytest.approx(-10.0)
|
|
521
|
+
|
|
522
|
+
def test_single_point(self):
|
|
523
|
+
assert _linear_slope([1.0], [5.0]) == 0.0
|
|
524
|
+
|
|
525
|
+
def test_empty(self):
|
|
526
|
+
assert _linear_slope([], []) == 0.0
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class TestComputeTrend:
|
|
530
|
+
def test_improving(self):
|
|
531
|
+
snaps = [
|
|
532
|
+
QualitySnapshot(timestamp=3.0, maintainability_index=90.0, total_loc=100,
|
|
533
|
+
total_symbols=10, issue_count=1, files_analyzed=5,
|
|
534
|
+
avg_complexity=2.0, comment_ratio=0.1),
|
|
535
|
+
QualitySnapshot(timestamp=2.0, maintainability_index=80.0, total_loc=100,
|
|
536
|
+
total_symbols=10, issue_count=2, files_analyzed=5,
|
|
537
|
+
avg_complexity=3.0, comment_ratio=0.1),
|
|
538
|
+
QualitySnapshot(timestamp=1.0, maintainability_index=70.0, total_loc=100,
|
|
539
|
+
total_symbols=10, issue_count=3, files_analyzed=5,
|
|
540
|
+
avg_complexity=4.0, comment_ratio=0.1),
|
|
541
|
+
]
|
|
542
|
+
result = compute_trend(snaps)
|
|
543
|
+
assert result.direction == "improving"
|
|
544
|
+
assert result.delta > 0
|
|
545
|
+
|
|
546
|
+
def test_degrading(self):
|
|
547
|
+
snaps = [
|
|
548
|
+
QualitySnapshot(timestamp=3.0, maintainability_index=50.0, total_loc=100,
|
|
549
|
+
total_symbols=10, issue_count=5, files_analyzed=5,
|
|
550
|
+
avg_complexity=8.0, comment_ratio=0.1),
|
|
551
|
+
QualitySnapshot(timestamp=1.0, maintainability_index=90.0, total_loc=100,
|
|
552
|
+
total_symbols=10, issue_count=1, files_analyzed=5,
|
|
553
|
+
avg_complexity=2.0, comment_ratio=0.1),
|
|
554
|
+
]
|
|
555
|
+
result = compute_trend(snaps)
|
|
556
|
+
assert result.direction == "degrading"
|
|
557
|
+
|
|
558
|
+
def test_stable(self):
|
|
559
|
+
snaps = [
|
|
560
|
+
QualitySnapshot(timestamp=2.0, maintainability_index=80.0, total_loc=100,
|
|
561
|
+
total_symbols=10, issue_count=2, files_analyzed=5,
|
|
562
|
+
avg_complexity=3.0, comment_ratio=0.1),
|
|
563
|
+
QualitySnapshot(timestamp=1.0, maintainability_index=80.0, total_loc=100,
|
|
564
|
+
total_symbols=10, issue_count=2, files_analyzed=5,
|
|
565
|
+
avg_complexity=3.0, comment_ratio=0.1),
|
|
566
|
+
]
|
|
567
|
+
result = compute_trend(snaps)
|
|
568
|
+
assert result.direction == "stable"
|
|
569
|
+
|
|
570
|
+
def test_empty_snapshots(self):
|
|
571
|
+
result = compute_trend([])
|
|
572
|
+
assert result.direction == "stable"
|
|
573
|
+
assert result.snapshot_count == 0
|
|
574
|
+
|
|
575
|
+
def test_single_snapshot(self):
|
|
576
|
+
snaps = [
|
|
577
|
+
QualitySnapshot(timestamp=1.0, maintainability_index=80.0, total_loc=100,
|
|
578
|
+
total_symbols=10, issue_count=2, files_analyzed=5,
|
|
579
|
+
avg_complexity=3.0, comment_ratio=0.1),
|
|
580
|
+
]
|
|
581
|
+
result = compute_trend(snaps)
|
|
582
|
+
assert result.direction == "stable"
|
|
583
|
+
|
|
584
|
+
def test_to_dict(self):
|
|
585
|
+
result = TrendResult("mi", 3, 70.0, 90.0, 20.0, 10.0, "improving")
|
|
586
|
+
d = result.to_dict()
|
|
587
|
+
assert d["metric_name"] == "mi"
|
|
588
|
+
assert d["direction"] == "improving"
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class TestQualityPolicy:
|
|
592
|
+
def test_defaults(self):
|
|
593
|
+
p = QualityPolicy()
|
|
594
|
+
assert p.min_maintainability == 40.0
|
|
595
|
+
assert p.max_complexity == 25
|
|
596
|
+
assert p.require_safety_pass is True
|
|
597
|
+
|
|
598
|
+
def test_to_dict(self):
|
|
599
|
+
d = QualityPolicy().to_dict()
|
|
600
|
+
assert "min_maintainability" in d
|
|
601
|
+
assert "max_issues" in d
|
|
602
|
+
|
|
603
|
+
def test_from_dict(self):
|
|
604
|
+
d = {"min_maintainability": 60.0, "max_complexity": 15}
|
|
605
|
+
p = QualityPolicy.from_dict(d)
|
|
606
|
+
assert p.min_maintainability == 60.0
|
|
607
|
+
assert p.max_complexity == 15
|
|
608
|
+
|
|
609
|
+
def test_roundtrip(self):
|
|
610
|
+
p = QualityPolicy(min_maintainability=55.0, max_dead_code=5)
|
|
611
|
+
restored = QualityPolicy.from_dict(p.to_dict())
|
|
612
|
+
assert restored.min_maintainability == p.min_maintainability
|
|
613
|
+
assert restored.max_dead_code == p.max_dead_code
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# ---------------------------------------------------------------------------
|
|
617
|
+
# CI PR
|
|
618
|
+
# ---------------------------------------------------------------------------
|
|
619
|
+
from semantic_code_intelligence.ci.pr import (
|
|
620
|
+
FileChange,
|
|
621
|
+
ChangeSummary,
|
|
622
|
+
ImpactResult,
|
|
623
|
+
RiskScore,
|
|
624
|
+
_risk_level,
|
|
625
|
+
compute_risk,
|
|
626
|
+
suggest_reviewers,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class TestFileChange:
|
|
631
|
+
def test_to_dict(self):
|
|
632
|
+
fc = FileChange(path="a.py", language="python", symbols_added=["foo"])
|
|
633
|
+
d = fc.to_dict()
|
|
634
|
+
assert d["path"] == "a.py"
|
|
635
|
+
assert d["symbols_added"] == ["foo"]
|
|
636
|
+
|
|
637
|
+
def test_defaults(self):
|
|
638
|
+
fc = FileChange(path="x.js")
|
|
639
|
+
assert fc.symbols_modified == []
|
|
640
|
+
assert fc.import_changes == []
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class TestChangeSummary:
|
|
644
|
+
def test_to_dict(self):
|
|
645
|
+
cs = ChangeSummary(files_changed=3, languages=["python"])
|
|
646
|
+
d = cs.to_dict()
|
|
647
|
+
assert d["files_changed"] == 3
|
|
648
|
+
assert "python" in d["languages"]
|
|
649
|
+
|
|
650
|
+
def test_defaults(self):
|
|
651
|
+
cs = ChangeSummary()
|
|
652
|
+
assert cs.files_changed == 0
|
|
653
|
+
assert cs.total_symbols_added == 0
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
class TestImpactResultPR:
|
|
657
|
+
def test_to_dict(self):
|
|
658
|
+
ir = ImpactResult(
|
|
659
|
+
changed_symbols=["foo"],
|
|
660
|
+
affected_files=["a.py"],
|
|
661
|
+
affected_symbols=["bar"],
|
|
662
|
+
)
|
|
663
|
+
d = ir.to_dict()
|
|
664
|
+
assert "foo" in d["changed_symbols"]
|
|
665
|
+
|
|
666
|
+
def test_defaults(self):
|
|
667
|
+
ir = ImpactResult()
|
|
668
|
+
assert ir.changed_symbols == []
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class TestRiskLevel:
|
|
672
|
+
def test_low(self):
|
|
673
|
+
assert _risk_level(10) == "low"
|
|
674
|
+
|
|
675
|
+
def test_medium(self):
|
|
676
|
+
assert _risk_level(30) == "medium"
|
|
677
|
+
|
|
678
|
+
def test_high(self):
|
|
679
|
+
assert _risk_level(60) == "high"
|
|
680
|
+
|
|
681
|
+
def test_critical(self):
|
|
682
|
+
assert _risk_level(80) == "critical"
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class TestRiskScore:
|
|
686
|
+
def test_to_dict(self):
|
|
687
|
+
rs = RiskScore(score=45, level="medium", factors=["big changeset"])
|
|
688
|
+
d = rs.to_dict()
|
|
689
|
+
assert d["score"] == 45
|
|
690
|
+
assert d["level"] == "medium"
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class TestComputeRisk:
|
|
694
|
+
def test_small_changeset(self):
|
|
695
|
+
cs = ChangeSummary(files_changed=2)
|
|
696
|
+
risk = compute_risk(cs)
|
|
697
|
+
assert risk.score < 25
|
|
698
|
+
assert risk.level == "low"
|
|
699
|
+
|
|
700
|
+
def test_large_changeset(self):
|
|
701
|
+
cs = ChangeSummary(files_changed=25, total_symbols_removed=15,
|
|
702
|
+
total_symbols_modified=15)
|
|
703
|
+
risk = compute_risk(cs)
|
|
704
|
+
assert risk.score > 25
|
|
705
|
+
|
|
706
|
+
def test_with_safety_issues(self):
|
|
707
|
+
from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
|
|
708
|
+
sr = SafetyReport(safe=False, issues=[SafetyIssue("p", "d"), SafetyIssue("p2", "d2")])
|
|
709
|
+
cs = ChangeSummary(files_changed=1)
|
|
710
|
+
risk = compute_risk(cs, safety_report=sr)
|
|
711
|
+
assert risk.score > 10
|
|
712
|
+
assert any("safety" in f for f in risk.factors)
|
|
713
|
+
|
|
714
|
+
def test_with_impact(self):
|
|
715
|
+
ir = ImpactResult(affected_symbols=["a", "b", "c", "d", "e", "f"])
|
|
716
|
+
cs = ChangeSummary(files_changed=5)
|
|
717
|
+
risk = compute_risk(cs, impact=ir)
|
|
718
|
+
assert risk.score > 10
|
|
719
|
+
|
|
720
|
+
def test_empty(self):
|
|
721
|
+
cs = ChangeSummary()
|
|
722
|
+
risk = compute_risk(cs)
|
|
723
|
+
assert risk.level == "low"
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class TestSuggestReviewers:
|
|
727
|
+
def test_groups_by_domain(self):
|
|
728
|
+
files = ["auth/login.py", "auth/logout.py", "api/routes.py"]
|
|
729
|
+
reviewers = suggest_reviewers(files)
|
|
730
|
+
assert len(reviewers) >= 2
|
|
731
|
+
|
|
732
|
+
def test_empty_files(self):
|
|
733
|
+
assert suggest_reviewers([]) == []
|
|
734
|
+
|
|
735
|
+
def test_single_file(self):
|
|
736
|
+
result = suggest_reviewers(["app.py"])
|
|
737
|
+
assert len(result) >= 1
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
# ---------------------------------------------------------------------------
|
|
741
|
+
# CI Hooks
|
|
742
|
+
# ---------------------------------------------------------------------------
|
|
743
|
+
from semantic_code_intelligence.ci.hooks import HookResult, run_precommit_check
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class TestHookResult:
|
|
747
|
+
def test_defaults(self):
|
|
748
|
+
hr = HookResult()
|
|
749
|
+
assert hr.passed is True
|
|
750
|
+
assert hr.files_checked == 0
|
|
751
|
+
assert hr.safety is None
|
|
752
|
+
|
|
753
|
+
def test_to_dict(self):
|
|
754
|
+
d = HookResult(passed=False, files_checked=3).to_dict()
|
|
755
|
+
assert d["passed"] is False
|
|
756
|
+
assert d["files_checked"] == 3
|
|
757
|
+
|
|
758
|
+
def test_with_safety(self):
|
|
759
|
+
from semantic_code_intelligence.llm.safety import SafetyReport
|
|
760
|
+
sr = SafetyReport(safe=True)
|
|
761
|
+
hr = HookResult(safety=sr)
|
|
762
|
+
d = hr.to_dict()
|
|
763
|
+
assert d["safety"]["safe"] is True
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class TestRunPrecommitCheck:
|
|
767
|
+
def test_safe_files(self):
|
|
768
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
769
|
+
f.write("x = 1\ny = 2\n")
|
|
770
|
+
f.flush()
|
|
771
|
+
result = run_precommit_check([f.name], run_plugins=False)
|
|
772
|
+
assert result.passed is True
|
|
773
|
+
assert result.files_checked == 1
|
|
774
|
+
|
|
775
|
+
def test_unsafe_files(self):
|
|
776
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
777
|
+
f.write("import os\nos.system('rm -rf /')\n")
|
|
778
|
+
f.flush()
|
|
779
|
+
result = run_precommit_check([f.name], run_plugins=False)
|
|
780
|
+
assert result.passed is False
|
|
781
|
+
|
|
782
|
+
def test_empty_files(self):
|
|
783
|
+
result = run_precommit_check([], run_plugins=False)
|
|
784
|
+
assert result.passed is True
|
|
785
|
+
assert result.files_checked == 0
|
|
786
|
+
|
|
787
|
+
def test_nonexistent_file(self):
|
|
788
|
+
result = run_precommit_check(["/nonexistent/file.py"], run_plugins=False)
|
|
789
|
+
assert result.files_checked == 1
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# ---------------------------------------------------------------------------
|
|
793
|
+
# CI Templates
|
|
794
|
+
# ---------------------------------------------------------------------------
|
|
795
|
+
from semantic_code_intelligence.ci.templates import (
|
|
796
|
+
generate_analysis_workflow,
|
|
797
|
+
generate_precommit_config,
|
|
798
|
+
generate_safety_workflow,
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
class TestGenerateAnalysisWorkflow:
|
|
803
|
+
def test_contains_yaml(self):
|
|
804
|
+
wf = generate_analysis_workflow()
|
|
805
|
+
assert "name: CodexA Analysis" in wf
|
|
806
|
+
|
|
807
|
+
def test_python_version(self):
|
|
808
|
+
wf = generate_analysis_workflow(python_version="3.13")
|
|
809
|
+
assert "3.13" in wf
|
|
810
|
+
|
|
811
|
+
def test_trigger(self):
|
|
812
|
+
wf = generate_analysis_workflow(trigger="push")
|
|
813
|
+
assert "push:" in wf
|
|
814
|
+
|
|
815
|
+
def test_contains_steps(self):
|
|
816
|
+
wf = generate_analysis_workflow()
|
|
817
|
+
assert "codexa init" in wf
|
|
818
|
+
assert "codexa quality" in wf
|
|
819
|
+
|
|
820
|
+
def test_permissions(self):
|
|
821
|
+
wf = generate_analysis_workflow()
|
|
822
|
+
assert "permissions:" in wf
|
|
823
|
+
assert "contents: read" in wf
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class TestGeneratePrecommitConfig:
|
|
827
|
+
def test_contains_hooks(self):
|
|
828
|
+
cfg = generate_precommit_config()
|
|
829
|
+
assert "codexa-safety" in cfg
|
|
830
|
+
assert "codexa-quality" in cfg
|
|
831
|
+
|
|
832
|
+
def test_repo_local(self):
|
|
833
|
+
cfg = generate_precommit_config()
|
|
834
|
+
assert "repo: local" in cfg
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class TestGenerateSafetyWorkflow:
|
|
838
|
+
def test_contains_yaml(self):
|
|
839
|
+
wf = generate_safety_workflow()
|
|
840
|
+
assert "name: CodexA Safety" in wf
|
|
841
|
+
|
|
842
|
+
def test_python_version(self):
|
|
843
|
+
wf = generate_safety_workflow(python_version="3.11")
|
|
844
|
+
assert "3.11" in wf
|
|
845
|
+
|
|
846
|
+
def test_permissions(self):
|
|
847
|
+
wf = generate_safety_workflow()
|
|
848
|
+
assert "contents: read" in wf
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
# ---------------------------------------------------------------------------
|
|
852
|
+
# CI Hotspots
|
|
853
|
+
# ---------------------------------------------------------------------------
|
|
854
|
+
from semantic_code_intelligence.ci.hotspots import (
|
|
855
|
+
HotspotFactor,
|
|
856
|
+
Hotspot,
|
|
857
|
+
HotspotReport,
|
|
858
|
+
_normalise,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class TestNormalise:
|
|
863
|
+
def test_zero(self):
|
|
864
|
+
assert _normalise(0.0, 10.0) == 0.0
|
|
865
|
+
|
|
866
|
+
def test_max(self):
|
|
867
|
+
assert _normalise(10.0, 10.0) == 1.0
|
|
868
|
+
|
|
869
|
+
def test_over_max(self):
|
|
870
|
+
assert _normalise(20.0, 10.0) == 1.0
|
|
871
|
+
|
|
872
|
+
def test_zero_max(self):
|
|
873
|
+
assert _normalise(5.0, 0.0) == 0.0
|
|
874
|
+
|
|
875
|
+
def test_negative_max(self):
|
|
876
|
+
assert _normalise(5.0, -1.0) == 0.0
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
class TestHotspotFactor:
|
|
880
|
+
def test_to_dict(self):
|
|
881
|
+
hf = HotspotFactor(name="complexity", raw_value=15.0, normalized=0.75, weight=0.3)
|
|
882
|
+
d = hf.to_dict()
|
|
883
|
+
assert d["name"] == "complexity"
|
|
884
|
+
assert d["normalized"] == 0.75
|
|
885
|
+
|
|
886
|
+
def test_rounding(self):
|
|
887
|
+
hf = HotspotFactor(name="x", raw_value=1.23456, normalized=0.12345, weight=0.3)
|
|
888
|
+
d = hf.to_dict()
|
|
889
|
+
assert d["raw_value"] == 1.23
|
|
890
|
+
assert d["normalized"] == 0.123
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
class TestHotspot:
|
|
894
|
+
def test_to_dict(self):
|
|
895
|
+
h = Hotspot(name="fn", file_path="a.py", kind="symbol", risk_score=85.3)
|
|
896
|
+
d = h.to_dict()
|
|
897
|
+
assert d["risk_score"] == 85.3
|
|
898
|
+
assert d["kind"] == "symbol"
|
|
899
|
+
|
|
900
|
+
def test_with_factors(self):
|
|
901
|
+
hf = HotspotFactor("x", 1.0, 0.5, 0.3)
|
|
902
|
+
h = Hotspot(name="fn", file_path="a.py", kind="symbol", risk_score=50.0, factors=[hf])
|
|
903
|
+
assert len(h.to_dict()["factors"]) == 1
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
class TestHotspotReport:
|
|
907
|
+
def test_to_dict(self):
|
|
908
|
+
hr = HotspotReport(files_analyzed=10, symbols_analyzed=50)
|
|
909
|
+
d = hr.to_dict()
|
|
910
|
+
assert d["files_analyzed"] == 10
|
|
911
|
+
assert d["hotspot_count"] == 0
|
|
912
|
+
|
|
913
|
+
def test_with_hotspots(self):
|
|
914
|
+
h = Hotspot(name="fn", file_path="a.py", kind="symbol", risk_score=80.0)
|
|
915
|
+
hr = HotspotReport(files_analyzed=5, symbols_analyzed=20, hotspots=[h])
|
|
916
|
+
assert hr.to_dict()["hotspot_count"] == 1
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
# ---------------------------------------------------------------------------
|
|
920
|
+
# CI Impact
|
|
921
|
+
# ---------------------------------------------------------------------------
|
|
922
|
+
from semantic_code_intelligence.ci.impact import (
|
|
923
|
+
AffectedSymbol,
|
|
924
|
+
AffectedModule,
|
|
925
|
+
DependencyChain,
|
|
926
|
+
ImpactReport as CIImpactReport,
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
class TestAffectedSymbol:
|
|
931
|
+
def test_to_dict(self):
|
|
932
|
+
s = AffectedSymbol("fn", "a.py", "function", "direct_caller", 1)
|
|
933
|
+
d = s.to_dict()
|
|
934
|
+
assert d["name"] == "fn"
|
|
935
|
+
assert d["depth"] == 1
|
|
936
|
+
assert d["relationship"] == "direct_caller"
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
class TestAffectedModule:
|
|
940
|
+
def test_to_dict(self):
|
|
941
|
+
m = AffectedModule("a.py", "imports_target", 1)
|
|
942
|
+
d = m.to_dict()
|
|
943
|
+
assert d["file_path"] == "a.py"
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class TestDependencyChain:
|
|
947
|
+
def test_to_dict(self):
|
|
948
|
+
dc = DependencyChain(path=["a.py", "b.py", "c.py"])
|
|
949
|
+
d = dc.to_dict()
|
|
950
|
+
assert len(d["path"]) == 3
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
class TestCIImpactReport:
|
|
954
|
+
def test_total_affected(self):
|
|
955
|
+
r = CIImpactReport(
|
|
956
|
+
target="fn", target_kind="symbol",
|
|
957
|
+
direct_symbols=[AffectedSymbol("a", "x.py", "function", "direct_caller", 1)],
|
|
958
|
+
transitive_symbols=[AffectedSymbol("b", "y.py", "function", "transitive_caller", 2)],
|
|
959
|
+
)
|
|
960
|
+
assert r.total_affected == 2
|
|
961
|
+
|
|
962
|
+
def test_to_dict(self):
|
|
963
|
+
r = CIImpactReport(target="fn", target_kind="symbol")
|
|
964
|
+
d = r.to_dict()
|
|
965
|
+
assert d["target"] == "fn"
|
|
966
|
+
assert d["total_affected"] == 0
|
|
967
|
+
|
|
968
|
+
def test_empty_report(self):
|
|
969
|
+
r = CIImpactReport(target="x", target_kind="file")
|
|
970
|
+
assert r.total_affected == 0
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
# ---------------------------------------------------------------------------
|
|
974
|
+
# CI Trace
|
|
975
|
+
# ---------------------------------------------------------------------------
|
|
976
|
+
from semantic_code_intelligence.ci.trace import (
|
|
977
|
+
TraceNode,
|
|
978
|
+
TraceEdge,
|
|
979
|
+
TraceResult,
|
|
980
|
+
trace_symbol,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
class TestTraceNode:
|
|
985
|
+
def test_to_dict(self):
|
|
986
|
+
tn = TraceNode("fn", "a.py", "function", -2)
|
|
987
|
+
d = tn.to_dict()
|
|
988
|
+
assert d["depth"] == -2
|
|
989
|
+
assert d["kind"] == "function"
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class TestTraceEdge:
|
|
993
|
+
def test_to_dict(self):
|
|
994
|
+
te = TraceEdge("caller", "callee", "a.py")
|
|
995
|
+
d = te.to_dict()
|
|
996
|
+
assert d["caller"] == "caller"
|
|
997
|
+
assert d["callee"] == "callee"
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
class TestTraceResult:
|
|
1001
|
+
def test_total_nodes(self):
|
|
1002
|
+
tr = TraceResult(
|
|
1003
|
+
target="fn", target_file="a.py",
|
|
1004
|
+
upstream=[TraceNode("a", "a.py", "function", -1)],
|
|
1005
|
+
downstream=[TraceNode("b", "b.py", "function", 1),
|
|
1006
|
+
TraceNode("c", "c.py", "function", 2)],
|
|
1007
|
+
)
|
|
1008
|
+
assert tr.total_nodes == 3
|
|
1009
|
+
|
|
1010
|
+
def test_to_dict(self):
|
|
1011
|
+
tr = TraceResult(target="fn", target_file="a.py")
|
|
1012
|
+
d = tr.to_dict()
|
|
1013
|
+
assert d["target"] == "fn"
|
|
1014
|
+
assert d["total_nodes"] == 0
|
|
1015
|
+
|
|
1016
|
+
def test_empty(self):
|
|
1017
|
+
tr = TraceResult(target="missing", target_file="")
|
|
1018
|
+
assert tr.total_nodes == 0
|
|
1019
|
+
assert tr.max_upstream_depth == 0
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
class TestTraceSymbol:
|
|
1023
|
+
def test_unknown_symbol(self):
|
|
1024
|
+
result = trace_symbol("nonexistent", [], CallGraph())
|
|
1025
|
+
assert result.target_file == ""
|
|
1026
|
+
assert result.total_nodes == 0
|
|
1027
|
+
|
|
1028
|
+
def test_symbol_with_callers(self):
|
|
1029
|
+
from semantic_code_intelligence.context.engine import CallGraph
|
|
1030
|
+
syms = [
|
|
1031
|
+
_sym(name="caller", body="target()", file_path="a.py"),
|
|
1032
|
+
_sym(name="target", body="pass", file_path="b.py"),
|
|
1033
|
+
]
|
|
1034
|
+
cg = CallGraph()
|
|
1035
|
+
cg.build(syms)
|
|
1036
|
+
result = trace_symbol("target", syms, cg)
|
|
1037
|
+
assert len(result.upstream) >= 1
|
|
1038
|
+
|
|
1039
|
+
def test_symbol_with_callees(self):
|
|
1040
|
+
from semantic_code_intelligence.context.engine import CallGraph
|
|
1041
|
+
syms = [
|
|
1042
|
+
_sym(name="entry", body="helper()", file_path="a.py"),
|
|
1043
|
+
_sym(name="helper", body="pass", file_path="a.py"),
|
|
1044
|
+
]
|
|
1045
|
+
cg = CallGraph()
|
|
1046
|
+
cg.build(syms)
|
|
1047
|
+
result = trace_symbol("entry", syms, cg)
|
|
1048
|
+
assert len(result.downstream) >= 1
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
# ---------------------------------------------------------------------------
|
|
1052
|
+
# LLM Safety
|
|
1053
|
+
# ---------------------------------------------------------------------------
|
|
1054
|
+
from semantic_code_intelligence.llm.safety import (
|
|
1055
|
+
SafetyIssue,
|
|
1056
|
+
SafetyReport,
|
|
1057
|
+
SafetyValidator,
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
class TestSafetyIssue:
|
|
1062
|
+
def test_to_dict(self):
|
|
1063
|
+
si = SafetyIssue("eval", "dangerous", line_number=5, severity="error")
|
|
1064
|
+
d = si.to_dict()
|
|
1065
|
+
assert d["pattern"] == "eval"
|
|
1066
|
+
assert d["line_number"] == 5
|
|
1067
|
+
|
|
1068
|
+
def test_defaults(self):
|
|
1069
|
+
si = SafetyIssue("p", "d")
|
|
1070
|
+
assert si.line_number == 0
|
|
1071
|
+
assert si.severity == "warning"
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
class TestSafetyReport:
|
|
1075
|
+
def test_safe(self):
|
|
1076
|
+
sr = SafetyReport()
|
|
1077
|
+
assert sr.safe is True
|
|
1078
|
+
assert sr.to_dict()["issue_count"] == 0
|
|
1079
|
+
|
|
1080
|
+
def test_unsafe(self):
|
|
1081
|
+
sr = SafetyReport(safe=False, issues=[SafetyIssue("p", "d")])
|
|
1082
|
+
assert sr.to_dict()["issue_count"] == 1
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
class TestSafetyValidator:
|
|
1086
|
+
def test_safe_code(self):
|
|
1087
|
+
v = SafetyValidator()
|
|
1088
|
+
report = v.validate("x = 1\ny = 2")
|
|
1089
|
+
assert report.safe is True
|
|
1090
|
+
|
|
1091
|
+
def test_os_system(self):
|
|
1092
|
+
v = SafetyValidator()
|
|
1093
|
+
report = v.validate("os.system('ls')")
|
|
1094
|
+
assert report.safe is False
|
|
1095
|
+
|
|
1096
|
+
def test_eval(self):
|
|
1097
|
+
v = SafetyValidator()
|
|
1098
|
+
report = v.validate("result = eval('1+1')")
|
|
1099
|
+
assert report.safe is False
|
|
1100
|
+
|
|
1101
|
+
def test_exec(self):
|
|
1102
|
+
v = SafetyValidator()
|
|
1103
|
+
report = v.validate("exec('print(1)')")
|
|
1104
|
+
assert report.safe is False
|
|
1105
|
+
|
|
1106
|
+
def test_subprocess_shell(self):
|
|
1107
|
+
v = SafetyValidator()
|
|
1108
|
+
report = v.validate("subprocess.run('cmd', shell=True)")
|
|
1109
|
+
assert report.safe is False
|
|
1110
|
+
|
|
1111
|
+
def test_rm_rf(self):
|
|
1112
|
+
v = SafetyValidator()
|
|
1113
|
+
report = v.validate("rm -rf /important/data")
|
|
1114
|
+
assert report.safe is False
|
|
1115
|
+
|
|
1116
|
+
def test_drop_table(self):
|
|
1117
|
+
v = SafetyValidator()
|
|
1118
|
+
report = v.validate("DROP TABLE users")
|
|
1119
|
+
assert report.safe is False
|
|
1120
|
+
|
|
1121
|
+
def test_path_traversal(self):
|
|
1122
|
+
v = SafetyValidator()
|
|
1123
|
+
report = v.validate("open('../../etc/passwd')")
|
|
1124
|
+
assert report.safe is False
|
|
1125
|
+
|
|
1126
|
+
def test_hardcoded_secret(self):
|
|
1127
|
+
v = SafetyValidator()
|
|
1128
|
+
report = v.validate("password = 'supersecretpassword123'")
|
|
1129
|
+
assert report.safe is False
|
|
1130
|
+
|
|
1131
|
+
def test_innerHTML(self):
|
|
1132
|
+
v = SafetyValidator()
|
|
1133
|
+
report = v.validate("element.innerHTML = userInput")
|
|
1134
|
+
assert report.safe is False
|
|
1135
|
+
|
|
1136
|
+
def test_md5(self):
|
|
1137
|
+
v = SafetyValidator()
|
|
1138
|
+
report = v.validate("hash = MD5(data)")
|
|
1139
|
+
assert report.safe is False
|
|
1140
|
+
|
|
1141
|
+
def test_http_url(self):
|
|
1142
|
+
v = SafetyValidator()
|
|
1143
|
+
report = v.validate("url = 'http://example.com/api'")
|
|
1144
|
+
assert report.safe is False
|
|
1145
|
+
|
|
1146
|
+
def test_http_localhost_ok(self):
|
|
1147
|
+
v = SafetyValidator()
|
|
1148
|
+
report = v.validate("url = 'http://localhost:8080'")
|
|
1149
|
+
assert report.safe is True
|
|
1150
|
+
|
|
1151
|
+
def test_verify_false(self):
|
|
1152
|
+
v = SafetyValidator()
|
|
1153
|
+
report = v.validate("requests.get(url, verify=False)")
|
|
1154
|
+
assert report.safe is False
|
|
1155
|
+
|
|
1156
|
+
def test_custom_patterns(self):
|
|
1157
|
+
v = SafetyValidator(extra_patterns=[
|
|
1158
|
+
(r"DANGER", "Custom danger pattern")
|
|
1159
|
+
])
|
|
1160
|
+
report = v.validate("DANGER: doing something bad")
|
|
1161
|
+
assert report.safe is False
|
|
1162
|
+
|
|
1163
|
+
def test_dynamic_import(self):
|
|
1164
|
+
v = SafetyValidator()
|
|
1165
|
+
report = v.validate("mod = __import__('os')")
|
|
1166
|
+
assert report.safe is False
|
|
1167
|
+
|
|
1168
|
+
def test_truncate_table(self):
|
|
1169
|
+
v = SafetyValidator()
|
|
1170
|
+
report = v.validate("TRUNCATE TABLE sessions")
|
|
1171
|
+
assert report.safe is False
|
|
1172
|
+
|
|
1173
|
+
def test_document_write(self):
|
|
1174
|
+
v = SafetyValidator()
|
|
1175
|
+
report = v.validate("document.write(payload)")
|
|
1176
|
+
assert report.safe is False
|
|
1177
|
+
|
|
1178
|
+
def test_sha1(self):
|
|
1179
|
+
v = SafetyValidator()
|
|
1180
|
+
report = v.validate("hash = sha1(data)")
|
|
1181
|
+
assert report.safe is False
|
|
1182
|
+
|
|
1183
|
+
def test_multiple_issues(self):
|
|
1184
|
+
v = SafetyValidator()
|
|
1185
|
+
report = v.validate("eval('x')\nos.system('y')\nexec('z')")
|
|
1186
|
+
assert len(report.issues) >= 3
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
# ---------------------------------------------------------------------------
|
|
1190
|
+
# LLM Streaming
|
|
1191
|
+
# ---------------------------------------------------------------------------
|
|
1192
|
+
from semantic_code_intelligence.llm.streaming import StreamEvent
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
class TestStreamEvent:
|
|
1196
|
+
def test_create(self):
|
|
1197
|
+
se = StreamEvent(kind="token", content="hello")
|
|
1198
|
+
assert se.kind == "token"
|
|
1199
|
+
assert se.content == "hello"
|
|
1200
|
+
|
|
1201
|
+
def test_to_sse(self):
|
|
1202
|
+
se = StreamEvent(kind="token", content="hello")
|
|
1203
|
+
sse = se.to_sse()
|
|
1204
|
+
assert "data: " in sse
|
|
1205
|
+
assert "hello" in sse
|
|
1206
|
+
|
|
1207
|
+
def test_to_sse_multiline(self):
|
|
1208
|
+
se = StreamEvent(kind="chunk", content="line1\nline2")
|
|
1209
|
+
sse = se.to_sse()
|
|
1210
|
+
assert "data: " in sse
|
|
1211
|
+
assert "line1\\nline2" in sse
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
# ---------------------------------------------------------------------------
|
|
1215
|
+
# LLM Providers
|
|
1216
|
+
# ---------------------------------------------------------------------------
|
|
1217
|
+
from semantic_code_intelligence.llm.provider import MessageRole, LLMMessage, LLMResponse
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
class TestMessageRole:
|
|
1221
|
+
def test_values(self):
|
|
1222
|
+
assert MessageRole.SYSTEM == "system"
|
|
1223
|
+
assert MessageRole.USER == "user"
|
|
1224
|
+
assert MessageRole.ASSISTANT == "assistant"
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
class TestLLMMessage:
|
|
1228
|
+
def test_create(self):
|
|
1229
|
+
m = LLMMessage(role=MessageRole.USER, content="Hello")
|
|
1230
|
+
assert m.role == "user"
|
|
1231
|
+
assert m.content == "Hello"
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
class TestLLMResponse:
|
|
1235
|
+
def test_create(self):
|
|
1236
|
+
r = LLMResponse(content="answer", model="gpt-4", usage={"tokens": 100})
|
|
1237
|
+
assert r.content == "answer"
|
|
1238
|
+
assert r.model == "gpt-4"
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
# ---------------------------------------------------------------------------
|
|
1242
|
+
# LLM Mock Provider
|
|
1243
|
+
# ---------------------------------------------------------------------------
|
|
1244
|
+
from semantic_code_intelligence.llm.mock_provider import MockProvider
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
class TestMockProvider:
|
|
1248
|
+
def test_chat(self):
|
|
1249
|
+
p = MockProvider()
|
|
1250
|
+
msgs = [LLMMessage(role=MessageRole.USER, content="test")]
|
|
1251
|
+
response = p.chat(msgs)
|
|
1252
|
+
assert isinstance(response, LLMResponse)
|
|
1253
|
+
assert len(response.content) > 0
|
|
1254
|
+
|
|
1255
|
+
def test_model_name(self):
|
|
1256
|
+
p = MockProvider()
|
|
1257
|
+
assert "mock" in p._model.lower()
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
# ---------------------------------------------------------------------------
|
|
1261
|
+
# LLM Conversation
|
|
1262
|
+
# ---------------------------------------------------------------------------
|
|
1263
|
+
from semantic_code_intelligence.llm.conversation import ConversationSession
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
class TestConversationSession:
|
|
1267
|
+
def test_create(self):
|
|
1268
|
+
cs = ConversationSession(session_id="test-1")
|
|
1269
|
+
assert cs.session_id == "test-1"
|
|
1270
|
+
|
|
1271
|
+
def test_add_message(self):
|
|
1272
|
+
cs = ConversationSession(session_id="s1")
|
|
1273
|
+
from semantic_code_intelligence.llm.provider import MessageRole
|
|
1274
|
+
cs.add_message(MessageRole.USER, "hello")
|
|
1275
|
+
cs.add_message(MessageRole.ASSISTANT, "hi")
|
|
1276
|
+
assert len(cs.messages) == 2
|
|
1277
|
+
|
|
1278
|
+
def test_messages_ordered(self):
|
|
1279
|
+
cs = ConversationSession(session_id="s1")
|
|
1280
|
+
from semantic_code_intelligence.llm.provider import MessageRole
|
|
1281
|
+
cs.add_message(MessageRole.USER, "first")
|
|
1282
|
+
cs.add_message(MessageRole.ASSISTANT, "second")
|
|
1283
|
+
assert cs.messages[0].content == "first"
|
|
1284
|
+
assert cs.messages[1].content == "second"
|
|
1285
|
+
|
|
1286
|
+
def test_to_dict(self):
|
|
1287
|
+
cs = ConversationSession(session_id="s1")
|
|
1288
|
+
from semantic_code_intelligence.llm.provider import MessageRole
|
|
1289
|
+
cs.add_message(MessageRole.USER, "hi")
|
|
1290
|
+
d = cs.to_dict()
|
|
1291
|
+
assert d["session_id"] == "s1"
|
|
1292
|
+
assert len(d["messages"]) == 1
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
# ---------------------------------------------------------------------------
|
|
1296
|
+
# Context Engine
|
|
1297
|
+
# ---------------------------------------------------------------------------
|
|
1298
|
+
from semantic_code_intelligence.context.engine import (
|
|
1299
|
+
ContextWindow,
|
|
1300
|
+
ContextBuilder,
|
|
1301
|
+
CallEdge,
|
|
1302
|
+
CallGraph,
|
|
1303
|
+
DependencyMap,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
class TestContextWindow:
|
|
1308
|
+
def test_to_dict(self):
|
|
1309
|
+
s = _sym(name="fn", body="pass")
|
|
1310
|
+
cw = ContextWindow(focal_symbol=s)
|
|
1311
|
+
d = cw.to_dict()
|
|
1312
|
+
assert d["focal_symbol"]["name"] == "fn"
|
|
1313
|
+
|
|
1314
|
+
def test_render(self):
|
|
1315
|
+
s = _sym(name="fn", body="pass\nreturn 1", file_path="a.py")
|
|
1316
|
+
cw = ContextWindow(focal_symbol=s, imports=[_sym(name="os", kind="import", body="import os")])
|
|
1317
|
+
text = cw.render()
|
|
1318
|
+
assert "fn" in text
|
|
1319
|
+
assert "Imports" in text
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
class TestContextBuilder:
|
|
1323
|
+
def test_index_file_with_content(self):
|
|
1324
|
+
cb = ContextBuilder()
|
|
1325
|
+
syms = cb.index_file("test.py", "def foo():\n pass\n")
|
|
1326
|
+
assert len(syms) >= 1
|
|
1327
|
+
|
|
1328
|
+
def test_find_symbol(self):
|
|
1329
|
+
cb = ContextBuilder()
|
|
1330
|
+
cb.index_file("test.py", "def foo():\n pass\ndef bar():\n pass\n")
|
|
1331
|
+
results = cb.find_symbol("foo")
|
|
1332
|
+
assert len(results) >= 1
|
|
1333
|
+
|
|
1334
|
+
def test_get_all_symbols(self):
|
|
1335
|
+
cb = ContextBuilder()
|
|
1336
|
+
cb.index_file("a.py", "def f1(): pass\n")
|
|
1337
|
+
cb.index_file("b.py", "def f2(): pass\n")
|
|
1338
|
+
all_syms = cb.get_all_symbols()
|
|
1339
|
+
assert len(all_syms) >= 2
|
|
1340
|
+
|
|
1341
|
+
def test_build_context(self):
|
|
1342
|
+
cb = ContextBuilder()
|
|
1343
|
+
syms = cb.index_file("test.py", "import os\ndef foo():\n pass\ndef bar():\n pass\n")
|
|
1344
|
+
fn = [s for s in syms if s.name == "foo"]
|
|
1345
|
+
if fn:
|
|
1346
|
+
cw = cb.build_context(fn[0])
|
|
1347
|
+
assert cw.focal_symbol.name == "foo"
|
|
1348
|
+
|
|
1349
|
+
def test_build_context_for_name(self):
|
|
1350
|
+
cb = ContextBuilder()
|
|
1351
|
+
cb.index_file("test.py", "def target():\n pass\n")
|
|
1352
|
+
windows = cb.build_context_for_name("target")
|
|
1353
|
+
assert len(windows) >= 1
|
|
1354
|
+
|
|
1355
|
+
def test_get_symbols_unknown_file(self):
|
|
1356
|
+
cb = ContextBuilder()
|
|
1357
|
+
assert cb.get_symbols("nonexistent.py") == []
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
class TestCallEdge:
|
|
1361
|
+
def test_to_dict(self):
|
|
1362
|
+
e = CallEdge("a.py:fn", "bar", "a.py", 10)
|
|
1363
|
+
d = e.to_dict()
|
|
1364
|
+
assert d["caller"] == "a.py:fn"
|
|
1365
|
+
assert d["callee"] == "bar"
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
class TestCallGraphDeep:
|
|
1369
|
+
def test_build(self):
|
|
1370
|
+
syms = [
|
|
1371
|
+
_sym(name="caller", body="target()", file_path="a.py"),
|
|
1372
|
+
_sym(name="target", body="pass", file_path="a.py"),
|
|
1373
|
+
]
|
|
1374
|
+
cg = CallGraph()
|
|
1375
|
+
cg.build(syms)
|
|
1376
|
+
assert len(cg.edges) >= 1
|
|
1377
|
+
|
|
1378
|
+
def test_callers_of(self):
|
|
1379
|
+
syms = [
|
|
1380
|
+
_sym(name="a", body="b()", file_path="x.py"),
|
|
1381
|
+
_sym(name="b", body="pass", file_path="x.py"),
|
|
1382
|
+
]
|
|
1383
|
+
cg = CallGraph()
|
|
1384
|
+
cg.build(syms)
|
|
1385
|
+
callers = cg.callers_of("b")
|
|
1386
|
+
assert len(callers) >= 1
|
|
1387
|
+
|
|
1388
|
+
def test_callees_of(self):
|
|
1389
|
+
syms = [
|
|
1390
|
+
_sym(name="a", body="b()", file_path="x.py"),
|
|
1391
|
+
_sym(name="b", body="pass", file_path="x.py"),
|
|
1392
|
+
]
|
|
1393
|
+
cg = CallGraph()
|
|
1394
|
+
cg.build(syms)
|
|
1395
|
+
callees = cg.callees_of("x.py:a")
|
|
1396
|
+
assert len(callees) >= 1
|
|
1397
|
+
|
|
1398
|
+
def test_no_self_reference(self):
|
|
1399
|
+
syms = [_sym(name="recursive", body="recursive()", file_path="a.py")]
|
|
1400
|
+
cg = CallGraph()
|
|
1401
|
+
cg.build(syms)
|
|
1402
|
+
# Should not have self-edge since build skips self-references
|
|
1403
|
+
callees = cg.callees_of("a.py:recursive")
|
|
1404
|
+
assert len(callees) == 0
|
|
1405
|
+
|
|
1406
|
+
def test_to_dict(self):
|
|
1407
|
+
cg = CallGraph()
|
|
1408
|
+
cg.build([])
|
|
1409
|
+
d = cg.to_dict()
|
|
1410
|
+
assert d["edge_count"] == 0
|
|
1411
|
+
assert d["node_count"] == 0
|
|
1412
|
+
|
|
1413
|
+
def test_empty_callers(self):
|
|
1414
|
+
cg = CallGraph()
|
|
1415
|
+
assert cg.callers_of("nonexistent") == []
|
|
1416
|
+
|
|
1417
|
+
def test_empty_callees(self):
|
|
1418
|
+
cg = CallGraph()
|
|
1419
|
+
assert cg.callees_of("nonexistent") == []
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
# ---------------------------------------------------------------------------
|
|
1423
|
+
# Context Memory
|
|
1424
|
+
# ---------------------------------------------------------------------------
|
|
1425
|
+
from semantic_code_intelligence.context.memory import (
|
|
1426
|
+
MemoryEntry,
|
|
1427
|
+
ReasoningStep,
|
|
1428
|
+
SessionMemory,
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
class TestMemoryEntry:
|
|
1433
|
+
def test_to_dict(self):
|
|
1434
|
+
me = MemoryEntry(key="k", content="c", kind="qa")
|
|
1435
|
+
d = me.to_dict()
|
|
1436
|
+
assert d["key"] == "k"
|
|
1437
|
+
assert d["kind"] == "qa"
|
|
1438
|
+
|
|
1439
|
+
def test_from_dict(self):
|
|
1440
|
+
d = {"key": "k", "content": "c", "kind": "insight", "timestamp": 1.0, "metadata": {}}
|
|
1441
|
+
me = MemoryEntry.from_dict(d)
|
|
1442
|
+
assert me.key == "k"
|
|
1443
|
+
assert me.kind == "insight"
|
|
1444
|
+
|
|
1445
|
+
def test_roundtrip(self):
|
|
1446
|
+
me = MemoryEntry(key="test", content="data", kind="general", metadata={"x": 1})
|
|
1447
|
+
restored = MemoryEntry.from_dict(me.to_dict())
|
|
1448
|
+
assert restored.key == me.key
|
|
1449
|
+
assert restored.content == me.content
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
class TestReasoningStep:
|
|
1453
|
+
def test_to_dict(self):
|
|
1454
|
+
rs = ReasoningStep(step_id=1, action="search", input_text="query", output_text="result")
|
|
1455
|
+
d = rs.to_dict()
|
|
1456
|
+
assert d["step_id"] == 1
|
|
1457
|
+
assert d["action"] == "search"
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
class TestSessionMemory:
|
|
1461
|
+
def test_add_and_search(self):
|
|
1462
|
+
sm = SessionMemory()
|
|
1463
|
+
sm.add("key1", "authentication logic")
|
|
1464
|
+
results = sm.search("authentication")
|
|
1465
|
+
assert len(results) >= 1
|
|
1466
|
+
|
|
1467
|
+
def test_max_entries(self):
|
|
1468
|
+
sm = SessionMemory(max_entries=3)
|
|
1469
|
+
for i in range(5):
|
|
1470
|
+
sm.add(f"k{i}", f"content{i}")
|
|
1471
|
+
assert len(sm.entries) == 3
|
|
1472
|
+
|
|
1473
|
+
def test_get_recent(self):
|
|
1474
|
+
sm = SessionMemory()
|
|
1475
|
+
sm.add("k1", "first")
|
|
1476
|
+
sm.add("k2", "second")
|
|
1477
|
+
recent = sm.get_recent(1)
|
|
1478
|
+
assert len(recent) == 1
|
|
1479
|
+
assert recent[0].key == "k2"
|
|
1480
|
+
|
|
1481
|
+
def test_clear(self):
|
|
1482
|
+
sm = SessionMemory()
|
|
1483
|
+
sm.add("k", "v")
|
|
1484
|
+
sm.start_chain("c1")
|
|
1485
|
+
sm.clear()
|
|
1486
|
+
assert len(sm.entries) == 0
|
|
1487
|
+
|
|
1488
|
+
def test_reasoning_chain(self):
|
|
1489
|
+
sm = SessionMemory()
|
|
1490
|
+
sm.start_chain("chain1")
|
|
1491
|
+
sm.add_step("chain1", "search", "query", "results")
|
|
1492
|
+
sm.add_step("chain1", "analyze", "results", "conclusion")
|
|
1493
|
+
chain = sm.get_chain("chain1")
|
|
1494
|
+
assert len(chain) == 2
|
|
1495
|
+
assert chain[0].step_id == 1
|
|
1496
|
+
assert chain[1].step_id == 2
|
|
1497
|
+
|
|
1498
|
+
def test_to_dict(self):
|
|
1499
|
+
sm = SessionMemory()
|
|
1500
|
+
sm.add("k", "v")
|
|
1501
|
+
d = sm.to_dict()
|
|
1502
|
+
assert "entries" in d
|
|
1503
|
+
assert "chains" in d
|
|
1504
|
+
|
|
1505
|
+
def test_search_empty(self):
|
|
1506
|
+
sm = SessionMemory()
|
|
1507
|
+
assert sm.search("anything") == []
|
|
1508
|
+
|
|
1509
|
+
def test_get_chain_nonexistent(self):
|
|
1510
|
+
sm = SessionMemory()
|
|
1511
|
+
assert sm.get_chain("missing") == []
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
# ---------------------------------------------------------------------------
|
|
1515
|
+
# Web Visualize
|
|
1516
|
+
# ---------------------------------------------------------------------------
|
|
1517
|
+
from semantic_code_intelligence.web.visualize import (
|
|
1518
|
+
render_call_graph,
|
|
1519
|
+
render_dependency_graph,
|
|
1520
|
+
render_workspace_graph,
|
|
1521
|
+
render_symbol_map,
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
class TestRenderCallGraph:
|
|
1526
|
+
def test_basic(self):
|
|
1527
|
+
edges = [{"caller": "a.py:fn1", "callee": "fn2", "file_path": "a.py"}]
|
|
1528
|
+
result = render_call_graph(edges)
|
|
1529
|
+
assert "flowchart" in result
|
|
1530
|
+
|
|
1531
|
+
def test_empty_edges(self):
|
|
1532
|
+
result = render_call_graph([])
|
|
1533
|
+
assert "No call edges found" in result
|
|
1534
|
+
|
|
1535
|
+
def test_custom_title(self):
|
|
1536
|
+
result = render_call_graph([], title="My Graph")
|
|
1537
|
+
assert "My Graph" in result
|
|
1538
|
+
|
|
1539
|
+
def test_direction(self):
|
|
1540
|
+
result = render_call_graph([], direction="TD")
|
|
1541
|
+
assert "flowchart TD" in result
|
|
1542
|
+
|
|
1543
|
+
def test_multiple_edges(self):
|
|
1544
|
+
edges = [
|
|
1545
|
+
{"caller": "a", "callee": "b", "file_path": "x.py"},
|
|
1546
|
+
{"caller": "b", "callee": "c", "file_path": "x.py"},
|
|
1547
|
+
]
|
|
1548
|
+
result = render_call_graph(edges)
|
|
1549
|
+
assert "-->" in result
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
class TestRenderDependencyGraph:
|
|
1553
|
+
def test_basic(self):
|
|
1554
|
+
deps = {"dependencies": [{"source_file": "a.py", "import_text": "import os"}]}
|
|
1555
|
+
result = render_dependency_graph(deps)
|
|
1556
|
+
assert "flowchart" in result
|
|
1557
|
+
|
|
1558
|
+
def test_empty(self):
|
|
1559
|
+
result = render_dependency_graph({})
|
|
1560
|
+
assert "No dependencies found" in result
|
|
1561
|
+
|
|
1562
|
+
def test_custom_title(self):
|
|
1563
|
+
result = render_dependency_graph({}, title="Deps")
|
|
1564
|
+
assert "Deps" in result
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
class TestRenderWorkspaceGraph:
|
|
1568
|
+
def test_basic(self):
|
|
1569
|
+
repos = [{"name": "repo1", "path": "/a", "file_count": 10, "vector_count": 100}]
|
|
1570
|
+
result = render_workspace_graph(repos)
|
|
1571
|
+
assert "repo1" in result
|
|
1572
|
+
|
|
1573
|
+
def test_empty_repos(self):
|
|
1574
|
+
result = render_workspace_graph([])
|
|
1575
|
+
assert "No repositories" in result
|
|
1576
|
+
|
|
1577
|
+
def test_custom_title(self):
|
|
1578
|
+
result = render_workspace_graph([], title="My WS")
|
|
1579
|
+
assert "My WS" in result
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
class TestRenderSymbolMap:
|
|
1583
|
+
def test_basic(self):
|
|
1584
|
+
syms = [
|
|
1585
|
+
{"name": "MyClass", "kind": "class", "parent": ""},
|
|
1586
|
+
{"name": "my_method", "kind": "method", "parent": "MyClass"},
|
|
1587
|
+
{"name": "standalone", "kind": "function", "parent": ""},
|
|
1588
|
+
]
|
|
1589
|
+
result = render_symbol_map(syms)
|
|
1590
|
+
assert "classDiagram" in result
|
|
1591
|
+
|
|
1592
|
+
def test_empty(self):
|
|
1593
|
+
result = render_symbol_map([])
|
|
1594
|
+
assert "classDiagram" in result
|
|
1595
|
+
|
|
1596
|
+
def test_custom_title(self):
|
|
1597
|
+
result = render_symbol_map([], title="Symbols")
|
|
1598
|
+
assert "Symbols" in result
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
# ---------------------------------------------------------------------------
|
|
1602
|
+
# Bridge Protocol
|
|
1603
|
+
# ---------------------------------------------------------------------------
|
|
1604
|
+
from semantic_code_intelligence.bridge.protocol import (
|
|
1605
|
+
RequestKind,
|
|
1606
|
+
AgentRequest,
|
|
1607
|
+
AgentResponse,
|
|
1608
|
+
BridgeCapabilities,
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
class TestRequestKindDeep:
|
|
1613
|
+
def test_all_values_are_strings(self):
|
|
1614
|
+
for kind in RequestKind:
|
|
1615
|
+
assert isinstance(kind.value, str)
|
|
1616
|
+
|
|
1617
|
+
def test_count(self):
|
|
1618
|
+
assert len(RequestKind) == 12
|
|
1619
|
+
|
|
1620
|
+
def test_invoke_tool(self):
|
|
1621
|
+
assert RequestKind.INVOKE_TOOL == "invoke_tool"
|
|
1622
|
+
|
|
1623
|
+
def test_list_tools(self):
|
|
1624
|
+
assert RequestKind.LIST_TOOLS == "list_tools"
|
|
1625
|
+
|
|
1626
|
+
def test_semantic_search(self):
|
|
1627
|
+
assert RequestKind.SEMANTIC_SEARCH == "semantic_search"
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
class TestAgentRequestDeep:
|
|
1631
|
+
def test_to_json_roundtrip(self):
|
|
1632
|
+
req = AgentRequest(kind="semantic_search", params={"query": "test"}, request_id="r1")
|
|
1633
|
+
j = req.to_json()
|
|
1634
|
+
restored = AgentRequest.from_json(j)
|
|
1635
|
+
assert restored.kind == req.kind
|
|
1636
|
+
assert restored.params == req.params
|
|
1637
|
+
|
|
1638
|
+
def test_from_dict_defaults(self):
|
|
1639
|
+
req = AgentRequest.from_dict({"kind": "test", "params": {}})
|
|
1640
|
+
assert req.request_id == ""
|
|
1641
|
+
assert req.source == ""
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
class TestAgentResponseDeep:
|
|
1645
|
+
def test_to_dict(self):
|
|
1646
|
+
resp = AgentResponse(request_id="r1", success=True, data={"key": "val"})
|
|
1647
|
+
d = resp.to_dict()
|
|
1648
|
+
assert d["success"] is True
|
|
1649
|
+
assert d["data"]["key"] == "val"
|
|
1650
|
+
|
|
1651
|
+
def test_error_response(self):
|
|
1652
|
+
resp = AgentResponse(request_id="r1", success=False, error="bad request")
|
|
1653
|
+
d = resp.to_dict()
|
|
1654
|
+
assert d["error"] == "bad request"
|
|
1655
|
+
|
|
1656
|
+
def test_from_dict(self):
|
|
1657
|
+
d = {"request_id": "r1", "success": True, "data": {}}
|
|
1658
|
+
# AgentResponse doesn't have a from_dict method, we should test the attributes instead
|
|
1659
|
+
resp = AgentResponse(request_id="r1", success=True)
|
|
1660
|
+
assert resp.request_id == "r1"
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
class TestBridgeCapabilitiesDeep:
|
|
1664
|
+
def test_version(self):
|
|
1665
|
+
cap = BridgeCapabilities()
|
|
1666
|
+
assert cap.version == "0.9.0"
|
|
1667
|
+
|
|
1668
|
+
def test_name(self):
|
|
1669
|
+
cap = BridgeCapabilities()
|
|
1670
|
+
assert cap.name == "CodexA Bridge"
|
|
1671
|
+
|
|
1672
|
+
def test_supported_requests(self):
|
|
1673
|
+
cap = BridgeCapabilities()
|
|
1674
|
+
assert "semantic_search" in cap.supported_requests
|
|
1675
|
+
assert "invoke_tool" in cap.supported_requests
|
|
1676
|
+
|
|
1677
|
+
def test_to_json(self):
|
|
1678
|
+
cap = BridgeCapabilities()
|
|
1679
|
+
j = cap.to_json()
|
|
1680
|
+
data = json.loads(j)
|
|
1681
|
+
assert "version" in data
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
# ---------------------------------------------------------------------------
|
|
1685
|
+
# Tools Protocol
|
|
1686
|
+
# ---------------------------------------------------------------------------
|
|
1687
|
+
from semantic_code_intelligence.tools.protocol import (
|
|
1688
|
+
ToolErrorCode,
|
|
1689
|
+
ToolInvocation,
|
|
1690
|
+
ToolError,
|
|
1691
|
+
ToolExecutionResult,
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
class TestToolErrorCodeDeep:
|
|
1696
|
+
def test_all_codes(self):
|
|
1697
|
+
codes = list(ToolErrorCode)
|
|
1698
|
+
assert len(codes) == 6
|
|
1699
|
+
|
|
1700
|
+
def test_values(self):
|
|
1701
|
+
assert ToolErrorCode.UNKNOWN_TOOL.value == "unknown_tool"
|
|
1702
|
+
assert ToolErrorCode.TIMEOUT.value == "timeout"
|
|
1703
|
+
assert ToolErrorCode.PERMISSION_DENIED.value == "permission_denied"
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
class TestToolInvocationDeep:
|
|
1707
|
+
def test_auto_request_id(self):
|
|
1708
|
+
inv = ToolInvocation(tool_name="test", arguments={})
|
|
1709
|
+
assert inv.request_id != ""
|
|
1710
|
+
|
|
1711
|
+
def test_auto_timestamp(self):
|
|
1712
|
+
inv = ToolInvocation(tool_name="test", arguments={})
|
|
1713
|
+
assert inv.timestamp > 0
|
|
1714
|
+
|
|
1715
|
+
def test_to_json(self):
|
|
1716
|
+
inv = ToolInvocation(tool_name="test", arguments={"q": "hello"})
|
|
1717
|
+
data = json.loads(inv.to_json())
|
|
1718
|
+
assert data["tool_name"] == "test"
|
|
1719
|
+
assert data["arguments"]["q"] == "hello"
|
|
1720
|
+
|
|
1721
|
+
def test_from_dict(self):
|
|
1722
|
+
d = {"tool_name": "search", "arguments": {"query": "test"},
|
|
1723
|
+
"request_id": "r1", "timestamp": 1.0}
|
|
1724
|
+
inv = ToolInvocation.from_dict(d)
|
|
1725
|
+
assert inv.tool_name == "search"
|
|
1726
|
+
|
|
1727
|
+
def test_roundtrip(self):
|
|
1728
|
+
inv = ToolInvocation(tool_name="test", arguments={"a": 1})
|
|
1729
|
+
restored = ToolInvocation.from_dict(inv.to_dict())
|
|
1730
|
+
assert restored.tool_name == inv.tool_name
|
|
1731
|
+
assert restored.arguments == inv.arguments
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
class TestToolErrorDeep:
|
|
1735
|
+
def test_to_dict(self):
|
|
1736
|
+
te = ToolError(tool_name="bad", error_code=ToolErrorCode.UNKNOWN_TOOL,
|
|
1737
|
+
error_message="not found")
|
|
1738
|
+
d = te.to_dict()
|
|
1739
|
+
assert d["error_code"] == "unknown_tool"
|
|
1740
|
+
|
|
1741
|
+
def test_from_dict(self):
|
|
1742
|
+
d = {"tool_name": "x", "error_code": "timeout", "error_message": "timed out",
|
|
1743
|
+
"request_id": "r1"}
|
|
1744
|
+
te = ToolError.from_dict(d)
|
|
1745
|
+
assert te.error_code == ToolErrorCode.TIMEOUT
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
class TestToolExecutionResultDeep:
|
|
1749
|
+
def test_success(self):
|
|
1750
|
+
r = ToolExecutionResult(
|
|
1751
|
+
tool_name="test", request_id="r1", success=True,
|
|
1752
|
+
result_payload={"data": "value"}, execution_time_ms=10.5,
|
|
1753
|
+
)
|
|
1754
|
+
assert r.success is True
|
|
1755
|
+
d = r.to_dict()
|
|
1756
|
+
assert d["execution_time_ms"] == 10.5
|
|
1757
|
+
|
|
1758
|
+
def test_failure(self):
|
|
1759
|
+
err = ToolError("test", ToolErrorCode.EXECUTION_ERROR, "failed")
|
|
1760
|
+
r = ToolExecutionResult(
|
|
1761
|
+
tool_name="test", request_id="r1", success=False, error=err,
|
|
1762
|
+
)
|
|
1763
|
+
d = r.to_dict()
|
|
1764
|
+
assert d["success"] is False
|
|
1765
|
+
|
|
1766
|
+
def test_from_dict(self):
|
|
1767
|
+
d = {"tool_name": "t", "request_id": "r1", "success": True,
|
|
1768
|
+
"result_payload": {}, "error": None, "execution_time_ms": 5.0,
|
|
1769
|
+
"timestamp": 1.0}
|
|
1770
|
+
r = ToolExecutionResult.from_dict(d)
|
|
1771
|
+
assert r.success is True
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
# ---------------------------------------------------------------------------
|
|
1775
|
+
# Tools Executor
|
|
1776
|
+
# ---------------------------------------------------------------------------
|
|
1777
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
class TestToolExecutorDeep:
|
|
1781
|
+
def test_list_tool_names(self):
|
|
1782
|
+
ex = ToolExecutor(Path("."))
|
|
1783
|
+
names = ex.list_tool_names()
|
|
1784
|
+
assert "semantic_search" in names
|
|
1785
|
+
assert len(names) >= 8
|
|
1786
|
+
|
|
1787
|
+
def test_available_tools(self):
|
|
1788
|
+
ex = ToolExecutor(Path("."))
|
|
1789
|
+
tools = ex.available_tools
|
|
1790
|
+
assert len(tools) >= 8
|
|
1791
|
+
assert any(t["name"] == "semantic_search" for t in tools)
|
|
1792
|
+
|
|
1793
|
+
def test_get_tool_schema_known(self):
|
|
1794
|
+
ex = ToolExecutor(Path("."))
|
|
1795
|
+
schema = ex.get_tool_schema("semantic_search")
|
|
1796
|
+
assert schema is not None
|
|
1797
|
+
assert schema["name"] == "semantic_search"
|
|
1798
|
+
|
|
1799
|
+
def test_get_tool_schema_unknown(self):
|
|
1800
|
+
ex = ToolExecutor(Path("."))
|
|
1801
|
+
assert ex.get_tool_schema("nonexistent_tool") is None
|
|
1802
|
+
|
|
1803
|
+
def test_execute_unknown_tool(self):
|
|
1804
|
+
ex = ToolExecutor(Path("."))
|
|
1805
|
+
inv = ToolInvocation(tool_name="nonexistent", arguments={})
|
|
1806
|
+
result = ex.execute(inv)
|
|
1807
|
+
assert result.success is False
|
|
1808
|
+
assert result.error is not None
|
|
1809
|
+
assert result.error.error_code == ToolErrorCode.UNKNOWN_TOOL
|
|
1810
|
+
|
|
1811
|
+
def test_register_plugin_tool(self):
|
|
1812
|
+
ex = ToolExecutor(Path("."))
|
|
1813
|
+
ex.register_plugin_tool(
|
|
1814
|
+
"custom_tool", "A custom tool", {"arg1": {"type": "string"}},
|
|
1815
|
+
lambda args: {"result": "ok"}
|
|
1816
|
+
)
|
|
1817
|
+
assert "custom_tool" in ex.list_tool_names()
|
|
1818
|
+
|
|
1819
|
+
def test_cannot_override_builtin(self):
|
|
1820
|
+
ex = ToolExecutor(Path("."))
|
|
1821
|
+
with pytest.raises(ValueError, match="[Bb]uilt.in"):
|
|
1822
|
+
ex.register_plugin_tool(
|
|
1823
|
+
"semantic_search", "Override", {}, lambda args: {}
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
def test_unregister_plugin_tool(self):
|
|
1827
|
+
ex = ToolExecutor(Path("."))
|
|
1828
|
+
ex.register_plugin_tool("temp", "Temp tool", {}, lambda a: {})
|
|
1829
|
+
assert "temp" in ex.list_tool_names()
|
|
1830
|
+
ex.unregister_plugin_tool("temp")
|
|
1831
|
+
assert "temp" not in ex.list_tool_names()
|
|
1832
|
+
|
|
1833
|
+
def test_execute_plugin_tool(self):
|
|
1834
|
+
ex = ToolExecutor(Path("."))
|
|
1835
|
+
ex.register_plugin_tool(
|
|
1836
|
+
"echo", "Echo tool", {"msg": {"type": "string", "required": True}},
|
|
1837
|
+
lambda msg="": {"echo": msg}
|
|
1838
|
+
)
|
|
1839
|
+
inv = ToolInvocation(tool_name="echo", arguments={"msg": "hello"})
|
|
1840
|
+
result = ex.execute(inv)
|
|
1841
|
+
assert result.success is True
|
|
1842
|
+
assert result.result_payload["echo"] == "hello"
|
|
1843
|
+
|
|
1844
|
+
def test_execute_batch(self):
|
|
1845
|
+
ex = ToolExecutor(Path("."))
|
|
1846
|
+
ex.register_plugin_tool("t1", "Tool 1", {}, lambda **kwargs: {"v": 1})
|
|
1847
|
+
ex.register_plugin_tool("t2", "Tool 2", {}, lambda **kwargs: {"v": 2})
|
|
1848
|
+
results = ex.execute_batch([
|
|
1849
|
+
ToolInvocation(tool_name="t1", arguments={}),
|
|
1850
|
+
ToolInvocation(tool_name="t2", arguments={}),
|
|
1851
|
+
])
|
|
1852
|
+
assert len(results) == 2
|
|
1853
|
+
assert all(r.success for r in results)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
# ---------------------------------------------------------------------------
|
|
1857
|
+
# Tools Registry
|
|
1858
|
+
# ---------------------------------------------------------------------------
|
|
1859
|
+
from semantic_code_intelligence.tools import ToolResult, ToolRegistry, TOOL_DEFINITIONS
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
class TestToolDefinitions:
|
|
1863
|
+
def test_count(self):
|
|
1864
|
+
assert len(TOOL_DEFINITIONS) == 11
|
|
1865
|
+
|
|
1866
|
+
def test_all_have_required_fields(self):
|
|
1867
|
+
for td in TOOL_DEFINITIONS:
|
|
1868
|
+
assert "name" in td
|
|
1869
|
+
assert "description" in td
|
|
1870
|
+
assert "parameters" in td
|
|
1871
|
+
|
|
1872
|
+
def test_semantic_search_params(self):
|
|
1873
|
+
td = next(t for t in TOOL_DEFINITIONS if t["name"] == "semantic_search")
|
|
1874
|
+
param_names = list(td["parameters"].keys())
|
|
1875
|
+
assert "query" in param_names
|
|
1876
|
+
|
|
1877
|
+
def test_explain_symbol_params(self):
|
|
1878
|
+
td = next(t for t in TOOL_DEFINITIONS if t["name"] == "explain_symbol")
|
|
1879
|
+
param_names = list(td["parameters"].keys())
|
|
1880
|
+
assert "symbol_name" in param_names
|
|
1881
|
+
|
|
1882
|
+
def test_summarize_repo_no_required(self):
|
|
1883
|
+
td = next(t for t in TOOL_DEFINITIONS if t["name"] == "summarize_repo")
|
|
1884
|
+
required = [name for name, p in td["parameters"].items() if p.get("required")]
|
|
1885
|
+
assert len(required) == 0
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
class TestToolResult:
|
|
1889
|
+
def test_success(self):
|
|
1890
|
+
tr = ToolResult(tool_name="test", success=True, data={"key": "val"})
|
|
1891
|
+
assert tr.success is True
|
|
1892
|
+
|
|
1893
|
+
def test_error(self):
|
|
1894
|
+
tr = ToolResult(tool_name="test", success=False, error="bad")
|
|
1895
|
+
assert tr.success is False
|
|
1896
|
+
assert tr.error == "bad"
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
class TestToolRegistryDeep:
|
|
1900
|
+
def test_create(self):
|
|
1901
|
+
tr = ToolRegistry(Path("."))
|
|
1902
|
+
assert tr is not None
|
|
1903
|
+
|
|
1904
|
+
def test_available_tools(self):
|
|
1905
|
+
tr = ToolRegistry(Path("."))
|
|
1906
|
+
tools = tr.tool_definitions
|
|
1907
|
+
assert len(tools) == 11
|
|
1908
|
+
|
|
1909
|
+
def test_invoke_unknown(self):
|
|
1910
|
+
tr = ToolRegistry(Path("."))
|
|
1911
|
+
result = tr.invoke("nonexistent", arg1="val")
|
|
1912
|
+
assert result.success is False
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
# ---------------------------------------------------------------------------
|
|
1916
|
+
# Plugins
|
|
1917
|
+
# ---------------------------------------------------------------------------
|
|
1918
|
+
from semantic_code_intelligence.plugins import PluginHook, PluginManager, PluginBase
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
class TestPluginHookDeep:
|
|
1922
|
+
def test_count(self):
|
|
1923
|
+
assert len(PluginHook) >= 22
|
|
1924
|
+
|
|
1925
|
+
def test_tool_hooks(self):
|
|
1926
|
+
names = [h.value for h in PluginHook]
|
|
1927
|
+
assert "register_tool" in names
|
|
1928
|
+
assert "pre_tool_invoke" in names
|
|
1929
|
+
assert "post_tool_invoke" in names
|
|
1930
|
+
|
|
1931
|
+
def test_all_string_values(self):
|
|
1932
|
+
for h in PluginHook:
|
|
1933
|
+
assert isinstance(h.value, str)
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
class TestPluginManagerDeep:
|
|
1937
|
+
def test_create(self):
|
|
1938
|
+
pm = PluginManager()
|
|
1939
|
+
assert pm is not None
|
|
1940
|
+
|
|
1941
|
+
def test_register_and_activate(self):
|
|
1942
|
+
pm = PluginManager()
|
|
1943
|
+
|
|
1944
|
+
from semantic_code_intelligence.plugins import PluginMetadata
|
|
1945
|
+
|
|
1946
|
+
class TestPlugin20(PluginBase):
|
|
1947
|
+
def metadata(self) -> PluginMetadata:
|
|
1948
|
+
return PluginMetadata(name="test20", version="1.0", description="test")
|
|
1949
|
+
|
|
1950
|
+
def on_hook(self, hook, data=None):
|
|
1951
|
+
return data or {}
|
|
1952
|
+
|
|
1953
|
+
plugin = TestPlugin20()
|
|
1954
|
+
pm.register(plugin)
|
|
1955
|
+
pm.activate("test20")
|
|
1956
|
+
assert "test20" in pm.active_plugins
|
|
1957
|
+
|
|
1958
|
+
def test_dispatch_no_handlers(self):
|
|
1959
|
+
pm = PluginManager()
|
|
1960
|
+
result = pm.dispatch(PluginHook.PRE_SEARCH, {"query": "test"})
|
|
1961
|
+
assert result == {"query": "test"}
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
# ---------------------------------------------------------------------------
|
|
1965
|
+
# Config Settings
|
|
1966
|
+
# ---------------------------------------------------------------------------
|
|
1967
|
+
from semantic_code_intelligence.config.settings import (
|
|
1968
|
+
AppConfig,
|
|
1969
|
+
EmbeddingConfig,
|
|
1970
|
+
SearchConfig,
|
|
1971
|
+
IndexConfig,
|
|
1972
|
+
LLMConfig,
|
|
1973
|
+
)
|
|
1974
|
+
|
|
1975
|
+
|
|
1976
|
+
class TestEmbeddingConfig:
|
|
1977
|
+
def test_defaults(self):
|
|
1978
|
+
ec = EmbeddingConfig()
|
|
1979
|
+
assert ec.model_name == "all-MiniLM-L6-v2"
|
|
1980
|
+
assert ec.chunk_size == 512
|
|
1981
|
+
|
|
1982
|
+
def test_custom(self):
|
|
1983
|
+
ec = EmbeddingConfig(model_name="custom", chunk_size=256)
|
|
1984
|
+
assert ec.model_name == "custom"
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
class TestSearchConfig:
|
|
1988
|
+
def test_defaults(self):
|
|
1989
|
+
sc = SearchConfig()
|
|
1990
|
+
assert sc.top_k == 10
|
|
1991
|
+
assert sc.similarity_threshold == 0.3
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
class TestIndexConfig:
|
|
1995
|
+
def test_defaults(self):
|
|
1996
|
+
ic = IndexConfig()
|
|
1997
|
+
assert ic.use_incremental is True
|
|
1998
|
+
assert len(ic.extensions) > 0
|
|
1999
|
+
|
|
2000
|
+
def test_ignore_dirs(self):
|
|
2001
|
+
ic = IndexConfig()
|
|
2002
|
+
assert len(ic.ignore_dirs) > 0
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
class TestLLMConfig:
|
|
2006
|
+
def test_defaults(self):
|
|
2007
|
+
lc = LLMConfig()
|
|
2008
|
+
assert lc.provider == "mock"
|
|
2009
|
+
assert lc.temperature == 0.2
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
class TestAppConfig:
|
|
2013
|
+
def test_defaults(self):
|
|
2014
|
+
ac = AppConfig()
|
|
2015
|
+
assert isinstance(ac.embedding, EmbeddingConfig)
|
|
2016
|
+
assert isinstance(ac.search, SearchConfig)
|
|
2017
|
+
assert isinstance(ac.index, IndexConfig)
|
|
2018
|
+
assert isinstance(ac.llm, LLMConfig)
|
|
2019
|
+
|
|
2020
|
+
def test_config_dir(self):
|
|
2021
|
+
d = AppConfig.config_dir(Path("/tmp/test"))
|
|
2022
|
+
assert ".codexa" in str(d)
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
# ---------------------------------------------------------------------------
|
|
2026
|
+
# Parsing
|
|
2027
|
+
# ---------------------------------------------------------------------------
|
|
2028
|
+
from semantic_code_intelligence.parsing.parser import (
|
|
2029
|
+
Symbol as ParserSymbol,
|
|
2030
|
+
detect_language,
|
|
2031
|
+
parse_file as parser_parse_file,
|
|
2032
|
+
)
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
class TestDetectLanguage:
|
|
2036
|
+
def test_python(self):
|
|
2037
|
+
assert detect_language("app.py") == "python"
|
|
2038
|
+
|
|
2039
|
+
def test_javascript(self):
|
|
2040
|
+
assert detect_language("app.js") == "javascript"
|
|
2041
|
+
|
|
2042
|
+
def test_typescript(self):
|
|
2043
|
+
assert detect_language("app.ts") == "typescript"
|
|
2044
|
+
|
|
2045
|
+
def test_java(self):
|
|
2046
|
+
assert detect_language("App.java") == "java"
|
|
2047
|
+
|
|
2048
|
+
def test_go(self):
|
|
2049
|
+
assert detect_language("main.go") == "go"
|
|
2050
|
+
|
|
2051
|
+
def test_rust(self):
|
|
2052
|
+
assert detect_language("lib.rs") == "rust"
|
|
2053
|
+
|
|
2054
|
+
def test_cpp(self):
|
|
2055
|
+
assert detect_language("main.cpp") == "cpp"
|
|
2056
|
+
|
|
2057
|
+
def test_csharp(self):
|
|
2058
|
+
assert detect_language("Program.cs") == "csharp"
|
|
2059
|
+
|
|
2060
|
+
def test_ruby(self):
|
|
2061
|
+
assert detect_language("app.rb") == "ruby"
|
|
2062
|
+
|
|
2063
|
+
def test_unknown(self):
|
|
2064
|
+
lang = detect_language("file.xyz")
|
|
2065
|
+
assert lang is None or lang == ""
|
|
2066
|
+
|
|
2067
|
+
def test_tsx(self):
|
|
2068
|
+
assert detect_language("App.tsx") == "tsx"
|
|
2069
|
+
|
|
2070
|
+
def test_php(self):
|
|
2071
|
+
assert detect_language("index.php") == "php"
|
|
2072
|
+
|
|
2073
|
+
|
|
2074
|
+
class TestParserSymbol:
|
|
2075
|
+
def test_to_dict(self):
|
|
2076
|
+
s = ParserSymbol(
|
|
2077
|
+
name="fn", kind="function", body="pass", file_path="a.py",
|
|
2078
|
+
start_line=1, end_line=2, start_col=0, end_col=0, parent="",
|
|
2079
|
+
)
|
|
2080
|
+
d = s.to_dict()
|
|
2081
|
+
assert d["name"] == "fn"
|
|
2082
|
+
assert d["kind"] == "function"
|
|
2083
|
+
|
|
2084
|
+
def test_is_dataclass(self):
|
|
2085
|
+
from dataclasses import fields
|
|
2086
|
+
f = fields(ParserSymbol)
|
|
2087
|
+
names = [ff.name for ff in f]
|
|
2088
|
+
assert "name" in names
|
|
2089
|
+
assert "kind" in names
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
class TestParseFile:
|
|
2093
|
+
def test_python_file(self):
|
|
2094
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
2095
|
+
f.write("def hello():\n pass\n\nclass World:\n def method(self):\n pass\n")
|
|
2096
|
+
f.flush()
|
|
2097
|
+
syms = parser_parse_file(f.name)
|
|
2098
|
+
names = [s.name for s in syms]
|
|
2099
|
+
assert "hello" in names
|
|
2100
|
+
|
|
2101
|
+
def test_empty_file(self):
|
|
2102
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
2103
|
+
f.write("")
|
|
2104
|
+
f.flush()
|
|
2105
|
+
syms = parser_parse_file(f.name)
|
|
2106
|
+
assert isinstance(syms, list)
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
# ---------------------------------------------------------------------------
|
|
2110
|
+
# Indexing Scanner
|
|
2111
|
+
# ---------------------------------------------------------------------------
|
|
2112
|
+
from semantic_code_intelligence.indexing.scanner import (
|
|
2113
|
+
ScannedFile,
|
|
2114
|
+
compute_file_hash,
|
|
2115
|
+
should_ignore,
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
class TestScannedFile:
|
|
2120
|
+
def test_fields(self):
|
|
2121
|
+
sf = ScannedFile(path=Path("a.py"), relative_path="a.py", extension=".py",
|
|
2122
|
+
size_bytes=100, content_hash="abc123")
|
|
2123
|
+
assert sf.relative_path == "a.py"
|
|
2124
|
+
assert sf.extension == ".py"
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
class TestComputeFileHash:
|
|
2128
|
+
def test_deterministic(self):
|
|
2129
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
2130
|
+
f.write("hello world")
|
|
2131
|
+
f.flush()
|
|
2132
|
+
h1 = compute_file_hash(f.name)
|
|
2133
|
+
h2 = compute_file_hash(f.name)
|
|
2134
|
+
assert h1 == h2
|
|
2135
|
+
assert len(h1) > 0
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
class TestShouldIgnore:
|
|
2139
|
+
def test_hidden_dirs(self):
|
|
2140
|
+
assert should_ignore(Path(".git/config"), Path("."), {".git"}) is True
|
|
2141
|
+
|
|
2142
|
+
def test_pycache(self):
|
|
2143
|
+
assert should_ignore(Path("__pycache__/module.pyc"), Path("."), {"__pycache__"}) is True
|
|
2144
|
+
|
|
2145
|
+
def test_normal_file(self):
|
|
2146
|
+
assert should_ignore(Path("src/app.py"), Path("."), {".git", "__pycache__"}) is False
|
|
2147
|
+
|
|
2148
|
+
def test_node_modules(self):
|
|
2149
|
+
assert should_ignore(Path("node_modules/pkg/index.js"), Path("."), {"node_modules"}) is True
|
|
2150
|
+
|
|
2151
|
+
|
|
2152
|
+
# ---------------------------------------------------------------------------
|
|
2153
|
+
# Indexing Chunker
|
|
2154
|
+
# ---------------------------------------------------------------------------
|
|
2155
|
+
from semantic_code_intelligence.indexing.chunker import CodeChunk, chunk_code
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
class TestCodeChunk:
|
|
2159
|
+
def test_fields(self):
|
|
2160
|
+
cc = CodeChunk(
|
|
2161
|
+
content="code", file_path="a.py", start_line=1, end_line=10,
|
|
2162
|
+
language="python", chunk_index=0,
|
|
2163
|
+
)
|
|
2164
|
+
assert cc.content == "code"
|
|
2165
|
+
assert cc.language == "python"
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
class TestChunkCode:
|
|
2169
|
+
def test_basic_chunking(self):
|
|
2170
|
+
code = "\n".join([f"line{i}" for i in range(100)])
|
|
2171
|
+
chunks = chunk_code(code, "test.py", chunk_size=50, chunk_overlap=10)
|
|
2172
|
+
assert len(chunks) >= 1
|
|
2173
|
+
|
|
2174
|
+
def test_empty_code(self):
|
|
2175
|
+
chunks = chunk_code("", "test.py")
|
|
2176
|
+
assert len(chunks) == 0
|
|
2177
|
+
|
|
2178
|
+
def test_small_code(self):
|
|
2179
|
+
chunks = chunk_code("single line", "test.py", chunk_size=100)
|
|
2180
|
+
assert len(chunks) == 1
|
|
2181
|
+
|
|
2182
|
+
|
|
2183
|
+
# ---------------------------------------------------------------------------
|
|
2184
|
+
# Storage VectorStore
|
|
2185
|
+
# ---------------------------------------------------------------------------
|
|
2186
|
+
from semantic_code_intelligence.storage.vector_store import VectorStore, ChunkMetadata
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
class TestChunkMetadata:
|
|
2190
|
+
def test_fields(self):
|
|
2191
|
+
cm = ChunkMetadata(
|
|
2192
|
+
file_path="a.py", start_line=1, end_line=10,
|
|
2193
|
+
content="code", language="python", chunk_index=0,
|
|
2194
|
+
)
|
|
2195
|
+
assert cm.file_path == "a.py"
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
class TestVectorStoreDeep:
|
|
2199
|
+
def test_create(self):
|
|
2200
|
+
vs = VectorStore(dimension=384)
|
|
2201
|
+
assert vs is not None
|
|
2202
|
+
|
|
2203
|
+
def test_add_and_search(self):
|
|
2204
|
+
import numpy as np
|
|
2205
|
+
vs = VectorStore(dimension=4)
|
|
2206
|
+
embedding = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
2207
|
+
meta = ChunkMetadata(file_path="a.py", start_line=1, end_line=2,
|
|
2208
|
+
chunk_index=0, content="test", language="python")
|
|
2209
|
+
vs.add(embedding, [meta])
|
|
2210
|
+
query = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
|
|
2211
|
+
results = vs.search(query, top_k=1)
|
|
2212
|
+
assert len(results) >= 1
|
|
2213
|
+
|
|
2214
|
+
def test_clear(self):
|
|
2215
|
+
vs = VectorStore(dimension=4)
|
|
2216
|
+
vs.clear()
|
|
2217
|
+
assert vs is not None
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
# ---------------------------------------------------------------------------
|
|
2221
|
+
# Storage HashStore
|
|
2222
|
+
# ---------------------------------------------------------------------------
|
|
2223
|
+
from semantic_code_intelligence.storage.hash_store import HashStore
|
|
2224
|
+
|
|
2225
|
+
|
|
2226
|
+
class TestHashStoreDeep:
|
|
2227
|
+
def test_create(self):
|
|
2228
|
+
hs = HashStore()
|
|
2229
|
+
assert hs is not None
|
|
2230
|
+
assert hs.count == 0
|
|
2231
|
+
|
|
2232
|
+
def test_store_and_check(self):
|
|
2233
|
+
hs = HashStore()
|
|
2234
|
+
hs.set("a.py", "hash1")
|
|
2235
|
+
assert hs.has_changed("a.py", "hash1") is False
|
|
2236
|
+
assert hs.has_changed("a.py", "hash2") is True
|
|
2237
|
+
|
|
2238
|
+
def test_unknown_file(self):
|
|
2239
|
+
hs = HashStore()
|
|
2240
|
+
assert hs.has_changed("missing.py", "hash1") is True
|
|
2241
|
+
|
|
2242
|
+
|
|
2243
|
+
# ---------------------------------------------------------------------------
|
|
2244
|
+
# Docs
|
|
2245
|
+
# ---------------------------------------------------------------------------
|
|
2246
|
+
from semantic_code_intelligence.docs import generate_all_docs
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
class TestDocsGeneration:
|
|
2250
|
+
def test_generate_all_docs(self):
|
|
2251
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2252
|
+
docs = generate_all_docs(Path(tmp))
|
|
2253
|
+
assert isinstance(docs, list)
|
|
2254
|
+
|
|
2255
|
+
def test_all_docs_are_strings(self):
|
|
2256
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2257
|
+
docs = generate_all_docs(Path(tmp))
|
|
2258
|
+
for name in docs:
|
|
2259
|
+
assert isinstance(name, str)
|
|
2260
|
+
|
|
2261
|
+
def test_cli_reference_exists(self):
|
|
2262
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2263
|
+
docs = generate_all_docs(Path(tmp))
|
|
2264
|
+
assert any("CLI" in k or "cli" in k for k in docs)
|
|
2265
|
+
|
|
2266
|
+
def test_architecture_exists(self):
|
|
2267
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2268
|
+
docs = generate_all_docs(Path(tmp))
|
|
2269
|
+
# May generate PLUGINS, BRIDGE, WEB, CI, etc.
|
|
2270
|
+
assert isinstance(docs, list)
|
|
2271
|
+
|
|
2272
|
+
def test_tool_protocol_exists(self):
|
|
2273
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2274
|
+
docs = generate_all_docs(Path(tmp))
|
|
2275
|
+
assert any("TOOL" in k or "tool" in k.lower() for k in docs)
|
|
2276
|
+
|
|
2277
|
+
|
|
2278
|
+
# ---------------------------------------------------------------------------
|
|
2279
|
+
# Scalability
|
|
2280
|
+
# ---------------------------------------------------------------------------
|
|
2281
|
+
from semantic_code_intelligence.scalability import (
|
|
2282
|
+
BatchProcessor,
|
|
2283
|
+
MemoryAwareEmbedder,
|
|
2284
|
+
ParallelScanner,
|
|
2285
|
+
)
|
|
2286
|
+
|
|
2287
|
+
|
|
2288
|
+
class TestBatchProcessorDeep:
|
|
2289
|
+
def test_create(self):
|
|
2290
|
+
bp = BatchProcessor(batch_size=10)
|
|
2291
|
+
assert bp is not None
|
|
2292
|
+
assert bp.batch_size == 10
|
|
2293
|
+
|
|
2294
|
+
def test_process(self):
|
|
2295
|
+
bp = BatchProcessor(batch_size=3)
|
|
2296
|
+
items = list(range(10))
|
|
2297
|
+
results, stats = bp.process(items, lambda batch: batch)
|
|
2298
|
+
assert results == items
|
|
2299
|
+
assert stats.total_items == 10
|
|
2300
|
+
|
|
2301
|
+
def test_single_batch(self):
|
|
2302
|
+
bp = BatchProcessor(batch_size=100)
|
|
2303
|
+
items = [1, 2, 3]
|
|
2304
|
+
results, stats = bp.process(items, lambda batch: batch)
|
|
2305
|
+
assert stats.batches_processed == 1
|
|
2306
|
+
|
|
2307
|
+
def test_empty(self):
|
|
2308
|
+
bp = BatchProcessor(batch_size=5)
|
|
2309
|
+
results, stats = bp.process([], lambda batch: batch)
|
|
2310
|
+
assert len(results) == 0
|
|
2311
|
+
assert stats.total_items == 0
|
|
2312
|
+
|
|
2313
|
+
|
|
2314
|
+
class TestMemoryAwareEmbedder:
|
|
2315
|
+
def test_create(self):
|
|
2316
|
+
mae = MemoryAwareEmbedder(model_name="all-MiniLM-L6-v2", batch_size=32)
|
|
2317
|
+
assert mae is not None
|
|
2318
|
+
|
|
2319
|
+
|
|
2320
|
+
class TestParallelScanner:
|
|
2321
|
+
def test_create(self):
|
|
2322
|
+
ps = ParallelScanner(max_workers=2)
|
|
2323
|
+
assert ps is not None
|
|
2324
|
+
|
|
2325
|
+
def test_scan_empty(self):
|
|
2326
|
+
ps = ParallelScanner(max_workers=2)
|
|
2327
|
+
results, errors = ps.scan_and_process([], lambda fp: str(fp))
|
|
2328
|
+
assert results == []
|
|
2329
|
+
assert errors == []
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
# ---------------------------------------------------------------------------
|
|
2333
|
+
# Workspace
|
|
2334
|
+
# ---------------------------------------------------------------------------
|
|
2335
|
+
from semantic_code_intelligence.workspace import RepoEntry, WorkspaceManifest, Workspace
|
|
2336
|
+
|
|
2337
|
+
|
|
2338
|
+
class TestRepoEntry:
|
|
2339
|
+
def test_to_dict(self):
|
|
2340
|
+
re = RepoEntry(name="myrepo", path="/path/to/repo")
|
|
2341
|
+
d = re.to_dict()
|
|
2342
|
+
assert d["name"] == "myrepo"
|
|
2343
|
+
assert d["path"] == "/path/to/repo"
|
|
2344
|
+
|
|
2345
|
+
def test_defaults(self):
|
|
2346
|
+
re = RepoEntry(name="r", path="/r")
|
|
2347
|
+
assert re.file_count == 0
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
class TestWorkspaceManifest:
|
|
2351
|
+
def test_create(self):
|
|
2352
|
+
wm = WorkspaceManifest()
|
|
2353
|
+
assert wm is not None
|
|
2354
|
+
assert wm.version == "1.0.0"
|
|
2355
|
+
|
|
2356
|
+
def test_to_dict(self):
|
|
2357
|
+
wm = WorkspaceManifest()
|
|
2358
|
+
d = wm.to_dict()
|
|
2359
|
+
assert isinstance(d, dict)
|
|
2360
|
+
assert "repos" in d
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
class TestWorkspace:
|
|
2364
|
+
def test_create(self):
|
|
2365
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2366
|
+
ws = Workspace(Path(tmp))
|
|
2367
|
+
assert ws is not None
|
|
2368
|
+
|
|
2369
|
+
def test_repos_empty(self):
|
|
2370
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2371
|
+
ws = Workspace(Path(tmp))
|
|
2372
|
+
assert isinstance(ws.repos, list)
|
|
2373
|
+
assert len(ws.repos) == 0
|
|
2374
|
+
|
|
2375
|
+
|
|
2376
|
+
# ---------------------------------------------------------------------------
|
|
2377
|
+
# Daemon Watcher
|
|
2378
|
+
# ---------------------------------------------------------------------------
|
|
2379
|
+
from semantic_code_intelligence.daemon.watcher import FileChangeEvent, FileWatcher
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
class TestFileChangeEvent:
|
|
2383
|
+
def test_create(self):
|
|
2384
|
+
ev = FileChangeEvent(path=Path("a.py"), relative_path="a.py", change_type="modified")
|
|
2385
|
+
assert ev.path == Path("a.py")
|
|
2386
|
+
assert ev.change_type == "modified"
|
|
2387
|
+
|
|
2388
|
+
def test_to_dict(self):
|
|
2389
|
+
ev = FileChangeEvent(path=Path("b.py"), relative_path="b.py", change_type="created")
|
|
2390
|
+
d = ev.to_dict()
|
|
2391
|
+
assert d["change_type"] == "created"
|
|
2392
|
+
assert d["relative_path"] == "b.py"
|
|
2393
|
+
|
|
2394
|
+
|
|
2395
|
+
class TestFileWatcherDeep:
|
|
2396
|
+
def test_create(self):
|
|
2397
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2398
|
+
fw = FileWatcher(Path(tmp))
|
|
2399
|
+
assert fw is not None
|
|
2400
|
+
|
|
2401
|
+
def test_has_start_stop(self):
|
|
2402
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2403
|
+
fw = FileWatcher(Path(tmp))
|
|
2404
|
+
assert hasattr(fw, "start")
|
|
2405
|
+
assert hasattr(fw, "stop")
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
# ---------------------------------------------------------------------------
|
|
2409
|
+
# Version & Meta
|
|
2410
|
+
# ---------------------------------------------------------------------------
|
|
2411
|
+
from semantic_code_intelligence import __version__, __app_name__
|
|
2412
|
+
|
|
2413
|
+
|
|
2414
|
+
class TestVersionMeta:
|
|
2415
|
+
def test_version_format(self):
|
|
2416
|
+
parts = __version__.split(".")
|
|
2417
|
+
assert len(parts) == 3
|
|
2418
|
+
|
|
2419
|
+
def test_app_name(self):
|
|
2420
|
+
assert __app_name__ == "codexa"
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
# ---------------------------------------------------------------------------
|
|
2424
|
+
# CLI Router
|
|
2425
|
+
# ---------------------------------------------------------------------------
|
|
2426
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
2427
|
+
from semantic_code_intelligence.cli.main import cli
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
class TestCLIRouterDeep:
|
|
2431
|
+
def test_command_count(self):
|
|
2432
|
+
register_commands(cli)
|
|
2433
|
+
# 31 commands
|
|
2434
|
+
assert len(cli.commands) >= 31
|
|
2435
|
+
|
|
2436
|
+
def test_tool_command_registered(self):
|
|
2437
|
+
register_commands(cli)
|
|
2438
|
+
assert "tool" in cli.commands
|
|
2439
|
+
|
|
2440
|
+
def test_serve_command_registered(self):
|
|
2441
|
+
register_commands(cli)
|
|
2442
|
+
assert "serve" in cli.commands
|
|
2443
|
+
|
|
2444
|
+
def test_quality_command_registered(self):
|
|
2445
|
+
register_commands(cli)
|
|
2446
|
+
assert "quality" in cli.commands
|
|
2447
|
+
|
|
2448
|
+
def test_impact_command_registered(self):
|
|
2449
|
+
register_commands(cli)
|
|
2450
|
+
assert "impact" in cli.commands
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
# ---------------------------------------------------------------------------
|
|
2454
|
+
# Cross-cutting: README and copilot-instructions.md
|
|
2455
|
+
# ---------------------------------------------------------------------------
|
|
2456
|
+
|
|
2457
|
+
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
class TestReadmeExists:
|
|
2461
|
+
def test_readme_exists(self):
|
|
2462
|
+
assert (_PROJECT_ROOT / "README.md").exists()
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
class TestCopilotInstructionsExists:
|
|
2466
|
+
_ci_path = _PROJECT_ROOT / ".github" / "copilot-instructions.md"
|
|
2467
|
+
|
|
2468
|
+
def test_file_exists(self):
|
|
2469
|
+
assert self._ci_path.exists()
|
|
2470
|
+
|
|
2471
|
+
def test_contains_codex_commands(self):
|
|
2472
|
+
content = self._ci_path.read_text(encoding="utf-8")
|
|
2473
|
+
assert "codexa search" in content
|
|
2474
|
+
assert "codexa tool run" in content
|
|
2475
|
+
|
|
2476
|
+
def test_contains_rules(self):
|
|
2477
|
+
content = self._ci_path.read_text(encoding="utf-8")
|
|
2478
|
+
assert "--json" in content
|
|
2479
|
+
|
|
2480
|
+
def test_contains_project_structure(self):
|
|
2481
|
+
content = self._ci_path.read_text(encoding="utf-8")
|
|
2482
|
+
assert "cli/" in content
|
|
2483
|
+
assert "tools/" in content
|
|
2484
|
+
assert "bridge/" in content
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
# ---------------------------------------------------------------------------
|
|
2488
|
+
# Embeddings (basic import & structure tests)
|
|
2489
|
+
# ---------------------------------------------------------------------------
|
|
2490
|
+
from semantic_code_intelligence.embeddings.enhanced import (
|
|
2491
|
+
preprocess_code_for_embedding,
|
|
2492
|
+
prepare_semantic_texts,
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
|
|
2496
|
+
class TestPreprocessCodeForEmbedding:
|
|
2497
|
+
def test_basic(self):
|
|
2498
|
+
result = preprocess_code_for_embedding("def foo():\n pass")
|
|
2499
|
+
assert isinstance(result, str)
|
|
2500
|
+
assert len(result) > 0
|
|
2501
|
+
|
|
2502
|
+
def test_empty(self):
|
|
2503
|
+
result = preprocess_code_for_embedding("")
|
|
2504
|
+
assert isinstance(result, str)
|
|
2505
|
+
|
|
2506
|
+
|
|
2507
|
+
class TestPrepareSemanticTexts:
|
|
2508
|
+
def test_basic(self):
|
|
2509
|
+
from semantic_code_intelligence.indexing.semantic_chunker import SemanticChunk
|
|
2510
|
+
chunks = [SemanticChunk(content="def foo(): pass", file_path="a.py",
|
|
2511
|
+
start_line=1, end_line=1, language="python",
|
|
2512
|
+
chunk_index=0, semantic_label="function foo")]
|
|
2513
|
+
texts = prepare_semantic_texts(chunks)
|
|
2514
|
+
assert len(texts) == 1
|
|
2515
|
+
|
|
2516
|
+
|
|
2517
|
+
# ---------------------------------------------------------------------------
|
|
2518
|
+
# Search Formatter
|
|
2519
|
+
# ---------------------------------------------------------------------------
|
|
2520
|
+
from semantic_code_intelligence.search.formatter import format_results_json
|
|
2521
|
+
|
|
2522
|
+
|
|
2523
|
+
class TestFormatResultsJson:
|
|
2524
|
+
def test_basic(self):
|
|
2525
|
+
from semantic_code_intelligence.services.search_service import SearchResult
|
|
2526
|
+
results = [SearchResult(file_path="a.py", content="code", score=0.9,
|
|
2527
|
+
start_line=1, end_line=1, language="python", chunk_index=0)]
|
|
2528
|
+
output = format_results_json("test query", results, top_k=5)
|
|
2529
|
+
parsed = json.loads(output)
|
|
2530
|
+
assert isinstance(parsed, (list, dict))
|
|
2531
|
+
|
|
2532
|
+
def test_empty(self):
|
|
2533
|
+
output = format_results_json("empty", [], top_k=5)
|
|
2534
|
+
parsed = json.loads(output)
|
|
2535
|
+
assert isinstance(parsed, (list, dict))
|
|
2536
|
+
|
|
2537
|
+
|
|
2538
|
+
# ---------------------------------------------------------------------------
|
|
2539
|
+
# Bridge VSCode
|
|
2540
|
+
# ---------------------------------------------------------------------------
|
|
2541
|
+
from semantic_code_intelligence.bridge.vscode import VSCodeBridge
|
|
2542
|
+
|
|
2543
|
+
|
|
2544
|
+
class TestVSCodeBridge:
|
|
2545
|
+
def test_create(self):
|
|
2546
|
+
vsb = VSCodeBridge(Path("."))
|
|
2547
|
+
assert vsb is not None
|
|
2548
|
+
|
|
2549
|
+
def test_has_methods(self):
|
|
2550
|
+
vsb = VSCodeBridge(Path("."))
|
|
2551
|
+
assert hasattr(vsb, "hover")
|
|
2552
|
+
assert hasattr(vsb, "diagnostics")
|
|
2553
|
+
assert hasattr(vsb, "completions")
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
# ---------------------------------------------------------------------------
|
|
2557
|
+
# Logging
|
|
2558
|
+
# ---------------------------------------------------------------------------
|
|
2559
|
+
from semantic_code_intelligence.utils.logging import get_logger, setup_logging
|
|
2560
|
+
|
|
2561
|
+
|
|
2562
|
+
class TestLogging:
|
|
2563
|
+
def test_get_logger(self):
|
|
2564
|
+
log = get_logger("test")
|
|
2565
|
+
assert log is not None
|
|
2566
|
+
|
|
2567
|
+
def test_setup_logging(self):
|
|
2568
|
+
setup_logging(verbose=False)
|
|
2569
|
+
setup_logging(verbose=True)
|
|
2570
|
+
|
|
2571
|
+
|
|
2572
|
+
# ---------------------------------------------------------------------------
|
|
2573
|
+
# Additional edge cases and integration-like tests
|
|
2574
|
+
# ---------------------------------------------------------------------------
|
|
2575
|
+
|
|
2576
|
+
|
|
2577
|
+
class TestToolRegistryInvocations:
|
|
2578
|
+
"""Test ToolRegistry invoke for each built-in tool (error paths)."""
|
|
2579
|
+
|
|
2580
|
+
def test_invoke_semantic_search_no_index(self):
|
|
2581
|
+
tr = ToolRegistry(Path("."))
|
|
2582
|
+
result = tr.invoke("semantic_search", query="test")
|
|
2583
|
+
# Should return ToolResult (might not have index)
|
|
2584
|
+
assert isinstance(result, ToolResult)
|
|
2585
|
+
|
|
2586
|
+
def test_invoke_explain_symbol(self):
|
|
2587
|
+
tr = ToolRegistry(Path("."))
|
|
2588
|
+
result = tr.invoke("explain_symbol", symbol_name="nonexistent")
|
|
2589
|
+
assert isinstance(result, ToolResult)
|
|
2590
|
+
|
|
2591
|
+
def test_invoke_explain_file(self):
|
|
2592
|
+
tr = ToolRegistry(Path("."))
|
|
2593
|
+
result = tr.invoke("explain_file", file_path="nonexistent.py")
|
|
2594
|
+
assert isinstance(result, ToolResult)
|
|
2595
|
+
|
|
2596
|
+
def test_invoke_summarize_repo(self):
|
|
2597
|
+
tr = ToolRegistry(Path("."))
|
|
2598
|
+
result = tr.invoke("summarize_repo")
|
|
2599
|
+
assert isinstance(result, ToolResult)
|
|
2600
|
+
|
|
2601
|
+
def test_invoke_find_references(self):
|
|
2602
|
+
tr = ToolRegistry(Path("."))
|
|
2603
|
+
result = tr.invoke("find_references", symbol_name="nonexistent")
|
|
2604
|
+
assert isinstance(result, ToolResult)
|
|
2605
|
+
|
|
2606
|
+
def test_invoke_get_dependencies(self):
|
|
2607
|
+
tr = ToolRegistry(Path("."))
|
|
2608
|
+
result = tr.invoke("get_dependencies", file_path="nonexistent.py")
|
|
2609
|
+
assert isinstance(result, ToolResult)
|
|
2610
|
+
|
|
2611
|
+
def test_invoke_get_call_graph(self):
|
|
2612
|
+
tr = ToolRegistry(Path("."))
|
|
2613
|
+
result = tr.invoke("get_call_graph", symbol_name="nonexistent")
|
|
2614
|
+
assert isinstance(result, ToolResult)
|
|
2615
|
+
|
|
2616
|
+
def test_invoke_get_context(self):
|
|
2617
|
+
tr = ToolRegistry(Path("."))
|
|
2618
|
+
result = tr.invoke("get_context", symbol_name="nonexistent")
|
|
2619
|
+
assert isinstance(result, ToolResult)
|
|
2620
|
+
|
|
2621
|
+
|
|
2622
|
+
class TestDependencyMapBasic:
|
|
2623
|
+
def test_create(self):
|
|
2624
|
+
dm = DependencyMap()
|
|
2625
|
+
assert dm is not None
|
|
2626
|
+
|
|
2627
|
+
def test_add_file(self):
|
|
2628
|
+
dm = DependencyMap()
|
|
2629
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
2630
|
+
f.write("import os\nimport sys\n")
|
|
2631
|
+
f.flush()
|
|
2632
|
+
dm.add_file(f.name)
|
|
2633
|
+
deps = dm.get_dependencies(f.name)
|
|
2634
|
+
assert isinstance(deps, list)
|
|
2635
|
+
|
|
2636
|
+
def test_get_all_files(self):
|
|
2637
|
+
dm = DependencyMap()
|
|
2638
|
+
assert isinstance(dm.get_all_files(), (list, set))
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
class TestBuildChangeSummary:
|
|
2642
|
+
"""Test build_change_summary from ci.pr."""
|
|
2643
|
+
|
|
2644
|
+
def test_with_python_file(self):
|
|
2645
|
+
from semantic_code_intelligence.ci.pr import build_change_summary
|
|
2646
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
2647
|
+
f.write("def hello():\n pass\n")
|
|
2648
|
+
f.flush()
|
|
2649
|
+
summary = build_change_summary([f.name])
|
|
2650
|
+
assert summary.files_changed == 1
|
|
2651
|
+
|
|
2652
|
+
def test_with_nonexistent_file(self):
|
|
2653
|
+
from semantic_code_intelligence.ci.pr import build_change_summary
|
|
2654
|
+
summary = build_change_summary(["/nonexistent/file.py"])
|
|
2655
|
+
assert summary.files_changed == 1
|
|
2656
|
+
|
|
2657
|
+
def test_empty(self):
|
|
2658
|
+
from semantic_code_intelligence.ci.pr import build_change_summary
|
|
2659
|
+
summary = build_change_summary([])
|
|
2660
|
+
assert summary.files_changed == 0
|
|
2661
|
+
|
|
2662
|
+
|
|
2663
|
+
class TestGateViolation:
|
|
2664
|
+
"""Test GateViolation dataclass from ci.metrics."""
|
|
2665
|
+
|
|
2666
|
+
def test_create(self):
|
|
2667
|
+
from semantic_code_intelligence.ci.metrics import GateViolation
|
|
2668
|
+
gv = GateViolation(rule="min_mi", message="MI too low", actual=30.0, threshold=40.0)
|
|
2669
|
+
d = gv.to_dict()
|
|
2670
|
+
assert d["rule"] == "min_mi"
|
|
2671
|
+
assert d["actual"] == 30.0
|
|
2672
|
+
|
|
2673
|
+
|
|
2674
|
+
class TestStreamEventSSE:
|
|
2675
|
+
"""Additional SSE formatting tests."""
|
|
2676
|
+
|
|
2677
|
+
def test_empty_data(self):
|
|
2678
|
+
se = StreamEvent(kind="heartbeat", content="")
|
|
2679
|
+
sse = se.to_sse()
|
|
2680
|
+
assert "heartbeat" in sse
|
|
2681
|
+
|
|
2682
|
+
def test_json_data(self):
|
|
2683
|
+
data = json.dumps({"key": "value"})
|
|
2684
|
+
se = StreamEvent(kind="data", content=data)
|
|
2685
|
+
sse = se.to_sse()
|
|
2686
|
+
assert "key" in sse
|
|
2687
|
+
|
|
2688
|
+
|
|
2689
|
+
class TestContextWindowRender:
|
|
2690
|
+
"""More context window rendering tests."""
|
|
2691
|
+
|
|
2692
|
+
def test_render_with_max_lines(self):
|
|
2693
|
+
s = _sym(body="\n".join(f"line{i}" for i in range(100)))
|
|
2694
|
+
cw = ContextWindow(focal_symbol=s)
|
|
2695
|
+
text = cw.render(max_lines=5)
|
|
2696
|
+
assert "more lines" in text
|
|
2697
|
+
|
|
2698
|
+
def test_render_with_related(self):
|
|
2699
|
+
s = _sym(name="main", body="pass")
|
|
2700
|
+
related = _sym(name="helper", body="pass")
|
|
2701
|
+
cw = ContextWindow(focal_symbol=s, related_symbols=[related])
|
|
2702
|
+
text = cw.render()
|
|
2703
|
+
assert "helper" in text
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
class TestCallGraphMultiFile:
|
|
2707
|
+
"""Test call graph across multiple files."""
|
|
2708
|
+
|
|
2709
|
+
def test_cross_file_calls(self):
|
|
2710
|
+
syms = [
|
|
2711
|
+
_sym(name="fn_a", body="fn_b()", file_path="a.py"),
|
|
2712
|
+
_sym(name="fn_b", body="fn_c()", file_path="b.py"),
|
|
2713
|
+
_sym(name="fn_c", body="pass", file_path="c.py"),
|
|
2714
|
+
]
|
|
2715
|
+
cg = CallGraph()
|
|
2716
|
+
cg.build(syms)
|
|
2717
|
+
assert len(cg.edges) >= 2
|
|
2718
|
+
|
|
2719
|
+
def test_multiple_callers(self):
|
|
2720
|
+
syms = [
|
|
2721
|
+
_sym(name="caller1", body="target()", file_path="a.py"),
|
|
2722
|
+
_sym(name="caller2", body="target()", file_path="b.py"),
|
|
2723
|
+
_sym(name="target", body="pass", file_path="c.py"),
|
|
2724
|
+
]
|
|
2725
|
+
cg = CallGraph()
|
|
2726
|
+
cg.build(syms)
|
|
2727
|
+
callers = cg.callers_of("target")
|
|
2728
|
+
assert len(callers) >= 2
|
|
2729
|
+
|
|
2730
|
+
|
|
2731
|
+
class TestQualityReportAggregation:
|
|
2732
|
+
"""Test QualityReport with mixed issues."""
|
|
2733
|
+
|
|
2734
|
+
def test_mixed_issues(self):
|
|
2735
|
+
from semantic_code_intelligence.llm.safety import SafetyReport, SafetyIssue
|
|
2736
|
+
r = QualityReport(
|
|
2737
|
+
files_analyzed=5,
|
|
2738
|
+
symbol_count=20,
|
|
2739
|
+
complexity_issues=[ComplexityResult("fn", "a.py", 1, 10, 15, "high")],
|
|
2740
|
+
dead_code=[DeadCodeResult("x", "function", "a.py", 1),
|
|
2741
|
+
DeadCodeResult("y", "function", "b.py", 5)],
|
|
2742
|
+
duplicates=[DuplicateResult("a", "f1.py", 1, "b", "f2.py", 2, 0.9)],
|
|
2743
|
+
safety=SafetyReport(safe=False, issues=[SafetyIssue("eval", "bad")]),
|
|
2744
|
+
)
|
|
2745
|
+
assert r.issue_count == 5 # 1 complexity + 2 dead + 1 dup + 1 safety
|
|
2746
|
+
|
|
2747
|
+
def test_to_dict_completeness(self):
|
|
2748
|
+
r = QualityReport(files_analyzed=3, symbol_count=15)
|
|
2749
|
+
d = r.to_dict()
|
|
2750
|
+
assert "complexity_issues" in d
|
|
2751
|
+
assert "dead_code" in d
|
|
2752
|
+
assert "duplicates" in d
|
|
2753
|
+
assert "safety" in d
|