code-graph-builder 0.2.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.
- code_graph_builder/__init__.py +82 -0
- code_graph_builder/builder.py +366 -0
- code_graph_builder/cgb_cli.py +32 -0
- code_graph_builder/cli.py +564 -0
- code_graph_builder/commands_cli.py +1288 -0
- code_graph_builder/config.py +340 -0
- code_graph_builder/constants.py +708 -0
- code_graph_builder/embeddings/__init__.py +40 -0
- code_graph_builder/embeddings/qwen3_embedder.py +573 -0
- code_graph_builder/embeddings/vector_store.py +584 -0
- code_graph_builder/examples/__init__.py +0 -0
- code_graph_builder/examples/example_configuration.py +276 -0
- code_graph_builder/examples/example_kuzu_usage.py +109 -0
- code_graph_builder/examples/example_semantic_search_full.py +347 -0
- code_graph_builder/examples/generate_wiki.py +915 -0
- code_graph_builder/examples/graph_export_example.py +100 -0
- code_graph_builder/examples/rag_example.py +206 -0
- code_graph_builder/examples/test_cli_demo.py +129 -0
- code_graph_builder/examples/test_embedding_api.py +153 -0
- code_graph_builder/examples/test_kuzu_local.py +190 -0
- code_graph_builder/examples/test_rag_redis.py +390 -0
- code_graph_builder/graph_updater.py +605 -0
- code_graph_builder/guidance/__init__.py +1 -0
- code_graph_builder/guidance/agent.py +123 -0
- code_graph_builder/guidance/prompts.py +74 -0
- code_graph_builder/guidance/toolset.py +264 -0
- code_graph_builder/language_spec.py +536 -0
- code_graph_builder/mcp/__init__.py +21 -0
- code_graph_builder/mcp/api_doc_generator.py +764 -0
- code_graph_builder/mcp/file_editor.py +207 -0
- code_graph_builder/mcp/pipeline.py +777 -0
- code_graph_builder/mcp/server.py +161 -0
- code_graph_builder/mcp/tools.py +1800 -0
- code_graph_builder/models.py +115 -0
- code_graph_builder/parser_loader.py +344 -0
- code_graph_builder/parsers/__init__.py +7 -0
- code_graph_builder/parsers/call_processor.py +306 -0
- code_graph_builder/parsers/call_resolver.py +139 -0
- code_graph_builder/parsers/definition_processor.py +796 -0
- code_graph_builder/parsers/factory.py +119 -0
- code_graph_builder/parsers/import_processor.py +293 -0
- code_graph_builder/parsers/structure_processor.py +145 -0
- code_graph_builder/parsers/type_inference.py +143 -0
- code_graph_builder/parsers/utils.py +134 -0
- code_graph_builder/rag/__init__.py +68 -0
- code_graph_builder/rag/camel_agent.py +429 -0
- code_graph_builder/rag/client.py +298 -0
- code_graph_builder/rag/config.py +239 -0
- code_graph_builder/rag/cypher_generator.py +67 -0
- code_graph_builder/rag/llm_backend.py +210 -0
- code_graph_builder/rag/markdown_generator.py +352 -0
- code_graph_builder/rag/prompt_templates.py +440 -0
- code_graph_builder/rag/rag_engine.py +640 -0
- code_graph_builder/rag/review_report.md +172 -0
- code_graph_builder/rag/tests/__init__.py +3 -0
- code_graph_builder/rag/tests/test_camel_agent.py +313 -0
- code_graph_builder/rag/tests/test_client.py +221 -0
- code_graph_builder/rag/tests/test_config.py +177 -0
- code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
- code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
- code_graph_builder/services/__init__.py +39 -0
- code_graph_builder/services/graph_service.py +465 -0
- code_graph_builder/services/kuzu_service.py +665 -0
- code_graph_builder/services/memory_service.py +171 -0
- code_graph_builder/settings.py +75 -0
- code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
- code_graph_builder/tests/__init__.py +1 -0
- code_graph_builder/tests/run_acceptance_check.py +378 -0
- code_graph_builder/tests/test_api_find.py +231 -0
- code_graph_builder/tests/test_api_find_integration.py +226 -0
- code_graph_builder/tests/test_basic.py +78 -0
- code_graph_builder/tests/test_c_api_extraction.py +388 -0
- code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
- code_graph_builder/tests/test_embedder.py +411 -0
- code_graph_builder/tests/test_integration_semantic.py +434 -0
- code_graph_builder/tests/test_mcp_protocol.py +298 -0
- code_graph_builder/tests/test_mcp_user_flow.py +190 -0
- code_graph_builder/tests/test_rag.py +404 -0
- code_graph_builder/tests/test_settings.py +135 -0
- code_graph_builder/tests/test_step1_graph_build.py +264 -0
- code_graph_builder/tests/test_step2_api_docs.py +323 -0
- code_graph_builder/tests/test_step3_embedding.py +278 -0
- code_graph_builder/tests/test_vector_store.py +552 -0
- code_graph_builder/tools/__init__.py +40 -0
- code_graph_builder/tools/graph_query.py +495 -0
- code_graph_builder/tools/semantic_search.py +387 -0
- code_graph_builder/types.py +333 -0
- code_graph_builder/utils/__init__.py +0 -0
- code_graph_builder/utils/path_utils.py +30 -0
- code_graph_builder-0.2.0.dist-info/METADATA +321 -0
- code_graph_builder-0.2.0.dist-info/RECORD +93 -0
- code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
- code_graph_builder-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Tests for RAG module.
|
|
2
|
+
|
|
3
|
+
Tests the RAG engine, CAMEL agent integration, and end-to-end workflows.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from ..rag import RAGConfig, create_rag_engine
|
|
14
|
+
from ..rag.camel_agent import CamelAgent, create_camel_agent
|
|
15
|
+
from ..rag.config import MoonshotConfig, RetrievalConfig
|
|
16
|
+
from ..rag.client import ChatResponse, LLMClient, create_llm_client
|
|
17
|
+
from ..rag.markdown_generator import AnalysisResult, MarkdownGenerator, SourceReference
|
|
18
|
+
from ..rag.prompt_templates import CodeAnalysisPrompts, CodeContext, RAGPrompts
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Fixtures
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def mock_api_key() -> str:
|
|
28
|
+
"""Mock API key for testing."""
|
|
29
|
+
return "sk-test-mock-key-12345"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def moonshot_config(mock_api_key: str) -> MoonshotConfig:
|
|
34
|
+
"""Create Moonshot config for testing."""
|
|
35
|
+
return MoonshotConfig(
|
|
36
|
+
api_key=mock_api_key,
|
|
37
|
+
model="kimi-k2.5",
|
|
38
|
+
max_tokens=1024,
|
|
39
|
+
temperature=0.5,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def rag_config(mock_api_key: str) -> RAGConfig:
|
|
45
|
+
"""Create RAG config for testing."""
|
|
46
|
+
return RAGConfig(
|
|
47
|
+
moonshot=MoonshotConfig(api_key=mock_api_key),
|
|
48
|
+
retrieval=RetrievalConfig(semantic_top_k=5),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def sample_code_context() -> CodeContext:
|
|
54
|
+
"""Create sample code context."""
|
|
55
|
+
return CodeContext(
|
|
56
|
+
source_code="def add(a, b):\n return a + b",
|
|
57
|
+
file_path="src/math.py",
|
|
58
|
+
qualified_name="math.add",
|
|
59
|
+
entity_type="Function",
|
|
60
|
+
docstring="Add two numbers.",
|
|
61
|
+
callers=["math.calculate"],
|
|
62
|
+
callees=[],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def mock_kimi_response() -> ChatResponse:
|
|
68
|
+
"""Create mock Kimi response."""
|
|
69
|
+
return ChatResponse(
|
|
70
|
+
content="This function adds two numbers together.",
|
|
71
|
+
usage={"prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70},
|
|
72
|
+
model="kimi-k2.5",
|
|
73
|
+
finish_reason="stop",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# Config Tests
|
|
79
|
+
# =============================================================================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestMoonshotConfig:
|
|
83
|
+
"""Test MoonshotConfig."""
|
|
84
|
+
|
|
85
|
+
def test_config_creation(self, mock_api_key: str) -> None:
|
|
86
|
+
"""Test creating config."""
|
|
87
|
+
config = MoonshotConfig(api_key=mock_api_key)
|
|
88
|
+
assert config.api_key == mock_api_key
|
|
89
|
+
assert config.model == "kimi-k2.5"
|
|
90
|
+
assert config.base_url == "https://api.moonshot.cn/v1"
|
|
91
|
+
|
|
92
|
+
def test_config_from_env(self, monkeypatch) -> None:
|
|
93
|
+
"""Test loading config from environment."""
|
|
94
|
+
monkeypatch.setenv("MOONSHOT_API_KEY", "sk-env-key")
|
|
95
|
+
monkeypatch.setenv("MOONSHOT_MODEL", "kimi-k2-turbo")
|
|
96
|
+
|
|
97
|
+
config = MoonshotConfig()
|
|
98
|
+
assert config.api_key == "sk-env-key"
|
|
99
|
+
# Note: model defaults to kimi-k2.5 unless explicitly provided to __init__
|
|
100
|
+
# The environment variable is only used by RAGConfig.from_env()
|
|
101
|
+
|
|
102
|
+
def test_config_validation(self) -> None:
|
|
103
|
+
"""Test config validation."""
|
|
104
|
+
config = MoonshotConfig(api_key="sk-valid")
|
|
105
|
+
config.validate() # Should not raise
|
|
106
|
+
|
|
107
|
+
with pytest.raises(ValueError, match="API key is required"):
|
|
108
|
+
invalid_config = MoonshotConfig(api_key=None)
|
|
109
|
+
invalid_config.validate()
|
|
110
|
+
|
|
111
|
+
with pytest.raises(ValueError, match="format is invalid"):
|
|
112
|
+
invalid_config = MoonshotConfig(api_key="invalid")
|
|
113
|
+
invalid_config.validate()
|
|
114
|
+
|
|
115
|
+
def test_config_to_dict(self, mock_api_key: str) -> None:
|
|
116
|
+
"""Test config serialization."""
|
|
117
|
+
config = MoonshotConfig(api_key=mock_api_key)
|
|
118
|
+
data = config.to_dict()
|
|
119
|
+
assert data["api_key"] == mock_api_key
|
|
120
|
+
assert data["model"] == "kimi-k2.5"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestRAGConfig:
|
|
124
|
+
"""Test RAGConfig."""
|
|
125
|
+
|
|
126
|
+
def test_config_creation(self, mock_api_key: str) -> None:
|
|
127
|
+
"""Test creating RAG config."""
|
|
128
|
+
config = RAGConfig(
|
|
129
|
+
moonshot=MoonshotConfig(api_key=mock_api_key),
|
|
130
|
+
)
|
|
131
|
+
assert config.moonshot.api_key == mock_api_key
|
|
132
|
+
assert config.retrieval.semantic_top_k == 10
|
|
133
|
+
|
|
134
|
+
def test_config_from_env(self, monkeypatch) -> None:
|
|
135
|
+
"""Test loading RAG config from environment."""
|
|
136
|
+
monkeypatch.setenv("MOONSHOT_API_KEY", "sk-env-key")
|
|
137
|
+
monkeypatch.setenv("RAG_SEMANTIC_TOP_K", "15")
|
|
138
|
+
monkeypatch.setenv("RAG_VERBOSE", "true")
|
|
139
|
+
|
|
140
|
+
config = RAGConfig.from_env()
|
|
141
|
+
assert config.moonshot.api_key == "sk-env-key"
|
|
142
|
+
assert config.retrieval.semantic_top_k == 15
|
|
143
|
+
assert config.verbose is True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# LLMClient Tests
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestLLMClient:
|
|
152
|
+
"""Test LLMClient."""
|
|
153
|
+
|
|
154
|
+
def test_client_creation(self, mock_api_key: str) -> None:
|
|
155
|
+
"""Test creating client."""
|
|
156
|
+
client = LLMClient(api_key=mock_api_key)
|
|
157
|
+
assert client.api_key == mock_api_key
|
|
158
|
+
assert client.model == "kimi-k2.5"
|
|
159
|
+
|
|
160
|
+
def test_client_missing_api_key(self) -> None:
|
|
161
|
+
"""Test client creation without API key."""
|
|
162
|
+
with pytest.raises(ValueError, match="API key is required"):
|
|
163
|
+
LLMClient(api_key=None)
|
|
164
|
+
|
|
165
|
+
@patch("requests.post")
|
|
166
|
+
def test_chat_request(self, mock_post, mock_api_key: str) -> None:
|
|
167
|
+
"""Test chat request."""
|
|
168
|
+
mock_post.return_value.json.return_value = {
|
|
169
|
+
"choices": [{"message": {"content": "Hello"}, "finish_reason": "stop"}],
|
|
170
|
+
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
|
|
171
|
+
"model": "kimi-k2.5",
|
|
172
|
+
}
|
|
173
|
+
mock_post.return_value.raise_for_status = MagicMock()
|
|
174
|
+
|
|
175
|
+
client = LLMClient(api_key=mock_api_key)
|
|
176
|
+
response = client.chat(query="Hello")
|
|
177
|
+
|
|
178
|
+
assert response.content == "Hello"
|
|
179
|
+
assert response.model == "kimi-k2.5"
|
|
180
|
+
mock_post.assert_called_once()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# =============================================================================
|
|
184
|
+
# Prompt Templates Tests
|
|
185
|
+
# =============================================================================
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestCodeContext:
|
|
189
|
+
"""Test CodeContext."""
|
|
190
|
+
|
|
191
|
+
def test_context_creation(self) -> None:
|
|
192
|
+
"""Test creating context."""
|
|
193
|
+
context = CodeContext(
|
|
194
|
+
source_code="def foo(): pass",
|
|
195
|
+
file_path="test.py",
|
|
196
|
+
qualified_name="test.foo",
|
|
197
|
+
)
|
|
198
|
+
assert context.source_code == "def foo(): pass"
|
|
199
|
+
assert context.file_path == "test.py"
|
|
200
|
+
|
|
201
|
+
def test_context_formatting(self, sample_code_context: CodeContext) -> None:
|
|
202
|
+
"""Test context formatting."""
|
|
203
|
+
formatted = sample_code_context.format_context()
|
|
204
|
+
assert "Entity: math.add" in formatted
|
|
205
|
+
assert "Type: Function" in formatted
|
|
206
|
+
assert "def add(a, b):" in formatted
|
|
207
|
+
assert "Called By:" in formatted
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestCodeAnalysisPrompts:
|
|
211
|
+
"""Test CodeAnalysisPrompts."""
|
|
212
|
+
|
|
213
|
+
def test_system_prompt(self) -> None:
|
|
214
|
+
"""Test getting system prompt."""
|
|
215
|
+
prompts = CodeAnalysisPrompts()
|
|
216
|
+
system = prompts.get_system_prompt()
|
|
217
|
+
assert "expert code analyst" in system.lower()
|
|
218
|
+
|
|
219
|
+
def test_format_explain_prompt(self, sample_code_context: CodeContext) -> None:
|
|
220
|
+
"""Test formatting explain prompt."""
|
|
221
|
+
prompts = CodeAnalysisPrompts()
|
|
222
|
+
prompt = prompts.format_explain_prompt(sample_code_context)
|
|
223
|
+
assert "explain the following code" in prompt.lower()
|
|
224
|
+
assert "def add(a, b):" in prompt
|
|
225
|
+
|
|
226
|
+
def test_format_query_prompt(self, sample_code_context: CodeContext) -> None:
|
|
227
|
+
"""Test formatting query prompt."""
|
|
228
|
+
prompts = CodeAnalysisPrompts()
|
|
229
|
+
prompt = prompts.format_query_prompt(
|
|
230
|
+
query="What does this do?",
|
|
231
|
+
context=sample_code_context,
|
|
232
|
+
)
|
|
233
|
+
assert "What does this do?" in prompt
|
|
234
|
+
assert "def add(a, b):" in prompt
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class TestRAGPrompts:
|
|
238
|
+
"""Test RAGPrompts."""
|
|
239
|
+
|
|
240
|
+
def test_format_rag_query(self, sample_code_context: CodeContext) -> None:
|
|
241
|
+
"""Test formatting RAG query."""
|
|
242
|
+
prompts = RAGPrompts()
|
|
243
|
+
system, user = prompts.format_rag_query(
|
|
244
|
+
query="Explain this function",
|
|
245
|
+
contexts=[sample_code_context],
|
|
246
|
+
)
|
|
247
|
+
assert "expert code analyst" in system.lower()
|
|
248
|
+
assert "Explain this function" in user
|
|
249
|
+
assert "math.add" in user
|
|
250
|
+
|
|
251
|
+
def test_format_rag_query_no_results(self) -> None:
|
|
252
|
+
"""Test formatting RAG query with no results."""
|
|
253
|
+
prompts = RAGPrompts()
|
|
254
|
+
system, user = prompts.format_rag_query(
|
|
255
|
+
query="Explain this",
|
|
256
|
+
contexts=[],
|
|
257
|
+
)
|
|
258
|
+
assert "No relevant code" in user
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# =============================================================================
|
|
262
|
+
# Markdown Generator Tests
|
|
263
|
+
# =============================================================================
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class TestMarkdownGenerator:
|
|
267
|
+
"""Test MarkdownGenerator."""
|
|
268
|
+
|
|
269
|
+
def test_generate_analysis_doc(self) -> None:
|
|
270
|
+
"""Test generating analysis document."""
|
|
271
|
+
generator = MarkdownGenerator()
|
|
272
|
+
result = AnalysisResult(
|
|
273
|
+
query="What is this?",
|
|
274
|
+
response="This is a test.",
|
|
275
|
+
sources=[SourceReference(
|
|
276
|
+
name="test",
|
|
277
|
+
qualified_name="module.test",
|
|
278
|
+
file_path="test.py",
|
|
279
|
+
)],
|
|
280
|
+
)
|
|
281
|
+
markdown = generator.generate_analysis_doc("Test Analysis", result)
|
|
282
|
+
assert "# Test Analysis" in markdown
|
|
283
|
+
assert "What is this?" in markdown
|
|
284
|
+
assert "This is a test." in markdown
|
|
285
|
+
assert "module.test" in markdown
|
|
286
|
+
|
|
287
|
+
def test_generate_code_documentation(
|
|
288
|
+
self,
|
|
289
|
+
sample_code_context: CodeContext,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Test generating code documentation."""
|
|
292
|
+
generator = MarkdownGenerator()
|
|
293
|
+
analysis = "This function adds numbers."
|
|
294
|
+
markdown = generator.generate_code_documentation(
|
|
295
|
+
sample_code_context,
|
|
296
|
+
analysis,
|
|
297
|
+
)
|
|
298
|
+
assert "# math.add" in markdown
|
|
299
|
+
assert "This function adds numbers." in markdown
|
|
300
|
+
assert "def add(a, b):" in markdown
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# =============================================================================
|
|
304
|
+
# CamelRAGAgent Tests
|
|
305
|
+
# =============================================================================
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestCamelAgent:
|
|
309
|
+
"""Test CamelAgent."""
|
|
310
|
+
|
|
311
|
+
def test_agent_creation(self) -> None:
|
|
312
|
+
"""Test creating agent."""
|
|
313
|
+
with patch.object(LLMClient, "__init__", return_value=None):
|
|
314
|
+
agent = CamelAgent(
|
|
315
|
+
role="Code Analyst",
|
|
316
|
+
goal="Analyze code",
|
|
317
|
+
backstory="Expert programmer",
|
|
318
|
+
)
|
|
319
|
+
assert agent.role == "Code Analyst"
|
|
320
|
+
assert agent.goal == "Analyze code"
|
|
321
|
+
|
|
322
|
+
@patch.object(LLMClient, "chat_with_messages")
|
|
323
|
+
def test_analyze(self, mock_chat) -> None:
|
|
324
|
+
"""Test code analysis."""
|
|
325
|
+
mock_chat.return_value = ChatResponse(
|
|
326
|
+
content="This adds two numbers.",
|
|
327
|
+
usage={},
|
|
328
|
+
model="kimi-k2.5",
|
|
329
|
+
finish_reason="stop",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
agent = CamelAgent(
|
|
333
|
+
role="Code Analyst",
|
|
334
|
+
goal="Analyze code",
|
|
335
|
+
backstory="Expert programmer",
|
|
336
|
+
llm_client=LLMClient(api_key="sk-test"),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
response = agent.analyze("Explain this function", code="def add(a, b): return a + b")
|
|
340
|
+
assert "adds two numbers" in response.content
|
|
341
|
+
|
|
342
|
+
def test_explain_code(self) -> None:
|
|
343
|
+
"""Test code explanation."""
|
|
344
|
+
with patch.object(LLMClient, "chat_with_messages") as mock_chat:
|
|
345
|
+
mock_chat.return_value = ChatResponse(
|
|
346
|
+
content="This function adds two numbers.",
|
|
347
|
+
usage={},
|
|
348
|
+
model="kimi-k2.5",
|
|
349
|
+
finish_reason="stop",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
agent = CamelAgent(
|
|
353
|
+
role="Code Analyst",
|
|
354
|
+
goal="Analyze code",
|
|
355
|
+
backstory="Expert programmer",
|
|
356
|
+
llm_client=LLMClient(api_key="sk-test"),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
response = agent.explain_code("def add(a, b): return a + b")
|
|
360
|
+
assert "adds two numbers" in response.content
|
|
361
|
+
|
|
362
|
+
def test_review_code(self) -> None:
|
|
363
|
+
"""Test code review."""
|
|
364
|
+
with patch.object(LLMClient, "chat_with_messages") as mock_chat:
|
|
365
|
+
mock_chat.return_value = ChatResponse(
|
|
366
|
+
content="Code looks good.",
|
|
367
|
+
usage={},
|
|
368
|
+
model="kimi-k2.5",
|
|
369
|
+
finish_reason="stop",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
agent = CamelAgent(
|
|
373
|
+
role="Code Reviewer",
|
|
374
|
+
goal="Review code",
|
|
375
|
+
backstory="Senior engineer",
|
|
376
|
+
llm_client=LLMClient(api_key="sk-test"),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
response = agent.review_code("def foo(): pass", review_type="general")
|
|
380
|
+
assert "good" in response.content.lower() or "Error" not in response.content
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# =============================================================================
|
|
384
|
+
# Factory Function Tests
|
|
385
|
+
# =============================================================================
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_create_llm_client(mock_api_key: str) -> None:
|
|
389
|
+
"""Test create_llm_client factory."""
|
|
390
|
+
with patch.object(LLMClient, "__init__", return_value=None):
|
|
391
|
+
client = create_llm_client(api_key=mock_api_key)
|
|
392
|
+
assert isinstance(client, LLMClient)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_create_camel_agent() -> None:
|
|
396
|
+
"""Test create_camel_agent factory."""
|
|
397
|
+
with patch.object(CamelAgent, "__init__", return_value=None) as mock_init:
|
|
398
|
+
mock_init.return_value = None
|
|
399
|
+
agent = create_camel_agent(
|
|
400
|
+
role="Analyst",
|
|
401
|
+
goal="Analyze",
|
|
402
|
+
backstory="Expert",
|
|
403
|
+
)
|
|
404
|
+
assert isinstance(agent, CamelAgent)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Tests for ~/.claude/settings.json loader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from code_graph_builder.settings import load_settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestLoadSettings:
|
|
15
|
+
"""Test load_settings with various file states."""
|
|
16
|
+
|
|
17
|
+
def test_missing_file_returns_empty(self, tmp_path: Path):
|
|
18
|
+
result = load_settings(tmp_path / "nonexistent.json")
|
|
19
|
+
assert result == {}
|
|
20
|
+
|
|
21
|
+
def test_valid_env_block_injects_vars(self, tmp_path: Path, monkeypatch):
|
|
22
|
+
settings_file = tmp_path / "settings.json"
|
|
23
|
+
settings_file.write_text(json.dumps({
|
|
24
|
+
"env": {
|
|
25
|
+
"LLM_API_KEY": "sk-test-key",
|
|
26
|
+
"LLM_BASE_URL": "https://test.example.com/v1",
|
|
27
|
+
}
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
# Ensure these are not in the environment
|
|
31
|
+
monkeypatch.delenv("LLM_API_KEY", raising=False)
|
|
32
|
+
monkeypatch.delenv("LLM_BASE_URL", raising=False)
|
|
33
|
+
|
|
34
|
+
result = load_settings(settings_file)
|
|
35
|
+
|
|
36
|
+
assert result["env"]["LLM_API_KEY"] == "sk-test-key"
|
|
37
|
+
assert os.environ["LLM_API_KEY"] == "sk-test-key"
|
|
38
|
+
assert os.environ["LLM_BASE_URL"] == "https://test.example.com/v1"
|
|
39
|
+
|
|
40
|
+
def test_env_var_takes_precedence(self, tmp_path: Path, monkeypatch):
|
|
41
|
+
settings_file = tmp_path / "settings.json"
|
|
42
|
+
settings_file.write_text(json.dumps({
|
|
43
|
+
"env": {
|
|
44
|
+
"LLM_API_KEY": "sk-from-settings",
|
|
45
|
+
}
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
# Pre-set the env var — should NOT be overwritten
|
|
49
|
+
monkeypatch.setenv("LLM_API_KEY", "sk-from-env")
|
|
50
|
+
|
|
51
|
+
load_settings(settings_file)
|
|
52
|
+
|
|
53
|
+
assert os.environ["LLM_API_KEY"] == "sk-from-env"
|
|
54
|
+
|
|
55
|
+
def test_malformed_json_returns_empty(self, tmp_path: Path):
|
|
56
|
+
settings_file = tmp_path / "settings.json"
|
|
57
|
+
settings_file.write_text("{ not valid json }")
|
|
58
|
+
|
|
59
|
+
result = load_settings(settings_file)
|
|
60
|
+
assert result == {}
|
|
61
|
+
|
|
62
|
+
def test_non_dict_root_returns_empty(self, tmp_path: Path):
|
|
63
|
+
settings_file = tmp_path / "settings.json"
|
|
64
|
+
settings_file.write_text(json.dumps([1, 2, 3]))
|
|
65
|
+
|
|
66
|
+
result = load_settings(settings_file)
|
|
67
|
+
assert result == {}
|
|
68
|
+
|
|
69
|
+
def test_no_env_block_is_fine(self, tmp_path: Path):
|
|
70
|
+
settings_file = tmp_path / "settings.json"
|
|
71
|
+
settings_file.write_text(json.dumps({"other_key": "value"}))
|
|
72
|
+
|
|
73
|
+
result = load_settings(settings_file)
|
|
74
|
+
assert result == {"other_key": "value"}
|
|
75
|
+
|
|
76
|
+
def test_non_string_values_skipped(self, tmp_path: Path, monkeypatch):
|
|
77
|
+
settings_file = tmp_path / "settings.json"
|
|
78
|
+
settings_file.write_text(json.dumps({
|
|
79
|
+
"env": {
|
|
80
|
+
"GOOD_KEY": "good-value",
|
|
81
|
+
"BAD_KEY": 12345,
|
|
82
|
+
"ALSO_BAD": None,
|
|
83
|
+
}
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
monkeypatch.delenv("GOOD_KEY", raising=False)
|
|
87
|
+
monkeypatch.delenv("BAD_KEY", raising=False)
|
|
88
|
+
monkeypatch.delenv("ALSO_BAD", raising=False)
|
|
89
|
+
|
|
90
|
+
load_settings(settings_file)
|
|
91
|
+
|
|
92
|
+
assert os.environ.get("GOOD_KEY") == "good-value"
|
|
93
|
+
assert "BAD_KEY" not in os.environ
|
|
94
|
+
assert "ALSO_BAD" not in os.environ
|
|
95
|
+
|
|
96
|
+
def test_embedding_config(self, tmp_path: Path, monkeypatch):
|
|
97
|
+
settings_file = tmp_path / "settings.json"
|
|
98
|
+
settings_file.write_text(json.dumps({
|
|
99
|
+
"env": {
|
|
100
|
+
"DASHSCOPE_API_KEY": "sk-dash-test",
|
|
101
|
+
"DASHSCOPE_BASE_URL": "https://custom.dashscope.com/api/v1",
|
|
102
|
+
}
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False)
|
|
106
|
+
monkeypatch.delenv("DASHSCOPE_BASE_URL", raising=False)
|
|
107
|
+
|
|
108
|
+
load_settings(settings_file)
|
|
109
|
+
|
|
110
|
+
assert os.environ["DASHSCOPE_API_KEY"] == "sk-dash-test"
|
|
111
|
+
assert os.environ["DASHSCOPE_BASE_URL"] == "https://custom.dashscope.com/api/v1"
|
|
112
|
+
|
|
113
|
+
def test_full_config(self, tmp_path: Path, monkeypatch):
|
|
114
|
+
"""Test a realistic full configuration."""
|
|
115
|
+
settings_file = tmp_path / "settings.json"
|
|
116
|
+
settings_file.write_text(json.dumps({
|
|
117
|
+
"env": {
|
|
118
|
+
"LLM_API_KEY": "sk-llm",
|
|
119
|
+
"LLM_BASE_URL": "https://api.openai.com/v1",
|
|
120
|
+
"LLM_MODEL": "gpt-4o",
|
|
121
|
+
"DASHSCOPE_API_KEY": "sk-dash",
|
|
122
|
+
"DASHSCOPE_BASE_URL": "https://dashscope.aliyuncs.com/api/v1",
|
|
123
|
+
}
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
for key in ["LLM_API_KEY", "LLM_BASE_URL", "LLM_MODEL",
|
|
127
|
+
"DASHSCOPE_API_KEY", "DASHSCOPE_BASE_URL"]:
|
|
128
|
+
monkeypatch.delenv(key, raising=False)
|
|
129
|
+
|
|
130
|
+
result = load_settings(settings_file)
|
|
131
|
+
|
|
132
|
+
assert os.environ["LLM_API_KEY"] == "sk-llm"
|
|
133
|
+
assert os.environ["LLM_MODEL"] == "gpt-4o"
|
|
134
|
+
assert os.environ["DASHSCOPE_API_KEY"] == "sk-dash"
|
|
135
|
+
assert len(result["env"]) == 5
|