---
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: 验证
读代码搞清楚      展示给用户确认      输出 .hurl 文件     语法检查 + 试跑
```

---

### Phase 1: 探索（Explore）

**目标：读代码，搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。**

#### 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 验证已删除** |

**业务流程模式**：

- 按用户描述的流程顺序编排请求
- 上一步的输出（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/                 # 接口契约验证
└── 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 1** | 必须读代码确认接口路径和字段，不能凭记忆或猜测 |
| **不跳过 Phase 2** | 必须向用户展示发现的接口和字段，确认后再生成 |
| **不编造字段** | 所有断言的字段名必须来自实际 schema 代码 |
| **不编造路径** | 所有接口路径必须来自实际路由代码 |
| **不遗漏字段** | schema 中每个参与序列化的字段都必须有对应断言 |
| **不硬编码 ID** | 所有依赖的 ID 通过 `[Captures]` 从上游请求获取 |
| **不假设响应格式** | 统一响应结构必须从代码中确认，不同项目格式不同 |
| **唯一值防冲突** | 创建类请求的唯一字段使用 `{{newUuid}}` 或 `{{newDate}}` |
| **自给自足** | 每个 .hurl 文件自己创建测试数据，不依赖外部数据准备 |

---

## AI 助手检查清单

生成 .hurl 文件后自检：

1. ✅ 文件头注释包含流程描述和前置条件
2. ✅ 认证步骤正确 capture 了 token / cookie
3. ✅ 所有依赖数据通过 API 链式创建（自给自足）
4. ✅ 每个成功响应断言了项目的统一响应格式
5. ✅ 查询详情接口**逐字段断言**（类型 + 值，基于 schema）
6. ✅ 分页接口断言了分页结构 + 第一条记录的字段
7. ✅ 修改操作后紧跟 GET 验证修改生效
8. ✅ 删除操作后紧跟 GET 验证已删除
9. ✅ 唯一字段使用了 `{{newUuid}}`
10. ✅ `{{变量}}` 引用无悬空（都有 env 或 capture 来源）
11. ✅ 文件放在了正确的目录位置
12. ✅ 特殊场景有明确的处理策略和注释提醒

---

## 附录：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` 或调整路径 |
