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.
- atf/__init__.py +48 -0
- atf/assets/__init__.py +0 -0
- atf/assets/report.css +243 -0
- atf/auth.py +99 -0
- atf/case_generator.py +737 -0
- atf/conftest.py +65 -0
- atf/core/__init__.py +40 -0
- atf/core/assert_handler.py +336 -0
- atf/core/config_manager.py +111 -0
- atf/core/globals.py +52 -0
- atf/core/log_manager.py +52 -0
- atf/core/login_handler.py +60 -0
- atf/core/request_handler.py +189 -0
- atf/core/variable_resolver.py +212 -0
- atf/handlers/__init__.py +10 -0
- atf/handlers/notification_handler.py +101 -0
- atf/handlers/report_generator.py +160 -0
- atf/handlers/teardown_handler.py +106 -0
- atf/mcp/__init__.py +1 -0
- atf/mcp/executor.py +469 -0
- atf/mcp/models.py +532 -0
- atf/mcp/tools/__init__.py +1 -0
- atf/mcp/tools/health_tool.py +58 -0
- atf/mcp/tools/metrics_tools.py +132 -0
- atf/mcp/tools/runner_tools.py +380 -0
- atf/mcp/tools/testcase_tools.py +603 -0
- atf/mcp/tools/unittest_tools.py +189 -0
- atf/mcp/utils.py +376 -0
- atf/mcp_server.py +169 -0
- atf/runner.py +134 -0
- atf/unit_case_generator.py +337 -0
- atf/utils/__init__.py +2 -0
- atf/utils/helpers.py +155 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/METADATA +409 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/RECORD +37 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/WHEEL +4 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/entry_points.txt +2 -0
atf/mcp/models.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic Models for MCP Server
|
|
3
|
+
所有数据模型定义,用于输入验证和响应格式化
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def contains_chinese(text: str) -> bool:
|
|
15
|
+
"""检测字符串是否包含中文字符"""
|
|
16
|
+
if not text:
|
|
17
|
+
return False
|
|
18
|
+
return bool(re.search(r'[\u4e00-\u9fff]', str(text)))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AssertionModel(BaseModel):
|
|
22
|
+
"""断言模型"""
|
|
23
|
+
model_config = ConfigDict(extra="forbid")
|
|
24
|
+
|
|
25
|
+
type: str
|
|
26
|
+
field: str | None = None
|
|
27
|
+
expected: Any | None = None
|
|
28
|
+
container: Any | None = None
|
|
29
|
+
query: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StepModel(BaseModel):
|
|
33
|
+
"""测试步骤模型"""
|
|
34
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
path: str
|
|
38
|
+
method: str
|
|
39
|
+
headers: dict | None = None
|
|
40
|
+
data: Any | None = None
|
|
41
|
+
params: dict | None = None
|
|
42
|
+
files: dict | None = None
|
|
43
|
+
project: str | None = None
|
|
44
|
+
assert_: list[AssertionModel] | None = Field(default=None, alias="assert")
|
|
45
|
+
|
|
46
|
+
@model_validator(mode="after")
|
|
47
|
+
def validate_required(self) -> "StepModel":
|
|
48
|
+
if not self.id:
|
|
49
|
+
raise ValueError("steps.id 不能为空")
|
|
50
|
+
if not self.path:
|
|
51
|
+
raise ValueError("steps.path 不能为空")
|
|
52
|
+
if not self.method:
|
|
53
|
+
raise ValueError("steps.method 不能为空")
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TeardownModel(BaseModel):
|
|
58
|
+
"""清理步骤模型"""
|
|
59
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
60
|
+
|
|
61
|
+
id: str
|
|
62
|
+
operation_type: Literal["api", "db"]
|
|
63
|
+
path: str | None = None
|
|
64
|
+
method: str | None = None
|
|
65
|
+
headers: dict | None = None
|
|
66
|
+
data: Any | None = None
|
|
67
|
+
params: dict | None = None
|
|
68
|
+
files: dict | None = None
|
|
69
|
+
project: str | None = None
|
|
70
|
+
assert_: list[AssertionModel] | None = Field(default=None, alias="assert")
|
|
71
|
+
query: str | None = None
|
|
72
|
+
|
|
73
|
+
@model_validator(mode="after")
|
|
74
|
+
def validate_operation(self) -> "TeardownModel":
|
|
75
|
+
if self.operation_type == "api":
|
|
76
|
+
missing = []
|
|
77
|
+
if not self.path:
|
|
78
|
+
missing.append("path")
|
|
79
|
+
if not self.method:
|
|
80
|
+
missing.append("method")
|
|
81
|
+
if self.headers is None:
|
|
82
|
+
missing.append("headers")
|
|
83
|
+
if self.data is None:
|
|
84
|
+
missing.append("data")
|
|
85
|
+
if missing:
|
|
86
|
+
raise ValueError(f"teardowns.api 缺少必填字段: {', '.join(missing)}")
|
|
87
|
+
if self.operation_type == "db" and not self.query:
|
|
88
|
+
raise ValueError("teardowns.db 缺少必填字段: query")
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AllureModel(BaseModel):
|
|
93
|
+
"""Allure 报告配置模型"""
|
|
94
|
+
model_config = ConfigDict(extra="forbid")
|
|
95
|
+
|
|
96
|
+
epic: str | None = None
|
|
97
|
+
feature: str | None = None
|
|
98
|
+
story: str | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ==================== 单元测试模型 ====================
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class MockModel(BaseModel):
|
|
105
|
+
"""Mock 配置模型"""
|
|
106
|
+
model_config = ConfigDict(extra="forbid")
|
|
107
|
+
|
|
108
|
+
target: str # mock 目标路径,如 "app.services.user_service.UserRepository"
|
|
109
|
+
method: str | None = None # 方法名
|
|
110
|
+
return_value: Any | None = None # 返回值
|
|
111
|
+
side_effect: Any | None = None # 副作用(异常或可调用对象)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class UnitTestInputModel(BaseModel):
|
|
115
|
+
"""单元测试输入参数模型"""
|
|
116
|
+
model_config = ConfigDict(extra="forbid")
|
|
117
|
+
|
|
118
|
+
args: list[Any] | None = None # 位置参数
|
|
119
|
+
kwargs: dict[str, Any] | None = None # 关键字参数
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class UnitAssertionModel(BaseModel):
|
|
123
|
+
"""单元测试断言模型"""
|
|
124
|
+
model_config = ConfigDict(extra="forbid")
|
|
125
|
+
|
|
126
|
+
type: str # equals, not_equals, contains, raises, called_once, called_with, etc.
|
|
127
|
+
field: str | None = None # JSONPath 字段路径
|
|
128
|
+
expected: Any | None = None # 期望值
|
|
129
|
+
mock: str | None = None # mock 名称(用于 mock 相关断言)
|
|
130
|
+
args: list[Any] | None = None # 期望的调用参数
|
|
131
|
+
kwargs: dict[str, Any] | None = None # 期望的关键字参数
|
|
132
|
+
exception: str | None = None # 期望的异常类型
|
|
133
|
+
message: str | None = None # 期望的异常消息
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class UnitTestCaseModel(BaseModel):
|
|
137
|
+
"""单个单元测试用例模型"""
|
|
138
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
139
|
+
|
|
140
|
+
id: str
|
|
141
|
+
description: str | None = None
|
|
142
|
+
mocks: list[MockModel] | None = None
|
|
143
|
+
inputs: UnitTestInputModel | None = None
|
|
144
|
+
assert_: list[UnitAssertionModel] | None = Field(default=None, alias="assert")
|
|
145
|
+
|
|
146
|
+
@model_validator(mode="after")
|
|
147
|
+
def validate_required(self) -> "UnitTestCaseModel":
|
|
148
|
+
if not self.id:
|
|
149
|
+
raise ValueError("unittest.cases.id 不能为空")
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class UnitTestTargetModel(BaseModel):
|
|
154
|
+
"""被测目标模型"""
|
|
155
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
156
|
+
|
|
157
|
+
module: str # 被测模块路径
|
|
158
|
+
class_: str | None = Field(default=None, alias="class") # 被测类名
|
|
159
|
+
function: str | None = None # 被测函数名
|
|
160
|
+
|
|
161
|
+
@model_validator(mode="after")
|
|
162
|
+
def validate_target(self) -> "UnitTestTargetModel":
|
|
163
|
+
if not self.module:
|
|
164
|
+
raise ValueError("unittest.target.module 不能为空")
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class UnitTestFixtureModel(BaseModel):
|
|
169
|
+
"""测试夹具模型"""
|
|
170
|
+
model_config = ConfigDict(extra="forbid")
|
|
171
|
+
|
|
172
|
+
type: str # patch, cleanup, setup_db, etc.
|
|
173
|
+
target: str | None = None
|
|
174
|
+
value: Any | None = None
|
|
175
|
+
action: str | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class UnitTestFixturesModel(BaseModel):
|
|
179
|
+
"""测试夹具集合模型"""
|
|
180
|
+
model_config = ConfigDict(extra="forbid")
|
|
181
|
+
|
|
182
|
+
setup: list[UnitTestFixtureModel] | None = None
|
|
183
|
+
teardown: list[UnitTestFixtureModel] | None = None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class UnitTestModel(BaseModel):
|
|
187
|
+
"""单元测试模型"""
|
|
188
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
189
|
+
|
|
190
|
+
name: str
|
|
191
|
+
description: str | None = None
|
|
192
|
+
env_type: Literal["venv", "conda", "uv"] = "venv" # 运行环境的虚拟环境类型
|
|
193
|
+
target: UnitTestTargetModel
|
|
194
|
+
allure: AllureModel | None = None
|
|
195
|
+
cases: list[UnitTestCaseModel]
|
|
196
|
+
fixtures: UnitTestFixturesModel | None = None
|
|
197
|
+
|
|
198
|
+
@model_validator(mode="after")
|
|
199
|
+
def validate_required(self) -> "UnitTestModel":
|
|
200
|
+
if not self.name:
|
|
201
|
+
raise ValueError("unittest.name 不能为空")
|
|
202
|
+
if contains_chinese(self.name):
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"unittest.name 不能包含中文字符: '{self.name}'\n"
|
|
205
|
+
"请使用英文命名,例如: user_service_test, calculate_total_test"
|
|
206
|
+
)
|
|
207
|
+
if not self.cases:
|
|
208
|
+
raise ValueError("unittest.cases 不能为空")
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ==================== 集成测试模型 ====================
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestcaseModel(BaseModel):
|
|
216
|
+
"""集成测试用例模型"""
|
|
217
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
218
|
+
|
|
219
|
+
name: str
|
|
220
|
+
description: str | None = None
|
|
221
|
+
host: str | None = None # 可选的测试服务地址
|
|
222
|
+
allure: AllureModel | None = None
|
|
223
|
+
steps: list[StepModel]
|
|
224
|
+
teardowns: list[TeardownModel] | None = None
|
|
225
|
+
|
|
226
|
+
@model_validator(mode="after")
|
|
227
|
+
def validate_required(self) -> "TestcaseModel":
|
|
228
|
+
if not self.name:
|
|
229
|
+
raise ValueError("testcase.name 不能为空")
|
|
230
|
+
if contains_chinese(self.name):
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"testcase.name 不能包含中文字符: '{self.name}'\n"
|
|
233
|
+
"请使用英文命名,例如: test_user_login, get_product_list"
|
|
234
|
+
)
|
|
235
|
+
if not self.steps:
|
|
236
|
+
raise ValueError("testcase.steps 不能为空")
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ==================== 响应模型 ====================
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class GenerateResponse(BaseModel):
|
|
244
|
+
"""生成操作响应"""
|
|
245
|
+
model_config = ConfigDict(extra="forbid")
|
|
246
|
+
|
|
247
|
+
status: Literal["ok", "error"]
|
|
248
|
+
request_id: str | None = None
|
|
249
|
+
written_files: list[str]
|
|
250
|
+
# 名称转换信息
|
|
251
|
+
name_mapping: dict | None = None # {"original": "Health Check API", "safe": "health_check_api", "class": "HealthCheckApi"}
|
|
252
|
+
# 语法校验结果
|
|
253
|
+
syntax_valid: bool | None = None
|
|
254
|
+
syntax_errors: list[str] | None = None
|
|
255
|
+
# dry_run 模式预览
|
|
256
|
+
dry_run: bool = False
|
|
257
|
+
code_preview: str | None = None # dry_run 模式下的代码预览
|
|
258
|
+
# 错误信息
|
|
259
|
+
error_code: str | None = None
|
|
260
|
+
retryable: bool | None = None
|
|
261
|
+
error_message: str | None = None
|
|
262
|
+
error_details: dict | None = None # 更详细的错误信息,用于大模型理解
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class HealthResponse(BaseModel):
|
|
266
|
+
"""健康检查响应"""
|
|
267
|
+
model_config = ConfigDict(extra="forbid")
|
|
268
|
+
|
|
269
|
+
status: Literal["ok", "error"]
|
|
270
|
+
request_id: str | None = None
|
|
271
|
+
version: str
|
|
272
|
+
repo_root: str
|
|
273
|
+
tests_root: str
|
|
274
|
+
test_cases_root: str
|
|
275
|
+
error_code: str | None = None
|
|
276
|
+
retryable: bool | None = None
|
|
277
|
+
error_message: str | None = None
|
|
278
|
+
error_details: dict | None = None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ListTestcasesResponse(BaseModel):
|
|
282
|
+
"""列出测试用例响应"""
|
|
283
|
+
model_config = ConfigDict(extra="forbid")
|
|
284
|
+
|
|
285
|
+
status: Literal["ok", "error"]
|
|
286
|
+
request_id: str | None = None
|
|
287
|
+
testcases: list[str]
|
|
288
|
+
error_code: str | None = None
|
|
289
|
+
retryable: bool | None = None
|
|
290
|
+
error_message: str | None = None
|
|
291
|
+
error_details: dict | None = None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class ReadTestcaseResponse(BaseModel):
|
|
295
|
+
"""读取测试用例响应"""
|
|
296
|
+
model_config = ConfigDict(extra="forbid")
|
|
297
|
+
|
|
298
|
+
status: Literal["ok", "error"]
|
|
299
|
+
request_id: str | None = None
|
|
300
|
+
yaml_path: str
|
|
301
|
+
mode: Literal["summary", "full"]
|
|
302
|
+
testcase: dict[str, Any] | None
|
|
303
|
+
error_code: str | None = None
|
|
304
|
+
retryable: bool | None = None
|
|
305
|
+
error_message: str | None = None
|
|
306
|
+
error_details: dict | None = None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class GetTestcaseResponse(BaseModel):
|
|
310
|
+
"""获取测试用例响应(整合读取 + 校验)"""
|
|
311
|
+
model_config = ConfigDict(extra="forbid")
|
|
312
|
+
|
|
313
|
+
status: Literal["ok", "error"]
|
|
314
|
+
request_id: str | None = None
|
|
315
|
+
yaml_path: str
|
|
316
|
+
mode: Literal["summary", "full"]
|
|
317
|
+
testcase: dict[str, Any] | None # 测试用例内容
|
|
318
|
+
is_valid: bool # 是否通过校验
|
|
319
|
+
errors: list[str] # 校验错误列表(空表示通过)
|
|
320
|
+
error_code: str | None = None
|
|
321
|
+
retryable: bool | None = None
|
|
322
|
+
error_message: str | None = None
|
|
323
|
+
error_details: dict | None = None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ValidateTestcaseResponse(BaseModel):
|
|
327
|
+
"""校验测试用例响应(已废弃,请使用 get_testcase)"""
|
|
328
|
+
model_config = ConfigDict(extra="forbid")
|
|
329
|
+
|
|
330
|
+
status: Literal["ok", "error"]
|
|
331
|
+
request_id: str | None = None
|
|
332
|
+
errors: list[str]
|
|
333
|
+
error_code: str | None = None
|
|
334
|
+
retryable: bool | None = None
|
|
335
|
+
error_message: str | None = None
|
|
336
|
+
error_details: dict | None = None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class RegenerateResponse(BaseModel):
|
|
340
|
+
"""重新生成响应"""
|
|
341
|
+
model_config = ConfigDict(extra="forbid")
|
|
342
|
+
|
|
343
|
+
status: Literal["ok", "error"]
|
|
344
|
+
request_id: str | None = None
|
|
345
|
+
written_files: list[str]
|
|
346
|
+
error_code: str | None = None
|
|
347
|
+
retryable: bool | None = None
|
|
348
|
+
error_message: str | None = None
|
|
349
|
+
error_details: dict | None = None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class DeleteTestcaseResponse(BaseModel):
|
|
353
|
+
"""删除测试用例响应"""
|
|
354
|
+
model_config = ConfigDict(extra="forbid")
|
|
355
|
+
|
|
356
|
+
status: Literal["ok", "error"]
|
|
357
|
+
request_id: str | None = None
|
|
358
|
+
deleted_files: list[str]
|
|
359
|
+
error_code: str | None = None
|
|
360
|
+
retryable: bool | None = None
|
|
361
|
+
error_message: str | None = None
|
|
362
|
+
error_details: dict | None = None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ==================== 测试执行结果模型 ====================
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class AssertionResultModel(BaseModel):
|
|
369
|
+
"""断言结果模型"""
|
|
370
|
+
model_config = ConfigDict(extra="forbid")
|
|
371
|
+
|
|
372
|
+
assertion_type: str # 断言类型: equals, contains, status_code, etc.
|
|
373
|
+
field: str | None = None # 字段路径
|
|
374
|
+
expected: Any | None = None # 期望值
|
|
375
|
+
actual: Any | None = None # 实际值
|
|
376
|
+
passed: bool # 是否通过
|
|
377
|
+
message: str | None = None # 失败时的消息
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class TestResultModel(BaseModel):
|
|
381
|
+
"""单个测试用例执行结果"""
|
|
382
|
+
model_config = ConfigDict(extra="forbid")
|
|
383
|
+
|
|
384
|
+
test_name: str # 测试名称
|
|
385
|
+
status: Literal["passed", "failed", "error", "skipped"] # 执行状态
|
|
386
|
+
duration: float # 执行时间(秒)
|
|
387
|
+
assertions: list[AssertionResultModel] # 断言结果列表
|
|
388
|
+
error_message: str | None = None # 错误信息
|
|
389
|
+
traceback: str | None = None # 错误堆栈
|
|
390
|
+
report_path: str | None = None # HTML 报告路径
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class RunTestcaseResponse(BaseModel):
|
|
394
|
+
"""单个测试用例执行响应"""
|
|
395
|
+
model_config = ConfigDict(extra="forbid")
|
|
396
|
+
|
|
397
|
+
status: Literal["ok", "error"]
|
|
398
|
+
request_id: str | None = None
|
|
399
|
+
test_name: str
|
|
400
|
+
yaml_path: str | None = None
|
|
401
|
+
py_path: str | None = None
|
|
402
|
+
result: TestResultModel | None = None
|
|
403
|
+
error_code: str | None = None
|
|
404
|
+
retryable: bool | None = None
|
|
405
|
+
error_message: str | None = None
|
|
406
|
+
error_details: dict | None = None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class BatchRunResponse(BaseModel):
|
|
410
|
+
"""批量执行响应"""
|
|
411
|
+
model_config = ConfigDict(extra="forbid")
|
|
412
|
+
|
|
413
|
+
status: Literal["ok", "error"]
|
|
414
|
+
request_id: str | None = None
|
|
415
|
+
total: int # 总数
|
|
416
|
+
passed: int # 通过
|
|
417
|
+
failed: int # 失败
|
|
418
|
+
skipped: int # 跳过
|
|
419
|
+
duration: float # 总耗时(秒)
|
|
420
|
+
results: list[TestResultModel] # 每个测试用例的结果
|
|
421
|
+
error_code: str | None = None
|
|
422
|
+
retryable: bool | None = None
|
|
423
|
+
error_message: str | None = None
|
|
424
|
+
error_details: dict | None = None
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class RunTestsResponse(BaseModel):
|
|
428
|
+
"""统一测试执行响应(支持单用例和批量)"""
|
|
429
|
+
model_config = ConfigDict(extra="forbid")
|
|
430
|
+
|
|
431
|
+
status: Literal["ok", "error"]
|
|
432
|
+
request_id: str | None = None
|
|
433
|
+
mode: Literal["single", "batch"] # 执行模式
|
|
434
|
+
# 单用例模式字段
|
|
435
|
+
test_name: str | None = None
|
|
436
|
+
yaml_path: str | None = None
|
|
437
|
+
py_path: str | None = None
|
|
438
|
+
result: TestResultModel | None = None
|
|
439
|
+
# 批量模式字段
|
|
440
|
+
total: int | None = None
|
|
441
|
+
passed: int | None = None
|
|
442
|
+
failed: int | None = None
|
|
443
|
+
skipped: int | None = None
|
|
444
|
+
duration: float | None = None
|
|
445
|
+
results: list[TestResultModel] | None = None
|
|
446
|
+
has_failures: bool | None = None
|
|
447
|
+
# 错误信息
|
|
448
|
+
error_code: str | None = None
|
|
449
|
+
retryable: bool | None = None
|
|
450
|
+
error_message: str | None = None
|
|
451
|
+
error_details: dict | None = None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class TestResultHistoryModel(BaseModel):
|
|
455
|
+
"""测试结果历史记录"""
|
|
456
|
+
model_config = ConfigDict(extra="forbid")
|
|
457
|
+
|
|
458
|
+
run_id: str # 运行ID
|
|
459
|
+
timestamp: str # 执行时间
|
|
460
|
+
total: int
|
|
461
|
+
passed: int
|
|
462
|
+
failed: int
|
|
463
|
+
skipped: int
|
|
464
|
+
duration: float
|
|
465
|
+
test_names: list[str] # 执行的测试用例列表
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class GetTestResultsResponse(BaseModel):
|
|
469
|
+
"""获取测试执行历史响应"""
|
|
470
|
+
model_config = ConfigDict(extra="forbid")
|
|
471
|
+
|
|
472
|
+
status: Literal["ok", "error"]
|
|
473
|
+
request_id: str | None = None
|
|
474
|
+
results: list[TestResultHistoryModel]
|
|
475
|
+
error_code: str | None = None
|
|
476
|
+
retryable: bool | None = None
|
|
477
|
+
error_message: str | None = None
|
|
478
|
+
error_details: dict | None = None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class McpMetricsResponse(BaseModel):
|
|
482
|
+
"""MCP 工具调用指标响应"""
|
|
483
|
+
model_config = ConfigDict(extra="forbid")
|
|
484
|
+
|
|
485
|
+
status: Literal["ok", "error"]
|
|
486
|
+
request_id: str | None = None
|
|
487
|
+
total: int | None = None
|
|
488
|
+
success: int | None = None
|
|
489
|
+
error: int | None = None
|
|
490
|
+
success_rate: float | None = None
|
|
491
|
+
avg_latency_ms: float | None = None
|
|
492
|
+
p95_latency_ms: float | None = None
|
|
493
|
+
error_codes: dict[str, int] | None = None
|
|
494
|
+
window_minutes: int | None = None
|
|
495
|
+
error_code: str | None = None
|
|
496
|
+
retryable: bool | None = None
|
|
497
|
+
error_message: str | None = None
|
|
498
|
+
error_details: dict | None = None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
__all__ = [
|
|
502
|
+
# Models
|
|
503
|
+
"AssertionModel",
|
|
504
|
+
"StepModel",
|
|
505
|
+
"TeardownModel",
|
|
506
|
+
"AllureModel",
|
|
507
|
+
"MockModel",
|
|
508
|
+
"UnitTestInputModel",
|
|
509
|
+
"UnitAssertionModel",
|
|
510
|
+
"UnitTestCaseModel",
|
|
511
|
+
"UnitTestTargetModel",
|
|
512
|
+
"UnitTestFixtureModel",
|
|
513
|
+
"UnitTestFixturesModel",
|
|
514
|
+
"UnitTestModel",
|
|
515
|
+
"TestcaseModel",
|
|
516
|
+
# Responses
|
|
517
|
+
"GenerateResponse",
|
|
518
|
+
"HealthResponse",
|
|
519
|
+
"ListTestcasesResponse",
|
|
520
|
+
"GetTestcaseResponse",
|
|
521
|
+
"ReadTestcaseResponse",
|
|
522
|
+
"ValidateTestcaseResponse",
|
|
523
|
+
"DeleteTestcaseResponse",
|
|
524
|
+
"AssertionResultModel",
|
|
525
|
+
"TestResultModel",
|
|
526
|
+
"RunTestcaseResponse",
|
|
527
|
+
"BatchRunResponse",
|
|
528
|
+
"RunTestsResponse",
|
|
529
|
+
"TestResultHistoryModel",
|
|
530
|
+
"GetTestResultsResponse",
|
|
531
|
+
"McpMetricsResponse",
|
|
532
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MCP Tools Package
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health Check Tool
|
|
3
|
+
MCP 服务健康检查工具
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from atf.mcp.models import HealthResponse
|
|
11
|
+
from atf.mcp.utils import (
|
|
12
|
+
REPO_ROOT,
|
|
13
|
+
TESTS_ROOT,
|
|
14
|
+
TEST_CASES_ROOT,
|
|
15
|
+
new_request_id,
|
|
16
|
+
log_tool_call,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MCP_VERSION = "0.1.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register_health_tool(mcp: FastMCP) -> None:
|
|
24
|
+
"""注册健康检查工具"""
|
|
25
|
+
|
|
26
|
+
@mcp.tool(
|
|
27
|
+
name="health_check",
|
|
28
|
+
title="MCP 服务健康检查",
|
|
29
|
+
description="返回服务版本与基础路径信息。",
|
|
30
|
+
)
|
|
31
|
+
def health_check() -> HealthResponse:
|
|
32
|
+
request_id = new_request_id()
|
|
33
|
+
start_time = time.perf_counter()
|
|
34
|
+
try:
|
|
35
|
+
response = HealthResponse(
|
|
36
|
+
status="ok",
|
|
37
|
+
request_id=request_id,
|
|
38
|
+
version=MCP_VERSION,
|
|
39
|
+
repo_root=str(REPO_ROOT),
|
|
40
|
+
tests_root=str(TESTS_ROOT),
|
|
41
|
+
test_cases_root=str(TEST_CASES_ROOT),
|
|
42
|
+
)
|
|
43
|
+
except Exception as exc:
|
|
44
|
+
response = HealthResponse(
|
|
45
|
+
status="error",
|
|
46
|
+
request_id=request_id,
|
|
47
|
+
version=MCP_VERSION,
|
|
48
|
+
repo_root=str(REPO_ROOT),
|
|
49
|
+
tests_root=str(TESTS_ROOT),
|
|
50
|
+
test_cases_root=str(TEST_CASES_ROOT),
|
|
51
|
+
error_code="MCP_HEALTH_ERROR",
|
|
52
|
+
retryable=False,
|
|
53
|
+
error_message=str(exc),
|
|
54
|
+
error_details={"error_type": "health_check_failed"},
|
|
55
|
+
)
|
|
56
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
57
|
+
log_tool_call("health_check", request_id, response.status, latency_ms, response.error_code)
|
|
58
|
+
return response
|