iflow-mcp_galaxyxieyu_api-auto-test 0.1.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.
@@ -0,0 +1,603 @@
1
+ """
2
+ Testcase CRUD Tools
3
+ 测试用例读写工具
4
+ """
5
+
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, Literal
9
+
10
+ from mcp.server.fastmcp import FastMCP
11
+ from pydantic import ValidationError
12
+
13
+ from atf.case_generator import CaseGenerator
14
+ from atf.core.log_manager import log
15
+ from atf.mcp.models import (
16
+ GenerateResponse,
17
+ GetTestcaseResponse,
18
+ ListTestcasesResponse,
19
+ ReadTestcaseResponse,
20
+ DeleteTestcaseResponse,
21
+ TestcaseModel,
22
+ ValidateTestcaseResponse,
23
+ )
24
+ from atf.mcp.utils import (
25
+ build_testcase_summary,
26
+ build_testcase_yaml,
27
+ build_error_payload,
28
+ contains_chinese,
29
+ format_validation_error,
30
+ load_yaml_file,
31
+ log_tool_call,
32
+ new_request_id,
33
+ parse_testcase_input,
34
+ resolve_tests_root,
35
+ resolve_yaml_path,
36
+ expected_py_path,
37
+ yaml,
38
+ )
39
+
40
+
41
+ def register_testcase_tools(mcp: FastMCP) -> None:
42
+ """注册测试用例相关工具"""
43
+
44
+ @mcp.tool(
45
+ name="list_testcases",
46
+ title="列出测试用例 YAML",
47
+ description="列出指定目录下的 YAML 测试用例文件,支持以下类型:\n\n"
48
+ "- **集成测试 (testcase)**: API 接口测试,YAML 顶层为 `testcase`\n"
49
+ "- **单元测试 (unittest)**: 单元测试,YAML 顶层为 `unittest`\n\n"
50
+ "**参数说明**:\n"
51
+ "- `root_path`: 可选,指定扫描目录,默认扫描 workspace/tests\n"
52
+ "- `test_type`: `all`(全部) | `integration`(集成) | `unit`(单元)\n"
53
+ "- `workspace`: **必须**,指定项目根目录\n\n"
54
+ "【给 AI 助手的强制规则】\n"
55
+ "- 只要用户想“创建/修改/生成用例”,不要在对话里手写完整 YAML 作为最终结果;应调用 `write_testcase` 写入到 `tests/cases/` 并生成 `tests/scripts/` 脚本。\n"
56
+ "- 当你不确定 YAML 是否存在时,优先调用本工具列出用例,再决定是更新还是新建。\n\n"
57
+ "示例:\n"
58
+ "```json\n"
59
+ "{\n"
60
+ " \"root_path\": \"tests/cases\",\n"
61
+ " \"test_type\": \"integration\",\n"
62
+ " \"workspace\": \"/Volumes/DATABASE/code/glam-cart/backend\"\n"
63
+ "}\n"
64
+ "```",
65
+ )
66
+ def list_testcases(
67
+ root_path: str | None = None,
68
+ test_type: Literal["all", "integration", "unit"] = "all",
69
+ workspace: str | None = None,
70
+ ) -> ListTestcasesResponse:
71
+ request_id = new_request_id()
72
+ start_time = time.perf_counter()
73
+ try:
74
+ base_dir, repo_root = resolve_tests_root(root_path, workspace)
75
+ all_yaml_files = list(base_dir.rglob("*.yaml"))
76
+
77
+ if test_type == "all":
78
+ testcases = sorted(
79
+ path.relative_to(repo_root).as_posix()
80
+ for path in all_yaml_files
81
+ )
82
+ else:
83
+ testcases = []
84
+ for path in all_yaml_files:
85
+ try:
86
+ data = load_yaml_file(path)
87
+ is_unit = "unittest" in data
88
+ is_integration = "testcase" in data
89
+
90
+ if test_type == "unit" and is_unit:
91
+ testcases.append(path.relative_to(repo_root).as_posix())
92
+ elif test_type == "integration" and is_integration:
93
+ testcases.append(path.relative_to(repo_root).as_posix())
94
+ except Exception:
95
+ continue
96
+ testcases = sorted(testcases)
97
+
98
+ response = ListTestcasesResponse(
99
+ status="ok",
100
+ request_id=request_id,
101
+ testcases=testcases,
102
+ )
103
+ except ValueError as exc:
104
+ log.error(f"MCP 列出测试用例参数验证失败: {exc}")
105
+ payload = build_error_payload(
106
+ code="MCP_INVALID_PATH",
107
+ message=str(exc),
108
+ retryable=False,
109
+ details={"error_type": "value_error"},
110
+ )
111
+ response = ListTestcasesResponse(
112
+ status="error",
113
+ request_id=request_id,
114
+ testcases=[],
115
+ **payload,
116
+ )
117
+ except Exception as exc:
118
+ log.error(f"MCP 列出测试用例失败: {exc}")
119
+ payload = build_error_payload(
120
+ code="MCP_LIST_TESTCASES_ERROR",
121
+ message=f"未知错误: {type(exc).__name__}: {str(exc)}",
122
+ retryable=False,
123
+ details={"error_type": "unknown_error", "exception_type": type(exc).__name__},
124
+ )
125
+ response = ListTestcasesResponse(
126
+ status="error",
127
+ request_id=request_id,
128
+ testcases=[],
129
+ **payload,
130
+ )
131
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
132
+ log_tool_call(
133
+ "list_testcases",
134
+ request_id,
135
+ response.status,
136
+ latency_ms,
137
+ response.error_code,
138
+ meta={"root_path": root_path, "test_type": test_type},
139
+ )
140
+ return response
141
+
142
+ @mcp.tool(
143
+ name="get_testcase",
144
+ title="获取测试用例",
145
+ description="获取指定 YAML 测试用例内容,同时返回校验结果(整合读取 + 校验)。\n\n"
146
+ "**功能特点**:\n"
147
+ "- 读取测试用例内容(摘要或完整)\n"
148
+ "- 自动校验用例结构是否规范\n"
149
+ "- 返回内容与校验状态,一次调用获取全部信息\n\n"
150
+ "**参数说明**:\n"
151
+ "- `yaml_path`: YAML 文件路径(相对于 workspace)\n"
152
+ "- `mode`: `summary`(摘要,只返回 name/steps/teardowns) | `full`(完整,返回原始 YAML)\n"
153
+ "- `workspace`: **必须**,指定项目根目录\n\n"
154
+ "**返回值说明**:\n"
155
+ "- `is_valid`: 是否通过校验\n"
156
+ "- `errors`: 校验错误列表(空数组表示通过)\n\n"
157
+ "【给 AI 助手的强制规则】\n"
158
+ "- 当用户要“修改现有用例”时,必须先调用本工具读取(建议 mode=summary)确认现状,再调用 `write_testcase` 覆盖写入;不要凭空臆造现有 YAML。\n"
159
+ "- 除非用户明确要求展示完整 YAML,否则优先用 summary,避免在对话中输出大段 YAML。\n\n"
160
+ "示例:\n"
161
+ "```json\n"
162
+ "{\n"
163
+ " \"yaml_path\": \"tests/cases/auth_integration.yaml\",\n"
164
+ " \"mode\": \"summary\",\n"
165
+ " \"workspace\": \"/Volumes/DATABASE/code/glam-cart/backend\"\n"
166
+ "}\n"
167
+ "```",
168
+ )
169
+ def get_testcase(
170
+ yaml_path: str,
171
+ mode: Literal["summary", "full"] = "summary",
172
+ workspace: str | None = None,
173
+ ) -> GetTestcaseResponse:
174
+ """获取测试用例内容并校验结构"""
175
+ validation_errors: list[str] = []
176
+ testcase_content: dict[str, Any] | None = None
177
+ request_id = new_request_id()
178
+ start_time = time.perf_counter()
179
+
180
+ try:
181
+ yaml_full_path, yaml_relative_path, _ = resolve_yaml_path(yaml_path, workspace)
182
+ raw_data = load_yaml_file(yaml_full_path)
183
+
184
+ # 尝试解析和校验
185
+ try:
186
+ testcase_model = parse_testcase_input(raw_data)
187
+ if mode == "summary":
188
+ testcase_content = build_testcase_summary(testcase_model)
189
+ else:
190
+ testcase_content = raw_data
191
+ validation_errors = []
192
+ is_valid = True
193
+ except ValidationError as exc:
194
+ validation_errors = format_validation_error(exc)
195
+ is_valid = False
196
+ if mode == "summary":
197
+ testcase_content = None
198
+ else:
199
+ testcase_content = raw_data
200
+
201
+ response = GetTestcaseResponse(
202
+ status="ok" if is_valid else "error",
203
+ request_id=request_id,
204
+ yaml_path=yaml_relative_path,
205
+ mode=mode,
206
+ testcase=testcase_content,
207
+ is_valid=is_valid,
208
+ errors=validation_errors,
209
+ error_code=None if is_valid else "MCP_TESTCASE_INVALID",
210
+ retryable=False if not is_valid else None,
211
+ )
212
+
213
+ except ValidationError as exc:
214
+ log.error(f"MCP 获取测试用例参数验证失败: {exc}")
215
+ payload = build_error_payload(
216
+ code="MCP_VALIDATION_ERROR",
217
+ message=f"参数验证失败: {exc}",
218
+ retryable=False,
219
+ details={"error_type": "validation_error", "details": format_validation_error(exc)},
220
+ )
221
+ response = GetTestcaseResponse(
222
+ status="error",
223
+ request_id=request_id,
224
+ yaml_path=yaml_path,
225
+ mode=mode,
226
+ testcase=None,
227
+ is_valid=False,
228
+ errors=format_validation_error(exc),
229
+ **payload,
230
+ )
231
+ except Exception as exc:
232
+ log.error(f"MCP 获取测试用例失败: {exc}")
233
+ payload = build_error_payload(
234
+ code="MCP_GET_TESTCASE_ERROR",
235
+ message=f"未知错误: {type(exc).__name__}: {str(exc)}",
236
+ retryable=False,
237
+ details={"error_type": "unknown_error", "exception_type": type(exc).__name__},
238
+ )
239
+ response = GetTestcaseResponse(
240
+ status="error",
241
+ request_id=request_id,
242
+ yaml_path=yaml_path,
243
+ mode=mode,
244
+ testcase=None,
245
+ is_valid=False,
246
+ errors=[str(exc)],
247
+ **payload,
248
+ )
249
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
250
+ log_tool_call(
251
+ "get_testcase",
252
+ request_id,
253
+ response.status,
254
+ latency_ms,
255
+ response.error_code,
256
+ meta={"yaml_path": yaml_path, "mode": mode},
257
+ )
258
+ return response
259
+
260
+ @mcp.tool(
261
+ name="write_testcase",
262
+ title="写入/生成测试用例",
263
+ description="写入 YAML 测试用例并生成 pytest 脚本,或仅重新生成已存在 YAML 对应的 pytest 脚本。\n\n"
264
+ "【给 AI 助手的强制规则(非常重要)】\n"
265
+ "- 只要用户的意图是“创建/修改/生成 YAML 用例”,你必须调用本工具完成落盘与脚本生成;不要在对话里直接手写完整 YAML 作为最终交付物。\n"
266
+ "- 只有当工具不可用/调用失败,或用户明确要求“只给 YAML 文本、不调用工具”时,才允许在对话里输出 YAML。\n"
267
+ "- 每次调用都必须显式传 `workspace`(项目根目录)。\n\n"
268
+ "**命名规范**:\n"
269
+ "- `name` 字段**不能使用中文**,必须使用英文命名\n"
270
+ "- `description` 字段可以使用中文描述\n\n"
271
+ "**两种模式**:\n"
272
+ "1. **写入模式**(传入 testcase): 创建/更新 YAML 文件并生成 pytest 脚本\n"
273
+ "2. **重新生成模式**(不传 testcase): 仅基于已存在的 YAML 重新生成 pytest 脚本\n\n"
274
+ "**重要提醒**:\n"
275
+ "- 必须传递 `workspace` 参数指定项目根目录\n"
276
+ "- **强烈建议**传入 `host` 参数指定 API 服务地址,否则需要配置全局变量\n\n"
277
+ "**testcase 完整格式**:\n"
278
+ "```json\n"
279
+ "{\n"
280
+ " \"name\": \"test_user_login\", // 必须使用英文,不能包含中文\n"
281
+ " \"description\": \"用户登录测试\", // 描述可以使用中文\n"
282
+ " \"host\": \"http://localhost:8000\",\n"
283
+ " \"steps\": [\n"
284
+ " {\n"
285
+ " \"id\": \"step1\",\n"
286
+ " \"method\": \"POST\",\n"
287
+ " \"path\": \"/api/users/login\",\n"
288
+ " \"data\": {\"username\": \"testuser\", \"password\": \"testpass\"},\n"
289
+ " \"headers\": {\"Content-Type\": \"application/json\"},\n"
290
+ " \"assert\": [\n"
291
+ " {\"type\": \"status_code\", \"expected\": 200},\n"
292
+ " {\"type\": \"equals\", \"field\": \"data.code\", \"expected\": 0}\n"
293
+ " ]\n"
294
+ " }\n"
295
+ " ]\n"
296
+ "}\n"
297
+ "```\n\n"
298
+ "**参数说明**:\n"
299
+ "- `yaml_path`: YAML 文件路径(相对于 workspace),**必须**\n"
300
+ "- `testcase`: 可选,测试用例数据,不传则仅重新生成 py\n"
301
+ "- `overwrite`: 默认 true,覆盖已存在的文件\n"
302
+ "- `dry_run`: 默认 false,设为 true 时仅预览生成的代码,不实际写入文件\n"
303
+ "- `workspace`: **必须**,指定项目根目录\n\n"
304
+ "**返回值增强**:\n"
305
+ "- `name_mapping`: 名称转换信息 {original, safe, class}\n"
306
+ "- `syntax_valid`: 生成代码是否通过语法校验\n"
307
+ "- `syntax_errors`: 语法错误列表\n"
308
+ "- `code_preview`: dry_run 模式下的代码预览\n\n"
309
+ "**目录结构**:\n"
310
+ "- YAML 文件保存在: `tests/cases/`\n"
311
+ "- py 脚本生成在: `tests/scripts/`\n\n"
312
+ "**示例**:\n"
313
+ "```json\n"
314
+ "# 写入 + 生成\n"
315
+ "{\n"
316
+ " \"yaml_path\": \"tests/cases/auth_test.yaml\",\n"
317
+ " \"testcase\": {\n"
318
+ " \"name\": \"test_user_login\",\n"
319
+ " \"description\": \"用户登录测试\",\n"
320
+ " \"steps\": [...]\n"
321
+ " },\n"
322
+ " \"workspace\": \"/Volumes/DATABASE/code/glam-cart/backend\"\n"
323
+ "}\n\n"
324
+ "# 重新生成 py(当 YAML 已存在时)\n"
325
+ "{\n"
326
+ " \"yaml_path\": \"tests/cases/auth_test.yaml\",\n"
327
+ " \"workspace\": \"/Volumes/DATABASE/code/glam-cart/backend\"\n"
328
+ "}\n"
329
+ "```\n\n"
330
+ "**提示**: 如果测试用例需要访问特定的 API 服务器,请务必在 `host` 字段中填写完整地址(如 `http://localhost:8000`)。如果不指定 `host`,测试将依赖项目的全局环境配置。",
331
+ )
332
+ def write_testcase(
333
+ yaml_path: str,
334
+ testcase: TestcaseModel | dict | str | None = None,
335
+ overwrite: bool = True,
336
+ dry_run: bool = False,
337
+ workspace: str | None = None,
338
+ ) -> GenerateResponse:
339
+ request_id = new_request_id()
340
+ start_time = time.perf_counter()
341
+ try:
342
+ yaml_full_path, yaml_relative_path, repo_root = resolve_yaml_path(yaml_path, workspace)
343
+
344
+ # 判断执行模式
345
+ is_write_mode = testcase is not None
346
+
347
+ if is_write_mode:
348
+ # ========== 写入模式 ==========
349
+ testcase_model = parse_testcase_input(testcase)
350
+
351
+ # 验证 name 字段不能包含中文
352
+ if contains_chinese(testcase_model.name):
353
+ raise ValueError(
354
+ f"测试用例 name 字段不能包含中文字符: '{testcase_model.name}'\n"
355
+ "请使用英文命名,例如: test_user_login, get_product_list"
356
+ )
357
+ else:
358
+ # ========== 重新生成模式 ==========
359
+ if not yaml_full_path.exists():
360
+ raise ValueError(f"YAML 文件不存在: {yaml_relative_path}")
361
+ yaml_data = load_yaml_file(yaml_full_path)
362
+ testcase_model = parse_testcase_input(yaml_data)
363
+ log.info(f"[MCP] 重新生成模式: 读取现有 YAML 文件")
364
+
365
+ # 检查路径是否存在
366
+ if not repo_root.exists() or not repo_root.is_dir():
367
+ raise ValueError(f"工作目录不存在: {repo_root}")
368
+
369
+ # 写入模式且非 dry_run:检查文件存在性
370
+ if is_write_mode and not dry_run:
371
+ if yaml_full_path.exists() and not overwrite:
372
+ raise ValueError("YAML 文件已存在,未开启覆盖写入")
373
+
374
+ # 写入模式且非 dry_run:写入 YAML
375
+ if is_write_mode and not dry_run:
376
+ yaml_full_path.parent.mkdir(parents=True, exist_ok=True)
377
+ test_data = build_testcase_yaml(testcase_model)
378
+ with yaml_full_path.open("w", encoding="utf-8") as file:
379
+ yaml.safe_dump(test_data, file, allow_unicode=True, sort_keys=False)
380
+
381
+ # base_dir 应该是 cases 目录,这样相对路径计算才正确
382
+ base_dir = str(repo_root / "tests" / "cases")
383
+ yaml_absolute_path = str(yaml_full_path)
384
+ output_dir = str(repo_root / "tests" / "scripts")
385
+
386
+ log.info(f"[MCP] write_testcase: mode={'写入' if is_write_mode else '重新生成'}, dry_run={dry_run}")
387
+
388
+ # 使用新的 generate_single 方法
389
+ result = CaseGenerator().generate_single(
390
+ yaml_file=yaml_absolute_path,
391
+ output_dir=output_dir,
392
+ base_dir=base_dir,
393
+ dry_run=dry_run
394
+ )
395
+
396
+ if not result["success"]:
397
+ payload = build_error_payload(
398
+ code="MCP_GENERATION_FAILED",
399
+ message=str(result.get("error")),
400
+ retryable=False,
401
+ details={
402
+ "error_type": "generation_failed",
403
+ "name_mapping": result.get("name_mapping"),
404
+ "syntax_errors": result.get("syntax_errors"),
405
+ },
406
+ )
407
+ response = GenerateResponse(
408
+ status="error",
409
+ request_id=request_id,
410
+ written_files=[],
411
+ name_mapping=result.get("name_mapping"),
412
+ syntax_valid=result.get("syntax_valid"),
413
+ syntax_errors=result.get("syntax_errors"),
414
+ code_preview=result.get("code_preview"),
415
+ **payload,
416
+ )
417
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
418
+ log_tool_call(
419
+ "write_testcase",
420
+ request_id,
421
+ response.status,
422
+ latency_ms,
423
+ response.error_code,
424
+ meta={"yaml_path": yaml_path, "dry_run": dry_run},
425
+ )
426
+ return response
427
+
428
+ py_relative_path = str(Path(result["file_path"]).relative_to(repo_root)) if result["file_path"] else None
429
+
430
+ response = GenerateResponse(
431
+ status="ok",
432
+ request_id=request_id,
433
+ written_files=[yaml_relative_path, py_relative_path] if not dry_run else [],
434
+ name_mapping=result.get("name_mapping"),
435
+ syntax_valid=result.get("syntax_valid"),
436
+ syntax_errors=result.get("syntax_errors"),
437
+ dry_run=dry_run,
438
+ code_preview=result.get("code_preview") if dry_run else None
439
+ )
440
+ except ValidationError as exc:
441
+ log.error(f"MCP 写入测试用例参数验证失败: {exc}")
442
+ error_details = {
443
+ "error_type": "validation_error",
444
+ "message": "参数格式错误,请检查以下字段:",
445
+ "details": format_validation_error(exc),
446
+ "hints": [
447
+ "assert.type 应该是: equals, not_equals, contains",
448
+ "assert.field 应该为具体的响应字段名,如 status, result 等",
449
+ "step.method 应该是: GET, POST, PUT, DELETE, PATCH 等 HTTP 方法"
450
+ ]
451
+ }
452
+ payload = build_error_payload(
453
+ code="MCP_VALIDATION_ERROR",
454
+ message=f"参数验证失败: {exc}",
455
+ retryable=False,
456
+ details=error_details,
457
+ )
458
+ response = GenerateResponse(
459
+ status="error",
460
+ request_id=request_id,
461
+ written_files=[],
462
+ **payload,
463
+ )
464
+ except ValueError as exc:
465
+ log.error(f"MCP 写入测试用例业务验证失败: {exc}")
466
+ payload = build_error_payload(
467
+ code="MCP_VALUE_ERROR",
468
+ message=str(exc),
469
+ retryable=False,
470
+ details={"error_type": "value_error", "message": str(exc)},
471
+ )
472
+ response = GenerateResponse(
473
+ status="error",
474
+ request_id=request_id,
475
+ written_files=[],
476
+ **payload,
477
+ )
478
+ except Exception as exc:
479
+ log.error(f"MCP 写入测试用例失败: {exc}")
480
+ payload = build_error_payload(
481
+ code="MCP_WRITE_TESTCASE_ERROR",
482
+ message=f"未知错误: {type(exc).__name__}: {str(exc)}",
483
+ retryable=False,
484
+ details={"error_type": "unknown_error", "exception_type": type(exc).__name__},
485
+ )
486
+ response = GenerateResponse(
487
+ status="error",
488
+ request_id=request_id,
489
+ written_files=[],
490
+ **payload,
491
+ )
492
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
493
+ log_tool_call(
494
+ "write_testcase",
495
+ request_id,
496
+ response.status,
497
+ latency_ms,
498
+ response.error_code,
499
+ meta={"yaml_path": yaml_path, "dry_run": dry_run},
500
+ )
501
+ return response
502
+
503
+ @mcp.tool(
504
+ name="delete_testcase",
505
+ title="删除测试用例",
506
+ description="删除 YAML 与对应的 pytest 文件。\n\n"
507
+ "**注意**:\n"
508
+ "- 删除操作不可恢复,请确认后再执行\n"
509
+ "- 默认同时删除 YAML 和生成的 py 文件\n\n"
510
+ "**参数说明**:\n"
511
+ "- `yaml_path`: YAML 文件路径(相对于 workspace)\n"
512
+ "- `delete_py`: 默认 true,同时删除对应的 py 文件\n"
513
+ "- `workspace`: **必须**,指定项目根目录\n\n"
514
+ "示例:\n"
515
+ "```json\n"
516
+ "{\n"
517
+ " \"yaml_path\": \"tests/cases/auth_integration.yaml\",\n"
518
+ " \"delete_py\": true,\n"
519
+ " \"workspace\": \"/Volumes/DATABASE/code/glam-cart/backend\"\n"
520
+ "}\n"
521
+ "```",
522
+ )
523
+ def delete_testcase(
524
+ yaml_path: str,
525
+ delete_py: bool = True,
526
+ workspace: str | None = None,
527
+ ) -> DeleteTestcaseResponse:
528
+ deleted_files: list[str] = []
529
+ request_id = new_request_id()
530
+ start_time = time.perf_counter()
531
+ try:
532
+ yaml_full_path, yaml_relative_path, _ = resolve_yaml_path(yaml_path, workspace)
533
+ if not yaml_full_path.exists():
534
+ raise ValueError(f"YAML 文件不存在: {yaml_relative_path}")
535
+ raw_data = load_yaml_file(yaml_full_path)
536
+ testcase_model = parse_testcase_input(raw_data)
537
+ py_full_path, py_relative_path = expected_py_path(
538
+ yaml_full_path=yaml_full_path,
539
+ testcase_name=testcase_model.name,
540
+ workspace=workspace,
541
+ )
542
+ yaml_full_path.unlink()
543
+ deleted_files.append(yaml_relative_path)
544
+ if delete_py and py_full_path.exists():
545
+ py_full_path.unlink()
546
+ deleted_files.append(py_relative_path)
547
+ response = DeleteTestcaseResponse(
548
+ status="ok",
549
+ request_id=request_id,
550
+ deleted_files=deleted_files,
551
+ )
552
+ except ValidationError as exc:
553
+ log.error(f"MCP 删除测试用例参数验证失败: {exc}")
554
+ payload = build_error_payload(
555
+ code="MCP_VALIDATION_ERROR",
556
+ message=f"参数验证失败: {exc}",
557
+ retryable=False,
558
+ details={"error_type": "validation_error", "details": format_validation_error(exc)},
559
+ )
560
+ response = DeleteTestcaseResponse(
561
+ status="error",
562
+ request_id=request_id,
563
+ deleted_files=[],
564
+ **payload,
565
+ )
566
+ except ValueError as exc:
567
+ log.error(f"MCP 删除测试用例业务验证失败: {exc}")
568
+ payload = build_error_payload(
569
+ code="MCP_VALUE_ERROR",
570
+ message=str(exc),
571
+ retryable=False,
572
+ details={"error_type": "value_error", "message": str(exc)},
573
+ )
574
+ response = DeleteTestcaseResponse(
575
+ status="error",
576
+ request_id=request_id,
577
+ deleted_files=[],
578
+ **payload,
579
+ )
580
+ except Exception as exc:
581
+ log.error(f"MCP 删除测试用例失败: {exc}")
582
+ payload = build_error_payload(
583
+ code="MCP_DELETE_TESTCASE_ERROR",
584
+ message=f"未知错误: {type(exc).__name__}: {str(exc)}",
585
+ retryable=False,
586
+ details={"error_type": "unknown_error", "exception_type": type(exc).__name__},
587
+ )
588
+ response = DeleteTestcaseResponse(
589
+ status="error",
590
+ request_id=request_id,
591
+ deleted_files=[],
592
+ **payload,
593
+ )
594
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
595
+ log_tool_call(
596
+ "delete_testcase",
597
+ request_id,
598
+ response.status,
599
+ latency_ms,
600
+ response.error_code,
601
+ meta={"yaml_path": yaml_path, "delete_py": delete_py},
602
+ )
603
+ return response