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,986 @@
|
|
|
1
|
+
"""Tests for Phase 19 — AI Agent Tooling Protocol.
|
|
2
|
+
|
|
3
|
+
Covers: tool protocol dataclasses (ToolInvocation, ToolExecutionResult,
|
|
4
|
+
ToolError, ToolErrorCode), ToolExecutor engine (validation, routing,
|
|
5
|
+
plugin tools, batch execution), bridge extensions (INVOKE_TOOL, LIST_TOOLS,
|
|
6
|
+
/tools/invoke, /tools/list, /tools/stream), CLI `tool` command (list, run,
|
|
7
|
+
schema), plugin hooks (REGISTER_TOOL, PRE_TOOL_INVOKE, POST_TOOL_INVOKE),
|
|
8
|
+
capability manifest (tools field), docs generation (AI_TOOL_PROTOCOL.md),
|
|
9
|
+
router (31 commands), version (0.19.0), and safety guardrails.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from click.testing import CliRunner
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =========================================================================
|
|
23
|
+
# Test helpers
|
|
24
|
+
# =========================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write_sample_project(root: Path) -> None:
|
|
28
|
+
"""Write a small project for tool testing."""
|
|
29
|
+
src = root / "src"
|
|
30
|
+
src.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
(src / "core.py").write_text(
|
|
33
|
+
"def helper():\n"
|
|
34
|
+
" return 42\n"
|
|
35
|
+
"\n"
|
|
36
|
+
"def compute(x):\n"
|
|
37
|
+
" return helper() + x\n",
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
(src / "api.py").write_text(
|
|
42
|
+
"from src.core import compute\n"
|
|
43
|
+
"\n"
|
|
44
|
+
"def handle_request(data):\n"
|
|
45
|
+
" return compute(data)\n",
|
|
46
|
+
encoding="utf-8",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =========================================================================
|
|
51
|
+
# ToolInvocation dataclass tests
|
|
52
|
+
# =========================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestToolInvocation:
|
|
56
|
+
"""Tests for ToolInvocation dataclass."""
|
|
57
|
+
|
|
58
|
+
def test_basic_creation(self):
|
|
59
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
60
|
+
|
|
61
|
+
inv = ToolInvocation(tool_name="semantic_search", arguments={"query": "parse"})
|
|
62
|
+
assert inv.tool_name == "semantic_search"
|
|
63
|
+
assert inv.arguments == {"query": "parse"}
|
|
64
|
+
assert inv.request_id # auto-generated
|
|
65
|
+
assert inv.timestamp > 0
|
|
66
|
+
|
|
67
|
+
def test_auto_request_id(self):
|
|
68
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
69
|
+
|
|
70
|
+
inv = ToolInvocation(tool_name="test")
|
|
71
|
+
assert len(inv.request_id) == 12
|
|
72
|
+
|
|
73
|
+
def test_custom_request_id(self):
|
|
74
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
75
|
+
|
|
76
|
+
inv = ToolInvocation(tool_name="test", request_id="custom-123")
|
|
77
|
+
assert inv.request_id == "custom-123"
|
|
78
|
+
|
|
79
|
+
def test_to_dict(self):
|
|
80
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
81
|
+
|
|
82
|
+
inv = ToolInvocation(
|
|
83
|
+
tool_name="explain_symbol",
|
|
84
|
+
arguments={"symbol_name": "Foo"},
|
|
85
|
+
request_id="r1",
|
|
86
|
+
)
|
|
87
|
+
d = inv.to_dict()
|
|
88
|
+
assert d["tool_name"] == "explain_symbol"
|
|
89
|
+
assert d["arguments"] == {"symbol_name": "Foo"}
|
|
90
|
+
assert d["request_id"] == "r1"
|
|
91
|
+
assert "timestamp" in d
|
|
92
|
+
|
|
93
|
+
def test_to_json(self):
|
|
94
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
95
|
+
|
|
96
|
+
inv = ToolInvocation(tool_name="test", request_id="r1")
|
|
97
|
+
j = inv.to_json()
|
|
98
|
+
parsed = json.loads(j)
|
|
99
|
+
assert parsed["tool_name"] == "test"
|
|
100
|
+
|
|
101
|
+
def test_from_dict(self):
|
|
102
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
103
|
+
|
|
104
|
+
data = {
|
|
105
|
+
"tool_name": "get_context",
|
|
106
|
+
"arguments": {"symbol_name": "Bar"},
|
|
107
|
+
"request_id": "abc",
|
|
108
|
+
"timestamp": 1234567890.0,
|
|
109
|
+
}
|
|
110
|
+
inv = ToolInvocation.from_dict(data)
|
|
111
|
+
assert inv.tool_name == "get_context"
|
|
112
|
+
assert inv.arguments["symbol_name"] == "Bar"
|
|
113
|
+
assert inv.request_id == "abc"
|
|
114
|
+
assert inv.timestamp == 1234567890.0
|
|
115
|
+
|
|
116
|
+
def test_from_json(self):
|
|
117
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
118
|
+
|
|
119
|
+
j = '{"tool_name": "test", "arguments": {}, "request_id": "x", "timestamp": 0}'
|
|
120
|
+
inv = ToolInvocation.from_json(j)
|
|
121
|
+
assert inv.tool_name == "test"
|
|
122
|
+
|
|
123
|
+
def test_roundtrip(self):
|
|
124
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
125
|
+
|
|
126
|
+
original = ToolInvocation(tool_name="find_references", arguments={"symbol_name": "X"})
|
|
127
|
+
restored = ToolInvocation.from_dict(original.to_dict())
|
|
128
|
+
assert restored.tool_name == original.tool_name
|
|
129
|
+
assert restored.arguments == original.arguments
|
|
130
|
+
assert restored.request_id == original.request_id
|
|
131
|
+
|
|
132
|
+
def test_empty_arguments(self):
|
|
133
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
134
|
+
|
|
135
|
+
inv = ToolInvocation(tool_name="summarize_repo")
|
|
136
|
+
assert inv.arguments == {}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# =========================================================================
|
|
140
|
+
# ToolError dataclass tests
|
|
141
|
+
# =========================================================================
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestToolError:
|
|
145
|
+
"""Tests for ToolError dataclass."""
|
|
146
|
+
|
|
147
|
+
def test_basic_creation(self):
|
|
148
|
+
from semantic_code_intelligence.tools.protocol import ToolError
|
|
149
|
+
|
|
150
|
+
err = ToolError(
|
|
151
|
+
tool_name="bad_tool",
|
|
152
|
+
error_code="unknown_tool",
|
|
153
|
+
error_message="Tool not found",
|
|
154
|
+
request_id="r1",
|
|
155
|
+
)
|
|
156
|
+
assert err.tool_name == "bad_tool"
|
|
157
|
+
assert err.error_code == "unknown_tool"
|
|
158
|
+
assert err.error_message == "Tool not found"
|
|
159
|
+
|
|
160
|
+
def test_to_dict(self):
|
|
161
|
+
from semantic_code_intelligence.tools.protocol import ToolError
|
|
162
|
+
|
|
163
|
+
err = ToolError(tool_name="t", error_code="e", error_message="m", request_id="r")
|
|
164
|
+
d = err.to_dict()
|
|
165
|
+
assert d["tool_name"] == "t"
|
|
166
|
+
assert d["error_code"] == "e"
|
|
167
|
+
assert d["error_message"] == "m"
|
|
168
|
+
assert d["request_id"] == "r"
|
|
169
|
+
|
|
170
|
+
def test_to_json(self):
|
|
171
|
+
from semantic_code_intelligence.tools.protocol import ToolError
|
|
172
|
+
|
|
173
|
+
err = ToolError(tool_name="t", error_code="e", error_message="m")
|
|
174
|
+
j = err.to_json()
|
|
175
|
+
parsed = json.loads(j)
|
|
176
|
+
assert parsed["error_code"] == "e"
|
|
177
|
+
|
|
178
|
+
def test_from_dict(self):
|
|
179
|
+
from semantic_code_intelligence.tools.protocol import ToolError
|
|
180
|
+
|
|
181
|
+
data = {"tool_name": "a", "error_code": "b", "error_message": "c", "request_id": "d"}
|
|
182
|
+
err = ToolError.from_dict(data)
|
|
183
|
+
assert err.tool_name == "a"
|
|
184
|
+
assert err.error_code == "b"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# =========================================================================
|
|
188
|
+
# ToolErrorCode enum tests
|
|
189
|
+
# =========================================================================
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestToolErrorCode:
|
|
193
|
+
"""Tests for ToolErrorCode enum."""
|
|
194
|
+
|
|
195
|
+
def test_all_codes_exist(self):
|
|
196
|
+
from semantic_code_intelligence.tools.protocol import ToolErrorCode
|
|
197
|
+
|
|
198
|
+
expected = {"unknown_tool", "invalid_arguments", "missing_required_arg",
|
|
199
|
+
"execution_error", "timeout", "permission_denied"}
|
|
200
|
+
actual = {c.value for c in ToolErrorCode}
|
|
201
|
+
assert expected == actual
|
|
202
|
+
|
|
203
|
+
def test_string_values(self):
|
|
204
|
+
from semantic_code_intelligence.tools.protocol import ToolErrorCode
|
|
205
|
+
|
|
206
|
+
assert ToolErrorCode.UNKNOWN_TOOL == "unknown_tool"
|
|
207
|
+
assert ToolErrorCode.EXECUTION_ERROR == "execution_error"
|
|
208
|
+
|
|
209
|
+
def test_is_str_enum(self):
|
|
210
|
+
from semantic_code_intelligence.tools.protocol import ToolErrorCode
|
|
211
|
+
|
|
212
|
+
assert isinstance(ToolErrorCode.TIMEOUT, str)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# =========================================================================
|
|
216
|
+
# ToolExecutionResult dataclass tests
|
|
217
|
+
# =========================================================================
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestToolExecutionResult:
|
|
221
|
+
"""Tests for ToolExecutionResult dataclass."""
|
|
222
|
+
|
|
223
|
+
def test_success_result(self):
|
|
224
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
225
|
+
|
|
226
|
+
r = ToolExecutionResult(
|
|
227
|
+
tool_name="semantic_search",
|
|
228
|
+
request_id="r1",
|
|
229
|
+
success=True,
|
|
230
|
+
result_payload={"results": [1, 2, 3]},
|
|
231
|
+
execution_time_ms=42.5,
|
|
232
|
+
)
|
|
233
|
+
assert r.success is True
|
|
234
|
+
assert r.result_payload == {"results": [1, 2, 3]}
|
|
235
|
+
|
|
236
|
+
def test_failure_result(self):
|
|
237
|
+
from semantic_code_intelligence.tools.protocol import ToolError, ToolExecutionResult
|
|
238
|
+
|
|
239
|
+
err = ToolError(tool_name="t", error_code="execution_error", error_message="boom")
|
|
240
|
+
r = ToolExecutionResult(
|
|
241
|
+
tool_name="t",
|
|
242
|
+
success=False,
|
|
243
|
+
error=err,
|
|
244
|
+
)
|
|
245
|
+
assert r.success is False
|
|
246
|
+
assert r.error is not None
|
|
247
|
+
assert r.error.error_message == "boom"
|
|
248
|
+
|
|
249
|
+
def test_to_dict_success(self):
|
|
250
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
251
|
+
|
|
252
|
+
r = ToolExecutionResult(
|
|
253
|
+
tool_name="test", request_id="r1", success=True,
|
|
254
|
+
result_payload={"data": 42}, execution_time_ms=10.0,
|
|
255
|
+
)
|
|
256
|
+
d = r.to_dict()
|
|
257
|
+
assert d["success"] is True
|
|
258
|
+
assert d["result_payload"]["data"] == 42
|
|
259
|
+
assert "error" not in d
|
|
260
|
+
|
|
261
|
+
def test_to_dict_failure(self):
|
|
262
|
+
from semantic_code_intelligence.tools.protocol import ToolError, ToolExecutionResult
|
|
263
|
+
|
|
264
|
+
err = ToolError(tool_name="t", error_code="x", error_message="y")
|
|
265
|
+
r = ToolExecutionResult(tool_name="t", success=False, error=err)
|
|
266
|
+
d = r.to_dict()
|
|
267
|
+
assert d["success"] is False
|
|
268
|
+
assert d["error"]["error_code"] == "x"
|
|
269
|
+
assert "result_payload" not in d
|
|
270
|
+
|
|
271
|
+
def test_to_json(self):
|
|
272
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
273
|
+
|
|
274
|
+
r = ToolExecutionResult(tool_name="t", success=True, result_payload={"k": "v"})
|
|
275
|
+
j = r.to_json(indent=2)
|
|
276
|
+
parsed = json.loads(j)
|
|
277
|
+
assert parsed["success"] is True
|
|
278
|
+
|
|
279
|
+
def test_from_dict_success(self):
|
|
280
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
281
|
+
|
|
282
|
+
data = {
|
|
283
|
+
"tool_name": "t",
|
|
284
|
+
"request_id": "r1",
|
|
285
|
+
"success": True,
|
|
286
|
+
"result_payload": {"v": 1},
|
|
287
|
+
"execution_time_ms": 5.0,
|
|
288
|
+
"timestamp": 100.0,
|
|
289
|
+
}
|
|
290
|
+
r = ToolExecutionResult.from_dict(data)
|
|
291
|
+
assert r.tool_name == "t"
|
|
292
|
+
assert r.success is True
|
|
293
|
+
assert r.result_payload == {"v": 1}
|
|
294
|
+
|
|
295
|
+
def test_from_dict_failure(self):
|
|
296
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
297
|
+
|
|
298
|
+
data = {
|
|
299
|
+
"tool_name": "t",
|
|
300
|
+
"success": False,
|
|
301
|
+
"error": {"tool_name": "t", "error_code": "c", "error_message": "m", "request_id": ""},
|
|
302
|
+
}
|
|
303
|
+
r = ToolExecutionResult.from_dict(data)
|
|
304
|
+
assert r.success is False
|
|
305
|
+
assert r.error is not None
|
|
306
|
+
assert r.error.error_code == "c"
|
|
307
|
+
|
|
308
|
+
def test_auto_timestamp(self):
|
|
309
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
310
|
+
|
|
311
|
+
before = time.time()
|
|
312
|
+
r = ToolExecutionResult(tool_name="t")
|
|
313
|
+
after = time.time()
|
|
314
|
+
assert before <= r.timestamp <= after
|
|
315
|
+
|
|
316
|
+
def test_roundtrip(self):
|
|
317
|
+
from semantic_code_intelligence.tools.protocol import ToolExecutionResult
|
|
318
|
+
|
|
319
|
+
original = ToolExecutionResult(
|
|
320
|
+
tool_name="test", request_id="abc", success=True,
|
|
321
|
+
result_payload={"x": [1, 2]}, execution_time_ms=3.14,
|
|
322
|
+
)
|
|
323
|
+
restored = ToolExecutionResult.from_dict(original.to_dict())
|
|
324
|
+
assert restored.tool_name == original.tool_name
|
|
325
|
+
assert restored.success == original.success
|
|
326
|
+
assert restored.result_payload == original.result_payload
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# =========================================================================
|
|
330
|
+
# ToolExecutor engine tests
|
|
331
|
+
# =========================================================================
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestToolExecutor:
|
|
335
|
+
"""Tests for the ToolExecutor engine."""
|
|
336
|
+
|
|
337
|
+
def test_creation(self, tmp_path):
|
|
338
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
339
|
+
|
|
340
|
+
executor = ToolExecutor(tmp_path)
|
|
341
|
+
assert executor is not None
|
|
342
|
+
|
|
343
|
+
def test_available_tools_includes_builtins(self, tmp_path):
|
|
344
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
345
|
+
|
|
346
|
+
executor = ToolExecutor(tmp_path)
|
|
347
|
+
names = executor.list_tool_names()
|
|
348
|
+
assert "semantic_search" in names
|
|
349
|
+
assert "explain_symbol" in names
|
|
350
|
+
assert "summarize_repo" in names
|
|
351
|
+
assert len(names) >= 8
|
|
352
|
+
|
|
353
|
+
def test_get_tool_schema(self, tmp_path):
|
|
354
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
355
|
+
|
|
356
|
+
executor = ToolExecutor(tmp_path)
|
|
357
|
+
schema = executor.get_tool_schema("semantic_search")
|
|
358
|
+
assert schema is not None
|
|
359
|
+
assert schema["name"] == "semantic_search"
|
|
360
|
+
assert "parameters" in schema
|
|
361
|
+
|
|
362
|
+
def test_get_tool_schema_unknown(self, tmp_path):
|
|
363
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
364
|
+
|
|
365
|
+
executor = ToolExecutor(tmp_path)
|
|
366
|
+
assert executor.get_tool_schema("nonexistent") is None
|
|
367
|
+
|
|
368
|
+
def test_execute_unknown_tool(self, tmp_path):
|
|
369
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
370
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
371
|
+
|
|
372
|
+
executor = ToolExecutor(tmp_path)
|
|
373
|
+
inv = ToolInvocation(tool_name="nonexistent_tool", arguments={})
|
|
374
|
+
result = executor.execute(inv)
|
|
375
|
+
assert result.success is False
|
|
376
|
+
assert result.error is not None
|
|
377
|
+
assert result.error.error_code == "unknown_tool"
|
|
378
|
+
|
|
379
|
+
def test_execute_missing_required_arg(self, tmp_path):
|
|
380
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
381
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
382
|
+
|
|
383
|
+
executor = ToolExecutor(tmp_path)
|
|
384
|
+
inv = ToolInvocation(tool_name="semantic_search", arguments={})
|
|
385
|
+
result = executor.execute(inv)
|
|
386
|
+
assert result.success is False
|
|
387
|
+
assert result.error is not None
|
|
388
|
+
assert result.error.error_code == "missing_required_arg"
|
|
389
|
+
|
|
390
|
+
def test_execute_summarize_repo(self, tmp_path):
|
|
391
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
392
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
393
|
+
|
|
394
|
+
_write_sample_project(tmp_path)
|
|
395
|
+
executor = ToolExecutor(tmp_path)
|
|
396
|
+
inv = ToolInvocation(tool_name="summarize_repo", arguments={})
|
|
397
|
+
result = executor.execute(inv)
|
|
398
|
+
assert result.success is True
|
|
399
|
+
assert result.execution_time_ms >= 0
|
|
400
|
+
assert result.tool_name == "summarize_repo"
|
|
401
|
+
|
|
402
|
+
def test_execute_has_timing(self, tmp_path):
|
|
403
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
404
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
405
|
+
|
|
406
|
+
executor = ToolExecutor(tmp_path)
|
|
407
|
+
inv = ToolInvocation(tool_name="summarize_repo", arguments={})
|
|
408
|
+
result = executor.execute(inv)
|
|
409
|
+
assert result.execution_time_ms >= 0
|
|
410
|
+
|
|
411
|
+
def test_execute_preserves_request_id(self, tmp_path):
|
|
412
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
413
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
414
|
+
|
|
415
|
+
executor = ToolExecutor(tmp_path)
|
|
416
|
+
inv = ToolInvocation(tool_name="summarize_repo", arguments={}, request_id="my-id-123")
|
|
417
|
+
result = executor.execute(inv)
|
|
418
|
+
assert result.request_id == "my-id-123"
|
|
419
|
+
|
|
420
|
+
def test_execute_batch(self, tmp_path):
|
|
421
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
422
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
423
|
+
|
|
424
|
+
executor = ToolExecutor(tmp_path)
|
|
425
|
+
invocations = [
|
|
426
|
+
ToolInvocation(tool_name="summarize_repo", arguments={}),
|
|
427
|
+
ToolInvocation(tool_name="nonexistent", arguments={}),
|
|
428
|
+
]
|
|
429
|
+
results = executor.execute_batch(invocations)
|
|
430
|
+
assert len(results) == 2
|
|
431
|
+
assert results[0].success is True
|
|
432
|
+
assert results[1].success is False
|
|
433
|
+
|
|
434
|
+
def test_registry_access(self, tmp_path):
|
|
435
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
436
|
+
|
|
437
|
+
executor = ToolExecutor(tmp_path)
|
|
438
|
+
assert executor.registry is not None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# =========================================================================
|
|
442
|
+
# Plugin tool registration tests
|
|
443
|
+
# =========================================================================
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class TestPluginToolRegistration:
|
|
447
|
+
"""Tests for plugin-registered tools in the ToolExecutor."""
|
|
448
|
+
|
|
449
|
+
def test_register_plugin_tool(self, tmp_path):
|
|
450
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
451
|
+
|
|
452
|
+
executor = ToolExecutor(tmp_path)
|
|
453
|
+
executor.register_plugin_tool(
|
|
454
|
+
name="my_custom_tool",
|
|
455
|
+
description="A test plugin tool",
|
|
456
|
+
parameters={"input": {"type": "string", "required": True}},
|
|
457
|
+
handler=lambda input: {"echo": input},
|
|
458
|
+
)
|
|
459
|
+
assert "my_custom_tool" in executor.list_tool_names()
|
|
460
|
+
|
|
461
|
+
def test_plugin_tool_schema(self, tmp_path):
|
|
462
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
463
|
+
|
|
464
|
+
executor = ToolExecutor(tmp_path)
|
|
465
|
+
executor.register_plugin_tool(
|
|
466
|
+
name="plugin_tool",
|
|
467
|
+
description="Plugin tool",
|
|
468
|
+
parameters={},
|
|
469
|
+
handler=lambda: {"ok": True},
|
|
470
|
+
)
|
|
471
|
+
schema = executor.get_tool_schema("plugin_tool")
|
|
472
|
+
assert schema is not None
|
|
473
|
+
assert schema["source"] == "plugin"
|
|
474
|
+
|
|
475
|
+
def test_execute_plugin_tool(self, tmp_path):
|
|
476
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
477
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
478
|
+
|
|
479
|
+
executor = ToolExecutor(tmp_path)
|
|
480
|
+
executor.register_plugin_tool(
|
|
481
|
+
name="echo_tool",
|
|
482
|
+
description="Echoes input",
|
|
483
|
+
parameters={"msg": {"type": "string", "required": True}},
|
|
484
|
+
handler=lambda msg: {"echoed": msg},
|
|
485
|
+
)
|
|
486
|
+
inv = ToolInvocation(tool_name="echo_tool", arguments={"msg": "hello"})
|
|
487
|
+
result = executor.execute(inv)
|
|
488
|
+
assert result.success is True
|
|
489
|
+
assert result.result_payload["echoed"] == "hello"
|
|
490
|
+
|
|
491
|
+
def test_plugin_tool_cannot_override_builtin(self, tmp_path):
|
|
492
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
493
|
+
|
|
494
|
+
executor = ToolExecutor(tmp_path)
|
|
495
|
+
with pytest.raises(ValueError, match="collides with built-in"):
|
|
496
|
+
executor.register_plugin_tool(
|
|
497
|
+
name="semantic_search",
|
|
498
|
+
description="override",
|
|
499
|
+
parameters={},
|
|
500
|
+
handler=lambda: {},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def test_unregister_plugin_tool(self, tmp_path):
|
|
504
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
505
|
+
|
|
506
|
+
executor = ToolExecutor(tmp_path)
|
|
507
|
+
executor.register_plugin_tool(
|
|
508
|
+
name="temp_tool",
|
|
509
|
+
description="Temporary",
|
|
510
|
+
parameters={},
|
|
511
|
+
handler=lambda: {},
|
|
512
|
+
)
|
|
513
|
+
assert "temp_tool" in executor.list_tool_names()
|
|
514
|
+
assert executor.unregister_plugin_tool("temp_tool") is True
|
|
515
|
+
assert "temp_tool" not in executor.list_tool_names()
|
|
516
|
+
|
|
517
|
+
def test_unregister_nonexistent(self, tmp_path):
|
|
518
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
519
|
+
|
|
520
|
+
executor = ToolExecutor(tmp_path)
|
|
521
|
+
assert executor.unregister_plugin_tool("nonexistent") is False
|
|
522
|
+
|
|
523
|
+
def test_plugin_tool_error_handling(self, tmp_path):
|
|
524
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
525
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
526
|
+
|
|
527
|
+
def bad_handler(**kwargs):
|
|
528
|
+
raise RuntimeError("Plugin tool failure")
|
|
529
|
+
|
|
530
|
+
executor = ToolExecutor(tmp_path)
|
|
531
|
+
executor.register_plugin_tool(
|
|
532
|
+
name="bad_tool",
|
|
533
|
+
description="Fails",
|
|
534
|
+
parameters={},
|
|
535
|
+
handler=bad_handler,
|
|
536
|
+
)
|
|
537
|
+
inv = ToolInvocation(tool_name="bad_tool", arguments={})
|
|
538
|
+
result = executor.execute(inv)
|
|
539
|
+
assert result.success is False
|
|
540
|
+
assert result.error is not None
|
|
541
|
+
assert "Plugin tool failure" in result.error.error_message
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# =========================================================================
|
|
545
|
+
# Bridge protocol extension tests
|
|
546
|
+
# =========================================================================
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
class TestBridgeProtocolExtensions:
|
|
550
|
+
"""Tests for Phase 19 bridge protocol additions."""
|
|
551
|
+
|
|
552
|
+
def test_request_kind_invoke_tool(self):
|
|
553
|
+
from semantic_code_intelligence.bridge.protocol import RequestKind
|
|
554
|
+
|
|
555
|
+
assert RequestKind.INVOKE_TOOL == "invoke_tool"
|
|
556
|
+
|
|
557
|
+
def test_request_kind_list_tools(self):
|
|
558
|
+
from semantic_code_intelligence.bridge.protocol import RequestKind
|
|
559
|
+
|
|
560
|
+
assert RequestKind.LIST_TOOLS == "list_tools"
|
|
561
|
+
|
|
562
|
+
def test_capabilities_include_new_kinds(self):
|
|
563
|
+
from semantic_code_intelligence.bridge.protocol import BridgeCapabilities
|
|
564
|
+
|
|
565
|
+
caps = BridgeCapabilities()
|
|
566
|
+
assert "invoke_tool" in caps.supported_requests
|
|
567
|
+
assert "list_tools" in caps.supported_requests
|
|
568
|
+
|
|
569
|
+
def test_capabilities_tools_field(self):
|
|
570
|
+
from semantic_code_intelligence.bridge.protocol import BridgeCapabilities
|
|
571
|
+
|
|
572
|
+
caps = BridgeCapabilities(tools=[{"name": "test_tool"}])
|
|
573
|
+
d = caps.to_dict()
|
|
574
|
+
assert "tools" in d
|
|
575
|
+
assert d["tools"] == [{"name": "test_tool"}]
|
|
576
|
+
|
|
577
|
+
def test_capabilities_no_tools_field_when_empty(self):
|
|
578
|
+
from semantic_code_intelligence.bridge.protocol import BridgeCapabilities
|
|
579
|
+
|
|
580
|
+
caps = BridgeCapabilities()
|
|
581
|
+
d = caps.to_dict()
|
|
582
|
+
assert "tools" not in d
|
|
583
|
+
|
|
584
|
+
def test_request_kind_count(self):
|
|
585
|
+
from semantic_code_intelligence.bridge.protocol import RequestKind
|
|
586
|
+
|
|
587
|
+
# 10 original + 2 new (INVOKE_TOOL, LIST_TOOLS) = 12
|
|
588
|
+
assert len(RequestKind) == 12
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# =========================================================================
|
|
592
|
+
# Bridge server tool endpoint tests
|
|
593
|
+
# =========================================================================
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class TestBridgeServerToolEndpoints:
|
|
597
|
+
"""Tests for the bridge server's tool-related endpoints."""
|
|
598
|
+
|
|
599
|
+
def test_dispatch_list_tools(self, tmp_path):
|
|
600
|
+
from semantic_code_intelligence.bridge.server import BridgeServer
|
|
601
|
+
from semantic_code_intelligence.bridge.protocol import AgentRequest
|
|
602
|
+
|
|
603
|
+
server = BridgeServer(tmp_path)
|
|
604
|
+
req = AgentRequest(kind="list_tools", params={}, request_id="t1")
|
|
605
|
+
resp = server.dispatch(req)
|
|
606
|
+
assert resp.success is True
|
|
607
|
+
assert "tools" in resp.data
|
|
608
|
+
assert resp.data["count"] >= 8
|
|
609
|
+
|
|
610
|
+
def test_dispatch_invoke_tool(self, tmp_path):
|
|
611
|
+
from semantic_code_intelligence.bridge.server import BridgeServer
|
|
612
|
+
from semantic_code_intelligence.bridge.protocol import AgentRequest
|
|
613
|
+
|
|
614
|
+
server = BridgeServer(tmp_path)
|
|
615
|
+
req = AgentRequest(
|
|
616
|
+
kind="invoke_tool",
|
|
617
|
+
params={"tool_name": "summarize_repo", "arguments": {}},
|
|
618
|
+
request_id="t2",
|
|
619
|
+
)
|
|
620
|
+
resp = server.dispatch(req)
|
|
621
|
+
assert resp.success is True
|
|
622
|
+
|
|
623
|
+
def test_dispatch_invoke_unknown_tool(self, tmp_path):
|
|
624
|
+
from semantic_code_intelligence.bridge.server import BridgeServer
|
|
625
|
+
from semantic_code_intelligence.bridge.protocol import AgentRequest
|
|
626
|
+
|
|
627
|
+
server = BridgeServer(tmp_path)
|
|
628
|
+
req = AgentRequest(
|
|
629
|
+
kind="invoke_tool",
|
|
630
|
+
params={"tool_name": "fake_tool", "arguments": {}},
|
|
631
|
+
request_id="t3",
|
|
632
|
+
)
|
|
633
|
+
resp = server.dispatch(req)
|
|
634
|
+
assert resp.success is False
|
|
635
|
+
|
|
636
|
+
def test_server_has_executor(self, tmp_path):
|
|
637
|
+
from semantic_code_intelligence.bridge.server import BridgeServer
|
|
638
|
+
|
|
639
|
+
server = BridgeServer(tmp_path)
|
|
640
|
+
assert server._executor is not None
|
|
641
|
+
|
|
642
|
+
def test_capabilities_include_tools(self, tmp_path):
|
|
643
|
+
from semantic_code_intelligence.bridge.server import BridgeServer
|
|
644
|
+
|
|
645
|
+
server = BridgeServer(tmp_path)
|
|
646
|
+
caps = server._capabilities
|
|
647
|
+
assert len(caps.tools) >= 8
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# =========================================================================
|
|
651
|
+
# Plugin hooks tests
|
|
652
|
+
# =========================================================================
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class TestPluginHooksPhase19:
|
|
656
|
+
"""Tests for Phase 19 plugin hooks."""
|
|
657
|
+
|
|
658
|
+
def test_register_tool_hook_exists(self):
|
|
659
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
660
|
+
|
|
661
|
+
assert hasattr(PluginHook, "REGISTER_TOOL")
|
|
662
|
+
assert PluginHook.REGISTER_TOOL == "register_tool"
|
|
663
|
+
|
|
664
|
+
def test_pre_tool_invoke_hook_exists(self):
|
|
665
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
666
|
+
|
|
667
|
+
assert hasattr(PluginHook, "PRE_TOOL_INVOKE")
|
|
668
|
+
assert PluginHook.PRE_TOOL_INVOKE == "pre_tool_invoke"
|
|
669
|
+
|
|
670
|
+
def test_post_tool_invoke_hook_exists(self):
|
|
671
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
672
|
+
|
|
673
|
+
assert hasattr(PluginHook, "POST_TOOL_INVOKE")
|
|
674
|
+
assert PluginHook.POST_TOOL_INVOKE == "post_tool_invoke"
|
|
675
|
+
|
|
676
|
+
def test_plugin_hook_count(self):
|
|
677
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
678
|
+
|
|
679
|
+
# 19 original + 3 new (REGISTER_TOOL, PRE_TOOL_INVOKE, POST_TOOL_INVOKE) = 22
|
|
680
|
+
assert len(PluginHook) == 22
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
# =========================================================================
|
|
684
|
+
# CLI tool command tests
|
|
685
|
+
# =========================================================================
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class TestCLIToolCommand:
|
|
689
|
+
"""Tests for the `codexa tool` CLI command group."""
|
|
690
|
+
|
|
691
|
+
def test_tool_group_exists(self):
|
|
692
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
693
|
+
|
|
694
|
+
assert tool_cmd.name == "tool"
|
|
695
|
+
|
|
696
|
+
def test_tool_list_subcommand(self):
|
|
697
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
698
|
+
|
|
699
|
+
runner = CliRunner()
|
|
700
|
+
result = runner.invoke(tool_cmd, ["list", "--json"])
|
|
701
|
+
assert result.exit_code == 0
|
|
702
|
+
data = json.loads(result.output)
|
|
703
|
+
assert "tools" in data
|
|
704
|
+
assert data["count"] >= 8
|
|
705
|
+
|
|
706
|
+
def test_tool_list_text_output(self):
|
|
707
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
708
|
+
|
|
709
|
+
runner = CliRunner()
|
|
710
|
+
result = runner.invoke(tool_cmd, ["list"])
|
|
711
|
+
assert result.exit_code == 0
|
|
712
|
+
assert "semantic_search" in result.output
|
|
713
|
+
|
|
714
|
+
def test_tool_run_subcommand(self, tmp_path):
|
|
715
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
716
|
+
|
|
717
|
+
runner = CliRunner()
|
|
718
|
+
result = runner.invoke(tool_cmd, [
|
|
719
|
+
"run", "summarize_repo",
|
|
720
|
+
"--path", str(tmp_path),
|
|
721
|
+
"--json",
|
|
722
|
+
])
|
|
723
|
+
assert result.exit_code == 0
|
|
724
|
+
data = json.loads(result.output)
|
|
725
|
+
assert data["tool_name"] == "summarize_repo"
|
|
726
|
+
|
|
727
|
+
def test_tool_run_with_args(self, tmp_path):
|
|
728
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
729
|
+
|
|
730
|
+
_write_sample_project(tmp_path)
|
|
731
|
+
runner = CliRunner()
|
|
732
|
+
result = runner.invoke(tool_cmd, [
|
|
733
|
+
"run", "semantic_search",
|
|
734
|
+
"--arg", "query=helper",
|
|
735
|
+
"--path", str(tmp_path),
|
|
736
|
+
"--json",
|
|
737
|
+
])
|
|
738
|
+
assert result.exit_code == 0
|
|
739
|
+
|
|
740
|
+
def test_tool_run_unknown_tool(self, tmp_path):
|
|
741
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
742
|
+
|
|
743
|
+
runner = CliRunner()
|
|
744
|
+
result = runner.invoke(tool_cmd, [
|
|
745
|
+
"run", "nonexistent_tool",
|
|
746
|
+
"--path", str(tmp_path),
|
|
747
|
+
"--json",
|
|
748
|
+
])
|
|
749
|
+
assert result.exit_code == 0 # exits cleanly with error in JSON
|
|
750
|
+
data = json.loads(result.output)
|
|
751
|
+
assert data["success"] is False
|
|
752
|
+
|
|
753
|
+
def test_tool_run_invalid_arg_format(self, tmp_path):
|
|
754
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
755
|
+
|
|
756
|
+
runner = CliRunner()
|
|
757
|
+
result = runner.invoke(tool_cmd, [
|
|
758
|
+
"run", "semantic_search",
|
|
759
|
+
"--arg", "no_equals_sign",
|
|
760
|
+
"--path", str(tmp_path),
|
|
761
|
+
])
|
|
762
|
+
# Should show error about invalid format
|
|
763
|
+
assert result.exit_code != 0 or "Invalid argument" in result.output
|
|
764
|
+
|
|
765
|
+
def test_tool_schema_subcommand(self):
|
|
766
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
767
|
+
|
|
768
|
+
runner = CliRunner()
|
|
769
|
+
result = runner.invoke(tool_cmd, ["schema", "semantic_search", "--json"])
|
|
770
|
+
assert result.exit_code == 0
|
|
771
|
+
data = json.loads(result.output)
|
|
772
|
+
assert data["name"] == "semantic_search"
|
|
773
|
+
|
|
774
|
+
def test_tool_schema_text_output(self):
|
|
775
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
776
|
+
|
|
777
|
+
runner = CliRunner()
|
|
778
|
+
result = runner.invoke(tool_cmd, ["schema", "explain_symbol"])
|
|
779
|
+
assert result.exit_code == 0
|
|
780
|
+
assert "explain_symbol" in result.output
|
|
781
|
+
|
|
782
|
+
def test_tool_schema_unknown(self):
|
|
783
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
784
|
+
|
|
785
|
+
runner = CliRunner()
|
|
786
|
+
result = runner.invoke(tool_cmd, ["schema", "nonexistent"])
|
|
787
|
+
assert result.exit_code == 0
|
|
788
|
+
assert "Unknown tool" in result.output
|
|
789
|
+
|
|
790
|
+
def test_tool_run_pipe_mode(self, tmp_path):
|
|
791
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
792
|
+
|
|
793
|
+
runner = CliRunner()
|
|
794
|
+
result = runner.invoke(tool_cmd, [
|
|
795
|
+
"run", "summarize_repo",
|
|
796
|
+
"--path", str(tmp_path),
|
|
797
|
+
"--pipe",
|
|
798
|
+
])
|
|
799
|
+
assert result.exit_code == 0
|
|
800
|
+
data = json.loads(result.output)
|
|
801
|
+
assert "tool_name" in data
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
# =========================================================================
|
|
805
|
+
# Router registration tests
|
|
806
|
+
# =========================================================================
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
class TestRouterPhase19:
|
|
810
|
+
"""Tests for router command registration."""
|
|
811
|
+
|
|
812
|
+
def test_command_count_31(self):
|
|
813
|
+
from semantic_code_intelligence.cli.main import cli
|
|
814
|
+
|
|
815
|
+
assert len(cli.commands) == 39
|
|
816
|
+
|
|
817
|
+
def test_tool_command_registered(self):
|
|
818
|
+
from semantic_code_intelligence.cli.main import cli
|
|
819
|
+
|
|
820
|
+
assert "tool" in cli.commands
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# =========================================================================
|
|
824
|
+
# Version tests
|
|
825
|
+
# =========================================================================
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
class TestVersionPhase19:
|
|
829
|
+
"""Tests for version 0.19.0."""
|
|
830
|
+
|
|
831
|
+
def test_version_string(self):
|
|
832
|
+
from semantic_code_intelligence import __version__
|
|
833
|
+
|
|
834
|
+
assert __version__ == "0.4.0"
|
|
835
|
+
|
|
836
|
+
def test_pyproject_version(self):
|
|
837
|
+
import tomllib
|
|
838
|
+
pyproject = Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
|
839
|
+
if pyproject.exists():
|
|
840
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
841
|
+
assert data["project"]["version"] == "0.4.0"
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
# =========================================================================
|
|
845
|
+
# Documentation generation tests
|
|
846
|
+
# =========================================================================
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class TestDocGenerationPhase19:
|
|
850
|
+
"""Tests for AI_TOOL_PROTOCOL.md documentation generation."""
|
|
851
|
+
|
|
852
|
+
def test_generate_ai_tool_protocol_reference(self):
|
|
853
|
+
from semantic_code_intelligence.docs import generate_ai_tool_protocol_reference
|
|
854
|
+
|
|
855
|
+
md = generate_ai_tool_protocol_reference()
|
|
856
|
+
assert "# AI Tool Protocol Reference" in md
|
|
857
|
+
assert "ToolInvocation" in md
|
|
858
|
+
assert "ToolExecutionResult" in md
|
|
859
|
+
assert "ToolError" in md
|
|
860
|
+
|
|
861
|
+
def test_protocol_reference_lists_tools(self):
|
|
862
|
+
from semantic_code_intelligence.docs import generate_ai_tool_protocol_reference
|
|
863
|
+
|
|
864
|
+
md = generate_ai_tool_protocol_reference()
|
|
865
|
+
assert "semantic_search" in md
|
|
866
|
+
assert "explain_symbol" in md
|
|
867
|
+
|
|
868
|
+
def test_protocol_reference_lists_endpoints(self):
|
|
869
|
+
from semantic_code_intelligence.docs import generate_ai_tool_protocol_reference
|
|
870
|
+
|
|
871
|
+
md = generate_ai_tool_protocol_reference()
|
|
872
|
+
assert "/tools/invoke" in md
|
|
873
|
+
assert "/tools/list" in md
|
|
874
|
+
assert "/tools/stream" in md
|
|
875
|
+
|
|
876
|
+
def test_protocol_reference_lists_error_codes(self):
|
|
877
|
+
from semantic_code_intelligence.docs import generate_ai_tool_protocol_reference
|
|
878
|
+
|
|
879
|
+
md = generate_ai_tool_protocol_reference()
|
|
880
|
+
assert "unknown_tool" in md
|
|
881
|
+
assert "missing_required_arg" in md
|
|
882
|
+
|
|
883
|
+
def test_protocol_reference_cli_usage(self):
|
|
884
|
+
from semantic_code_intelligence.docs import generate_ai_tool_protocol_reference
|
|
885
|
+
|
|
886
|
+
md = generate_ai_tool_protocol_reference()
|
|
887
|
+
assert "codexa tool list" in md
|
|
888
|
+
assert "codexa tool run" in md
|
|
889
|
+
|
|
890
|
+
def test_generate_all_docs_includes_protocol(self, tmp_path):
|
|
891
|
+
from semantic_code_intelligence.docs import generate_all_docs
|
|
892
|
+
|
|
893
|
+
generated = generate_all_docs(tmp_path)
|
|
894
|
+
assert "AI_TOOL_PROTOCOL.md" in generated
|
|
895
|
+
content = (tmp_path / "AI_TOOL_PROTOCOL.md").read_text(encoding="utf-8")
|
|
896
|
+
assert "AI Tool Protocol" in content
|
|
897
|
+
|
|
898
|
+
def test_docs_count(self, tmp_path):
|
|
899
|
+
from semantic_code_intelligence.docs import generate_all_docs
|
|
900
|
+
|
|
901
|
+
generated = generate_all_docs(tmp_path)
|
|
902
|
+
# Should be 10 (9 previous + AI_TOOL_PROTOCOL.md)
|
|
903
|
+
assert len(generated) >= 10
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
# =========================================================================
|
|
907
|
+
# Safety guardrails tests
|
|
908
|
+
# =========================================================================
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
class TestSafetyGuardrails:
|
|
912
|
+
"""Tests for AI safety guardrails in the tooling protocol."""
|
|
913
|
+
|
|
914
|
+
def test_tools_are_readonly(self):
|
|
915
|
+
"""Verify no tool definition enables code execution."""
|
|
916
|
+
from semantic_code_intelligence.tools import TOOL_DEFINITIONS
|
|
917
|
+
|
|
918
|
+
for tool in TOOL_DEFINITIONS:
|
|
919
|
+
name = tool["name"]
|
|
920
|
+
assert "exec" not in name.lower(), f"Tool {name} may execute code"
|
|
921
|
+
assert "run" not in name.lower() or name == "summarize_repo", f"Tool {name} may run code"
|
|
922
|
+
|
|
923
|
+
def test_unknown_tool_rejected(self, tmp_path):
|
|
924
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
925
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
926
|
+
|
|
927
|
+
executor = ToolExecutor(tmp_path)
|
|
928
|
+
inv = ToolInvocation(tool_name="__import__", arguments={})
|
|
929
|
+
result = executor.execute(inv)
|
|
930
|
+
assert result.success is False
|
|
931
|
+
|
|
932
|
+
def test_plugin_cannot_override_builtin(self, tmp_path):
|
|
933
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
934
|
+
|
|
935
|
+
executor = ToolExecutor(tmp_path)
|
|
936
|
+
with pytest.raises(ValueError):
|
|
937
|
+
executor.register_plugin_tool(
|
|
938
|
+
name="get_context",
|
|
939
|
+
description="override",
|
|
940
|
+
parameters={},
|
|
941
|
+
handler=lambda: {},
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
def test_argument_validation_enforced(self, tmp_path):
|
|
945
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
946
|
+
from semantic_code_intelligence.tools.protocol import ToolInvocation
|
|
947
|
+
|
|
948
|
+
executor = ToolExecutor(tmp_path)
|
|
949
|
+
# find_references requires symbol_name
|
|
950
|
+
inv = ToolInvocation(tool_name="find_references", arguments={})
|
|
951
|
+
result = executor.execute(inv)
|
|
952
|
+
assert result.success is False
|
|
953
|
+
assert result.error.error_code == "missing_required_arg"
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
# =========================================================================
|
|
957
|
+
# Module structure tests
|
|
958
|
+
# =========================================================================
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
class TestModuleStructure:
|
|
962
|
+
"""Tests for Phase 19 module structure."""
|
|
963
|
+
|
|
964
|
+
def test_tools_protocol_importable(self):
|
|
965
|
+
from semantic_code_intelligence.tools.protocol import (
|
|
966
|
+
ToolError,
|
|
967
|
+
ToolErrorCode,
|
|
968
|
+
ToolExecutionResult,
|
|
969
|
+
ToolInvocation,
|
|
970
|
+
)
|
|
971
|
+
assert ToolInvocation is not None
|
|
972
|
+
assert ToolExecutionResult is not None
|
|
973
|
+
assert ToolError is not None
|
|
974
|
+
assert ToolErrorCode is not None
|
|
975
|
+
|
|
976
|
+
def test_tools_executor_importable(self):
|
|
977
|
+
from semantic_code_intelligence.tools.executor import ToolExecutor
|
|
978
|
+
assert ToolExecutor is not None
|
|
979
|
+
|
|
980
|
+
def test_tool_cmd_importable(self):
|
|
981
|
+
from semantic_code_intelligence.cli.commands.tool_cmd import tool_cmd
|
|
982
|
+
assert tool_cmd is not None
|
|
983
|
+
|
|
984
|
+
def test_docs_function_importable(self):
|
|
985
|
+
from semantic_code_intelligence.docs import generate_ai_tool_protocol_reference
|
|
986
|
+
assert callable(generate_ai_tool_protocol_reference)
|