Files
junhong_cmp_fiber/.opencode/skills/hurl-test/SKILL.md
huang 2b408230a6
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
skill
2026-03-31 10:00:01 +08:00

953 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: hurl-test
description: Hurl 接口测试生成器。用户描述要测试的接口或业务流程,自动探索代码、确认需求、生成完整的 .hurl 测试文件(含 DTO 驱动的字段完整性断言。触发词测试、hurl、写测试、接口测试。
---
# Hurl 接口测试生成器
**用户描述要测试什么,你来读代码、问确认、生成 .hurl 文件。**
适用于任何后端项目Go / Python / Node / Java 等),不预设框架和目录结构。
---
## 触发条件
以下情况必须使用本 Skill
- 用户说"测试 XX 接口"、"写 hurl 测试"、"给 XX 加测试"
- 用户说"测试 XX 流程"、"测试 XX 的业务逻辑"
- 用户说"验证 XX 接口的字段"、"测试接口契约"
- 用户提到 hurl、.hurl、接口测试、集成测试、冒烟测试
---
## 五阶段工作流(必须按顺序执行)
```
Phase 1: 探索 → Phase 2: 确认 → Phase 3: 生成 → Phase 4: 验证 → Phase 5: 研判
读代码搞清楚 展示给用户确认 输出 .hurl 文件 语法检查+试跑 分析失败/报告发现
```
> **核心原则:测试是用来发现问题的,不是用来全部通过的。**
> 断言基于代码定义Handler + DTO生成与实际行为的差异是"发现",不是要修掉的"错误"。
---
### Phase 1: 探索Explore
**目标:读代码,搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。要求准确,不求快。**
#### 真理源层级(必须遵守)
断言的字段名和类型基于以下优先级确定,**高优先级推翻低优先级**
| 优先级 | 来源 | 说明 | 怎么找 |
|-------|------|------|--------|
| **1最高** | Handler 实际实现 | Handler 返回什么类型、调用哪个 response 方法、是否做了 model→DTO 转换 | 读 handler 函数体,看 `return response.Success(c, xxx)` 中 xxx 的类型 |
| **2** | DTO / Response 结构体 | Handler 引用的 DTO 的 json tag、字段类型 | 读 DTO struct 定义 |
| **3最低** | 路由注册的 Output 声明 | RouteSpec 中声明的 Output 类型,仅作为文档参考,可能过期 | 读路由注册代码 |
**关键规则**
- **不能只读 DTO 就生成断言**。必须追溯到 Handler 确认它确实用了这个 DTO
- 如果 Handler 直接返回 GORM model 而非 DTO断言用 model 的字段名(可能是大写 `ID` 而非小写 `id`
- 如果 Handler 用了 `response.SuccessWithPagination()`,分页字段以该函数的结构为准,而非 DTO 定义的分页结构
- 当 Handler 实现与 DTO 定义不一致时,**以 Handler 为准生成断言,同时在 Phase 5 中将不一致标记为发现**
#### 1.1 项目画像(首次使用时必须执行,后续复用)
首次为项目生成 Hurl 测试时,先回答以下问题(通过读代码,不要猜):
| 问题 | 怎么找 |
|------|--------|
| **语言/框架** | 看 go.mod / package.json / requirements.txt / pom.xml |
| **路由注册在哪** | 搜索 `router``app.Get``@GetMapping``@app.route` 等关键词 |
| **请求/响应 schema 定义在哪** | 搜索 DTO / schema / serializer / model 目录,看 json tag 或装饰器 |
| **统一响应格式是什么** | 找 response helper 文件(如 `response.go``response.py`),记录 JSON 结构 |
| **认证方式是什么** | 找 auth middleware确定是 Bearer Token / Cookie / API Key / Basic Auth |
| **登录接口是什么** | 找登录 handler记录路径、请求体、响应中 token 的位置 |
| **分页格式是什么** | 找列表接口的响应结构,记录 items/total/page 等字段名 |
| **已有 hurl 测试吗** | 搜索 `*.hurl` 文件,复用已有的约定 |
将画像结果**写入 `tests/hurl/.project-profile.md` 文件持久化保存**。
#### 画像持久化(关键机制)
**首次使用时**:完成 1.1 探索后,将画像写入 `tests/hurl/.project-profile.md`,格式如下:
```markdown
# 项目画像Hurl 测试自动生成用)
<!-- 由 hurl-test skill 自动生成,请勿手动修改 -->
<!-- 如需刷新,删除此文件后重新运行 skill -->
## 技术栈
- 语言: Go 1.25
- 框架: Fiber v2
- ORM: GORM
## 路由定义位置
- 路由注册入口: internal/routes/routes.go
- 按模块拆分: internal/routes/{module}.go
- 路由注册函数: Register(router, doc, basePath, method, path, handler, spec)
## Schema 定义位置
- DTO 目录: internal/model/dto/
- 命名规则: {module}_dto.go
- 字段标签: json / validate / description
## 统一响应格式
{code: int, msg: string, data: any, timestamp: string(RFC3339)}
- 成功: code=0, msg="success"
- 错误: code!=0
## 分页格式
{items: [], total: int, page: int, size: int}
- 包裹在 data 字段内: $.data.items / $.data.total
## 认证方式
- 后台: POST /api/auth/admin-login → $.data.access_token → Authorization: Bearer {token}
- C端: JWT → Authorization: Bearer {token}
## 默认测试账号
- 用户名: admin
- 密码: Admin@123456
## 服务端口
- 默认: 3000
```
**后续使用时**:检查 `tests/hurl/.project-profile.md` 是否存在:
- **存在** → 直接读取,跳过 1.1 的探索步骤,节省时间
- **不存在** → 执行 1.1 完整探索,然后生成此文件
- **用户说"刷新画像"** → 删除旧文件,重新执行 1.1
#### 1.2 找接口定义
根据用户要测的模块,定位路由注册代码,提取:
- **HTTP 方法**GET / POST / PUT / DELETE / PATCH
- **路由路径**(含路径参数格式,如 `/users/:id``/users/{id}`
- **接口说明**注释、Summary、装饰器描述
- **是否需要认证**
- **请求 schema 类型名**Input / Request DTO
- **响应 schema 类型名**Output / Response DTO
#### 1.3 读 schema 定义DTO / struct / class / type
定位请求和响应的 schema 定义文件,提取每个字段的:
- **字段名**JSON 序列化后的名称json tag / @JsonProperty / serializer field
- **语言类型**string / int / bool / 数组 / 嵌套对象 / 可空等
- **是否必填**validate tag / required 装饰器 / 非空标注
- **是否可空**:指针类型 / Optional / nullable
- **是否参与序列化**`json:"-"` / @JsonIgnore / exclude
- **是否 omitempty**`json:",omitempty"` / 条件序列化
- **字段描述**description tag / docstring / 注释
#### 1.4 识别业务依赖
读 service / business logic 层,识别:
- 创建操作需要哪些前置数据(如创建订单需要先有商品和用户)
- 是否有唯一性约束(如用户名不能重复)
- 是否依赖外部服务支付网关、短信、OAuth 等)
- 业务流转逻辑(状态机、级联操作)
---
### Phase 2: 确认Clarify
**目标:向用户展示发现的内容,确认模糊点。不要闷头生成。**
#### 2.1 必须展示的内容
```
我梳理了相关代码,发现以下信息:
📋 涉及接口:
- [方法] [路径] - [说明](认证: 是/否)
- ...
📦 响应字段(基于 {SchemaName}
- [字段名]: [类型] - [说明]
- ...(共 N 个字段,将全部生成断言)
🔗 依赖关系:
- [创建 X 需要先创建 Y]
- ...
⚠️ 特殊情况:
- [涉及外部服务 / 文件上传 / 特殊认证等]
```
#### 2.2 按需确认(只问有歧义的)
| 场景 | 要问的 |
|------|--------|
| 流程范围不明确 | "要测到哪一步?" |
| 多种用户角色 | "用哪种身份测?" |
| 是否测异常 | "需要包含异常 case 吗?(参数校验失败、权限不足等)" |
| 是否测数据隔离 | "需要验证不同用户间数据不可见吗?" |
| 涉及第三方 | "XX 部分怎么处理?绕过 / 模拟回调 / 跳过?" |
| 前置数据来源 | "XX 依赖数据是通过 API 创建还是假设已存在?" |
**如果用户说"越完整越好"或"都要"→ 默认全部包含,不再追问。**
---
### Phase 3: 生成Generate
**目标:生成完整的 .hurl 文件,字段断言基于 schema 代码,不能编造。**
#### 3.1 文件头注释
```hurl
# ============================================================
# 测试:{测试名称}
# 生成时间:{日期}
# 涉及模块:{module1, module2, ...}
# 涉及接口:{N} 个
# 断言数量:{N} 条
# 前置条件:{服务运行 + 必要的前置条件}
# ============================================================
# 流程:
# 1. {步骤描述}
# 2. {步骤描述}
# ...
# ============================================================
```
#### 3.2 请求生成规则
**认证**
- 根据 Phase 1 画像中的登录接口和 token 位置生成
- token 必须通过 `[Captures]` 捕获,后续请求引用
- 如果是 Cookie 认证,用 `[Cookies]` 或 cookie capture
**CRUD 标准模式**
| 操作 | 生成要求 |
|------|---------|
| **创建POST** | capture 返回的 ID**所有**唯一约束字段用 `{{newUuid}}` 防冲突 |
| **查询详情GET** | **逐字段断言**(类型 + 值,见 3.3 |
| **查询列表GET** | 分页结构断言 + items[0] 逐字段断言 |
| **修改PUT/PATCH** | 修改后**紧跟一个 GET 验证修改生效** |
| **删除DELETE** | 删除后**紧跟一个 GET 验证已删除** |
#### 唯一值防冲突规范(强制执行)
**所有创建类请求POST凡是有唯一约束的字段必须使用 `{{newUuid}}` 或包含 UUID 后缀**,确保测试可重复运行。
| 场景 | 正确做法 | 错误做法 |
|------|---------|---------|
| 编码/Code 字段 | `"code": "TEST-{{newUuid}}"` | `"code": "TEST-001"` |
| 名称(有唯一约束) | `"name": "测试-{{newUuid}}"` | `"name": "测试角色"` |
| 用户名 | `"username": "hurl_{{newUuid}}"` | `"username": "test_user"` |
| 手机号 | `"phone": "189{{newUuid}}"` 取前11位或放在 env 文件 | `"phone": "18899990001"` |
| 邮箱 | `"email": "{{newUuid}}@test.com"` | `"email": "test@test.com"` |
**后续步骤需要引用的值(如登录用的 username/password**
- 必须定义在 `env/dev.env` 中作为变量
- 在创建请求和后续引用处都通过 `{{变量名}}` 引用,保持一致
- env 文件中的值也应该带有测试标识前缀(如 `hurl_test_`),避免与真实数据冲突
- **env 中有唯一约束的变量值每次运行前可能需要更新**(文档中注明)
**业务流程模式**
- 按用户描述的流程顺序编排请求
- 上一步的输出ID、状态等通过 `[Captures]` 传给下一步
- 关键步骤加中间状态验证(如创建订单后验证状态为"待支付"
#### 3.3 schema 到断言的映射
读到 schema 字段后,按以下规则生成 jsonpath 断言:
**通用类型映射(所有语言)**
| Schema 类型特征 | Hurl 断言 |
|----------------|-----------|
| 字符串string / str / String | `isString` |
| 整数int / integer / long / Int | `isInteger` |
| 浮点float / double / decimal / Float | `isNumber` |
| 布尔bool / boolean / Boolean | `isBoolean` |
| 数组 / 列表([] / List / Array | `isList` |
| 嵌套对象struct / class / dict / object | `isObject`,并递归检查子字段 |
| 可空类型(指针 / Optional / nullable | `exists`(不强制类型,因为可能是 null |
| 不参与序列化json:"-" / @JsonIgnore / exclude=True | **跳过,不生成断言** |
| 条件序列化omitempty / if not None | `exists` 或不生成(取决于场景) |
**Go 特定映射**
| Go 类型 | Hurl 断言 |
|---------|-----------|
| `string` | `isString` |
| `int`, `int8/16/32/64`, `uint`, `uint8/16/32/64` | `isInteger` |
| `float32`, `float64` | `isNumber` |
| `bool` | `isBoolean` |
| `[]T` | `isList` |
| `*string`, `*int`, `*uint` 等指针 | `exists` |
| `time.Time` | `isString`(通常序列化为字符串) |
| `map[string]any` | `isObject` |
**Python 特定映射Pydantic / Django / FastAPI**
| Python 类型 | Hurl 断言 |
|------------|-----------|
| `str` | `isString` |
| `int` | `isInteger` |
| `float`, `Decimal` | `isNumber` |
| `bool` | `isBoolean` |
| `list[T]`, `List[T]` | `isList` |
| `Optional[T]`, `T | None` | `exists` |
| `dict`, `Dict` | `isObject` |
| `datetime`, `date` | `isString` |
**TypeScript/JavaScript 特定映射**
| TS/JS 类型 | Hurl 断言 |
|-----------|-----------|
| `string` | `isString` |
| `number`(整数上下文) | `isInteger` |
| `number`(通用) | `isNumber` |
| `boolean` | `isBoolean` |
| `T[]`, `Array<T>` | `isList` |
| `T \| null`, `T \| undefined` | `exists` |
| `object`, `Record<>` | `isObject` |
| `Date` | `isString` |
**Java 特定映射**
| Java 类型 | Hurl 断言 |
|----------|-----------|
| `String` | `isString` |
| `Integer`, `Long`, `int`, `long` | `isInteger` |
| `Double`, `Float`, `BigDecimal` | `isNumber` |
| `Boolean`, `boolean` | `isBoolean` |
| `List<T>` | `isList` |
| `@Nullable`, `Optional<T>` | `exists` |
| `Map<K,V>` | `isObject` |
| `LocalDateTime`, `Instant` | `isString` |
#### 3.4 统一响应格式断言
根据 Phase 1 画像中发现的统一响应格式,为**每个成功响应**添加格式断言。
示例:如果项目的统一格式是 `{code, msg, data, timestamp}`
```hurl
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.msg" == "success"
jsonpath "$.timestamp" isIsoDate
```
示例:如果项目的格式是 `{status, message, result}`
```hurl
[Asserts]
jsonpath "$.status" == "ok"
jsonpath "$.message" isString
```
示例:如果项目无统一包装,直接返回数据:
```hurl
[Asserts]
# 直接断言业务字段
jsonpath "$.id" isInteger
jsonpath "$.name" isString
```
**不要假设响应格式,必须从代码中确认。**
#### 3.5 分页断言
根据 Phase 1 画像中发现的分页结构生成。
示例:如果是 `{items, total, page, size}` 格式:
```hurl
jsonpath "$.data.items" isList
jsonpath "$.data.total" isInteger
jsonpath "$.data.total" >= 1
jsonpath "$.data.page" isInteger
jsonpath "$.data.size" isInteger
# items 内元素逐字段断言
jsonpath "$.data.items[0].{field}" {type_assert}
```
示例:如果是 `{results, count, next, previous}` 格式Django 风格):
```hurl
jsonpath "$.results" isList
jsonpath "$.count" isInteger
jsonpath "$.count" >= 1
# results 内元素逐字段断言
jsonpath "$.results[0].{field}" {type_assert}
```
**根据实际代码调整字段名,不硬编码。**
#### 3.6 异常 Case 模板
**参数校验失败**
```hurl
# ── 异常:参数校验失败 ──
POST {{base_url}}/{path}
Authorization: Bearer {{token}}
Content-Type: application/json
{
"required_field": ""
}
HTTP {expected_error_status}
[Asserts]
# 断言错误响应格式(根据项目约定调整)
```
> HTTP 状态码根据项目实际返回确定:有的项目错误也返回 200 + 业务错误码,有的返回 400/422。
**未认证访问**
```hurl
# ── 异常:未认证访问 ──
GET {{base_url}}/{protected_path}
HTTP {expected_unauth_status}
```
**越权访问**(如果用户要求):
```hurl
# ── 异常:用户 B 不能访问用户 A 的资源 ──
GET {{base_url}}/{path}/{{user_a_resource_id}}
Authorization: Bearer {{user_b_token}}
HTTP {expected_forbidden_status}
```
#### 3.7 特殊场景处理
| 场景 | 处理策略 |
|------|---------|
| **短信/邮件验证码** | 建议服务端加 test_mode 开关,固定验证码写入 env 文件;注释提醒用户 |
| **第三方支付** | 优先用项目内部支付方式(如钱包支付);如需测回调,直接 POST 回调接口模拟 |
| **OAuth 登录(微信/Google/GitHub** | 建议服务端加 test_mode 支持直接传 openid/email注释提醒用户 |
| **文件上传** | 用 Hurl 的 `[Multipart]` 语法 + testdata 目录下的样本文件 |
| **外部 API 依赖** | 只测参数校验和错误响应格式,不断言业务结果;注释说明依赖 |
| **WebSocket** | Hurl 不支持,注释说明跳过 |
| **异步任务结果** | 用 Hurl 的 `retry` + `retry-interval` 轮询直到状态变更 |
异步轮询示例:
```hurl
# 等待异步任务完成(最多重试 10 次,间隔 500ms
GET {{base_url}}/{path}/{{task_id}}
Authorization: Bearer {{token}}
[Options]
retry: 10
retry-interval: 500ms
HTTP 200
[Asserts]
jsonpath "$.data.status" == "completed"
```
#### 3.8 文件输出
**目录结构**(首次使用时创建,如不存在):
```
tests/hurl/
├── env/
│ └── dev.env # 环境变量
├── testdata/ # 测试用的样本文件
├── flows/ # 业务流程测试
├── modules/ # 按模块的接口测试
│ └── {module}/
│ └── 01-crud.hurl
├── negative/ # 异常/边界测试
├── contract/ # 接口契约验证
├── reports/ # Phase 5 发现报告(自动生成)
│ └── findings-{name}-{date}.md
└── Makefile # 快捷命令
```
文件放置规则:
| 用户描述 | 输出路径 |
|---------|---------|
| 测试 XX 流程 / 业务流程 | `tests/hurl/flows/{flow-name}.hurl` |
| 测试 XX 模块的 CRUD / 接口 | `tests/hurl/modules/{module}/01-crud.hurl` |
| 测试异常/边界/权限 | `tests/hurl/negative/{name}.hurl` |
| 测试接口契约/字段对齐 | `tests/hurl/contract/{name}.hurl` |
**如果项目已有 hurl 测试目录结构,沿用已有约定,不要另起炉灶。**
#### 3.9 env 和 Makefile
**env/dev.env**(首次创建时生成,内容基于 Phase 1 画像):
```properties
# 服务地址
base_url=http://localhost:{port}
# 认证信息(根据项目实际填写)
admin_username={默认用户名}
admin_password={默认密码}
# 测试模式变量(如果有特殊场景)
# test_sms_code=888888
# test_openid=test_openid_001
```
**Makefile**(首次创建时生成):
```makefile
SHELL := /bin/bash
ENV ?= dev
HURL_OPTS := --variables-file env/$(ENV).env --test
.PHONY: test test-flows test-modules test-negative report
test: ## 运行所有测试
hurl $(HURL_OPTS) .
test-flows: ## 运行业务流程测试
hurl $(HURL_OPTS) flows/
test-modules: ## 运行模块接口测试
hurl $(HURL_OPTS) modules/
test-negative: ## 运行异常测试
hurl $(HURL_OPTS) negative/
report: ## 生成 HTML 报告
hurl $(HURL_OPTS) --report-html build/report/ .
```
---
### Phase 4: 验证Verify
**目标:确保生成的 .hurl 文件语法正确、可运行。**
#### 4.1 语法自检
- [ ] 每个请求之间有空行分隔
- [ ] `[Captures]``[Asserts]` 拼写正确(大小写敏感)
- [ ] 所有 `{{变量}}` 引用都有来源env 文件定义 或 上游 `[Captures]`
- [ ] JSON body 无尾逗号、格式正确
- [ ] `HTTP {status}` 在请求之后、`[Captures]` / `[Asserts]` 之前
- [ ] 请求和断言之间没有多余空行(`HTTP` 行必须紧跟请求)
- [ ] 文件上传路径相对于 hurl 文件位置正确
#### 4.2 运行测试
```bash
hurl --variables-file tests/hurl/env/dev.env --test tests/hurl/{生成的文件}
```
- 服务在跑 → 执行,如有失败分析修正
- 服务没跑 → 跳过,告知用户手动验证命令
---
### Phase 5: 研判Triage
**目标:分析测试失败原因,区分"测试自身问题"和"接口问题",生成发现报告。**
> **核心原则:不因实际返回与预期不符而盲目修改断言。差异是"发现",需要分类研判。**
#### 5.1 失败分类A/B/C 三类)
每个断言失败必须归入以下三类之一:
| 类型 | 含义 | 判断标准 | 处置方式 |
|------|------|---------|---------|
| **A 类 — 测试自身错误** | JSON path 写错、请求参数遗漏、env 变量缺失、前置步骤失败导致 ID 为空 | 改正后 DTO/Handler 代码能对应上 | ✅ **自动修正测试**,不打扰用户 |
| **B 类 — 代码一致性问题** | DTO 定义和 Handler 实际返回不一致(如 DTO 写 `json:"id"` 但 Handler 返回 `"ID"`DTO 定义了分页结构但 Handler 用了另一种) | DTO 说的是 AHandler 做的是 B两个都是代码 | ⚠️ **以 Handler 为准修正断言** + **写入发现报告** |
| **C 类 — 接口行为 Bug** | 接口行为不符合合理预期(如详情接口缺少列表接口有的字段;创建接口返回 500同一个 DTO 在不同接口表现不一致) | Handler 的行为本身不合理,不是 DTO 过期的问题 | 🐛 **保留原始断言(测试会失败)** + **写入发现报告** |
#### 5.2 分类判断流程
```
断言失败
├─ jsonpath 写错 / 变量未定义 / 前置步骤失败?
│ → A 类:修正测试
├─ DTO 定义字段名 ≠ 实际返回字段名?(如 id vs ID, list vs items
│ │
│ ├─ Handler 代码确实返回了不同的结构?
│ │ → B 类:以 Handler 为准,报告不一致
│ │
│ └─ Handler 代码引用了 DTO 但行为不符?
│ → C 类:接口 Bug
├─ 某个字段在接口 A 有、接口 B 没有?(如列表有但详情没有)
│ → C 类:接口一致性 Bug
├─ 接口返回 5xx 错误?
│ → C 类:服务端 Bug
└─ 业务逻辑不符预期?(如已传 packages 但响应中 packages 为空)
→ C 类:业务逻辑 Bug
```
#### 5.3 发现报告格式
报告输出到 **`tests/hurl/reports/findings-{测试文件名}-{日期}.md`**,格式如下:
```markdown
# 🔍 Hurl 测试发现报告
- 测试文件:`flows/package-resource-full-flow.hurl`
- 生成时间2026-03-30
- 总请求数25
- 通过20
- 失败5A类: 2, B类: 1, C类: 2
---
## A 类(测试自身错误)— 已自动修正
### A-1: 角色名冲突导致创建失败
- **位置**:第 2 步 POST /api/admin/roles
- **原因**:角色名使用固定值,重复运行时 409 冲突
- **修正**:角色名改为 `hurl测试角色-{{newUuid}}`
---
## B 类(代码一致性问题)— 已按 Handler 修正断言
### B-1: 角色接口返回原始 GORM 模型而非 DTO
- **位置**POST /api/admin/roles → 响应
- **DTO 定义**`RoleResponse``json:"id"`(小写)
- **Handler 实际**:直接返回 `model.Role`,字段为 `ID`(大写 GORM 默认)
- **影响**:前端按 API 文档对接会取不到 `id` 字段
- **建议**Handler 添加 model → DTO 转换
---
## C 类(接口行为 Bug— 断言保留,测试会失败
### C-1: 套餐详情接口缺少 one_time_commission_amount
- **位置**GET /api/admin/packages/:id代理视角
- **预期**`PackageResponse` DTO 定义了 `one_time_commission_amount` 字段
- **实际**:列表接口 GET /packages 返回该字段,详情接口不返回
- **影响**:代理端查看单个套餐时看不到佣金信息
- **建议**Detail handler 增加佣金信息增强逻辑
### C-2: 批量分配对已授权店铺返回 500
- **位置**POST /api/admin/shop-package-batch-allocations
- **预期**对已有授权的店铺应返回业务错误码4xx而非 500
- **实际**:返回 `{code: 2001, msg: "内部服务器错误"}`
- **影响**:前端无法区分是参数错误还是服务异常
- **建议**:添加"已存在授权"的前置检查,返回明确的业务错误
```
#### 5.4 研判完成后的交付物
| 交付物 | 说明 |
|-------|------|
| **修正后的 .hurl 文件** | A 类已自动修正B 类按 Handler 实际修正C 类**保留原始断言**(预期失败) |
| **发现报告** | `tests/hurl/reports/findings-{name}-{date}.md` |
| **终端摘要** | 向用户输出发现数量和关键 C 类问题摘要 |
#### 5.5 用户交互
研判完成后,向用户展示摘要并询问:
```
## 研判结果
- A 类测试错误N 个,已自动修正
- B 类一致性问题N 个,已按实际修正并记录
- C 类(接口 BugN 个,断言保留(测试会失败直到代码修复)
### C 类发现摘要:
1. [C-1] 套餐详情缺少 one_time_commission_amount
2. [C-2] 批量分配对已授权店铺返回 500
详细报告已写入: tests/hurl/reports/findings-xxx.md
是否需要对某个 C 类发现调整处置?(如确认为已知行为,改为 B 类)
```
---
## 红线规则
| 规则 | 说明 |
|------|------|
| **不跳过 Phase 1** | 必须读代码确认接口路径和字段,不能凭记忆或猜测 |
| **不跳过 Phase 2** | 必须向用户展示发现的接口和字段,确认后再生成 |
| **不编造字段** | 所有断言的字段名必须来自实际 schema 代码 |
| **不编造路径** | 所有接口路径必须来自实际路由代码 |
| **不遗漏字段** | schema 中每个参与序列化的字段都必须有对应断言 |
| **不硬编码 ID** | 所有依赖的 ID 通过 `[Captures]` 从上游请求获取 |
| **不假设响应格式** | 统一响应结构必须从代码中确认,不同项目格式不同 |
| **唯一值防冲突** | 创建类请求的**所有**唯一约束字段必须使用 `{{newUuid}}`(详见 3.2 规范) |
| **自给自足** | 每个 .hurl 文件自己创建测试数据,不依赖外部数据准备 |
| **不为通过而改断言** | 断言失败时走 Phase 5 研判流程C 类 Bug 必须保留断言并写入发现报告 |
| **Handler 追溯** | 断言字段名必须追溯到 Handler 实际返回的类型,不能只看 DTO 定义(详见 Phase 1 真理源层级) |
---
## AI 助手检查清单
### 生成阶段自检Phase 3 完成后)
1. ✅ 文件头注释包含流程描述和前置条件
2. ✅ 认证步骤正确 capture 了 token / cookie
3. ✅ 所有依赖数据通过 API 链式创建(自给自足)
4. ✅ 每个成功响应断言了项目的统一响应格式
5. ✅ 查询详情接口**逐字段断言**(类型 + 值,基于 Handler 实际返回)
6. ✅ 分页接口断言了分页结构 + 第一条记录的字段
7. ✅ 修改操作后紧跟 GET 验证修改生效
8. ✅ 删除操作后紧跟 GET 验证已删除
9. ✅ **所有**创建请求的唯一约束字段使用了 `{{newUuid}}`
10.`{{变量}}` 引用无悬空(都有 env 或 capture 来源)
11. ✅ 文件放在了正确的目录位置
12. ✅ 特殊场景有明确的处理策略和注释提醒
13. ✅ 断言字段名已追溯到 Handler 实际返回类型(非仅依赖 DTO
### 研判阶段自检Phase 5 完成后)
14. ✅ 每个断言失败都已分类A/B/C
15. ✅ A 类错误已自动修正
16. ✅ B 类不一致已按 Handler 修正断言,并写入发现报告
17. ✅ C 类 Bug 保留了原始断言(测试预期失败),并写入发现报告
18. ✅ 发现报告已输出到 `tests/hurl/reports/findings-{name}-{date}.md`
19. ✅ 向用户展示了研判结果摘要
---
## 附录Hurl 语法速查
**生成 .hurl 文件时必须参照本速查,不可凭记忆编造语法。**
### 文件结构
一个 .hurl 文件由多个 entry 组成,每个 entry = 请求 + 可选响应:
```
请求1
响应1可选
请求2
响应2可选
```
entry 之间用空行分隔。
### 请求格式
```hurl
METHOD URL
Header1: value1
Header2: value2
[Options]
key: value
[Query]
param1: value1
[Form]
field1: value1
[Multipart]
file1: file,path/to/file;
[BasicAuth]
username: password
[Cookies]
name: value
BODYJSON / XML / multiline string / file
```
**规则**
- Method + URL 是第一行,必须
- Headers 紧跟 URL 之后(无 section 标记)
- Sections`[Query]``[Form]``[Options]` 等)顺序任意
- Body 必须在最后
- JSON body 直接写 `{ }` 即可,自动设置 Content-Type: application/json
### 响应格式
```hurl
HTTP {status_code}
Header1: expected_value1
[Captures]
var_name: jsonpath "$.path"
[Asserts]
jsonpath "$.field" == "value"
```
**规则**
- `HTTP {status}` 紧跟请求之后(中间不能有空行)
- `HTTP *` 表示不检查状态码
- Headers 检查紧跟 HTTP 行之后
- `[Captures]``[Asserts]` 顺序任意
### 变量和模板
```hurl
# 引用变量(从 env 文件、命令行或上游 capture 获取)
GET {{base_url}}/api/users/{{user_id}}
# 内置函数
POST {{base_url}}/api/users
{
"email": "{{newUuid}}@test.com",
"created_at": "{{newDate}}"
}
```
可用函数:
- `{{newUuid}}` — 生成 UUID v4
- `{{newDate}}` — 生成 RFC 3339 UTC 时间戳
### Capture 语法
```hurl
[Captures]
# JSONPath
token: jsonpath "$.data.access_token"
user_id: jsonpath "$.data.id"
first_item: jsonpath "$.items[0].name"
# Header
location: header "Location"
# Cookie
session: cookie "SESSIONID"
# Body整个响应体作为字符串
full_body: body
# Status code
code: status
# 正则表达式
csrf: regex "name=\"csrf\" value=\"([^\"]+)\""
# 响应时间(毫秒)
response_time: duration
```
### Assert 语法
```hurl
[Asserts]
# ── 状态码 ──
status == 200
status >= 200
status < 300
# ── JSONPath 断言 ──
jsonpath "$.name" == "Alice" # 等于
jsonpath "$.name" != "Bob" # 不等于
jsonpath "$.age" > 18 # 大于
jsonpath "$.age" >= 18 # 大于等于
jsonpath "$.count" < 100 # 小于
jsonpath "$.items" count == 5 # 集合长度
jsonpath "$.name" startsWith "Al" # 前缀
jsonpath "$.name" endsWith "ce" # 后缀
jsonpath "$.name" contains "lic" # 包含
jsonpath "$.date" matches /\\d{4}-\\d{2}-\\d{2}/ # 正则
# ── 类型断言 ──
jsonpath "$.name" isString
jsonpath "$.age" isInteger
jsonpath "$.score" isFloat
jsonpath "$.count" isNumber # 整数或浮点
jsonpath "$.active" isBoolean
jsonpath "$.items" isList
jsonpath "$.meta" isObject
jsonpath "$.id" isUuid
jsonpath "$.created_at" isIsoDate # RFC 3339 格式
jsonpath "$.field" isEmpty # 空集合
# ── 存在性 ──
jsonpath "$.field" exists
jsonpath "$.field" not exists
# ── 否定 ──
jsonpath "$.name" not contains "Bob"
jsonpath "$.status" not == "deleted"
# ── Header 断言 ──
header "Content-Type" contains "application/json"
header "X-Request-Id" exists
# ── 性能 ──
duration < 1000 # 响应时间(毫秒)
# ── Body 断言 ──
body contains "Hello"
bytes count == 1024
```
### Options逐请求配置
```hurl
GET {{base_url}}/api/task/{{task_id}}
[Options]
retry: 10 # 最大重试次数(-1 = 无限)
retry-interval: 500ms # 重试间隔
delay: 2s # 请求前等待
location: true # 跟随重定向
insecure: true # 允许不安全 SSL
verbose: true # 输出详细日志
very-verbose: true # 输出更详细日志
skip: true # 跳过此请求
variable: key=value # 定义变量
HTTP 200
```
### Multipart 文件上传
```hurl
POST {{base_url}}/api/upload
[Multipart]
file: file,testdata/sample.xlsx;
field1: value1
# 指定 Content-Type
file2: file,testdata/data.bin; application/octet-stream
```
### 运行命令
```bash
# 运行单个文件
hurl --test file.hurl
# 带变量文件
hurl --variables-file env/dev.env --test file.hurl
# 运行目录下所有 .hurl
hurl --test tests/hurl/
# 生成 HTML 报告
hurl --test --report-html build/report/ tests/hurl/
# 生成 JUnit 报告CI 用)
hurl --test --report-junit build/report.xml tests/hurl/
# 并行执行(--test 默认并行,同文件内串行)
hurl --test --jobs 4 tests/hurl/
# 指定单个变量
hurl --variable base_url=http://localhost:3000 --test file.hurl
# 失败后继续执行
hurl --test --continue-on-error tests/hurl/
```
### 常见错误
| 错误 | 原因 | 修正 |
|------|------|------|
| `HTTP 200` 和请求之间有空行 | 空行会被当作 entry 分隔符 | 删除空行HTTP 行紧跟请求 |
| JSON body 有尾逗号 | Hurl 严格解析 JSON | 删除最后一个逗号 |
| `jsonpath` 写成 `json_path``JsonPath` | 关键字大小写敏感 | 必须小写 `jsonpath` |
| `[Captures]` 写成 `[Capture]` | 必须是复数 | `[Captures]``[Asserts]``[Options]` |
| 变量 `{{ var }}` 有空格 | 允许,但建议统一 | `{{var}}``{{ var }}` 都可以 |
| `isIsoDate` 用在非 RFC 3339 格式 | 只认 `YYYY-MM-DDTHH:mm:ss` 格式 | 如果是其他格式用 `matches` |
| `file,path;` 路径含 `..` | Hurl 禁止相对父目录 | 用 `--file-root` 或调整路径 |