---
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 说的是 A，Handler 做的是 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
- 失败：5（A类: 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 类（接口 Bug）：N 个，断言保留（测试会失败直到代码修复）

### 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
BODY（JSON / 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` 或调整路径 |
