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,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