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,221 @@
|
|
|
1
|
+
"""Tests for LLM client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from unittest.mock import Mock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from code_graph_builder.rag.client import (
|
|
11
|
+
ChatResponse,
|
|
12
|
+
LLMClient,
|
|
13
|
+
create_llm_client,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestChatResponse:
|
|
18
|
+
"""Tests for ChatResponse dataclass."""
|
|
19
|
+
|
|
20
|
+
def test_creation(self):
|
|
21
|
+
"""Test basic creation."""
|
|
22
|
+
response = ChatResponse(
|
|
23
|
+
content="Test response",
|
|
24
|
+
usage={"prompt_tokens": 10, "completion_tokens": 20},
|
|
25
|
+
model="kimi-k2.5",
|
|
26
|
+
finish_reason="stop",
|
|
27
|
+
)
|
|
28
|
+
assert response.content == "Test response"
|
|
29
|
+
assert response.usage["prompt_tokens"] == 10
|
|
30
|
+
assert response.model == "kimi-k2.5"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestLLMClient:
|
|
34
|
+
"""Tests for LLMClient."""
|
|
35
|
+
|
|
36
|
+
def test_default_init(self):
|
|
37
|
+
"""Test default initialization."""
|
|
38
|
+
client = LLMClient(api_key="sk-test")
|
|
39
|
+
assert client.api_key == "sk-test"
|
|
40
|
+
assert client.model == "kimi-k2.5"
|
|
41
|
+
assert client.base_url == "https://api.moonshot.cn/v1"
|
|
42
|
+
assert client.max_tokens == 4096
|
|
43
|
+
|
|
44
|
+
def test_custom_init(self):
|
|
45
|
+
"""Test custom initialization."""
|
|
46
|
+
client = LLMClient(
|
|
47
|
+
api_key="sk-test",
|
|
48
|
+
model="custom-model",
|
|
49
|
+
base_url="https://custom.api.com/",
|
|
50
|
+
max_tokens=2048,
|
|
51
|
+
temperature=0.5,
|
|
52
|
+
timeout=60,
|
|
53
|
+
)
|
|
54
|
+
assert client.model == "custom-model"
|
|
55
|
+
assert client.base_url == "https://custom.api.com" # trailing slash removed
|
|
56
|
+
assert client.max_tokens == 2048
|
|
57
|
+
assert client.temperature == 0.5
|
|
58
|
+
assert client.timeout == 60
|
|
59
|
+
|
|
60
|
+
def test_init_missing_api_key(self):
|
|
61
|
+
"""Test initialization fails without API key."""
|
|
62
|
+
with pytest.raises(ValueError, match="API key is required"):
|
|
63
|
+
LLMClient(api_key=None)
|
|
64
|
+
|
|
65
|
+
def test_get_headers(self):
|
|
66
|
+
"""Test getting headers."""
|
|
67
|
+
client = LLMClient(api_key="sk-test")
|
|
68
|
+
headers = client._get_headers()
|
|
69
|
+
assert headers["Authorization"] == "Bearer sk-test"
|
|
70
|
+
assert headers["Content-Type"] == "application/json"
|
|
71
|
+
|
|
72
|
+
@patch("code_graph_builder.rag.client.requests.post")
|
|
73
|
+
def test_chat_success(self, mock_post):
|
|
74
|
+
"""Test successful chat completion."""
|
|
75
|
+
mock_response = Mock()
|
|
76
|
+
mock_response.json.return_value = {
|
|
77
|
+
"choices": [
|
|
78
|
+
{
|
|
79
|
+
"message": {"content": "Test response"},
|
|
80
|
+
"finish_reason": "stop",
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"usage": {"prompt_tokens": 10, "completion_tokens": 20},
|
|
84
|
+
"model": "kimi-k2.5",
|
|
85
|
+
}
|
|
86
|
+
mock_response.raise_for_status = Mock()
|
|
87
|
+
mock_post.return_value = mock_response
|
|
88
|
+
|
|
89
|
+
client = LLMClient(api_key="sk-test")
|
|
90
|
+
response = client.chat("Hello")
|
|
91
|
+
|
|
92
|
+
assert response.content == "Test response"
|
|
93
|
+
assert response.model == "kimi-k2.5"
|
|
94
|
+
mock_post.assert_called_once()
|
|
95
|
+
|
|
96
|
+
@patch("code_graph_builder.rag.client.requests.post")
|
|
97
|
+
def test_chat_with_context(self, mock_post):
|
|
98
|
+
"""Test chat with context."""
|
|
99
|
+
mock_response = Mock()
|
|
100
|
+
mock_response.json.return_value = {
|
|
101
|
+
"choices": [
|
|
102
|
+
{
|
|
103
|
+
"message": {"content": "Response"},
|
|
104
|
+
"finish_reason": "stop",
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"usage": {},
|
|
108
|
+
"model": "kimi-k2.5",
|
|
109
|
+
}
|
|
110
|
+
mock_response.raise_for_status = Mock()
|
|
111
|
+
mock_post.return_value = mock_response
|
|
112
|
+
|
|
113
|
+
client = LLMClient(api_key="sk-test")
|
|
114
|
+
response = client.chat(
|
|
115
|
+
query="Explain",
|
|
116
|
+
context="def foo(): pass",
|
|
117
|
+
system_prompt="You are helpful.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert response.content == "Response"
|
|
121
|
+
# Check that context was included in the call
|
|
122
|
+
call_args = mock_post.call_args
|
|
123
|
+
json_data = call_args.kwargs["json"]
|
|
124
|
+
assert any("Context:" in msg["content"] for msg in json_data["messages"])
|
|
125
|
+
|
|
126
|
+
@patch("code_graph_builder.rag.client.requests.post")
|
|
127
|
+
def test_chat_http_error(self, mock_post):
|
|
128
|
+
"""Test chat with HTTP error."""
|
|
129
|
+
from requests.exceptions import HTTPError
|
|
130
|
+
|
|
131
|
+
mock_response = Mock()
|
|
132
|
+
mock_response.json.return_value = {
|
|
133
|
+
"error": {"message": "Invalid API key"}
|
|
134
|
+
}
|
|
135
|
+
mock_response.raise_for_status.side_effect = HTTPError(
|
|
136
|
+
"401 Client Error",
|
|
137
|
+
response=mock_response,
|
|
138
|
+
)
|
|
139
|
+
mock_post.return_value = mock_response
|
|
140
|
+
|
|
141
|
+
client = LLMClient(api_key="sk-test")
|
|
142
|
+
with pytest.raises(RuntimeError, match="API request failed"):
|
|
143
|
+
client.chat("Hello")
|
|
144
|
+
|
|
145
|
+
@patch("code_graph_builder.rag.client.requests.post")
|
|
146
|
+
def test_chat_timeout(self, mock_post):
|
|
147
|
+
"""Test chat with timeout."""
|
|
148
|
+
from requests.exceptions import Timeout
|
|
149
|
+
|
|
150
|
+
mock_post.side_effect = Timeout("Request timed out")
|
|
151
|
+
|
|
152
|
+
client = LLMClient(api_key="sk-test", timeout=5)
|
|
153
|
+
with pytest.raises(RuntimeError, match="timeout"):
|
|
154
|
+
client.chat("Hello")
|
|
155
|
+
|
|
156
|
+
@patch("code_graph_builder.rag.client.requests.post")
|
|
157
|
+
def test_chat_with_messages(self, mock_post):
|
|
158
|
+
"""Test chat with raw messages."""
|
|
159
|
+
mock_response = Mock()
|
|
160
|
+
mock_response.json.return_value = {
|
|
161
|
+
"choices": [
|
|
162
|
+
{
|
|
163
|
+
"message": {"content": "Response"},
|
|
164
|
+
"finish_reason": "stop",
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"usage": {},
|
|
168
|
+
"model": "kimi-k2.5",
|
|
169
|
+
}
|
|
170
|
+
mock_response.raise_for_status = Mock()
|
|
171
|
+
mock_post.return_value = mock_response
|
|
172
|
+
|
|
173
|
+
client = LLMClient(api_key="sk-test")
|
|
174
|
+
messages = [
|
|
175
|
+
{"role": "system", "content": "You are helpful."},
|
|
176
|
+
{"role": "user", "content": "Hello"},
|
|
177
|
+
]
|
|
178
|
+
response = client.chat_with_messages(messages)
|
|
179
|
+
|
|
180
|
+
assert response.content == "Response"
|
|
181
|
+
call_args = mock_post.call_args
|
|
182
|
+
json_data = call_args.kwargs["json"]
|
|
183
|
+
assert json_data["messages"] == messages
|
|
184
|
+
|
|
185
|
+
@patch("code_graph_builder.rag.client.requests.get")
|
|
186
|
+
def test_health_check_success(self, mock_get):
|
|
187
|
+
"""Test successful health check."""
|
|
188
|
+
mock_response = Mock()
|
|
189
|
+
mock_response.status_code = 200
|
|
190
|
+
mock_get.return_value = mock_response
|
|
191
|
+
|
|
192
|
+
client = LLMClient(api_key="sk-test")
|
|
193
|
+
assert client.health_check() is True
|
|
194
|
+
|
|
195
|
+
@patch("code_graph_builder.rag.client.requests.get")
|
|
196
|
+
def test_health_check_failure(self, mock_get):
|
|
197
|
+
"""Test failed health check."""
|
|
198
|
+
mock_get.side_effect = Exception("Connection error")
|
|
199
|
+
|
|
200
|
+
client = LLMClient(api_key="sk-test")
|
|
201
|
+
assert client.health_check() is False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestCreateLLMClient:
|
|
205
|
+
"""Tests for create_llm_client factory function."""
|
|
206
|
+
|
|
207
|
+
def test_create_with_defaults(self):
|
|
208
|
+
"""Test creating client with defaults."""
|
|
209
|
+
client = create_llm_client(api_key="sk-test")
|
|
210
|
+
assert isinstance(client, LLMClient)
|
|
211
|
+
assert client.model == "kimi-k2.5"
|
|
212
|
+
|
|
213
|
+
def test_create_with_custom_model(self):
|
|
214
|
+
"""Test creating client with custom model."""
|
|
215
|
+
client = create_llm_client(
|
|
216
|
+
api_key="sk-test",
|
|
217
|
+
model="custom-model",
|
|
218
|
+
max_tokens=2048,
|
|
219
|
+
)
|
|
220
|
+
assert client.model == "custom-model"
|
|
221
|
+
assert client.max_tokens == 2048
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Tests for RAG configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from code_graph_builder.rag.config import (
|
|
11
|
+
MoonshotConfig,
|
|
12
|
+
OutputConfig,
|
|
13
|
+
RAGConfig,
|
|
14
|
+
RetrievalConfig,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestMoonshotConfig:
|
|
19
|
+
"""Tests for MoonshotConfig."""
|
|
20
|
+
|
|
21
|
+
def test_default_values(self):
|
|
22
|
+
"""Test default configuration values."""
|
|
23
|
+
config = MoonshotConfig(api_key="sk-test")
|
|
24
|
+
assert config.model == "kimi-k2.5"
|
|
25
|
+
assert config.base_url == "https://api.moonshot.cn/v1"
|
|
26
|
+
assert config.max_tokens == 4096
|
|
27
|
+
assert config.temperature == 0.7
|
|
28
|
+
assert config.timeout == 120
|
|
29
|
+
|
|
30
|
+
def test_custom_values(self):
|
|
31
|
+
"""Test custom configuration values."""
|
|
32
|
+
config = MoonshotConfig(
|
|
33
|
+
api_key="sk-test",
|
|
34
|
+
model="kimi-k2.5",
|
|
35
|
+
base_url="https://custom.api.com",
|
|
36
|
+
max_tokens=2048,
|
|
37
|
+
temperature=0.5,
|
|
38
|
+
timeout=60,
|
|
39
|
+
)
|
|
40
|
+
assert config.model == "kimi-k2.5"
|
|
41
|
+
assert config.base_url == "https://custom.api.com"
|
|
42
|
+
assert config.max_tokens == 2048
|
|
43
|
+
assert config.temperature == 0.5
|
|
44
|
+
assert config.timeout == 60
|
|
45
|
+
|
|
46
|
+
def test_api_key_from_env(self, monkeypatch):
|
|
47
|
+
"""Test loading API key from environment."""
|
|
48
|
+
monkeypatch.setenv("MOONSHOT_API_KEY", "sk-from-env")
|
|
49
|
+
config = MoonshotConfig()
|
|
50
|
+
assert config.api_key == "sk-from-env"
|
|
51
|
+
|
|
52
|
+
def test_validate_missing_api_key(self):
|
|
53
|
+
"""Test validation fails with missing API key."""
|
|
54
|
+
config = MoonshotConfig(api_key=None)
|
|
55
|
+
with pytest.raises(ValueError, match="API key is required"):
|
|
56
|
+
config.validate()
|
|
57
|
+
|
|
58
|
+
def test_validate_invalid_api_key_format(self):
|
|
59
|
+
"""Test validation fails with invalid API key format."""
|
|
60
|
+
config = MoonshotConfig(api_key="invalid-key")
|
|
61
|
+
with pytest.raises(ValueError, match="start with 'sk-'"):
|
|
62
|
+
config.validate()
|
|
63
|
+
|
|
64
|
+
def test_validate_invalid_temperature(self):
|
|
65
|
+
"""Test validation fails with invalid temperature."""
|
|
66
|
+
config = MoonshotConfig(api_key="sk-test", temperature=3.0)
|
|
67
|
+
with pytest.raises(ValueError, match="between 0 and 2"):
|
|
68
|
+
config.validate()
|
|
69
|
+
|
|
70
|
+
def test_to_dict(self):
|
|
71
|
+
"""Test conversion to dictionary."""
|
|
72
|
+
config = MoonshotConfig(api_key="sk-test")
|
|
73
|
+
data = config.to_dict()
|
|
74
|
+
assert data["model"] == "kimi-k2.5"
|
|
75
|
+
assert data["api_key"] == "sk-test"
|
|
76
|
+
assert "base_url" in data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestRetrievalConfig:
|
|
80
|
+
"""Tests for RetrievalConfig."""
|
|
81
|
+
|
|
82
|
+
def test_default_values(self):
|
|
83
|
+
"""Test default configuration values."""
|
|
84
|
+
config = RetrievalConfig()
|
|
85
|
+
assert config.semantic_top_k == 10
|
|
86
|
+
assert config.graph_max_depth == 2
|
|
87
|
+
assert config.include_callers is True
|
|
88
|
+
assert config.include_callees is True
|
|
89
|
+
assert config.include_related is True
|
|
90
|
+
assert config.max_context_tokens == 8000
|
|
91
|
+
assert config.code_chunk_size == 2000
|
|
92
|
+
|
|
93
|
+
def test_custom_values(self):
|
|
94
|
+
"""Test custom configuration values."""
|
|
95
|
+
config = RetrievalConfig(
|
|
96
|
+
semantic_top_k=20,
|
|
97
|
+
graph_max_depth=3,
|
|
98
|
+
include_callers=False,
|
|
99
|
+
include_callees=False,
|
|
100
|
+
include_related=False,
|
|
101
|
+
)
|
|
102
|
+
assert config.semantic_top_k == 20
|
|
103
|
+
assert config.graph_max_depth == 3
|
|
104
|
+
assert config.include_callers is False
|
|
105
|
+
|
|
106
|
+
def test_to_dict(self):
|
|
107
|
+
"""Test conversion to dictionary."""
|
|
108
|
+
config = RetrievalConfig()
|
|
109
|
+
data = config.to_dict()
|
|
110
|
+
assert "semantic_top_k" in data
|
|
111
|
+
assert "graph_max_depth" in data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestOutputConfig:
|
|
115
|
+
"""Tests for OutputConfig."""
|
|
116
|
+
|
|
117
|
+
def test_default_values(self):
|
|
118
|
+
"""Test default configuration values."""
|
|
119
|
+
config = OutputConfig()
|
|
120
|
+
assert config.format == "markdown"
|
|
121
|
+
assert config.include_source_links is True
|
|
122
|
+
assert config.include_code_snippets is True
|
|
123
|
+
assert isinstance(config.output_dir, Path)
|
|
124
|
+
|
|
125
|
+
def test_custom_output_dir(self):
|
|
126
|
+
"""Test custom output directory."""
|
|
127
|
+
config = OutputConfig(output_dir="/custom/path")
|
|
128
|
+
assert isinstance(config.output_dir, Path)
|
|
129
|
+
assert str(config.output_dir) == "/custom/path"
|
|
130
|
+
|
|
131
|
+
def test_to_dict(self):
|
|
132
|
+
"""Test conversion to dictionary."""
|
|
133
|
+
config = OutputConfig()
|
|
134
|
+
data = config.to_dict()
|
|
135
|
+
assert data["format"] == "markdown"
|
|
136
|
+
assert "output_dir" in data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestRAGConfig:
|
|
140
|
+
"""Tests for RAGConfig."""
|
|
141
|
+
|
|
142
|
+
def test_default_values(self):
|
|
143
|
+
"""Test default configuration values."""
|
|
144
|
+
config = RAGConfig(moonshot=MoonshotConfig(api_key="sk-test"))
|
|
145
|
+
assert isinstance(config.moonshot, MoonshotConfig)
|
|
146
|
+
assert isinstance(config.retrieval, RetrievalConfig)
|
|
147
|
+
assert isinstance(config.output, OutputConfig)
|
|
148
|
+
assert config.verbose is False
|
|
149
|
+
|
|
150
|
+
def test_from_env(self, monkeypatch):
|
|
151
|
+
"""Test creating config from environment variables."""
|
|
152
|
+
monkeypatch.setenv("MOONSHOT_API_KEY", "sk-env-key")
|
|
153
|
+
monkeypatch.setenv("MOONSHOT_MODEL", "kimi-k2.5")
|
|
154
|
+
monkeypatch.setenv("RAG_SEMANTIC_TOP_K", "15")
|
|
155
|
+
monkeypatch.setenv("RAG_OUTPUT_FORMAT", "json")
|
|
156
|
+
monkeypatch.setenv("RAG_VERBOSE", "true")
|
|
157
|
+
|
|
158
|
+
config = RAGConfig.from_env()
|
|
159
|
+
assert config.moonshot.api_key == "sk-env-key"
|
|
160
|
+
assert config.moonshot.model == "kimi-k2.5"
|
|
161
|
+
assert config.retrieval.semantic_top_k == 15
|
|
162
|
+
assert config.output.format == "json"
|
|
163
|
+
assert config.verbose is True
|
|
164
|
+
|
|
165
|
+
def test_validate(self):
|
|
166
|
+
"""Test configuration validation."""
|
|
167
|
+
config = RAGConfig(moonshot=MoonshotConfig(api_key="sk-test"))
|
|
168
|
+
config.validate() # Should not raise
|
|
169
|
+
|
|
170
|
+
def test_to_dict(self):
|
|
171
|
+
"""Test conversion to dictionary."""
|
|
172
|
+
config = RAGConfig(moonshot=MoonshotConfig(api_key="sk-test"))
|
|
173
|
+
data = config.to_dict()
|
|
174
|
+
assert "moonshot" in data
|
|
175
|
+
assert "retrieval" in data
|
|
176
|
+
assert "output" in data
|
|
177
|
+
assert "verbose" in data
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Tests for markdown generator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from code_graph_builder.rag.markdown_generator import (
|
|
11
|
+
AnalysisResult,
|
|
12
|
+
MarkdownGenerator,
|
|
13
|
+
SourceReference,
|
|
14
|
+
create_source_reference_from_context,
|
|
15
|
+
format_code_block,
|
|
16
|
+
)
|
|
17
|
+
from code_graph_builder.rag.prompt_templates import CodeContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestSourceReference:
|
|
21
|
+
"""Tests for SourceReference."""
|
|
22
|
+
|
|
23
|
+
def test_basic_creation(self):
|
|
24
|
+
"""Test basic creation."""
|
|
25
|
+
ref = SourceReference(
|
|
26
|
+
name="foo",
|
|
27
|
+
qualified_name="test.foo",
|
|
28
|
+
file_path="test.py",
|
|
29
|
+
line_start=10,
|
|
30
|
+
line_end=20,
|
|
31
|
+
entity_type="Function",
|
|
32
|
+
)
|
|
33
|
+
assert ref.name == "foo"
|
|
34
|
+
assert ref.qualified_name == "test.foo"
|
|
35
|
+
|
|
36
|
+
def test_format_link(self):
|
|
37
|
+
"""Test link formatting."""
|
|
38
|
+
ref = SourceReference(
|
|
39
|
+
name="foo",
|
|
40
|
+
qualified_name="test.foo",
|
|
41
|
+
file_path="test.py",
|
|
42
|
+
line_start=10,
|
|
43
|
+
line_end=20,
|
|
44
|
+
)
|
|
45
|
+
link = ref.format_link()
|
|
46
|
+
assert "[test.foo]" in link
|
|
47
|
+
assert "test.py:10-20" in link
|
|
48
|
+
|
|
49
|
+
def test_format_link_single_line(self):
|
|
50
|
+
"""Test link formatting for single line."""
|
|
51
|
+
ref = SourceReference(
|
|
52
|
+
name="foo",
|
|
53
|
+
qualified_name="test.foo",
|
|
54
|
+
file_path="test.py",
|
|
55
|
+
line_start=10,
|
|
56
|
+
line_end=10,
|
|
57
|
+
)
|
|
58
|
+
link = ref.format_link()
|
|
59
|
+
assert "test.py:10" in link
|
|
60
|
+
assert "-10" not in link
|
|
61
|
+
|
|
62
|
+
def test_to_dict(self):
|
|
63
|
+
"""Test conversion to dictionary."""
|
|
64
|
+
ref = SourceReference(
|
|
65
|
+
name="foo",
|
|
66
|
+
qualified_name="test.foo",
|
|
67
|
+
file_path="test.py",
|
|
68
|
+
)
|
|
69
|
+
data = ref.to_dict()
|
|
70
|
+
assert data["name"] == "foo"
|
|
71
|
+
assert data["qualified_name"] == "test.foo"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestAnalysisResult:
|
|
75
|
+
"""Tests for AnalysisResult."""
|
|
76
|
+
|
|
77
|
+
def test_basic_creation(self):
|
|
78
|
+
"""Test basic creation."""
|
|
79
|
+
result = AnalysisResult(
|
|
80
|
+
query="Test query",
|
|
81
|
+
response="Test response",
|
|
82
|
+
)
|
|
83
|
+
assert result.query == "Test query"
|
|
84
|
+
assert result.response == "Test response"
|
|
85
|
+
assert result.sources == []
|
|
86
|
+
|
|
87
|
+
def test_with_sources(self):
|
|
88
|
+
"""Test creation with sources."""
|
|
89
|
+
sources = [
|
|
90
|
+
SourceReference(name="foo", qualified_name="test.foo", file_path="test.py"),
|
|
91
|
+
]
|
|
92
|
+
result = AnalysisResult(
|
|
93
|
+
query="Test",
|
|
94
|
+
response="Response",
|
|
95
|
+
sources=sources,
|
|
96
|
+
metadata={"key": "value"},
|
|
97
|
+
)
|
|
98
|
+
assert len(result.sources) == 1
|
|
99
|
+
assert result.metadata["key"] == "value"
|
|
100
|
+
|
|
101
|
+
def test_to_dict(self):
|
|
102
|
+
"""Test conversion to dictionary."""
|
|
103
|
+
result = AnalysisResult(query="Test", response="Response")
|
|
104
|
+
data = result.to_dict()
|
|
105
|
+
assert data["query"] == "Test"
|
|
106
|
+
assert data["response"] == "Response"
|
|
107
|
+
assert "timestamp" in data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestMarkdownGenerator:
|
|
111
|
+
"""Tests for MarkdownGenerator."""
|
|
112
|
+
|
|
113
|
+
def test_default_init(self):
|
|
114
|
+
"""Test default initialization."""
|
|
115
|
+
gen = MarkdownGenerator()
|
|
116
|
+
assert gen.include_toc is True
|
|
117
|
+
assert gen.include_timestamp is True
|
|
118
|
+
assert gen.include_metadata is True
|
|
119
|
+
|
|
120
|
+
def test_custom_init(self):
|
|
121
|
+
"""Test custom initialization."""
|
|
122
|
+
gen = MarkdownGenerator(
|
|
123
|
+
include_toc=False,
|
|
124
|
+
include_timestamp=False,
|
|
125
|
+
include_metadata=False,
|
|
126
|
+
)
|
|
127
|
+
assert gen.include_toc is False
|
|
128
|
+
|
|
129
|
+
def test_generate_analysis_doc(self):
|
|
130
|
+
"""Test generating analysis document."""
|
|
131
|
+
gen = MarkdownGenerator()
|
|
132
|
+
result = AnalysisResult(
|
|
133
|
+
query="What does this do?",
|
|
134
|
+
response="It does something.",
|
|
135
|
+
sources=[
|
|
136
|
+
SourceReference(
|
|
137
|
+
name="foo",
|
|
138
|
+
qualified_name="test.foo",
|
|
139
|
+
file_path="test.py",
|
|
140
|
+
entity_type="Function",
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
doc = gen.generate_analysis_doc("Test Analysis", result)
|
|
145
|
+
assert "# Test Analysis" in doc
|
|
146
|
+
assert "## Query" in doc
|
|
147
|
+
assert "What does this do?" in doc
|
|
148
|
+
assert "## Analysis" in doc
|
|
149
|
+
assert "It does something." in doc
|
|
150
|
+
assert "## Sources" in doc
|
|
151
|
+
assert "test.foo" in doc
|
|
152
|
+
|
|
153
|
+
def test_generate_analysis_doc_no_toc(self):
|
|
154
|
+
"""Test generating document without TOC."""
|
|
155
|
+
gen = MarkdownGenerator(include_toc=False)
|
|
156
|
+
result = AnalysisResult(query="Test", response="Response")
|
|
157
|
+
doc = gen.generate_analysis_doc("Test", result)
|
|
158
|
+
assert "## Table of Contents" not in doc
|
|
159
|
+
|
|
160
|
+
def test_generate_code_documentation(self):
|
|
161
|
+
"""Test generating code documentation."""
|
|
162
|
+
gen = MarkdownGenerator()
|
|
163
|
+
ctx = CodeContext(
|
|
164
|
+
source_code="def foo(): pass",
|
|
165
|
+
file_path="test.py",
|
|
166
|
+
qualified_name="test.foo",
|
|
167
|
+
entity_type="Function",
|
|
168
|
+
)
|
|
169
|
+
doc = gen.generate_code_documentation(ctx, "This is a test function.")
|
|
170
|
+
assert "# test.foo" in doc
|
|
171
|
+
assert "**Type:** Function" in doc
|
|
172
|
+
assert "**File:** `test.py`" in doc
|
|
173
|
+
assert "This is a test function." in doc
|
|
174
|
+
assert "## Source Code" in doc
|
|
175
|
+
|
|
176
|
+
def test_generate_comparison_doc(self):
|
|
177
|
+
"""Test generating comparison document."""
|
|
178
|
+
gen = MarkdownGenerator()
|
|
179
|
+
contexts = [
|
|
180
|
+
CodeContext(source_code="def foo(): pass", qualified_name="foo"),
|
|
181
|
+
CodeContext(source_code="def bar(): pass", qualified_name="bar"),
|
|
182
|
+
]
|
|
183
|
+
doc = gen.generate_comparison_doc(
|
|
184
|
+
title="Comparison",
|
|
185
|
+
query="Compare these",
|
|
186
|
+
contexts=contexts,
|
|
187
|
+
analysis="They are similar.",
|
|
188
|
+
)
|
|
189
|
+
assert "# Comparison" in doc
|
|
190
|
+
assert "**Query:** Compare these" in doc
|
|
191
|
+
assert "## Comparison Analysis" in doc
|
|
192
|
+
assert "## Compared Entities" in doc
|
|
193
|
+
|
|
194
|
+
def test_save_document(self):
|
|
195
|
+
"""Test saving document."""
|
|
196
|
+
gen = MarkdownGenerator()
|
|
197
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
198
|
+
path = gen.save_document("# Test", f"{tmpdir}/test.md")
|
|
199
|
+
assert path.exists()
|
|
200
|
+
assert path.read_text() == "# Test"
|
|
201
|
+
|
|
202
|
+
def test_save_document_creates_dirs(self):
|
|
203
|
+
"""Test saving document creates directories."""
|
|
204
|
+
gen = MarkdownGenerator()
|
|
205
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
206
|
+
path = gen.save_document("# Test", f"{tmpdir}/nested/dir/test.md")
|
|
207
|
+
assert path.exists()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestConvenienceFunctions:
|
|
211
|
+
"""Tests for convenience functions."""
|
|
212
|
+
|
|
213
|
+
def test_create_source_reference_from_context(self):
|
|
214
|
+
"""Test creating source reference from context."""
|
|
215
|
+
ctx = CodeContext(
|
|
216
|
+
source_code="def foo(): pass",
|
|
217
|
+
file_path="test.py",
|
|
218
|
+
qualified_name="test.foo",
|
|
219
|
+
entity_type="Function",
|
|
220
|
+
)
|
|
221
|
+
ref = create_source_reference_from_context(ctx)
|
|
222
|
+
assert isinstance(ref, SourceReference)
|
|
223
|
+
assert ref.name == "foo"
|
|
224
|
+
assert ref.qualified_name == "test.foo"
|
|
225
|
+
assert ref.file_path == "test.py"
|
|
226
|
+
assert ref.entity_type == "Function"
|
|
227
|
+
|
|
228
|
+
def test_format_code_block(self):
|
|
229
|
+
"""Test formatting code block."""
|
|
230
|
+
code = "def foo(): pass"
|
|
231
|
+
formatted = format_code_block(code)
|
|
232
|
+
assert formatted.startswith("```python")
|
|
233
|
+
assert formatted.endswith("```")
|
|
234
|
+
assert "def foo(): pass" in formatted
|
|
235
|
+
|
|
236
|
+
def test_format_code_block_custom_language(self):
|
|
237
|
+
"""Test formatting code block with custom language."""
|
|
238
|
+
code = "console.log('test')"
|
|
239
|
+
formatted = format_code_block(code, language="javascript")
|
|
240
|
+
assert formatted.startswith("```javascript")
|