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.
Files changed (93) hide show
  1. code_graph_builder/__init__.py +82 -0
  2. code_graph_builder/builder.py +366 -0
  3. code_graph_builder/cgb_cli.py +32 -0
  4. code_graph_builder/cli.py +564 -0
  5. code_graph_builder/commands_cli.py +1288 -0
  6. code_graph_builder/config.py +340 -0
  7. code_graph_builder/constants.py +708 -0
  8. code_graph_builder/embeddings/__init__.py +40 -0
  9. code_graph_builder/embeddings/qwen3_embedder.py +573 -0
  10. code_graph_builder/embeddings/vector_store.py +584 -0
  11. code_graph_builder/examples/__init__.py +0 -0
  12. code_graph_builder/examples/example_configuration.py +276 -0
  13. code_graph_builder/examples/example_kuzu_usage.py +109 -0
  14. code_graph_builder/examples/example_semantic_search_full.py +347 -0
  15. code_graph_builder/examples/generate_wiki.py +915 -0
  16. code_graph_builder/examples/graph_export_example.py +100 -0
  17. code_graph_builder/examples/rag_example.py +206 -0
  18. code_graph_builder/examples/test_cli_demo.py +129 -0
  19. code_graph_builder/examples/test_embedding_api.py +153 -0
  20. code_graph_builder/examples/test_kuzu_local.py +190 -0
  21. code_graph_builder/examples/test_rag_redis.py +390 -0
  22. code_graph_builder/graph_updater.py +605 -0
  23. code_graph_builder/guidance/__init__.py +1 -0
  24. code_graph_builder/guidance/agent.py +123 -0
  25. code_graph_builder/guidance/prompts.py +74 -0
  26. code_graph_builder/guidance/toolset.py +264 -0
  27. code_graph_builder/language_spec.py +536 -0
  28. code_graph_builder/mcp/__init__.py +21 -0
  29. code_graph_builder/mcp/api_doc_generator.py +764 -0
  30. code_graph_builder/mcp/file_editor.py +207 -0
  31. code_graph_builder/mcp/pipeline.py +777 -0
  32. code_graph_builder/mcp/server.py +161 -0
  33. code_graph_builder/mcp/tools.py +1800 -0
  34. code_graph_builder/models.py +115 -0
  35. code_graph_builder/parser_loader.py +344 -0
  36. code_graph_builder/parsers/__init__.py +7 -0
  37. code_graph_builder/parsers/call_processor.py +306 -0
  38. code_graph_builder/parsers/call_resolver.py +139 -0
  39. code_graph_builder/parsers/definition_processor.py +796 -0
  40. code_graph_builder/parsers/factory.py +119 -0
  41. code_graph_builder/parsers/import_processor.py +293 -0
  42. code_graph_builder/parsers/structure_processor.py +145 -0
  43. code_graph_builder/parsers/type_inference.py +143 -0
  44. code_graph_builder/parsers/utils.py +134 -0
  45. code_graph_builder/rag/__init__.py +68 -0
  46. code_graph_builder/rag/camel_agent.py +429 -0
  47. code_graph_builder/rag/client.py +298 -0
  48. code_graph_builder/rag/config.py +239 -0
  49. code_graph_builder/rag/cypher_generator.py +67 -0
  50. code_graph_builder/rag/llm_backend.py +210 -0
  51. code_graph_builder/rag/markdown_generator.py +352 -0
  52. code_graph_builder/rag/prompt_templates.py +440 -0
  53. code_graph_builder/rag/rag_engine.py +640 -0
  54. code_graph_builder/rag/review_report.md +172 -0
  55. code_graph_builder/rag/tests/__init__.py +3 -0
  56. code_graph_builder/rag/tests/test_camel_agent.py +313 -0
  57. code_graph_builder/rag/tests/test_client.py +221 -0
  58. code_graph_builder/rag/tests/test_config.py +177 -0
  59. code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
  60. code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
  61. code_graph_builder/services/__init__.py +39 -0
  62. code_graph_builder/services/graph_service.py +465 -0
  63. code_graph_builder/services/kuzu_service.py +665 -0
  64. code_graph_builder/services/memory_service.py +171 -0
  65. code_graph_builder/settings.py +75 -0
  66. code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
  67. code_graph_builder/tests/__init__.py +1 -0
  68. code_graph_builder/tests/run_acceptance_check.py +378 -0
  69. code_graph_builder/tests/test_api_find.py +231 -0
  70. code_graph_builder/tests/test_api_find_integration.py +226 -0
  71. code_graph_builder/tests/test_basic.py +78 -0
  72. code_graph_builder/tests/test_c_api_extraction.py +388 -0
  73. code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
  74. code_graph_builder/tests/test_embedder.py +411 -0
  75. code_graph_builder/tests/test_integration_semantic.py +434 -0
  76. code_graph_builder/tests/test_mcp_protocol.py +298 -0
  77. code_graph_builder/tests/test_mcp_user_flow.py +190 -0
  78. code_graph_builder/tests/test_rag.py +404 -0
  79. code_graph_builder/tests/test_settings.py +135 -0
  80. code_graph_builder/tests/test_step1_graph_build.py +264 -0
  81. code_graph_builder/tests/test_step2_api_docs.py +323 -0
  82. code_graph_builder/tests/test_step3_embedding.py +278 -0
  83. code_graph_builder/tests/test_vector_store.py +552 -0
  84. code_graph_builder/tools/__init__.py +40 -0
  85. code_graph_builder/tools/graph_query.py +495 -0
  86. code_graph_builder/tools/semantic_search.py +387 -0
  87. code_graph_builder/types.py +333 -0
  88. code_graph_builder/utils/__init__.py +0 -0
  89. code_graph_builder/utils/path_utils.py +30 -0
  90. code_graph_builder-0.2.0.dist-info/METADATA +321 -0
  91. code_graph_builder-0.2.0.dist-info/RECORD +93 -0
  92. code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
  93. 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")