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,298 @@
1
+ """MCP protocol layer tests: tool registration, dispatch, error handling.
2
+
3
+ Tests the MCP server's tool listing, call_tool dispatch, ToolError propagation,
4
+ and JSON serialization — without requiring a live stdio transport.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ TINYCC_PATH = Path(__file__).resolve().parents[3] / "tinycc"
17
+
18
+ pytestmark = pytest.mark.skipif(
19
+ not TINYCC_PATH.exists(),
20
+ reason=f"tinycc source not found at {TINYCC_PATH}",
21
+ )
22
+
23
+
24
+ @pytest.fixture(scope="module")
25
+ def registry(tmp_path_factory):
26
+ from code_graph_builder.mcp.tools import MCPToolsRegistry
27
+
28
+ workspace = tmp_path_factory.mktemp("workspace")
29
+ reg = MCPToolsRegistry(workspace=workspace)
30
+ yield reg
31
+ reg.close()
32
+
33
+
34
+ @pytest.fixture(scope="module")
35
+ def indexed_registry(tmp_path_factory):
36
+ """Registry with tinycc indexed (graph + api-docs, skip embed/wiki)."""
37
+ from code_graph_builder.mcp.tools import MCPToolsRegistry
38
+
39
+ workspace = tmp_path_factory.mktemp("indexed_workspace")
40
+ reg = MCPToolsRegistry(workspace=workspace)
41
+ asyncio.get_event_loop().run_until_complete(
42
+ reg._handle_initialize_repository(
43
+ repo_path=str(TINYCC_PATH),
44
+ rebuild=True,
45
+ skip_wiki=True,
46
+ skip_embed=True,
47
+ )
48
+ )
49
+ yield reg
50
+ reg.close()
51
+
52
+
53
+ def _run(coro):
54
+ return asyncio.get_event_loop().run_until_complete(coro)
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Tool registration & discovery
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ class TestToolRegistration:
63
+ """Verify tools are correctly registered and discoverable."""
64
+
65
+ def test_tools_list_not_empty(self, registry):
66
+ tools = registry.tools()
67
+ assert len(tools) > 0
68
+
69
+ def test_all_tools_have_name(self, registry):
70
+ for t in registry.tools():
71
+ assert t.name, f"Tool missing name: {t}"
72
+
73
+ def test_all_tools_have_description(self, registry):
74
+ for t in registry.tools():
75
+ assert t.description, f"Tool {t.name} missing description"
76
+
77
+ def test_all_tools_have_input_schema(self, registry):
78
+ for t in registry.tools():
79
+ assert isinstance(t.input_schema, dict), f"Tool {t.name} missing input_schema"
80
+ assert "type" in t.input_schema
81
+
82
+ def test_expected_tools_present(self, registry):
83
+ names = {t.name for t in registry.tools()}
84
+ expected = {
85
+ "initialize_repository", "get_repository_info",
86
+ "list_repositories", "switch_repository",
87
+ "query_code_graph", "get_code_snippet",
88
+ "semantic_search", "find_api",
89
+ "list_wiki_pages", "get_wiki_page",
90
+ "locate_function", "list_api_interfaces",
91
+ "list_api_docs", "get_api_doc",
92
+ "generate_wiki", "rebuild_embeddings",
93
+ "build_graph", "generate_api_docs",
94
+ }
95
+ missing = expected - names
96
+ assert not missing, f"Missing expected tools: {missing}"
97
+
98
+ def test_every_tool_has_handler(self, registry):
99
+ for t in registry.tools():
100
+ handler = registry.get_handler(t.name)
101
+ assert handler is not None, f"Tool {t.name} has no handler"
102
+ assert callable(handler)
103
+
104
+ def test_unknown_tool_returns_none(self, registry):
105
+ assert registry.get_handler("nonexistent_tool") is None
106
+
107
+ def test_input_schema_is_valid_jsonschema(self, registry):
108
+ for t in registry.tools():
109
+ schema = t.input_schema
110
+ assert schema.get("type") == "object"
111
+ assert "properties" in schema
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # call_tool dispatch simulation
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ class TestCallToolDispatch:
120
+ """Simulate the server.call_tool dispatch logic."""
121
+
122
+ def _simulate_call_tool(self, registry, name: str, arguments: dict):
123
+ """Replicate server.py call_tool logic without MCP server."""
124
+ handler = registry.get_handler(name)
125
+ if handler is None:
126
+ raise ValueError(f"Unknown tool: {name}")
127
+
128
+ kwargs = dict(arguments or {})
129
+ result = _run(handler(**kwargs))
130
+
131
+ if isinstance(result, (dict, list)):
132
+ text = json.dumps(result, ensure_ascii=False, indent=2, default=str)
133
+ else:
134
+ text = str(result)
135
+ return json.loads(text) if text.startswith(("{", "[")) else text
136
+
137
+ def test_dispatch_list_repositories(self, registry):
138
+ result = self._simulate_call_tool(registry, "list_repositories", {})
139
+ assert isinstance(result, dict)
140
+
141
+ def test_dispatch_unknown_tool_raises(self, registry):
142
+ with pytest.raises(ValueError, match="Unknown tool"):
143
+ self._simulate_call_tool(registry, "nonexistent", {})
144
+
145
+ def test_dispatch_get_repository_info_no_repo(self, registry):
146
+ """Should raise ToolError when no repo is indexed."""
147
+ from code_graph_builder.mcp.tools import ToolError
148
+
149
+ with pytest.raises(ToolError):
150
+ self._simulate_call_tool(registry, "get_repository_info", {})
151
+
152
+ def test_dispatch_get_repository_info_with_repo(self, indexed_registry):
153
+ result = self._simulate_call_tool(
154
+ indexed_registry, "get_repository_info", {}
155
+ )
156
+ assert isinstance(result, dict)
157
+ assert "repo_name" in result or "repo_path" in result or "status" in result
158
+
159
+ def test_dispatch_result_is_json_serializable(self, indexed_registry):
160
+ result = self._simulate_call_tool(
161
+ indexed_registry, "list_repositories", {}
162
+ )
163
+ # Should not raise
164
+ json.dumps(result, default=str)
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # ToolError propagation
169
+ # ---------------------------------------------------------------------------
170
+
171
+
172
+ class TestToolErrorHandling:
173
+ """Verify ToolError is properly raised and structured."""
174
+
175
+ def test_require_active_raises_toolerror(self, registry):
176
+ from code_graph_builder.mcp.tools import ToolError
177
+
178
+ # Tools that require an active repo and take no required args
179
+ tools_no_args = [
180
+ "get_repository_info",
181
+ "list_wiki_pages", "list_api_interfaces",
182
+ "list_api_docs",
183
+ ]
184
+ for tool_name in tools_no_args:
185
+ with pytest.raises(ToolError):
186
+ _run(registry.get_handler(tool_name)())
187
+
188
+ # semantic_search requires query arg — should still raise ToolError (no repo)
189
+ with pytest.raises(ToolError):
190
+ _run(registry.get_handler("semantic_search")(query="test"))
191
+
192
+ def test_find_api_without_embeddings_raises(self, indexed_registry):
193
+ """find_api without embeddings should raise ToolError."""
194
+ from code_graph_builder.mcp.tools import ToolError
195
+
196
+ # indexed_registry was created with skip_embed=True
197
+ with pytest.raises(ToolError):
198
+ _run(indexed_registry._handle_find_api(query="test"))
199
+
200
+ def test_switch_nonexistent_repo_raises(self, registry):
201
+ from code_graph_builder.mcp.tools import ToolError
202
+
203
+ with pytest.raises(ToolError):
204
+ _run(registry._handle_switch_repository(repo_name="nonexistent_abc"))
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # State management
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ class TestStateManagement:
213
+ """Verify repository state management."""
214
+
215
+ def test_list_repos_empty_initially(self, registry):
216
+ result = _run(registry._handle_list_repositories())
217
+ assert isinstance(result, dict)
218
+
219
+ def test_list_repos_after_index(self, indexed_registry):
220
+ result = _run(indexed_registry._handle_list_repositories())
221
+ assert isinstance(result, dict)
222
+ repos = result.get("repositories", [])
223
+ assert len(repos) > 0, "Should list the indexed repo"
224
+
225
+ def test_indexed_repo_has_entry(self, indexed_registry):
226
+ result = _run(indexed_registry._handle_list_repositories())
227
+ repos = result.get("repositories", [])
228
+ assert len(repos) > 0, "Should have at least one indexed repo"
229
+ # Check any field contains tinycc reference
230
+ repo = repos[0]
231
+ repo_str = str(repo).lower()
232
+ assert "tinycc" in repo_str or len(repos) > 0, f"Repo entry: {repo}"
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # Tool handlers (graph-only, no embedding needed)
237
+ # ---------------------------------------------------------------------------
238
+
239
+
240
+ class TestGraphOnlyTools:
241
+ """Test tools that only need a graph (no embeddings)."""
242
+
243
+ def test_list_api_interfaces(self, indexed_registry):
244
+ result = _run(indexed_registry._handle_list_api_interfaces())
245
+ assert isinstance(result, dict)
246
+
247
+ def test_list_api_docs(self, indexed_registry):
248
+ result = _run(indexed_registry._handle_list_api_docs())
249
+ assert isinstance(result, (dict, str))
250
+
251
+ def test_get_api_doc_known_function(self, indexed_registry):
252
+ """Should return API doc for a function that exists."""
253
+ from code_graph_builder.mcp.tools import ToolError
254
+
255
+ # First get a real qualified name from list_api_interfaces
256
+ apis = _run(indexed_registry._handle_list_api_interfaces())
257
+ # Find any function qn from the result
258
+ qn = None
259
+ for item in apis.get("interfaces", apis.get("functions", [])):
260
+ if isinstance(item, dict) and item.get("qualified_name"):
261
+ qn = item["qualified_name"]
262
+ break
263
+ if qn is None:
264
+ pytest.skip("No APIs found to test get_api_doc")
265
+
266
+ try:
267
+ result = _run(indexed_registry._handle_get_api_doc(qualified_name=qn))
268
+ assert result is not None
269
+ except ToolError:
270
+ pass # Acceptable if doc file doesn't match exactly
271
+
272
+ def test_list_wiki_pages_no_wiki(self, indexed_registry):
273
+ """Wiki was skipped, should handle gracefully."""
274
+ from code_graph_builder.mcp.tools import ToolError
275
+
276
+ try:
277
+ result = _run(indexed_registry._handle_list_wiki_pages())
278
+ assert isinstance(result, (dict, list))
279
+ except ToolError:
280
+ pass # Acceptable if wiki not generated
281
+
282
+ def test_get_code_snippet(self, indexed_registry):
283
+ """get_code_snippet should return source or raise ToolError."""
284
+ from code_graph_builder.mcp.tools import ToolError
285
+
286
+ # Use a function known to exist in the graph
287
+ try:
288
+ result = _run(indexed_registry._handle_get_code_snippet(
289
+ qualified_name="tinycc.tcc.tcc_compile"
290
+ ))
291
+ assert result is not None
292
+ except ToolError as e:
293
+ # ToolError with "Not found" is acceptable behavior
294
+ assert "Not found" in str(e) or "error" in str(e)
295
+
296
+ def test_generate_api_docs_standalone(self, indexed_registry):
297
+ result = _run(indexed_registry._handle_generate_api_docs(rebuild=False))
298
+ assert isinstance(result, dict)
@@ -0,0 +1,190 @@
1
+ """End-to-end user flow test: simulates what happens after a user installs
2
+ the MCP server and starts using it with a real codebase.
3
+
4
+ Flow:
5
+ 1. User starts MCP server (list_tools)
6
+ 2. User indexes a repo (initialize_repository)
7
+ 3. User queries APIs (find_api, list_api_docs, get_api_doc)
8
+ 4. User switches context (list_repositories, get_repository_info)
9
+ 5. User browses docs (list_api_interfaces)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import json
16
+ import os
17
+ from pathlib import Path
18
+
19
+ import pytest
20
+
21
+ TINYCC_PATH = Path(__file__).resolve().parents[3] / "tinycc"
22
+
23
+ pytestmark = [
24
+ pytest.mark.skipif(
25
+ not TINYCC_PATH.exists(),
26
+ reason=f"tinycc source not found at {TINYCC_PATH}",
27
+ ),
28
+ pytest.mark.skipif(
29
+ not os.environ.get("DASHSCOPE_API_KEY"),
30
+ reason="DASHSCOPE_API_KEY not set",
31
+ ),
32
+ ]
33
+
34
+
35
+ def _run(coro):
36
+ return asyncio.get_event_loop().run_until_complete(coro)
37
+
38
+
39
+ @pytest.fixture(scope="module")
40
+ def workspace(tmp_path_factory):
41
+ return tmp_path_factory.mktemp("user_workspace")
42
+
43
+
44
+ @pytest.fixture(scope="module")
45
+ def registry(workspace):
46
+ from code_graph_builder.mcp.tools import MCPToolsRegistry
47
+
48
+ reg = MCPToolsRegistry(workspace=workspace)
49
+ yield reg
50
+ reg.close()
51
+
52
+
53
+ def _call(registry, tool_name: str, args: dict | None = None):
54
+ """Simulate MCP call_tool: dispatch → handler → JSON serialize → parse."""
55
+ handler = registry.get_handler(tool_name)
56
+ assert handler is not None, f"Tool '{tool_name}' not found"
57
+ result = _run(handler(**(args or {})))
58
+ # Round-trip through JSON like the real MCP server does
59
+ text = json.dumps(result, ensure_ascii=False, default=str)
60
+ return json.loads(text)
61
+
62
+
63
+ # The tests below MUST run in order — each step depends on the previous.
64
+ # pytest-ordering is not needed; pytest preserves definition order within a class.
65
+
66
+
67
+ class TestUserFlow:
68
+ """Simulates the complete user journey after MCP installation."""
69
+
70
+ # --- Step 1: Discovery ---
71
+
72
+ def test_01_list_tools(self, registry):
73
+ """User's MCP client calls list_tools on first connect."""
74
+ tools = registry.tools()
75
+ names = [t.name for t in tools]
76
+ assert len(names) >= 10, f"Expected many tools, got {len(names)}"
77
+ assert "initialize_repository" in names
78
+ assert "find_api" in names
79
+ print(f" → {len(names)} tools available")
80
+
81
+ # --- Step 2: Index repository ---
82
+
83
+ def test_02_initialize_repository(self, registry):
84
+ """User says: 'Index /path/to/tinycc'."""
85
+ result = _call(registry, "initialize_repository", {
86
+ "repo_path": str(TINYCC_PATH),
87
+ "rebuild": True,
88
+ "skip_wiki": True, # Skip wiki to save time
89
+ "skip_embed": False, # Need embeddings for find_api
90
+ })
91
+ assert result["status"] == "success", f"Init failed: {result}"
92
+ print(f" → Indexed: {result.get('graph', {})}")
93
+
94
+ # --- Step 3: Check repo info ---
95
+
96
+ def test_03_get_repository_info(self, registry):
97
+ """User asks: 'What repo is active?'"""
98
+ result = _call(registry, "get_repository_info")
99
+ assert "tinycc" in str(result).lower() or "repo" in str(result).lower()
100
+ print(f" → Repo info keys: {list(result.keys())}")
101
+
102
+ def test_04_list_repositories(self, registry):
103
+ """User asks: 'What repos have I indexed?'"""
104
+ result = _call(registry, "list_repositories")
105
+ repos = result.get("repositories", [])
106
+ assert len(repos) >= 1
107
+ print(f" → {len(repos)} repo(s) indexed")
108
+
109
+ # --- Step 4: Browse API documentation ---
110
+
111
+ def test_05_list_api_docs_index(self, registry):
112
+ """User asks: 'Show me the API docs overview.'"""
113
+ result = _call(registry, "list_api_docs")
114
+ # Should return L1 index content
115
+ assert result is not None
116
+ content = str(result)
117
+ assert "module" in content.lower() or "模块" in content
118
+ print(f" → Index returned ({len(content)} chars)")
119
+
120
+ def test_06_list_api_interfaces(self, registry):
121
+ """User asks: 'What public APIs are available?'"""
122
+ result = _call(registry, "list_api_interfaces")
123
+ assert isinstance(result, dict)
124
+ print(f" → API interfaces keys: {list(result.keys())}")
125
+
126
+ # --- Step 5: Semantic search ---
127
+
128
+ def test_07_find_api_compile(self, registry):
129
+ """User asks: 'Find APIs related to compiling source code.'"""
130
+ result = _call(registry, "find_api", {"query": "compile source code", "top_k": 5})
131
+ assert result["result_count"] > 0
132
+ assert result["api_docs_available"] is True
133
+ top = result["results"][0]
134
+ assert top["qualified_name"]
135
+ assert top["score"] > 0
136
+ print(f" → Top result: {top['qualified_name']} (score={top['score']:.3f})")
137
+
138
+ def test_08_find_api_parse(self, registry):
139
+ """User asks: 'How does expression parsing work?'"""
140
+ result = _call(registry, "find_api", {"query": "parse expression", "top_k": 5})
141
+ assert result["result_count"] > 0
142
+ # At least one result should have an API doc attached
143
+ with_doc = sum(1 for r in result["results"] if r.get("api_doc"))
144
+ print(f" → {result['result_count']} results, {with_doc} with API docs")
145
+
146
+ def test_09_find_api_chinese(self, registry):
147
+ """User asks in Chinese: '内存分配相关的函数'."""
148
+ result = _call(registry, "find_api", {"query": "内存分配", "top_k": 3})
149
+ assert result["result_count"] > 0
150
+ print(f" → Chinese query returned {result['result_count']} results")
151
+
152
+ # --- Step 6: Verify API doc content quality ---
153
+
154
+ def test_10_api_doc_has_signature(self, registry):
155
+ """API docs attached to search results should have C signatures."""
156
+ result = _call(registry, "find_api", {"query": "compile", "top_k": 10})
157
+ for r in result["results"]:
158
+ doc = r.get("api_doc") or ""
159
+ if "签名:" in doc and "(" in doc:
160
+ print(f" → Found signature in: {r['qualified_name']}")
161
+ return
162
+ # Acceptable if signatures exist in some results
163
+ assert result["result_count"] > 0
164
+
165
+ def test_11_api_doc_has_call_tree(self, registry):
166
+ """API docs should include call relationship info."""
167
+ result = _call(registry, "find_api", {"query": "generate code output", "top_k": 10})
168
+ for r in result["results"]:
169
+ doc = r.get("api_doc") or ""
170
+ if "被调用" in doc or "调用树" in doc:
171
+ print(f" → Call info in: {r['qualified_name']}")
172
+ return
173
+ assert result["result_count"] > 0
174
+
175
+ # --- Step 7: Full round-trip JSON serialization ---
176
+
177
+ def test_12_all_results_json_serializable(self, registry):
178
+ """Every tool result must survive JSON round-trip (MCP requirement)."""
179
+ test_calls = [
180
+ ("list_repositories", {}),
181
+ ("get_repository_info", {}),
182
+ ("list_api_docs", {}),
183
+ ("list_api_interfaces", {}),
184
+ ("find_api", {"query": "function", "top_k": 2}),
185
+ ]
186
+ for tool_name, args in test_calls:
187
+ result = _call(registry, tool_name, args)
188
+ # _call already does JSON round-trip; if we get here, it worked
189
+ assert result is not None, f"{tool_name} returned None"
190
+ print(" → All 5 tools passed JSON round-trip")