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,592 @@
|
|
|
1
|
+
"""Tests for LLM provider abstraction, reasoning engine, safety validator,
|
|
2
|
+
context memory, and CLI commands introduced in Phase 8.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import textwrap
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
from unittest.mock import MagicMock, patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# LLM Provider core types
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
from semantic_code_intelligence.llm.provider import (
|
|
20
|
+
LLMMessage,
|
|
21
|
+
LLMProvider,
|
|
22
|
+
LLMResponse,
|
|
23
|
+
MessageRole,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestLLMMessage:
|
|
28
|
+
def test_to_dict(self):
|
|
29
|
+
msg = LLMMessage(role=MessageRole.USER, content="hello")
|
|
30
|
+
assert msg.to_dict() == {"role": "user", "content": "hello"}
|
|
31
|
+
|
|
32
|
+
def test_roles(self):
|
|
33
|
+
assert MessageRole.SYSTEM.value == "system"
|
|
34
|
+
assert MessageRole.ASSISTANT.value == "assistant"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestLLMResponse:
|
|
38
|
+
def test_to_dict(self):
|
|
39
|
+
resp = LLMResponse(content="answer", model="gpt-4", provider="openai", usage={"total_tokens": 10})
|
|
40
|
+
d = resp.to_dict()
|
|
41
|
+
assert d["content"] == "answer"
|
|
42
|
+
assert d["model"] == "gpt-4"
|
|
43
|
+
assert d["provider"] == "openai"
|
|
44
|
+
assert d["usage"]["total_tokens"] == 10
|
|
45
|
+
|
|
46
|
+
def test_defaults(self):
|
|
47
|
+
resp = LLMResponse(content="hi")
|
|
48
|
+
assert resp.model == ""
|
|
49
|
+
assert resp.provider == ""
|
|
50
|
+
assert resp.usage == {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Mock Provider
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
from semantic_code_intelligence.llm.mock_provider import MockProvider
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestMockProvider:
|
|
60
|
+
def test_name(self):
|
|
61
|
+
p = MockProvider()
|
|
62
|
+
assert p.name == "mock"
|
|
63
|
+
|
|
64
|
+
def test_complete_default(self):
|
|
65
|
+
p = MockProvider(default_response="test response")
|
|
66
|
+
resp = p.complete("prompt")
|
|
67
|
+
assert resp.content == "test response"
|
|
68
|
+
assert resp.provider == "mock"
|
|
69
|
+
|
|
70
|
+
def test_chat(self):
|
|
71
|
+
p = MockProvider()
|
|
72
|
+
msgs = [LLMMessage(role=MessageRole.USER, content="hi")]
|
|
73
|
+
resp = p.chat(msgs)
|
|
74
|
+
assert resp.content == "This is a mock LLM response."
|
|
75
|
+
assert len(p.call_history) == 1
|
|
76
|
+
assert p.call_history[0]["method"] == "chat"
|
|
77
|
+
|
|
78
|
+
def test_enqueue_response(self):
|
|
79
|
+
p = MockProvider()
|
|
80
|
+
p.enqueue_response("first")
|
|
81
|
+
p.enqueue_response("second")
|
|
82
|
+
assert p.complete("a").content == "first"
|
|
83
|
+
assert p.complete("b").content == "second"
|
|
84
|
+
assert p.complete("c").content == "This is a mock LLM response."
|
|
85
|
+
|
|
86
|
+
def test_call_history(self):
|
|
87
|
+
p = MockProvider()
|
|
88
|
+
p.complete("p1")
|
|
89
|
+
p.chat([LLMMessage(role=MessageRole.USER, content="p2")])
|
|
90
|
+
assert len(p.call_history) == 2
|
|
91
|
+
|
|
92
|
+
def test_is_available(self):
|
|
93
|
+
# LLMProvider default is True; mock inherits
|
|
94
|
+
p = MockProvider()
|
|
95
|
+
assert p.is_available() is True
|
|
96
|
+
|
|
97
|
+
def test_usage_tokens(self):
|
|
98
|
+
p = MockProvider(default_response="short")
|
|
99
|
+
resp = p.complete("a longer prompt text here")
|
|
100
|
+
assert "prompt_tokens" in resp.usage
|
|
101
|
+
assert "completion_tokens" in resp.usage
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# OpenAI Provider (only initialization, no API calls)
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestOpenAIProvider:
|
|
111
|
+
def test_name(self):
|
|
112
|
+
p = OpenAIProvider(api_key="test-key")
|
|
113
|
+
assert p.name == "openai"
|
|
114
|
+
|
|
115
|
+
def test_is_available(self):
|
|
116
|
+
p = OpenAIProvider(api_key="key")
|
|
117
|
+
assert p.is_available() is True
|
|
118
|
+
|
|
119
|
+
def test_not_available_without_key(self):
|
|
120
|
+
p = OpenAIProvider(api_key="")
|
|
121
|
+
assert p.is_available() is False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Ollama Provider (initialization only)
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestOllamaProvider:
|
|
131
|
+
def test_name(self):
|
|
132
|
+
p = OllamaProvider()
|
|
133
|
+
assert p.name == "ollama"
|
|
134
|
+
|
|
135
|
+
def test_is_available_offline(self):
|
|
136
|
+
# Ollama is unlikely to be running in CI, so expect False
|
|
137
|
+
p = OllamaProvider(base_url="http://127.0.0.1:99999")
|
|
138
|
+
assert p.is_available() is False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Safety Validator
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
from semantic_code_intelligence.llm.safety import SafetyIssue, SafetyReport, SafetyValidator
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestSafetyValidator:
|
|
148
|
+
def test_safe_code(self):
|
|
149
|
+
v = SafetyValidator()
|
|
150
|
+
report = v.validate("x = 1 + 2\nprint(x)")
|
|
151
|
+
assert report.safe is True
|
|
152
|
+
assert len(report.issues) == 0
|
|
153
|
+
|
|
154
|
+
def test_detects_eval(self):
|
|
155
|
+
v = SafetyValidator()
|
|
156
|
+
report = v.validate("result = eval(user_input)")
|
|
157
|
+
assert report.safe is False
|
|
158
|
+
assert any("eval" in i.description for i in report.issues)
|
|
159
|
+
|
|
160
|
+
def test_detects_exec(self):
|
|
161
|
+
v = SafetyValidator()
|
|
162
|
+
report = v.validate("exec(code)")
|
|
163
|
+
assert report.safe is False
|
|
164
|
+
|
|
165
|
+
def test_detects_os_system(self):
|
|
166
|
+
v = SafetyValidator()
|
|
167
|
+
report = v.validate('os.system("rm -rf /")')
|
|
168
|
+
assert report.safe is False
|
|
169
|
+
|
|
170
|
+
def test_detects_shell_true(self):
|
|
171
|
+
v = SafetyValidator()
|
|
172
|
+
report = v.validate('subprocess.run(cmd, shell=True)')
|
|
173
|
+
assert report.safe is False
|
|
174
|
+
|
|
175
|
+
def test_detects_sql_drop(self):
|
|
176
|
+
v = SafetyValidator()
|
|
177
|
+
report = v.validate("DROP TABLE users;")
|
|
178
|
+
assert report.safe is False
|
|
179
|
+
|
|
180
|
+
def test_is_safe_shortcut(self):
|
|
181
|
+
v = SafetyValidator()
|
|
182
|
+
assert v.is_safe("x = 1") is True
|
|
183
|
+
assert v.is_safe("eval('code')") is False
|
|
184
|
+
|
|
185
|
+
def test_custom_patterns(self):
|
|
186
|
+
v = SafetyValidator(extra_patterns=[(r"DANGER", "custom danger pattern")])
|
|
187
|
+
report = v.validate("DANGER zone")
|
|
188
|
+
assert report.safe is False
|
|
189
|
+
assert report.issues[0].description == "custom danger pattern"
|
|
190
|
+
|
|
191
|
+
def test_report_to_dict(self):
|
|
192
|
+
report = SafetyReport(safe=False, issues=[
|
|
193
|
+
SafetyIssue(pattern="test", description="desc", line_number=5),
|
|
194
|
+
])
|
|
195
|
+
d = report.to_dict()
|
|
196
|
+
assert d["safe"] is False
|
|
197
|
+
assert d["issue_count"] == 1
|
|
198
|
+
assert d["issues"][0]["line_number"] == 5
|
|
199
|
+
|
|
200
|
+
def test_line_numbers(self):
|
|
201
|
+
v = SafetyValidator()
|
|
202
|
+
code = "x = 1\ny = 2\nresult = eval(z)"
|
|
203
|
+
report = v.validate(code)
|
|
204
|
+
assert report.issues[0].line_number == 3
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Context Memory
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
from semantic_code_intelligence.context.memory import (
|
|
211
|
+
MemoryEntry,
|
|
212
|
+
ReasoningStep,
|
|
213
|
+
SessionMemory,
|
|
214
|
+
WorkspaceMemory,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestMemoryEntry:
|
|
219
|
+
def test_to_dict_roundtrip(self):
|
|
220
|
+
entry = MemoryEntry(key="q1", content="answer", kind="qa", metadata={"score": 0.9})
|
|
221
|
+
d = entry.to_dict()
|
|
222
|
+
restored = MemoryEntry.from_dict(d)
|
|
223
|
+
assert restored.key == "q1"
|
|
224
|
+
assert restored.content == "answer"
|
|
225
|
+
assert restored.kind == "qa"
|
|
226
|
+
|
|
227
|
+
def test_defaults(self):
|
|
228
|
+
entry = MemoryEntry(key="k", content="c")
|
|
229
|
+
assert entry.kind == "general"
|
|
230
|
+
assert entry.timestamp > 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TestReasoningStep:
|
|
234
|
+
def test_to_dict(self):
|
|
235
|
+
step = ReasoningStep(step_id=1, action="search", input_text="query", output_text="results")
|
|
236
|
+
d = step.to_dict()
|
|
237
|
+
assert d["step_id"] == 1
|
|
238
|
+
assert d["action"] == "search"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestSessionMemory:
|
|
242
|
+
def test_add_and_search(self):
|
|
243
|
+
mem = SessionMemory()
|
|
244
|
+
mem.add("q1", "How does auth work?", kind="qa")
|
|
245
|
+
results = mem.search("auth")
|
|
246
|
+
assert len(results) == 1
|
|
247
|
+
assert results[0].key == "q1"
|
|
248
|
+
|
|
249
|
+
def test_get_recent(self):
|
|
250
|
+
mem = SessionMemory()
|
|
251
|
+
for i in range(15):
|
|
252
|
+
mem.add(f"key{i}", f"content{i}")
|
|
253
|
+
recent = mem.get_recent(5)
|
|
254
|
+
assert len(recent) == 5
|
|
255
|
+
assert recent[-1].key == "key14"
|
|
256
|
+
|
|
257
|
+
def test_max_entries(self):
|
|
258
|
+
mem = SessionMemory(max_entries=5)
|
|
259
|
+
for i in range(10):
|
|
260
|
+
mem.add(f"k{i}", f"c{i}")
|
|
261
|
+
assert len(mem.entries) == 5
|
|
262
|
+
|
|
263
|
+
def test_clear(self):
|
|
264
|
+
mem = SessionMemory()
|
|
265
|
+
mem.add("k", "v")
|
|
266
|
+
mem.start_chain("chain1")
|
|
267
|
+
mem.clear()
|
|
268
|
+
assert len(mem.entries) == 0
|
|
269
|
+
|
|
270
|
+
def test_reasoning_chain(self):
|
|
271
|
+
mem = SessionMemory()
|
|
272
|
+
mem.start_chain("c1")
|
|
273
|
+
mem.add_step("c1", "search", "query", "results")
|
|
274
|
+
mem.add_step("c1", "analyze", "results", "insights")
|
|
275
|
+
chain = mem.get_chain("c1")
|
|
276
|
+
assert len(chain) == 2
|
|
277
|
+
assert chain[0].step_id == 1
|
|
278
|
+
assert chain[1].action == "analyze"
|
|
279
|
+
|
|
280
|
+
def test_to_dict(self):
|
|
281
|
+
mem = SessionMemory()
|
|
282
|
+
mem.add("k", "v")
|
|
283
|
+
mem.start_chain("c1")
|
|
284
|
+
mem.add_step("c1", "search", "q", "r")
|
|
285
|
+
d = mem.to_dict()
|
|
286
|
+
assert "entries" in d
|
|
287
|
+
assert "chains" in d
|
|
288
|
+
assert len(d["chains"]["c1"]) == 1
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class TestWorkspaceMemory:
|
|
292
|
+
def test_add_and_get(self, tmp_path):
|
|
293
|
+
# Set up a .codexa dir structure
|
|
294
|
+
codex_dir = tmp_path / ".codexa"
|
|
295
|
+
codex_dir.mkdir()
|
|
296
|
+
with patch(
|
|
297
|
+
"semantic_code_intelligence.config.settings.AppConfig.config_dir",
|
|
298
|
+
return_value=codex_dir,
|
|
299
|
+
):
|
|
300
|
+
mem = WorkspaceMemory(tmp_path)
|
|
301
|
+
mem.add("test_key", "test_value", kind="insight")
|
|
302
|
+
entry = mem.get("test_key")
|
|
303
|
+
assert entry is not None
|
|
304
|
+
assert entry.content == "test_value"
|
|
305
|
+
|
|
306
|
+
def test_persistence(self, tmp_path):
|
|
307
|
+
codex_dir = tmp_path / ".codexa"
|
|
308
|
+
codex_dir.mkdir()
|
|
309
|
+
with patch(
|
|
310
|
+
"semantic_code_intelligence.config.settings.AppConfig.config_dir",
|
|
311
|
+
return_value=codex_dir,
|
|
312
|
+
):
|
|
313
|
+
mem1 = WorkspaceMemory(tmp_path)
|
|
314
|
+
mem1.add("k1", "v1")
|
|
315
|
+
|
|
316
|
+
# Create a new instance — should load from disk
|
|
317
|
+
mem2 = WorkspaceMemory(tmp_path)
|
|
318
|
+
entry = mem2.get("k1")
|
|
319
|
+
assert entry is not None
|
|
320
|
+
assert entry.content == "v1"
|
|
321
|
+
|
|
322
|
+
def test_remove(self, tmp_path):
|
|
323
|
+
codex_dir = tmp_path / ".codexa"
|
|
324
|
+
codex_dir.mkdir()
|
|
325
|
+
with patch(
|
|
326
|
+
"semantic_code_intelligence.config.settings.AppConfig.config_dir",
|
|
327
|
+
return_value=codex_dir,
|
|
328
|
+
):
|
|
329
|
+
mem = WorkspaceMemory(tmp_path)
|
|
330
|
+
mem.add("k", "v")
|
|
331
|
+
assert mem.remove("k") is True
|
|
332
|
+
assert mem.get("k") is None
|
|
333
|
+
|
|
334
|
+
def test_search(self, tmp_path):
|
|
335
|
+
codex_dir = tmp_path / ".codexa"
|
|
336
|
+
codex_dir.mkdir()
|
|
337
|
+
with patch(
|
|
338
|
+
"semantic_code_intelligence.config.settings.AppConfig.config_dir",
|
|
339
|
+
return_value=codex_dir,
|
|
340
|
+
):
|
|
341
|
+
mem = WorkspaceMemory(tmp_path)
|
|
342
|
+
mem.add("auth", "JWT token validation")
|
|
343
|
+
mem.add("db", "Database connection pooling")
|
|
344
|
+
results = mem.search("JWT")
|
|
345
|
+
assert len(results) == 1
|
|
346
|
+
|
|
347
|
+
def test_clear(self, tmp_path):
|
|
348
|
+
codex_dir = tmp_path / ".codexa"
|
|
349
|
+
codex_dir.mkdir()
|
|
350
|
+
with patch(
|
|
351
|
+
"semantic_code_intelligence.config.settings.AppConfig.config_dir",
|
|
352
|
+
return_value=codex_dir,
|
|
353
|
+
):
|
|
354
|
+
mem = WorkspaceMemory(tmp_path)
|
|
355
|
+
mem.add("k", "v")
|
|
356
|
+
mem.clear()
|
|
357
|
+
assert len(mem.entries) == 0
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Reasoning Engine
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
from semantic_code_intelligence.llm.reasoning import (
|
|
364
|
+
AskResult,
|
|
365
|
+
RefactorResult,
|
|
366
|
+
ReasoningEngine,
|
|
367
|
+
ReviewResult,
|
|
368
|
+
SuggestResult,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class TestAskResult:
|
|
373
|
+
def test_to_dict(self):
|
|
374
|
+
r = AskResult(question="q", answer="a", context_snippets=[{"file": "x.py"}])
|
|
375
|
+
d = r.to_dict()
|
|
376
|
+
assert d["question"] == "q"
|
|
377
|
+
assert d["answer"] == "a"
|
|
378
|
+
assert len(d["context_snippets"]) == 1
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class TestReviewResult:
|
|
382
|
+
def test_to_dict(self):
|
|
383
|
+
r = ReviewResult(file_path="x.py", summary="looks good", issues=[])
|
|
384
|
+
d = r.to_dict()
|
|
385
|
+
assert d["file_path"] == "x.py"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class TestRefactorResult:
|
|
389
|
+
def test_to_dict(self):
|
|
390
|
+
r = RefactorResult(file_path="x.py", explanation="improved")
|
|
391
|
+
d = r.to_dict()
|
|
392
|
+
assert d["explanation"] == "improved"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class TestSuggestResult:
|
|
396
|
+
def test_to_dict(self):
|
|
397
|
+
r = SuggestResult(target="func", suggestions=[{"title": "s1"}])
|
|
398
|
+
d = r.to_dict()
|
|
399
|
+
assert d["target"] == "func"
|
|
400
|
+
assert len(d["suggestions"]) == 1
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestReasoningEngine:
|
|
404
|
+
def _make_engine(self, tmp_path, mock_response="mock answer"):
|
|
405
|
+
"""Helper: create an engine with a mock provider and a dummy project."""
|
|
406
|
+
provider = MockProvider(default_response=mock_response)
|
|
407
|
+
# Create a minimal project structure
|
|
408
|
+
codex_dir = tmp_path / ".codexa"
|
|
409
|
+
codex_dir.mkdir(parents=True)
|
|
410
|
+
index_dir = codex_dir / "index"
|
|
411
|
+
index_dir.mkdir()
|
|
412
|
+
# Minimal config
|
|
413
|
+
config = {
|
|
414
|
+
"project_root": str(tmp_path),
|
|
415
|
+
"embedding": {},
|
|
416
|
+
"search": {},
|
|
417
|
+
"index": {"ignore_dirs": [], "extensions": [".py"]},
|
|
418
|
+
"llm": {"provider": "mock"},
|
|
419
|
+
}
|
|
420
|
+
(codex_dir / "config.json").write_text(json.dumps(config))
|
|
421
|
+
|
|
422
|
+
# Create a sample source file
|
|
423
|
+
src = tmp_path / "sample.py"
|
|
424
|
+
src.write_text("def hello():\n return 'world'\n")
|
|
425
|
+
|
|
426
|
+
engine = ReasoningEngine(provider, tmp_path)
|
|
427
|
+
return engine, provider
|
|
428
|
+
|
|
429
|
+
def test_ask(self, tmp_path):
|
|
430
|
+
engine, provider = self._make_engine(tmp_path, "The answer is 42.")
|
|
431
|
+
result = engine.ask("What is the meaning?")
|
|
432
|
+
assert result.answer == "The answer is 42."
|
|
433
|
+
assert result.question == "What is the meaning?"
|
|
434
|
+
|
|
435
|
+
def test_review(self, tmp_path):
|
|
436
|
+
engine, provider = self._make_engine(
|
|
437
|
+
tmp_path,
|
|
438
|
+
json.dumps({"issues": [{"severity": "warning", "line": 1, "message": "Missing docstring"}], "summary": "Needs docs"}),
|
|
439
|
+
)
|
|
440
|
+
src = tmp_path / "sample.py"
|
|
441
|
+
result = engine.review(str(src))
|
|
442
|
+
assert result.file_path == str(src)
|
|
443
|
+
assert len(result.issues) == 1
|
|
444
|
+
assert result.summary == "Needs docs"
|
|
445
|
+
|
|
446
|
+
def test_review_file_not_found(self, tmp_path):
|
|
447
|
+
engine, _ = self._make_engine(tmp_path)
|
|
448
|
+
result = engine.review(str(tmp_path / "nonexistent.py"))
|
|
449
|
+
assert "not found" in result.summary.lower() or "empty" in result.summary.lower()
|
|
450
|
+
|
|
451
|
+
def test_refactor(self, tmp_path):
|
|
452
|
+
engine, provider = self._make_engine(
|
|
453
|
+
tmp_path,
|
|
454
|
+
json.dumps({"refactored_code": "def hello():\n '''Say hello.'''\n return 'world'\n", "explanation": "Added docstring"}),
|
|
455
|
+
)
|
|
456
|
+
src = tmp_path / "sample.py"
|
|
457
|
+
result = engine.refactor(str(src), "Add docstrings")
|
|
458
|
+
assert result.refactored_code != ""
|
|
459
|
+
assert "docstring" in result.explanation.lower()
|
|
460
|
+
|
|
461
|
+
def test_suggest(self, tmp_path):
|
|
462
|
+
engine, provider = self._make_engine(
|
|
463
|
+
tmp_path,
|
|
464
|
+
json.dumps({"suggestions": [{"title": "Add type hints", "description": "Use type annotations", "reason": "Better IDE support", "priority": "medium"}]}),
|
|
465
|
+
)
|
|
466
|
+
result = engine.suggest("hello")
|
|
467
|
+
assert len(result.suggestions) == 1
|
|
468
|
+
assert result.suggestions[0]["title"] == "Add type hints"
|
|
469
|
+
|
|
470
|
+
def test_suggest_raw_fallback(self, tmp_path):
|
|
471
|
+
engine, _ = self._make_engine(tmp_path, "Just some plain text")
|
|
472
|
+
result = engine.suggest("hello")
|
|
473
|
+
assert len(result.suggestions) == 1
|
|
474
|
+
assert result.suggestions[0]["title"] == "Raw response"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ---------------------------------------------------------------------------
|
|
478
|
+
# LLMConfig in settings
|
|
479
|
+
# ---------------------------------------------------------------------------
|
|
480
|
+
from semantic_code_intelligence.config.settings import AppConfig, LLMConfig, load_config
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class TestLLMConfig:
|
|
484
|
+
def test_defaults(self):
|
|
485
|
+
cfg = LLMConfig()
|
|
486
|
+
assert cfg.provider == "mock"
|
|
487
|
+
assert cfg.model == "gpt-3.5-turbo"
|
|
488
|
+
assert cfg.temperature == 0.2
|
|
489
|
+
assert cfg.max_tokens == 2048
|
|
490
|
+
|
|
491
|
+
def test_app_config_has_llm(self):
|
|
492
|
+
app = AppConfig()
|
|
493
|
+
assert isinstance(app.llm, LLMConfig)
|
|
494
|
+
assert app.llm.provider == "mock"
|
|
495
|
+
|
|
496
|
+
def test_serialisation_roundtrip(self, tmp_path):
|
|
497
|
+
from semantic_code_intelligence.config.settings import save_config
|
|
498
|
+
|
|
499
|
+
app = AppConfig(project_root=str(tmp_path))
|
|
500
|
+
app.llm.provider = "openai"
|
|
501
|
+
app.llm.model = "gpt-4"
|
|
502
|
+
|
|
503
|
+
codex_dir = tmp_path / ".codexa"
|
|
504
|
+
codex_dir.mkdir()
|
|
505
|
+
save_config(app, tmp_path)
|
|
506
|
+
|
|
507
|
+
loaded = load_config(tmp_path)
|
|
508
|
+
assert loaded.llm.provider == "openai"
|
|
509
|
+
assert loaded.llm.model == "gpt-4"
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
# Plugin AI hooks
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
from semantic_code_intelligence.plugins import PluginHook
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class TestPluginAIHooks:
|
|
519
|
+
def test_pre_ai_hook_exists(self):
|
|
520
|
+
assert PluginHook.PRE_AI.value == "pre_ai"
|
|
521
|
+
|
|
522
|
+
def test_post_ai_hook_exists(self):
|
|
523
|
+
assert PluginHook.POST_AI.value == "post_ai"
|
|
524
|
+
|
|
525
|
+
def test_all_hooks_count(self):
|
|
526
|
+
# Expect 11 hooks: 3 indexing + 2 search + 2 analysis + 2 AI + 1 file + 1 custom
|
|
527
|
+
assert len(PluginHook) == 22
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ---------------------------------------------------------------------------
|
|
531
|
+
# CLI commands (smoke tests via Click testing)
|
|
532
|
+
# ---------------------------------------------------------------------------
|
|
533
|
+
from click.testing import CliRunner
|
|
534
|
+
|
|
535
|
+
from semantic_code_intelligence.cli.main import cli
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class TestCLICommands:
|
|
539
|
+
"""Smoke tests to verify the 4 new commands are registered and invocable."""
|
|
540
|
+
|
|
541
|
+
def test_ask_help(self):
|
|
542
|
+
runner = CliRunner()
|
|
543
|
+
result = runner.invoke(cli, ["ask", "--help"])
|
|
544
|
+
assert result.exit_code == 0
|
|
545
|
+
assert "Ask a natural-language question" in result.output
|
|
546
|
+
|
|
547
|
+
def test_review_help(self):
|
|
548
|
+
runner = CliRunner()
|
|
549
|
+
result = runner.invoke(cli, ["review", "--help"])
|
|
550
|
+
assert result.exit_code == 0
|
|
551
|
+
assert "Review a source file" in result.output
|
|
552
|
+
|
|
553
|
+
def test_refactor_help(self):
|
|
554
|
+
runner = CliRunner()
|
|
555
|
+
result = runner.invoke(cli, ["refactor", "--help"])
|
|
556
|
+
assert result.exit_code == 0
|
|
557
|
+
assert "Suggest refactored code" in result.output
|
|
558
|
+
|
|
559
|
+
def test_suggest_help(self):
|
|
560
|
+
runner = CliRunner()
|
|
561
|
+
result = runner.invoke(cli, ["suggest", "--help"])
|
|
562
|
+
assert result.exit_code == 0
|
|
563
|
+
assert "intelligent suggestions" in result.output.lower() or "suggestions" in result.output.lower()
|
|
564
|
+
|
|
565
|
+
def test_total_commands(self):
|
|
566
|
+
"""Ensure 11 commands are registered (7 original + 4 new)."""
|
|
567
|
+
runner = CliRunner()
|
|
568
|
+
result = runner.invoke(cli, ["--help"])
|
|
569
|
+
assert result.exit_code == 0
|
|
570
|
+
# Count command names listed in help
|
|
571
|
+
commands = [
|
|
572
|
+
"init", "index", "search", "explain", "summary", "watch", "deps",
|
|
573
|
+
"ask", "review", "refactor", "suggest",
|
|
574
|
+
]
|
|
575
|
+
for cmd in commands:
|
|
576
|
+
assert cmd in result.output, f"Command '{cmd}' not found in help output"
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
# Router test (updated count)
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class TestRouterPhase8:
|
|
586
|
+
def test_register_commands_count(self):
|
|
587
|
+
"""Router should register exactly 17 commands."""
|
|
588
|
+
import click
|
|
589
|
+
|
|
590
|
+
group = click.Group()
|
|
591
|
+
register_commands(group)
|
|
592
|
+
assert len(group.commands) == 39
|