## Purpose

定义 HTTP 类型拨测目标：通过 `type: http` 配置 HTTP/HTTPS 探测，支持请求方法、headers、body、SSL 校验、重定向和响应体大小限制，捕获 HTTP 状态码、响应头、响应体和执行耗时，按 expect 规则校验并生成 matched 判定。

## Requirements

### Requirement: HTTP target 配置
系统 SHALL 支持 `type: http` 的 target 配置，通过 `http.url` 描述目标 HTTP 地址，并通过可选字段控制请求方法、请求体、headers、SSL 校验、重定向和响应体大小限制。

#### Scenario: 解析最简 HTTP target
- **WHEN** YAML 中 target 配置 `type: http` 和 `http.url: "https://example.com"`
- **THEN** 系统 SHALL 将其解析为 HTTP checker，并填充 `method=GET`、`maxBodyBytes=100MB`、`ignoreSSL=false`、`maxRedirects=0`、headers、body、interval、timeout、group 和 expect 配置

#### Scenario: HTTP target 缺少 url
- **WHEN** YAML 中 target 配置 `type: http` 但缺少 `http.url`
- **THEN** 系统 SHALL 以配置错误退出，并提示该 target 缺少 http.url 字段

#### Scenario: HTTP target 覆盖请求方法
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.method: POST`
- **THEN** 该 target SHALL 使用自身 method 字段的值，而不是内置默认值 GET

#### Scenario: HTTP target 覆盖响应体大小限制
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.maxBodyBytes`
- **THEN** 该 target SHALL 使用自身 maxBodyBytes 字段的值，而不是内置默认值 100MB

#### Scenario: HTTP target 覆盖 headers
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.headers`
- **THEN** 该 target SHALL 使用自身 headers 字段的值

#### Scenario: HTTP target 配置 ignoreSSL
- **WHEN** YAML 中 HTTP target 设置 `http.ignoreSSL: true`
- **THEN** 系统 SHALL 解析该字段并在执行时跳过 SSL 证书校验

#### Scenario: HTTP target 配置 maxRedirects
- **WHEN** YAML 中 HTTP target 设置 `http.maxRedirects: 5`
- **THEN** 系统 SHALL 解析该字段并在执行时允许最多跟随 5 次重定向

#### Scenario: HTTP 序列化展示摘要
- **WHEN** 系统同步 HTTP target 到 targets 表
- **THEN** `target` 展示摘要 SHALL 为 HTTP URL，`config` JSON SHALL 包含 resolved 后的 url、method、headers、body、ignoreSSL、maxBodyBytes 和 maxRedirects

### Requirement: HTTP checker 执行
系统 SHALL 按 HTTP target 配置发起 HTTP 请求，使用引擎注入的 `ctx.signal` 响应超时。系统 SHALL 支持手动重定向跟随（`redirect: "manual"`），在 `maxRedirects` 限制内跟随 301/302/303/307/308 重定向。系统 SHALL 记录完整执行耗时和 HTTP observation，并在网络错误、超时、响应体超限或字符编码不支持时产生结构化失败信息。

#### Scenario: HTTP 请求成功
- **WHEN** HTTP target 指向可正常响应的 HTTP 服务，且未配置 expect 或 `expect.status` 为 `[200]`
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含 statusCode、headers、bodyPreview、contentType、contentLength 的 observation

#### Scenario: 使用 ctx.signal 响应超时
- **WHEN** 引擎注入的 `ctx.signal` 在 HTTP 请求过程中 abort
- **THEN** 系统 SHALL 记录 `matched=false`，observation SHALL 为 null，failure 的 kind 为 `error`，phase 为 `request`，message 包含超时信息

#### Scenario: 网络错误
- **WHEN** HTTP 请求因网络错误（DNS 解析失败、连接被拒绝等）失败
- **THEN** 系统 SHALL 记录 `matched=false`，observation SHALL 为 null，failure 的 kind 为 `error`，phase 为 `request`

#### Scenario: 重定向跟随
- **WHEN** HTTP target 配置 `http.maxRedirects: 3` 且服务端返回 301/302/303/307/308 重定向
- **THEN** 系统 SHALL 在 maxRedirects 限制内自动跟随重定向，并返回最终响应供 expect 校验

#### Scenario: 重定向次数耗尽
- **WHEN** HTTP target 配置 `http.maxRedirects: 1` 且服务端返回超过 1 次重定向
- **THEN** 系统 SHALL 将最后一次重定向响应作为最终结果返回，不继续跟随

#### Scenario: POST 重定向为 GET
- **WHEN** HTTP target 使用 POST 方法且收到 301 或 302 重定向，或收到 303 重定向
- **THEN** 系统 SHALL 将后续请求方法改为 GET，移除 Content-Type 和 Content-Length 请求头，并移除 body

#### Scenario: 跨域重定向移除敏感头
- **WHEN** 重定向到不同 origin 的目标
- **THEN** 系统 SHALL 移除 Authorization 和 Cookie 请求头

#### Scenario: 响应体读取与大小限制
- **WHEN** HTTP target 配置了 body expect 且响应体未超过 `maxBodyBytes`
- **THEN** 系统 SHALL 读取完整响应体，按 Content-Type 字符编码解码，并用于 body expect 校验

#### Scenario: 响应体超过大小限制
- **WHEN** HTTP target 配置了 body expect 且响应体超过 `maxBodyBytes`
- **THEN** 系统 SHALL 停止读取，记录 `matched=false`，failure 的 kind 为 `error`，phase 为 `body`，message 包含响应体超过大小限制的信息

#### Scenario: 不支持的字符编码
- **WHEN** HTTP 响应的 Content-Type 指定了不支持的 charset
- **THEN** 系统 SHALL 记录 `matched=false`，failure 的 kind 为 `error`，phase 为 `body`，message 包含不支持的字符编码信息

#### Scenario: 无 body expect 时不读取响应体
- **WHEN** HTTP target 未配置 `expect.body` 或 body expect 为空数组
- **THEN** 系统 SHALL NOT 读取响应体内容，bodyPreview SHALL 为 null

#### Scenario: GET/HEAD 请求不发送 body
- **WHEN** HTTP target 配置 `http.method: GET` 或 `http.method: HEAD`
- **THEN** 系统 SHALL NOT 发送请求体，即使配置了 `http.body`

### Requirement: HTTP expect 校验
系统 SHALL 支持 HTTP 专属 expect，包括 `status`、`headers`、`body` 和 `durationMs`，并按 status、headers、early-timeout（仅当配置了 body expect 时）、body、durationMs 的阶段顺序快速失败。`status` SHALL 保持状态码数组语义，支持精确数字（100-599）和范围模式（`1xx` 到 `5xx`），未配置时在 Resolved expect 中默认 `[200]`。`headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`，header key 大小写不敏感。`body` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`，支持直接 ValueMatcher 以及 json/css/xpath 提取器。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验完整执行耗时。

#### Scenario: 默认 status 成功语义
- **WHEN** HTTP target 未显式配置 `expect.status`
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中使用默认 `status: [200]` 进行校验

#### Scenario: status 精确值匹配
- **WHEN** HTTP target 配置 `expect.status: [200, 201]` 且响应状态码为 201
- **THEN** 系统 SHALL 判定 status 阶段通过，继续后续 expect 阶段

#### Scenario: status 范围模式匹配
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]` 且响应状态码为 204
- **THEN** 系统 SHALL 判定 status 阶段通过

#### Scenario: status 不匹配快速失败
- **WHEN** HTTP target 配置 `expect.status: [200]` 且响应状态码为 500
- **THEN** 系统 SHALL 立即返回 `matched=false`，failure 的 phase 为 `status`

#### Scenario: headers 校验通过
- **WHEN** HTTP target 配置 `expect.headers: {Content-Type: {contains: "application/json"}}` 且响应包含对应 header
- **THEN** 系统 SHALL 判定 headers 阶段通过，header key 匹配大小写不敏感

#### Scenario: headers 校验失败
- **WHEN** HTTP target 配置 `expect.headers: {X-Custom: {exists: true}}` 且响应不包含该 header
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 phase 为 `headers`

#### Scenario: body ContentExpectations 校验通过
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok"}, {json: {path: "$.status", equals: "success"}}]` 且响应体满足全部条件
- **THEN** 系统 SHALL 判定 body 阶段通过

#### Scenario: body ContentExpectations 快速失败
- **WHEN** HTTP target 配置两条 body expectation 且第一条失败
- **THEN** 系统 SHALL 返回第一条失败 expectation 的 failure，phase 为 `body`，并 MUST NOT 执行第二条 expectation

#### Scenario: body 非 JSON 响应触发 JSONPath 失败
- **WHEN** HTTP target 配置 json body expectation 但响应体不是合法 JSON
- **THEN** 系统 SHALL 记录 `matched=false`，failure 的 kind 为 `error`，phase 为 `body`

#### Scenario: early-timeout 快速失败
- **WHEN** HTTP target 配置了 body expect 和 `expect.durationMs: {lte: 500}`，且请求状态码和 headers 通过后已耗时超过 500ms
- **THEN** 系统 SHALL 在读取响应体之前判定 durationMs 超限，不读取响应体，直接返回 `matched=false`，failure 的 phase 为 `duration`

#### Scenario: durationMs 校验
- **WHEN** HTTP target 配置 `expect.durationMs: {lte: 1000}` 且完整执行耗时超过 1000ms
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 phase 为 `duration`

### Requirement: HTTP checker 启动期配置校验
系统 SHALL 在启动期对 HTTP checker 的配置契约和语义执行严格校验。HTTP target 的 `http` 分组 SHALL 只允许 `url`、`method`、`headers`、`body`、`ignoreSSL`、`maxBodyBytes`、`maxRedirects` 字段。HTTP expect SHALL 只允许 `status`、`headers`、`body`、`durationMs` 字段。未知字段、非法类型、非法 URL 协议、非法 status 值、非法 maxBodyBytes、非法 ContentExpectations 和不可编译正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw HTTP expect 输入。

#### Scenario: http.url 为空字符串
- **WHEN** YAML 中 HTTP target 配置 `http.url: ""`
- **THEN** 系统 SHALL 以配置错误退出，提示缺少 http.url 字段

#### Scenario: http.url 协议非法
- **WHEN** YAML 中 HTTP target 的 `http.url` 不以 `http://` 或 `https://` 开头
- **THEN** 系统 SHALL 以配置错误退出，提示 URL 格式不合法

#### Scenario: http.url 格式非法
- **WHEN** YAML 中 HTTP target 的 `http.url` 不是合法 URL
- **THEN** 系统 SHALL 以配置错误退出，提示 URL 格式不合法

#### Scenario: http.maxBodyBytes 格式非法
- **WHEN** YAML 中 HTTP target 的 `http.maxBodyBytes` 不是合法 size 值
- **THEN** 系统 SHALL 以配置错误退出，提示 maxBodyBytes 格式错误

#### Scenario: http 分组未知字段失败
- **WHEN** YAML 中 HTTP target 的 `http` 分组包含未知字段
- **THEN** 系统 SHALL 以配置错误退出，提示 http 分组包含未知字段

#### Scenario: expect.status 数字非法
- **WHEN** YAML 中 HTTP target 的 `expect.status` 包含非整数或不在 100-599 范围内的数字
- **THEN** 系统 SHALL 以配置错误退出，提示 status 数字不合法

#### Scenario: expect.status 模式非法
- **WHEN** YAML 中 HTTP target 的 `expect.status` 包含不符合 `1xx` 到 `5xx` 格式的字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 status 模式不合法

#### Scenario: expect.body 必须为数组
- **WHEN** YAML 中 HTTP target 的 `expect.body` 已配置但不是数组
- **THEN** 系统 SHALL 以配置错误退出，提示 expect.body 必须为数组

#### Scenario: HTTP expect 未知字段失败
- **WHEN** YAML 中 HTTP target 的 expect 包含 `connected`、`exitCode`、`maxDurationMs` 或其他非 HTTP expect 字段
- **THEN** 系统 SHALL 以配置错误退出，提示 expect 包含未知字段

### Requirement: HTTP observation 与 detail
HTTP checker SHALL 在 observation 中记录 statusCode（number）、headers（Record<string, string>，截断至前 20 个）、bodyPreview（string | null，截断至前 1024 字符）、contentType（string | null）、contentLength（number | null）。API detail SHALL 由 `buildDetail` 从 observation 动态构造，格式为 `HTTP {statusCode}`。网络错误或超时导致无法收集 observation 时，observation SHALL 为 null，detail SHALL 为 null。

#### Scenario: 正常响应 observation
- **WHEN** HTTP 请求成功返回
- **THEN** observation SHALL 包含 statusCode、截断后的 headers、截断后的 bodyPreview、contentType 和 contentLength

#### Scenario: 未读取响应体时 bodyPreview 为 null
- **WHEN** HTTP target 未配置 body expect
- **THEN** observation.bodyPreview SHALL 为 null

#### Scenario: 请求失败 observation 为 null
- **WHEN** HTTP 请求因网络错误或超时失败
- **THEN** observation SHALL 为 null，detail SHALL 为 null

#### Scenario: detail 格式
- **WHEN** API 序列化 HTTP CheckResult
- **THEN** detail SHALL 为 `HTTP {statusCode}` 格式（如 `HTTP 200`）
