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,643 @@
|
|
|
1
|
+
"""Tests for Phase 9 — External AI Cooperation Layer.
|
|
2
|
+
|
|
3
|
+
Covers: bridge protocol, context provider, HTTP bridge server, VSCode
|
|
4
|
+
extension bridge, and CLI commands (serve, context).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.error
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
from unittest.mock import MagicMock, patch
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
# =========================================================================
|
|
21
|
+
# Protocol tests
|
|
22
|
+
# =========================================================================
|
|
23
|
+
|
|
24
|
+
from semantic_code_intelligence.bridge.protocol import (
|
|
25
|
+
AgentRequest,
|
|
26
|
+
AgentResponse,
|
|
27
|
+
BridgeCapabilities,
|
|
28
|
+
RequestKind,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestRequestKind:
|
|
33
|
+
def test_all_values(self):
|
|
34
|
+
kinds = [k.value for k in RequestKind]
|
|
35
|
+
assert "semantic_search" in kinds
|
|
36
|
+
assert "explain_symbol" in kinds
|
|
37
|
+
assert "explain_file" in kinds
|
|
38
|
+
assert "get_context" in kinds
|
|
39
|
+
assert "get_dependencies" in kinds
|
|
40
|
+
assert "get_call_graph" in kinds
|
|
41
|
+
assert "summarize_repo" in kinds
|
|
42
|
+
assert "find_references" in kinds
|
|
43
|
+
assert "validate_code" in kinds
|
|
44
|
+
assert "list_capabilities" in kinds
|
|
45
|
+
|
|
46
|
+
def test_count(self):
|
|
47
|
+
assert len(RequestKind) == 12
|
|
48
|
+
|
|
49
|
+
def test_string_enum(self):
|
|
50
|
+
assert RequestKind.SEMANTIC_SEARCH == "semantic_search"
|
|
51
|
+
assert isinstance(RequestKind.SEMANTIC_SEARCH, str)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestAgentRequest:
|
|
55
|
+
def test_create(self):
|
|
56
|
+
req = AgentRequest(kind="semantic_search", params={"query": "auth"})
|
|
57
|
+
assert req.kind == "semantic_search"
|
|
58
|
+
assert req.params["query"] == "auth"
|
|
59
|
+
assert req.request_id == ""
|
|
60
|
+
assert req.source == ""
|
|
61
|
+
|
|
62
|
+
def test_to_dict(self):
|
|
63
|
+
req = AgentRequest(
|
|
64
|
+
kind="explain_symbol",
|
|
65
|
+
params={"symbol_name": "foo"},
|
|
66
|
+
request_id="r1",
|
|
67
|
+
source="copilot",
|
|
68
|
+
)
|
|
69
|
+
d = req.to_dict()
|
|
70
|
+
assert d["kind"] == "explain_symbol"
|
|
71
|
+
assert d["params"]["symbol_name"] == "foo"
|
|
72
|
+
assert d["request_id"] == "r1"
|
|
73
|
+
assert d["source"] == "copilot"
|
|
74
|
+
|
|
75
|
+
def test_to_json(self):
|
|
76
|
+
req = AgentRequest(kind="validate_code", params={"code": "print(1)"})
|
|
77
|
+
j = req.to_json()
|
|
78
|
+
parsed = json.loads(j)
|
|
79
|
+
assert parsed["kind"] == "validate_code"
|
|
80
|
+
|
|
81
|
+
def test_from_dict(self):
|
|
82
|
+
d = {"kind": "semantic_search", "params": {"query": "hello"}, "request_id": "x"}
|
|
83
|
+
req = AgentRequest.from_dict(d)
|
|
84
|
+
assert req.kind == "semantic_search"
|
|
85
|
+
assert req.params["query"] == "hello"
|
|
86
|
+
assert req.request_id == "x"
|
|
87
|
+
|
|
88
|
+
def test_from_json(self):
|
|
89
|
+
j = json.dumps({"kind": "explain_file", "params": {"file_path": "a.py"}})
|
|
90
|
+
req = AgentRequest.from_json(j)
|
|
91
|
+
assert req.kind == "explain_file"
|
|
92
|
+
assert req.params["file_path"] == "a.py"
|
|
93
|
+
|
|
94
|
+
def test_from_dict_defaults(self):
|
|
95
|
+
d = {"kind": "list_capabilities"}
|
|
96
|
+
req = AgentRequest.from_dict(d)
|
|
97
|
+
assert req.params == {}
|
|
98
|
+
assert req.request_id == ""
|
|
99
|
+
assert req.source == ""
|
|
100
|
+
|
|
101
|
+
def test_roundtrip(self):
|
|
102
|
+
original = AgentRequest(
|
|
103
|
+
kind="get_dependencies",
|
|
104
|
+
params={"file_path": "src/main.py"},
|
|
105
|
+
request_id="abc-123",
|
|
106
|
+
source="cursor",
|
|
107
|
+
)
|
|
108
|
+
rebuilt = AgentRequest.from_json(original.to_json())
|
|
109
|
+
assert rebuilt.kind == original.kind
|
|
110
|
+
assert rebuilt.params == original.params
|
|
111
|
+
assert rebuilt.request_id == original.request_id
|
|
112
|
+
assert rebuilt.source == original.source
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestAgentResponse:
|
|
116
|
+
def test_success(self):
|
|
117
|
+
resp = AgentResponse(success=True, data={"count": 5})
|
|
118
|
+
assert resp.success is True
|
|
119
|
+
d = resp.to_dict()
|
|
120
|
+
assert "data" in d
|
|
121
|
+
assert "error" not in d
|
|
122
|
+
|
|
123
|
+
def test_failure(self):
|
|
124
|
+
resp = AgentResponse(success=False, error="not found")
|
|
125
|
+
d = resp.to_dict()
|
|
126
|
+
assert d["success"] is False
|
|
127
|
+
assert "error" in d
|
|
128
|
+
assert "data" not in d
|
|
129
|
+
|
|
130
|
+
def test_elapsed(self):
|
|
131
|
+
resp = AgentResponse(success=True, data={}, elapsed_ms=12.345)
|
|
132
|
+
d = resp.to_dict()
|
|
133
|
+
assert d["elapsed_ms"] == 12.35
|
|
134
|
+
|
|
135
|
+
def test_to_json(self):
|
|
136
|
+
resp = AgentResponse(success=True, data={"key": "val"}, request_id="r1")
|
|
137
|
+
parsed = json.loads(resp.to_json())
|
|
138
|
+
assert parsed["success"] is True
|
|
139
|
+
assert parsed["request_id"] == "r1"
|
|
140
|
+
|
|
141
|
+
def test_to_json_indented(self):
|
|
142
|
+
resp = AgentResponse(success=True, data={})
|
|
143
|
+
text = resp.to_json(indent=2)
|
|
144
|
+
assert "\n" in text
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestBridgeCapabilities:
|
|
148
|
+
def test_defaults(self):
|
|
149
|
+
cap = BridgeCapabilities()
|
|
150
|
+
assert cap.version == "0.9.0"
|
|
151
|
+
assert cap.name == "CodexA Bridge"
|
|
152
|
+
assert "semantic_search" in cap.supported_requests
|
|
153
|
+
assert len(cap.supported_requests) == 12
|
|
154
|
+
|
|
155
|
+
def test_to_dict(self):
|
|
156
|
+
cap = BridgeCapabilities()
|
|
157
|
+
d = cap.to_dict()
|
|
158
|
+
assert d["version"] == "0.9.0"
|
|
159
|
+
assert isinstance(d["supported_requests"], list)
|
|
160
|
+
|
|
161
|
+
def test_to_json(self):
|
|
162
|
+
cap = BridgeCapabilities()
|
|
163
|
+
parsed = json.loads(cap.to_json())
|
|
164
|
+
assert parsed["name"] == "CodexA Bridge"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# =========================================================================
|
|
168
|
+
# ContextProvider tests (with mocked subsystems)
|
|
169
|
+
# =========================================================================
|
|
170
|
+
|
|
171
|
+
from semantic_code_intelligence.bridge.context_provider import ContextProvider
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TestContextProvider:
|
|
175
|
+
@pytest.fixture
|
|
176
|
+
def provider(self, tmp_path: Path) -> ContextProvider:
|
|
177
|
+
return ContextProvider(tmp_path)
|
|
178
|
+
|
|
179
|
+
def test_init(self, provider: ContextProvider):
|
|
180
|
+
assert provider._indexed is False
|
|
181
|
+
assert provider._builder is None
|
|
182
|
+
assert provider._validator is not None
|
|
183
|
+
|
|
184
|
+
def test_validate_code_safe(self, provider: ContextProvider):
|
|
185
|
+
report = provider.validate_code("x = 1 + 2")
|
|
186
|
+
assert isinstance(report, dict)
|
|
187
|
+
assert "safe" in report
|
|
188
|
+
|
|
189
|
+
def test_validate_code_unsafe(self, provider: ContextProvider):
|
|
190
|
+
report = provider.validate_code("eval(input())")
|
|
191
|
+
assert isinstance(report, dict)
|
|
192
|
+
# Should flag the eval
|
|
193
|
+
assert report.get("is_safe") is False or len(report.get("issues", [])) > 0
|
|
194
|
+
|
|
195
|
+
@patch("semantic_code_intelligence.bridge.context_provider.search_codebase")
|
|
196
|
+
def test_context_for_query_empty(self, mock_search, provider):
|
|
197
|
+
mock_search.return_value = []
|
|
198
|
+
result = provider.context_for_query(query="test")
|
|
199
|
+
assert result["query"] == "test"
|
|
200
|
+
assert result["snippet_count"] == 0
|
|
201
|
+
|
|
202
|
+
@patch("semantic_code_intelligence.bridge.context_provider.search_codebase")
|
|
203
|
+
def test_context_for_query_with_results(self, mock_search, provider):
|
|
204
|
+
mock_result = MagicMock()
|
|
205
|
+
mock_result.to_dict.return_value = {"file": "a.py", "score": 0.9}
|
|
206
|
+
mock_search.return_value = [mock_result]
|
|
207
|
+
result = provider.context_for_query(query="auth")
|
|
208
|
+
assert result["snippet_count"] == 1
|
|
209
|
+
assert result["snippets"][0]["file"] == "a.py"
|
|
210
|
+
|
|
211
|
+
@patch("semantic_code_intelligence.bridge.context_provider.search_codebase")
|
|
212
|
+
def test_context_for_query_exception(self, mock_search, provider):
|
|
213
|
+
mock_search.side_effect = Exception("oops")
|
|
214
|
+
result = provider.context_for_query(query="fail")
|
|
215
|
+
assert result["snippet_count"] == 0
|
|
216
|
+
|
|
217
|
+
def test_context_for_symbol_not_found(self, provider):
|
|
218
|
+
"""Symbol not found returns found=False."""
|
|
219
|
+
with patch.object(provider, "_ensure_indexed") as mock_idx:
|
|
220
|
+
mock_builder = MagicMock()
|
|
221
|
+
mock_builder.find_symbol.return_value = []
|
|
222
|
+
mock_idx.return_value = mock_builder
|
|
223
|
+
result = provider.context_for_symbol("nonexistent")
|
|
224
|
+
assert result["found"] is False
|
|
225
|
+
|
|
226
|
+
def test_context_for_repo(self, provider):
|
|
227
|
+
"""Repo summary delegates to summarize_repository."""
|
|
228
|
+
with patch.object(provider, "_ensure_indexed") as mock_idx:
|
|
229
|
+
mock_builder = MagicMock()
|
|
230
|
+
mock_idx.return_value = mock_builder
|
|
231
|
+
with patch(
|
|
232
|
+
"semantic_code_intelligence.bridge.context_provider.summarize_repository"
|
|
233
|
+
) as mock_sum:
|
|
234
|
+
mock_summary = MagicMock()
|
|
235
|
+
mock_summary.to_dict.return_value = {"total_files": 10}
|
|
236
|
+
mock_sum.return_value = mock_summary
|
|
237
|
+
result = provider.context_for_repo()
|
|
238
|
+
assert result["total_files"] == 10
|
|
239
|
+
|
|
240
|
+
def test_get_dependencies(self, provider):
|
|
241
|
+
"""get_dependencies returns dict with expected keys."""
|
|
242
|
+
with patch.object(provider, "_ensure_indexed") as mock_idx:
|
|
243
|
+
mock_builder = MagicMock()
|
|
244
|
+
mock_builder._file_contents = {}
|
|
245
|
+
mock_idx.return_value = mock_builder
|
|
246
|
+
result = provider.get_dependencies("missing.py")
|
|
247
|
+
assert "file_path" in result
|
|
248
|
+
assert "dependencies" in result
|
|
249
|
+
|
|
250
|
+
def test_get_call_graph(self, provider):
|
|
251
|
+
"""get_call_graph returns expected structure."""
|
|
252
|
+
with patch.object(provider, "_ensure_indexed") as mock_idx:
|
|
253
|
+
mock_builder = MagicMock()
|
|
254
|
+
mock_builder.get_all_symbols.return_value = []
|
|
255
|
+
mock_idx.return_value = mock_builder
|
|
256
|
+
result = provider.get_call_graph("some_func")
|
|
257
|
+
assert result["symbol_name"] == "some_func"
|
|
258
|
+
assert "callers" in result
|
|
259
|
+
assert "callees" in result
|
|
260
|
+
|
|
261
|
+
def test_find_references(self, provider):
|
|
262
|
+
"""find_references returns expected structure."""
|
|
263
|
+
with patch.object(provider, "_ensure_indexed") as mock_idx:
|
|
264
|
+
mock_builder = MagicMock()
|
|
265
|
+
mock_builder.get_all_symbols.return_value = []
|
|
266
|
+
mock_idx.return_value = mock_builder
|
|
267
|
+
result = provider.find_references("some_func")
|
|
268
|
+
assert result["symbol_name"] == "some_func"
|
|
269
|
+
assert result["reference_count"] == 0
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# =========================================================================
|
|
273
|
+
# BridgeServer tests
|
|
274
|
+
# =========================================================================
|
|
275
|
+
|
|
276
|
+
from semantic_code_intelligence.bridge.server import BridgeServer, _dispatch
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TestDispatch:
|
|
280
|
+
"""Direct dispatch (no HTTP round-trip)."""
|
|
281
|
+
|
|
282
|
+
@pytest.fixture
|
|
283
|
+
def provider(self, tmp_path: Path):
|
|
284
|
+
return ContextProvider(tmp_path)
|
|
285
|
+
|
|
286
|
+
@pytest.fixture
|
|
287
|
+
def caps(self):
|
|
288
|
+
return BridgeCapabilities()
|
|
289
|
+
|
|
290
|
+
def test_list_capabilities(self, provider, caps):
|
|
291
|
+
req = AgentRequest(kind=RequestKind.LIST_CAPABILITIES)
|
|
292
|
+
resp = _dispatch(req, provider, caps)
|
|
293
|
+
assert resp.success is True
|
|
294
|
+
assert "supported_requests" in resp.data
|
|
295
|
+
|
|
296
|
+
def test_validate_code(self, provider, caps):
|
|
297
|
+
req = AgentRequest(
|
|
298
|
+
kind=RequestKind.VALIDATE_CODE,
|
|
299
|
+
params={"code": "x = 1"},
|
|
300
|
+
)
|
|
301
|
+
resp = _dispatch(req, provider, caps)
|
|
302
|
+
assert resp.success is True
|
|
303
|
+
assert "safe" in resp.data
|
|
304
|
+
|
|
305
|
+
@patch("semantic_code_intelligence.bridge.context_provider.search_codebase")
|
|
306
|
+
def test_semantic_search(self, mock_search, provider, caps):
|
|
307
|
+
mock_search.return_value = []
|
|
308
|
+
req = AgentRequest(
|
|
309
|
+
kind=RequestKind.SEMANTIC_SEARCH,
|
|
310
|
+
params={"query": "test"},
|
|
311
|
+
)
|
|
312
|
+
resp = _dispatch(req, provider, caps)
|
|
313
|
+
assert resp.success is True
|
|
314
|
+
assert resp.data["query"] == "test"
|
|
315
|
+
|
|
316
|
+
def test_unknown_kind(self, provider, caps):
|
|
317
|
+
req = AgentRequest(kind="totally_unknown")
|
|
318
|
+
resp = _dispatch(req, provider, caps)
|
|
319
|
+
assert resp.success is False
|
|
320
|
+
assert "Unknown" in resp.error
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class TestBridgeServer:
|
|
324
|
+
"""BridgeServer lifecycle and direct dispatch."""
|
|
325
|
+
|
|
326
|
+
def test_init(self, tmp_path: Path):
|
|
327
|
+
server = BridgeServer(tmp_path)
|
|
328
|
+
assert server.url == "http://127.0.0.1:24842"
|
|
329
|
+
|
|
330
|
+
def test_custom_host_port(self, tmp_path: Path):
|
|
331
|
+
server = BridgeServer(tmp_path, host="0.0.0.0", port=9999)
|
|
332
|
+
assert server.url == "http://0.0.0.0:9999"
|
|
333
|
+
|
|
334
|
+
def test_direct_dispatch(self, tmp_path: Path):
|
|
335
|
+
server = BridgeServer(tmp_path)
|
|
336
|
+
req = AgentRequest(
|
|
337
|
+
kind=RequestKind.LIST_CAPABILITIES,
|
|
338
|
+
request_id="test-1",
|
|
339
|
+
)
|
|
340
|
+
resp = server.dispatch(req)
|
|
341
|
+
assert resp.success is True
|
|
342
|
+
assert resp.request_id == "test-1"
|
|
343
|
+
assert resp.elapsed_ms >= 0
|
|
344
|
+
|
|
345
|
+
def test_direct_dispatch_validate(self, tmp_path: Path):
|
|
346
|
+
server = BridgeServer(tmp_path)
|
|
347
|
+
req = AgentRequest(
|
|
348
|
+
kind=RequestKind.VALIDATE_CODE,
|
|
349
|
+
params={"code": "print('hello')"},
|
|
350
|
+
)
|
|
351
|
+
resp = server.dispatch(req)
|
|
352
|
+
assert resp.success is True
|
|
353
|
+
|
|
354
|
+
def test_background_start_stop(self, tmp_path: Path):
|
|
355
|
+
"""Start, query, and stop a background server."""
|
|
356
|
+
server = BridgeServer(tmp_path, port=0)
|
|
357
|
+
# port=0 will fail because HTTPServer needs a real port, pick a high one
|
|
358
|
+
server = BridgeServer(tmp_path, port=39871)
|
|
359
|
+
server.start_background()
|
|
360
|
+
try:
|
|
361
|
+
time.sleep(0.3) # let the server thread start
|
|
362
|
+
# Health check
|
|
363
|
+
url = f"{server.url}/health"
|
|
364
|
+
req = urllib.request.Request(url)
|
|
365
|
+
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
366
|
+
data = json.loads(resp.read())
|
|
367
|
+
assert data["status"] == "ok"
|
|
368
|
+
|
|
369
|
+
# Capabilities
|
|
370
|
+
url2 = f"{server.url}/"
|
|
371
|
+
req2 = urllib.request.Request(url2)
|
|
372
|
+
with urllib.request.urlopen(req2, timeout=2) as resp2:
|
|
373
|
+
data2 = json.loads(resp2.read())
|
|
374
|
+
assert data2["name"] == "CodexA Bridge"
|
|
375
|
+
|
|
376
|
+
# POST /request — list_capabilities
|
|
377
|
+
post_data = json.dumps({
|
|
378
|
+
"kind": "list_capabilities",
|
|
379
|
+
"params": {},
|
|
380
|
+
"request_id": "live-1",
|
|
381
|
+
}).encode()
|
|
382
|
+
req3 = urllib.request.Request(
|
|
383
|
+
f"{server.url}/request",
|
|
384
|
+
data=post_data,
|
|
385
|
+
headers={"Content-Type": "application/json"},
|
|
386
|
+
)
|
|
387
|
+
with urllib.request.urlopen(req3, timeout=2) as resp3:
|
|
388
|
+
data3 = json.loads(resp3.read())
|
|
389
|
+
assert data3["success"] is True
|
|
390
|
+
assert data3["request_id"] == "live-1"
|
|
391
|
+
finally:
|
|
392
|
+
server.stop()
|
|
393
|
+
|
|
394
|
+
def test_background_post_invalid_json(self, tmp_path: Path):
|
|
395
|
+
server = BridgeServer(tmp_path, port=39872)
|
|
396
|
+
server.start_background()
|
|
397
|
+
try:
|
|
398
|
+
time.sleep(0.3)
|
|
399
|
+
post_data = b"this is not json"
|
|
400
|
+
req = urllib.request.Request(
|
|
401
|
+
f"{server.url}/request",
|
|
402
|
+
data=post_data,
|
|
403
|
+
headers={"Content-Type": "application/json"},
|
|
404
|
+
)
|
|
405
|
+
try:
|
|
406
|
+
urllib.request.urlopen(req, timeout=2)
|
|
407
|
+
assert False, "Should have raised"
|
|
408
|
+
except urllib.error.HTTPError as e:
|
|
409
|
+
assert e.code == 400
|
|
410
|
+
finally:
|
|
411
|
+
server.stop()
|
|
412
|
+
|
|
413
|
+
def test_background_404(self, tmp_path: Path):
|
|
414
|
+
server = BridgeServer(tmp_path, port=39873)
|
|
415
|
+
server.start_background()
|
|
416
|
+
try:
|
|
417
|
+
time.sleep(0.3)
|
|
418
|
+
req = urllib.request.Request(f"{server.url}/nonexistent")
|
|
419
|
+
try:
|
|
420
|
+
urllib.request.urlopen(req, timeout=2)
|
|
421
|
+
assert False, "Should have raised"
|
|
422
|
+
except urllib.error.HTTPError as e:
|
|
423
|
+
assert e.code == 404
|
|
424
|
+
finally:
|
|
425
|
+
server.stop()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# =========================================================================
|
|
429
|
+
# VSCodeBridge tests
|
|
430
|
+
# =========================================================================
|
|
431
|
+
|
|
432
|
+
from semantic_code_intelligence.bridge.vscode import (
|
|
433
|
+
VSCodeBridge,
|
|
434
|
+
generate_extension_manifest,
|
|
435
|
+
_to_diagnostic,
|
|
436
|
+
_to_hover,
|
|
437
|
+
_to_completion_items,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class TestVSCodeHelpers:
|
|
442
|
+
def test_to_diagnostic(self):
|
|
443
|
+
issue = {"severity": "high", "description": "eval usage", "line": 5}
|
|
444
|
+
d = _to_diagnostic(issue)
|
|
445
|
+
assert d["severity"] == 1
|
|
446
|
+
assert d["source"] == "CodexA"
|
|
447
|
+
assert d["range"]["start"]["line"] == 5
|
|
448
|
+
|
|
449
|
+
def test_to_diagnostic_default_severity(self):
|
|
450
|
+
issue = {"description": "minor issue"}
|
|
451
|
+
d = _to_diagnostic(issue)
|
|
452
|
+
assert d["severity"] == 2 # medium default
|
|
453
|
+
|
|
454
|
+
def test_to_hover_full(self):
|
|
455
|
+
ctx = {
|
|
456
|
+
"explanation": "Authenticates a user",
|
|
457
|
+
"type": "function",
|
|
458
|
+
"file": "auth.py",
|
|
459
|
+
"callers": ["login", "signup", "reset"],
|
|
460
|
+
}
|
|
461
|
+
h = _to_hover(ctx)
|
|
462
|
+
value = h["contents"]["value"]
|
|
463
|
+
assert "Authenticates a user" in value
|
|
464
|
+
assert "function" in value
|
|
465
|
+
assert "auth.py" in value
|
|
466
|
+
assert "login" in value
|
|
467
|
+
|
|
468
|
+
def test_to_hover_empty(self):
|
|
469
|
+
h = _to_hover({})
|
|
470
|
+
assert h["contents"]["value"] == ""
|
|
471
|
+
|
|
472
|
+
def test_to_completion_items(self):
|
|
473
|
+
results = [
|
|
474
|
+
{"symbol": "auth_check", "explanation": "checks auth", "snippet": "def auth_check():"},
|
|
475
|
+
{"file": "utils.py"},
|
|
476
|
+
]
|
|
477
|
+
items = _to_completion_items(results)
|
|
478
|
+
assert len(items) == 2
|
|
479
|
+
assert items[0]["label"] == "auth_check"
|
|
480
|
+
assert items[1]["label"] == "utils.py"
|
|
481
|
+
|
|
482
|
+
def test_to_completion_items_limit(self):
|
|
483
|
+
results = [{"symbol": f"sym_{i}"} for i in range(30)]
|
|
484
|
+
items = _to_completion_items(results)
|
|
485
|
+
assert len(items) == 20 # capped at 20
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class TestVSCodeBridge:
|
|
489
|
+
@pytest.fixture
|
|
490
|
+
def bridge(self, tmp_path: Path) -> VSCodeBridge:
|
|
491
|
+
provider = ContextProvider(tmp_path)
|
|
492
|
+
return VSCodeBridge(provider=provider)
|
|
493
|
+
|
|
494
|
+
def test_diagnostics_safe(self, bridge):
|
|
495
|
+
diags = bridge.diagnostics("x = 1")
|
|
496
|
+
assert isinstance(diags, list)
|
|
497
|
+
|
|
498
|
+
def test_diagnostics_unsafe(self, bridge):
|
|
499
|
+
diags = bridge.diagnostics("eval(input())")
|
|
500
|
+
assert len(diags) > 0
|
|
501
|
+
assert diags[0]["source"] == "CodexA"
|
|
502
|
+
|
|
503
|
+
@patch("semantic_code_intelligence.bridge.context_provider.search_codebase")
|
|
504
|
+
def test_completions(self, mock_search, bridge):
|
|
505
|
+
mock_search.return_value = []
|
|
506
|
+
items = bridge.completions("auth")
|
|
507
|
+
assert isinstance(items, list)
|
|
508
|
+
|
|
509
|
+
def test_code_actions_safe(self, bridge):
|
|
510
|
+
actions = bridge.code_actions("x = 1")
|
|
511
|
+
assert isinstance(actions, list)
|
|
512
|
+
|
|
513
|
+
def test_code_actions_unsafe(self, bridge):
|
|
514
|
+
actions = bridge.code_actions("eval(input())")
|
|
515
|
+
assert len(actions) > 0
|
|
516
|
+
assert "CodexA" in actions[0]["title"]
|
|
517
|
+
|
|
518
|
+
def test_hover_not_found(self, bridge):
|
|
519
|
+
with patch.object(bridge.provider, "_ensure_indexed") as mock_idx:
|
|
520
|
+
mock_builder = MagicMock()
|
|
521
|
+
mock_builder.find_symbol.return_value = []
|
|
522
|
+
mock_idx.return_value = mock_builder
|
|
523
|
+
h = bridge.hover("nonexistent")
|
|
524
|
+
# Should still return contents (possibly empty)
|
|
525
|
+
assert "contents" in h
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class TestExtensionManifest:
|
|
529
|
+
def test_default_manifest(self):
|
|
530
|
+
m = generate_extension_manifest()
|
|
531
|
+
assert m["name"] == "codexa-bridge"
|
|
532
|
+
assert m["version"] == "0.9.0"
|
|
533
|
+
assert m["engines"]["vscode"] == "^1.85.0"
|
|
534
|
+
assert len(m["contributes"]["commands"]) == 4
|
|
535
|
+
|
|
536
|
+
def test_custom_port(self):
|
|
537
|
+
m = generate_extension_manifest(server_port=8080)
|
|
538
|
+
port_conf = m["contributes"]["configuration"]["properties"]["codexa.bridge.port"]
|
|
539
|
+
assert port_conf["default"] == 8080
|
|
540
|
+
|
|
541
|
+
def test_custom_name(self):
|
|
542
|
+
m = generate_extension_manifest(extension_name="my-ext", display_name="My Ext")
|
|
543
|
+
assert m["name"] == "my-ext"
|
|
544
|
+
assert m["displayName"] == "My Ext"
|
|
545
|
+
|
|
546
|
+
def test_json_serialisable(self):
|
|
547
|
+
m = generate_extension_manifest()
|
|
548
|
+
j = json.dumps(m)
|
|
549
|
+
assert "codexa-bridge" in j
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# =========================================================================
|
|
553
|
+
# CLI command tests (serve & context)
|
|
554
|
+
# =========================================================================
|
|
555
|
+
|
|
556
|
+
from click.testing import CliRunner
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class TestServeCLI:
|
|
560
|
+
def test_serve_help(self):
|
|
561
|
+
from semantic_code_intelligence.cli.commands.serve_cmd import serve_cmd
|
|
562
|
+
runner = CliRunner()
|
|
563
|
+
result = runner.invoke(serve_cmd, ["--help"])
|
|
564
|
+
assert result.exit_code == 0
|
|
565
|
+
assert "bridge server" in result.output.lower()
|
|
566
|
+
|
|
567
|
+
def test_serve_options(self):
|
|
568
|
+
from semantic_code_intelligence.cli.commands.serve_cmd import serve_cmd
|
|
569
|
+
runner = CliRunner()
|
|
570
|
+
result = runner.invoke(serve_cmd, ["--help"])
|
|
571
|
+
assert "--host" in result.output
|
|
572
|
+
assert "--port" in result.output
|
|
573
|
+
assert "--path" in result.output
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class TestContextCLI:
|
|
577
|
+
def test_context_help(self):
|
|
578
|
+
from semantic_code_intelligence.cli.commands.context_cmd import context_cmd
|
|
579
|
+
runner = CliRunner()
|
|
580
|
+
result = runner.invoke(context_cmd, ["--help"])
|
|
581
|
+
assert result.exit_code == 0
|
|
582
|
+
assert "query" in result.output
|
|
583
|
+
assert "symbol" in result.output
|
|
584
|
+
assert "file" in result.output
|
|
585
|
+
assert "repo" in result.output
|
|
586
|
+
|
|
587
|
+
def test_context_options(self):
|
|
588
|
+
from semantic_code_intelligence.cli.commands.context_cmd import context_cmd
|
|
589
|
+
runner = CliRunner()
|
|
590
|
+
result = runner.invoke(context_cmd, ["--help"])
|
|
591
|
+
assert "--top-k" in result.output
|
|
592
|
+
assert "--json" in result.output
|
|
593
|
+
|
|
594
|
+
@patch("semantic_code_intelligence.bridge.context_provider.search_codebase")
|
|
595
|
+
def test_context_query_json(self, mock_search, tmp_path):
|
|
596
|
+
mock_search.return_value = []
|
|
597
|
+
from semantic_code_intelligence.cli.commands.context_cmd import context_cmd
|
|
598
|
+
runner = CliRunner()
|
|
599
|
+
result = runner.invoke(
|
|
600
|
+
context_cmd,
|
|
601
|
+
["query", "test", "--json", "--path", str(tmp_path)],
|
|
602
|
+
)
|
|
603
|
+
assert result.exit_code == 0
|
|
604
|
+
data = json.loads(result.output)
|
|
605
|
+
assert data["query"] == "test"
|
|
606
|
+
|
|
607
|
+
def test_context_repo_json(self, tmp_path):
|
|
608
|
+
from semantic_code_intelligence.cli.commands.context_cmd import context_cmd
|
|
609
|
+
runner = CliRunner()
|
|
610
|
+
with patch(
|
|
611
|
+
"semantic_code_intelligence.bridge.context_provider.ContextProvider.context_for_repo"
|
|
612
|
+
) as mock_repo:
|
|
613
|
+
mock_repo.return_value = {"total_files": 5}
|
|
614
|
+
result = runner.invoke(
|
|
615
|
+
context_cmd,
|
|
616
|
+
["repo", "--json", "--path", str(tmp_path)],
|
|
617
|
+
)
|
|
618
|
+
assert result.exit_code == 0
|
|
619
|
+
data = json.loads(result.output)
|
|
620
|
+
assert data["total_files"] == 5
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# =========================================================================
|
|
624
|
+
# Bridge __init__ imports
|
|
625
|
+
# =========================================================================
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class TestBridgeImports:
|
|
629
|
+
def test_all_exports(self):
|
|
630
|
+
from semantic_code_intelligence.bridge import (
|
|
631
|
+
AgentRequest,
|
|
632
|
+
AgentResponse,
|
|
633
|
+
BridgeCapabilities,
|
|
634
|
+
ContextProvider,
|
|
635
|
+
BridgeServer,
|
|
636
|
+
VSCodeBridge,
|
|
637
|
+
)
|
|
638
|
+
assert AgentRequest is not None
|
|
639
|
+
assert AgentResponse is not None
|
|
640
|
+
assert BridgeCapabilities is not None
|
|
641
|
+
assert ContextProvider is not None
|
|
642
|
+
assert BridgeServer is not None
|
|
643
|
+
assert VSCodeBridge is not None
|