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,378 @@
1
+ #!/usr/bin/env python3
2
+ """阶段二验收检查脚本.
3
+
4
+ 执行所有验收检查项并生成报告。
5
+
6
+ 用法:
7
+ python run_acceptance_check.py [--tinycc-path PATH]
8
+
9
+ 选项:
10
+ --tinycc-path PATH TinyCC 项目路径 [默认: /tmp/tinycc]
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import subprocess
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Sequence
24
+
25
+
26
+ class Colors:
27
+ """终端颜色."""
28
+
29
+ GREEN = "\033[92m"
30
+ RED = "\033[91m"
31
+ YELLOW = "\033[93m"
32
+ BLUE = "\033[94m"
33
+ RESET = "\033[0m"
34
+
35
+
36
+ def print_header(text: str) -> None:
37
+ """打印标题."""
38
+ print(f"\n{Colors.BLUE}{'=' * 60}{Colors.RESET}")
39
+ print(f"{Colors.BLUE}{text}{Colors.RESET}")
40
+ print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
41
+
42
+
43
+ def print_success(text: str) -> None:
44
+ """打印成功信息."""
45
+ print(f"{Colors.GREEN}✓ {text}{Colors.RESET}")
46
+
47
+
48
+ def print_failure(text: str) -> None:
49
+ """打印失败信息."""
50
+ print(f"{Colors.RED}✗ {text}{Colors.RESET}")
51
+
52
+
53
+ def print_warning(text: str) -> None:
54
+ """打印警告信息."""
55
+ print(f"{Colors.YELLOW}⚠ {text}{Colors.RESET}")
56
+
57
+
58
+ def run_command(cmd: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
59
+ """运行命令并返回结果."""
60
+ try:
61
+ result = subprocess.run(
62
+ cmd,
63
+ capture_output=True,
64
+ text=True,
65
+ cwd=cwd,
66
+ timeout=300,
67
+ )
68
+ return result.returncode, result.stdout, result.stderr
69
+ except subprocess.TimeoutExpired:
70
+ return -1, "", "Command timed out"
71
+ except Exception as e:
72
+ return -1, "", str(e)
73
+
74
+
75
+ class AcceptanceChecker:
76
+ """验收检查器."""
77
+
78
+ def __init__(self, project_root: Path, tinycc_path: Path) -> None:
79
+ self.project_root = project_root
80
+ self.tinycc_path = tinycc_path
81
+ self.results: dict[str, bool] = {}
82
+ self.errors: dict[str, str] = {}
83
+
84
+ def check_code_quality(self) -> None:
85
+ """检查代码质量."""
86
+ print_header("代码质量检查")
87
+
88
+ # ruff check
89
+ print("运行 ruff check...")
90
+ returncode, stdout, stderr = run_command(
91
+ ["uv", "run", "ruff", "check", "code_graph_builder/"],
92
+ cwd=self.project_root,
93
+ )
94
+ if returncode == 0:
95
+ print_success("ruff check 通过")
96
+ self.results["ruff_check"] = True
97
+ else:
98
+ print_failure("ruff check 失败")
99
+ self.errors["ruff_check"] = stderr or stdout
100
+ self.results["ruff_check"] = False
101
+
102
+ # ruff format check
103
+ print("运行 ruff format --check...")
104
+ returncode, stdout, stderr = run_command(
105
+ ["uv", "run", "ruff", "format", "--check", "code_graph_builder/"],
106
+ cwd=self.project_root,
107
+ )
108
+ if returncode == 0:
109
+ print_success("ruff format 通过")
110
+ self.results["ruff_format"] = True
111
+ else:
112
+ print_failure("ruff format 失败")
113
+ self.errors["ruff_format"] = stderr or stdout
114
+ self.results["ruff_format"] = False
115
+
116
+ # ty type check
117
+ print("运行 ty 类型检查...")
118
+ returncode, stdout, stderr = run_command(
119
+ ["uv", "run", "ty", "code_graph_builder/"],
120
+ cwd=self.project_root,
121
+ )
122
+ if returncode == 0:
123
+ print_success("ty 类型检查通过")
124
+ self.results["ty_check"] = True
125
+ else:
126
+ print_failure("ty 类型检查失败")
127
+ self.errors["ty_check"] = stderr or stdout
128
+ self.results["ty_check"] = False
129
+
130
+ def check_unit_tests(self) -> None:
131
+ """检查单元测试."""
132
+ print_header("单元测试检查")
133
+
134
+ print("运行 code_graph_builder 单元测试...")
135
+ returncode, stdout, stderr = run_command(
136
+ [
137
+ "uv",
138
+ "run",
139
+ "pytest",
140
+ "code_graph_builder/tests/",
141
+ "-v",
142
+ "--tb=short",
143
+ ],
144
+ cwd=self.project_root,
145
+ )
146
+
147
+ if returncode == 0:
148
+ print_success("所有单元测试通过")
149
+ self.results["unit_tests"] = True
150
+ else:
151
+ print_failure("单元测试失败")
152
+ self.errors["unit_tests"] = stderr or stdout
153
+ self.results["unit_tests"] = False
154
+
155
+ def check_test_coverage(self) -> None:
156
+ """检查测试覆盖率."""
157
+ print_header("测试覆盖率检查")
158
+
159
+ print("运行覆盖率测试...")
160
+ returncode, stdout, stderr = run_command(
161
+ [
162
+ "uv",
163
+ "run",
164
+ "pytest",
165
+ "code_graph_builder/tests/",
166
+ "--cov=code_graph_builder",
167
+ "--cov-report=term-missing",
168
+ ],
169
+ cwd=self.project_root,
170
+ )
171
+
172
+ if returncode == 0:
173
+ # 解析覆盖率
174
+ for line in (stdout + stderr).split("\n"):
175
+ if "TOTAL" in line and "%" in line:
176
+ parts = line.split()
177
+ for part in parts:
178
+ if "%" in part:
179
+ try:
180
+ coverage = float(part.replace("%", ""))
181
+ if coverage >= 80:
182
+ print_success(f"测试覆盖率: {coverage}% (>= 80%)")
183
+ self.results["test_coverage"] = True
184
+ else:
185
+ print_failure(f"测试覆盖率: {coverage}% (< 80%)")
186
+ self.results["test_coverage"] = False
187
+ return
188
+ except ValueError:
189
+ continue
190
+ print_warning("无法解析覆盖率数据")
191
+ self.results["test_coverage"] = False
192
+ else:
193
+ print_failure("覆盖率测试失败")
194
+ self.errors["test_coverage"] = stderr or stdout
195
+ self.results["test_coverage"] = False
196
+
197
+ def check_tinycc_parsing(self) -> None:
198
+ """检查 TinyCC 项目解析."""
199
+ print_header("TinyCC 项目解析检查")
200
+
201
+ if not self.tinycc_path.exists():
202
+ print_warning(f"TinyCC 项目不存在: {self.tinycc_path}")
203
+ print("跳过此检查")
204
+ self.results["tinycc_parse"] = None
205
+ return
206
+
207
+ print(f"解析 TinyCC 项目: {self.tinycc_path}")
208
+ start_time = time.time()
209
+
210
+ try:
211
+ # 动态导入避免依赖问题
212
+ sys.path.insert(0, str(self.project_root))
213
+ from code_graph_builder.builder import CodeGraphBuilder
214
+
215
+ builder = CodeGraphBuilder(str(self.tinycc_path))
216
+ result = builder.build_graph(clean=True)
217
+
218
+ elapsed_time = time.time() - start_time
219
+
220
+ print(f"解析时间: {elapsed_time:.2f} 秒")
221
+ print(f"创建节点数: {result.nodes_created}")
222
+ print(f"发现函数数: {result.functions_found}")
223
+ print(f"创建关系数: {result.relationships_created}")
224
+
225
+ # 性能检查
226
+ if elapsed_time <= 5.0:
227
+ print_success(f"解析时间达标: {elapsed_time:.2f}s <= 5s")
228
+ self.results["parse_time"] = True
229
+ else:
230
+ print_failure(f"解析时间超标: {elapsed_time:.2f}s > 5s")
231
+ self.results["parse_time"] = False
232
+
233
+ # 功能检查 (TinyCC 大约有 1611 个函数)
234
+ if result.functions_found >= 1400:
235
+ print_success(f"函数识别达标: {result.functions_found} >= 1400")
236
+ self.results["function_count"] = True
237
+ else:
238
+ print_failure(f"函数识别不足: {result.functions_found} < 1400")
239
+ self.results["function_count"] = False
240
+
241
+ # 关系检查
242
+ if result.relationships_created > 0:
243
+ print_success(f"关系创建成功: {result.relationships_created}")
244
+ self.results["relationships"] = True
245
+ else:
246
+ print_failure("未创建任何关系")
247
+ self.results["relationships"] = False
248
+
249
+ self.results["tinycc_parse"] = all([
250
+ self.results.get("parse_time", False),
251
+ self.results.get("function_count", False),
252
+ self.results.get("relationships", False),
253
+ ])
254
+
255
+ except Exception as e:
256
+ print_failure(f"TinyCC 解析失败: {e}")
257
+ self.errors["tinycc_parse"] = str(e)
258
+ self.results["tinycc_parse"] = False
259
+
260
+ def check_call_resolution_scenarios(self) -> None:
261
+ """检查调用解析场景测试."""
262
+ print_header("调用解析场景测试")
263
+
264
+ print("运行调用解析场景测试...")
265
+ returncode, stdout, stderr = run_command(
266
+ [
267
+ "uv",
268
+ "run",
269
+ "pytest",
270
+ "code_graph_builder/tests/test_call_resolution_scenarios.py",
271
+ "-v",
272
+ "--tb=short",
273
+ ],
274
+ cwd=self.project_root,
275
+ )
276
+
277
+ if returncode == 0:
278
+ print_success("调用解析场景测试通过")
279
+ self.results["call_resolution"] = True
280
+ else:
281
+ print_failure("调用解析场景测试失败")
282
+ self.errors["call_resolution"] = stderr or stdout
283
+ self.results["call_resolution"] = False
284
+
285
+ def generate_report(self) -> bool:
286
+ """生成验收报告."""
287
+ print_header("验收报告")
288
+
289
+ # 必须通过的检查项
290
+ required_checks = [
291
+ "ruff_check",
292
+ "ruff_format",
293
+ "ty_check",
294
+ "unit_tests",
295
+ "call_resolution",
296
+ ]
297
+
298
+ # 可选但重要的检查项
299
+ important_checks = [
300
+ "test_coverage",
301
+ "tinycc_parse",
302
+ ]
303
+
304
+ all_passed = True
305
+
306
+ print("必须通过的检查项:")
307
+ for check in required_checks:
308
+ result = self.results.get(check)
309
+ if result is True:
310
+ print_success(f" {check}")
311
+ elif result is False:
312
+ print_failure(f" {check}")
313
+ all_passed = False
314
+ else:
315
+ print_warning(f" {check}: 未执行")
316
+ all_passed = False
317
+
318
+ print("\n重要检查项:")
319
+ for check in important_checks:
320
+ result = self.results.get(check)
321
+ if result is True:
322
+ print_success(f" {check}")
323
+ elif result is False:
324
+ print_failure(f" {check}")
325
+ else:
326
+ print_warning(f" {check}: 未执行/跳过")
327
+
328
+ print("\n" + "=" * 60)
329
+ if all_passed:
330
+ print_success("阶段二验收通过!")
331
+ else:
332
+ print_failure("阶段二验收未通过,请修复上述问题")
333
+ print("=" * 60)
334
+
335
+ # 显示错误详情
336
+ if self.errors:
337
+ print("\n错误详情:")
338
+ for check, error in self.errors.items():
339
+ print(f"\n{Colors.YELLOW}{check}:{Colors.RESET}")
340
+ print(error[:500]) # 限制错误输出长度
341
+
342
+ return all_passed
343
+
344
+
345
+ def main(argv: Sequence[str] | None = None) -> int:
346
+ """主函数."""
347
+ parser = argparse.ArgumentParser(description="阶段二验收检查")
348
+ parser.add_argument(
349
+ "--tinycc-path",
350
+ type=Path,
351
+ default=Path("/tmp/tinycc"),
352
+ help="TinyCC 项目路径 (默认: /tmp/tinycc)",
353
+ )
354
+ args = parser.parse_args(argv)
355
+
356
+ project_root = Path(__file__).parent.parent.parent
357
+
358
+ print_header("Code Graph Builder 阶段二验收检查")
359
+ print(f"项目根目录: {project_root}")
360
+ print(f"TinyCC 路径: {args.tinycc_path}")
361
+
362
+ checker = AcceptanceChecker(project_root, args.tinycc_path)
363
+
364
+ # 执行所有检查
365
+ checker.check_code_quality()
366
+ checker.check_unit_tests()
367
+ checker.check_test_coverage()
368
+ checker.check_call_resolution_scenarios()
369
+ checker.check_tinycc_parsing()
370
+
371
+ # 生成报告
372
+ passed = checker.generate_report()
373
+
374
+ return 0 if passed else 1
375
+
376
+
377
+ if __name__ == "__main__":
378
+ sys.exit(main())
@@ -0,0 +1,231 @@
1
+ """Tests for api-find / find_api aggregation logic.
2
+
3
+ Covers:
4
+ - API doc generator: _sanitise_filename, _render_func_detail
5
+ - cmd_api_find CLI: result structure, API doc attachment
6
+ - _handle_find_api MCP: result structure, API doc attachment
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Unit tests for api_doc_generator helpers
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ class TestSanitiseFilename:
22
+ def test_forward_slash(self):
23
+ from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
24
+
25
+ assert _sanitise_filename("project/api/init") == "project_api_init"
26
+
27
+ def test_backslash(self):
28
+ from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
29
+
30
+ assert _sanitise_filename("project\\api\\init") == "project_api_init"
31
+
32
+ def test_no_separators(self):
33
+ from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
34
+
35
+ assert _sanitise_filename("simple_name") == "simple_name"
36
+
37
+ def test_mixed_separators(self):
38
+ from code_graph_builder.mcp.api_doc_generator import _sanitise_filename
39
+
40
+ assert _sanitise_filename("a/b\\c") == "a_b_c"
41
+
42
+
43
+ class TestRenderFuncDetail:
44
+ def test_basic_rendering(self):
45
+ from code_graph_builder.mcp.api_doc_generator import _render_func_detail
46
+
47
+ func = {
48
+ "qn": "project.api.init",
49
+ "name": "init",
50
+ "signature": "int init(void)",
51
+ "return_type": "int",
52
+ "visibility": "public",
53
+ "path": "src/api.c",
54
+ "start_line": 10,
55
+ "end_line": 20,
56
+ "module_qn": "project.api",
57
+ "docstring": "Initialize the API subsystem.",
58
+ }
59
+
60
+ content = _render_func_detail(func, callers=[], callees=[])
61
+
62
+ assert "# init" in content
63
+ assert "`int init(void)`" in content
64
+ assert "`int`" in content
65
+ assert "public" in content
66
+ assert "Initialize the API subsystem." in content
67
+ assert "*(无调用者)*" in content
68
+
69
+ def test_with_callers_and_callees(self):
70
+ from code_graph_builder.mcp.api_doc_generator import _render_func_detail
71
+
72
+ func = {
73
+ "qn": "mod.foo",
74
+ "name": "foo",
75
+ "signature": "void foo()",
76
+ "return_type": None,
77
+ "visibility": "static",
78
+ "path": "src/mod.c",
79
+ "start_line": 5,
80
+ "end_line": 15,
81
+ "module_qn": "mod",
82
+ "docstring": None,
83
+ }
84
+
85
+ callers = [{"qn": "mod.bar", "path": "src/mod.c", "start_line": 30}]
86
+ callees = [{"qn": "mod.baz", "path": "src/mod.c", "start_line": 50}]
87
+
88
+ content = _render_func_detail(func, callers=callers, callees=callees)
89
+
90
+ assert "被调用 (1)" in content
91
+ assert "mod.bar" in content
92
+ # No docstring section when docstring is None
93
+ assert "## 描述" not in content
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Unit tests for generate_api_docs pipeline
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ class TestGenerateApiDocs:
102
+ def test_generates_files(self, tmp_path: Path):
103
+ from code_graph_builder.mcp.api_doc_generator import generate_api_docs
104
+
105
+ func_rows = [
106
+ {
107
+ "result": [
108
+ "mymod", # module_qn
109
+ "src/mymod.c", # module_path
110
+ "mymod.do_stuff", # qn
111
+ "do_stuff", # name
112
+ "int do_stuff(int x)", # signature
113
+ "int", # return_type
114
+ "public", # visibility
115
+ "x: int", # parameters
116
+ "Does stuff.", # docstring
117
+ 1, # start_line
118
+ 10, # end_line
119
+ "src/mymod.c", # path
120
+ ]
121
+ }
122
+ ]
123
+
124
+ result = generate_api_docs(
125
+ func_rows=func_rows,
126
+ type_rows=[],
127
+ call_rows=[],
128
+ output_dir=tmp_path,
129
+ )
130
+
131
+ assert result["module_count"] == 1
132
+ assert result["func_count"] == 1
133
+ assert result["type_count"] == 0
134
+
135
+ # Check generated files
136
+ assert (tmp_path / "api_docs" / "index.md").exists()
137
+ assert (tmp_path / "api_docs" / "modules" / "mymod.md").exists()
138
+ assert (tmp_path / "api_docs" / "funcs" / "mymod.do_stuff.md").exists()
139
+
140
+ # Check L3 content
141
+ func_doc = (tmp_path / "api_docs" / "funcs" / "mymod.do_stuff.md").read_text()
142
+ assert "# do_stuff" in func_doc
143
+ assert "Does stuff." in func_doc
144
+
145
+ def test_call_graph_wiring(self, tmp_path: Path):
146
+ from code_graph_builder.mcp.api_doc_generator import generate_api_docs
147
+
148
+ func_rows = [
149
+ {
150
+ "result": [
151
+ "m", "m.c", "m.caller", "caller", "void caller()", None,
152
+ "public", "", None, 1, 5, "m.c",
153
+ ]
154
+ },
155
+ {
156
+ "result": [
157
+ "m", "m.c", "m.callee", "callee", "void callee()", None,
158
+ "static", "", None, 10, 15, "m.c",
159
+ ]
160
+ },
161
+ ]
162
+
163
+ call_rows = [
164
+ {"result": ["m.caller", "m.callee", "m.c", 3]},
165
+ ]
166
+
167
+ generate_api_docs(
168
+ func_rows=func_rows,
169
+ type_rows=[],
170
+ call_rows=call_rows,
171
+ output_dir=tmp_path,
172
+ )
173
+
174
+ caller_doc = (tmp_path / "api_docs" / "funcs" / "m.caller.md").read_text()
175
+ assert "callee" in caller_doc
176
+
177
+ callee_doc = (tmp_path / "api_docs" / "funcs" / "m.callee.md").read_text()
178
+ assert "caller" in callee_doc
179
+ assert "被调用 (1)" in callee_doc
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Integration-style tests for the find_api aggregation logic
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ class TestFindApiAggregation:
188
+ """Test the core aggregation logic shared by cmd_api_find and _handle_find_api.
189
+
190
+ These tests exercise the filename-matching and doc-attachment logic
191
+ without requiring a live database or embeddings model.
192
+ """
193
+
194
+ def test_doc_attachment_when_file_exists(self, tmp_path: Path):
195
+ """When a matching API doc file exists, its content is attached."""
196
+ funcs_dir = tmp_path / "api_docs" / "funcs"
197
+ funcs_dir.mkdir(parents=True)
198
+
199
+ doc_content = "# mymod.do_stuff\n\n- **Signature**: `int do_stuff(int x)`\n"
200
+ (funcs_dir / "mymod.do_stuff.md").write_text(doc_content)
201
+
202
+ # Simulate the attachment logic from cmd_api_find / _handle_find_api
203
+ qn = "mymod.do_stuff"
204
+ safe_qn = qn.replace("/", "_").replace("\\", "_")
205
+ doc_file = funcs_dir / f"{safe_qn}.md"
206
+
207
+ assert doc_file.exists()
208
+ assert doc_file.read_text() == doc_content
209
+
210
+ def test_doc_attachment_when_file_missing(self, tmp_path: Path):
211
+ """When no API doc exists for a result, api_doc should be None."""
212
+ funcs_dir = tmp_path / "api_docs" / "funcs"
213
+ funcs_dir.mkdir(parents=True)
214
+
215
+ qn = "nonexistent.function"
216
+ safe_qn = qn.replace("/", "_").replace("\\", "_")
217
+ doc_file = funcs_dir / f"{safe_qn}.md"
218
+
219
+ assert not doc_file.exists()
220
+
221
+ def test_sanitise_slash_in_qn(self, tmp_path: Path):
222
+ """Qualified names with slashes are sanitised to underscores for lookup."""
223
+ funcs_dir = tmp_path / "api_docs" / "funcs"
224
+ funcs_dir.mkdir(parents=True)
225
+
226
+ (funcs_dir / "path_to_func.md").write_text("doc")
227
+
228
+ qn = "path/to/func"
229
+ safe_qn = qn.replace("/", "_").replace("\\", "_")
230
+ doc_file = funcs_dir / f"{safe_qn}.md"
231
+ assert doc_file.exists()