## Purpose

定义 UDP checker 的 target 配置、defaults、执行语义、响应编码、expect 校验、失败结构和状态摘要。

## Requirements

### Requirement: udp target 配置
系统 SHALL 支持 `type: udp` 的 target 配置，通过 `udp.host` 和 `udp.port` 描述目标 UDP 地址，并通过可选字段控制 payload、编码和响应大小限制。

#### Scenario: 解析最简 udp target
- **WHEN** YAML 中 target 配置 `type: udp`、`udp.host: "127.0.0.1"` 和 `udp.port: 9000`
- **THEN** 系统 SHALL 将其解析为 udp checker，并填充 `payload=""`、`encoding="text"`、`responseEncoding="text"`、`maxResponseBytes=4096`、interval、timeout、group 和 expect 配置

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

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

#### Scenario: udp port 范围非法
- **WHEN** YAML 中 udp target 的 `udp.port` 不是 1 到 65535 之间的整数
- **THEN** 系统 SHALL 以配置错误退出，并提示 udp.port 必须为合法 UDP 端口

#### Scenario: 省略 payload 发送空 datagram
- **WHEN** YAML 中 udp target 未配置 `udp.payload`
- **THEN** 系统 SHALL 使用空字符串作为 payload，并在执行时发送零长度 UDP datagram

#### Scenario: udp defaults 覆盖通用 UDP 参数
- **WHEN** YAML 中配置 `defaults.udp.encoding: "hex"`、`defaults.udp.responseEncoding: "hex"` 和 `defaults.udp.maxResponseBytes: "8KB"`
- **THEN** 未显式配置对应字段的 udp target SHALL 使用 defaults.udp 中的值

#### Scenario: per-target UDP 参数覆盖 defaults
- **WHEN** defaults.udp 配置了 encoding、responseEncoding 或 maxResponseBytes，且某个 udp target 显式配置对应字段
- **THEN** 该 target SHALL 使用自身 udp 分组中的值

#### Scenario: udp 分组未知字段失败
- **WHEN** YAML 中 udp target 的 `udp` 分组包含 `dnsQuery`、`expectResponse` 或其他未知字段
- **THEN** 系统 SHALL 以配置错误退出，提示 udp 分组包含未知字段

#### Scenario: udp 序列化展示摘要
- **WHEN** 系统同步 udp target 到 targets 表
- **THEN** `target` 展示摘要 SHALL 为 `udp <host>:<port>`，`config` JSON SHALL 包含 resolved 后的 host、port、payload、encoding、responseEncoding 和 maxResponseBytes

### Requirement: udp payload 编码
系统 SHALL 支持将 `udp.payload` 按 `udp.encoding` 解码为发送字节，编码类型限定为 `text`、`hex` 和 `base64`。

#### Scenario: text payload 编码
- **WHEN** udp target 配置 `udp.payload: "PING"` 且 `udp.encoding` 未配置或为 `text`
- **THEN** 系统 SHALL 以 UTF-8 字节发送 `PING`

#### Scenario: hex payload 编码
- **WHEN** udp target 配置 `udp.payload: "50494e47"` 和 `udp.encoding: "hex"`
- **THEN** 系统 SHALL 解码 hex 后发送字节内容 `PING`

#### Scenario: base64 payload 编码
- **WHEN** udp target 配置 `udp.payload: "UElORw=="` 和 `udp.encoding: "base64"`
- **THEN** 系统 SHALL 解码 base64 后发送字节内容 `PING`

#### Scenario: 非法 encoding 失败
- **WHEN** YAML 中 udp target 配置 `udp.encoding: "json"`
- **THEN** 系统 SHALL 以配置错误退出，提示 udp.encoding 必须为 `text`、`hex` 或 `base64`

#### Scenario: 非法 responseEncoding 失败
- **WHEN** YAML 中 udp target 配置 `udp.responseEncoding: "json"`
- **THEN** 系统 SHALL 以配置错误退出，提示 udp.responseEncoding 必须为 `text`、`hex` 或 `base64`

#### Scenario: 非法 hex payload 失败
- **WHEN** YAML 中 udp target 配置 `udp.encoding: "hex"` 但 `udp.payload` 不是合法 hex 字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 udp.payload 与 udp.encoding 不匹配

#### Scenario: 非法 base64 payload 失败
- **WHEN** YAML 中 udp target 配置 `udp.encoding: "base64"` 但 `udp.payload` 不是合法 base64 字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 udp.payload 与 udp.encoding 不匹配

### Requirement: udp checker 执行
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram，等待第一个 UDP 响应 datagram，记录完整执行耗时和 UDP observation，并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。

#### Scenario: UDP 请求响应成功
- **WHEN** udp target 指向会返回 `PONG` 的 UDP 服务，且未配置 expect 或 `expect.responded` 为 `true`
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含响应大小的 observation，并关闭 socket

#### Scenario: 使用 hostname 执行 UDP 探测
- **WHEN** udp target 的 `udp.host` 为可解析域名或 `localhost`
- **THEN** 系统 SHALL 使用 Bun connected UDP socket 完成发送和接收，不要求配置 IP 地址

#### Scenario: 只处理第一个响应 datagram
- **WHEN** UDP 服务对一次请求返回多个 datagram
- **THEN** 系统 SHALL 仅使用第一个收到的 UDP datagram 执行 expect 校验，并关闭 socket

#### Scenario: UDP 无响应且默认期望响应
- **WHEN** udp target 指向在 timeout 内不返回 UDP datagram 的服务，且未配置 expect 或 `expect.responded` 为 `true`
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`，observation SHALL 包含 responded=false 和 error，failure 的 kind 为 `error`，phase 为 `response`，message 包含超时或未响应信息

#### Scenario: 期望无响应且实际无响应
- **WHEN** udp target 配置 `expect.responded: false`，且 timeout 内未收到 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=true`，observation SHALL 包含 responded=false，API detail SHALL 表示未收到响应

#### Scenario: 期望无响应但实际收到响应
- **WHEN** udp target 配置 `expect.responded: false`，但收到了 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=false`，observation SHALL 包含 responded=true 和响应摘要，failure 的 kind 为 `mismatch`，phase 为 `responded`

#### Scenario: UDP socket 底层错误
- **WHEN** Bun UDP socket 在发送或接收过程中触发 error 事件
- **THEN** 系统 SHALL best-effort 关闭 socket，并记录 `matched=false`、failure.kind=`error` 和可读错误信息，并在可收集时记录 observation

#### Scenario: ICMP unreachable 不作为 UDP 响应
- **WHEN** 底层系统因目标端口不可达产生 ICMP unreachable
- **THEN** 系统 SHALL NOT 将其视为 `responded=true` 的 UDP datagram 响应

#### Scenario: UDP 执行超时关闭 socket
- **WHEN** 引擎注入的 `ctx.signal` 在 UDP 发送或等待响应过程中 abort
- **THEN** 系统 SHALL best-effort 关闭 UDP socket，并记录结构化超时或未响应结果

### Requirement: udp 响应大小限制
系统 SHALL 使用 `udp.maxResponseBytes` 限制单个 UDP 响应 datagram 的可处理字节数，默认值为 4096，支持数字或 size string。

#### Scenario: 响应大小未超过限制
- **WHEN** udp target 配置 `udp.maxResponseBytes: 4096`，且实际响应为 16 字节
- **THEN** 系统 SHALL 允许后续 expect 校验继续执行

#### Scenario: 响应大小超过限制
- **WHEN** udp target 配置 `udp.maxResponseBytes: 4`，且实际响应为 16 字节
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 kind 为 `error`，phase 为 `response`，message 包含响应超过大小限制的信息

#### Scenario: Bun 标记 datagram 被截断
- **WHEN** Bun UDP data 回调中的 `flags.truncated` 为 `true`
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 kind 为 `error`，phase 为 `response`，message 包含响应被截断的信息

#### Scenario: maxResponseBytes 格式非法
- **WHEN** YAML 中 udp target 或 defaults.udp 的 `maxResponseBytes` 不是非负整数或合法 size string
- **THEN** 系统 SHALL 以配置错误退出，提示 maxResponseBytes 格式错误

### Requirement: udp expect 校验
系统 SHALL 支持 udp 专属 expect，包括 `responded`、`response`、`responseSize`、`sourceHost`、`sourcePort` 和 `durationMs`，并按 responded、responseSize、response、sourceHost、sourcePort、durationMs 的阶段顺序快速失败。`responded` SHALL 保持布尔状态语义，未配置时在 Resolved expect 中默认 `true`。`response` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`，且作用于按 `udp.responseEncoding` 转换后的响应文本。`responseSize`、`sourceHost`、`sourcePort` 和 `durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。

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

#### Scenario: response ContentExpectations 校验通过
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`，且按 `responseEncoding` 转换后的响应文本包含 `PONG`
- **THEN** 系统 SHALL 判定 response 阶段通过

#### Scenario: response JSON 校验通过
- **WHEN** udp target 收到文本响应 `{"status":"ok"}` 且配置 `expect.response: [{json: {path: "$.status", equals: "ok"}}]`
- **THEN** 系统 SHALL 判定 response 阶段通过

#### Scenario: response ContentExpectations 校验失败
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`，但按 `responseEncoding` 转换后的响应文本不包含 `PONG`
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 kind 为 `mismatch`，phase 为 `response`，path 指向失败的 response expectation

#### Scenario: responseEncoding 为 hex
- **WHEN** udp target 配置 `udp.responseEncoding: "hex"` 且收到字节内容 `PONG`
- **THEN** 系统 SHALL 将响应转换为小写 hex 字符串 `504f4e47` 后执行 `expect.response` expectation

#### Scenario: responseSize matcher 校验通过
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`，且实际响应为 4 字节
- **THEN** 系统 SHALL 判定 responseSize 阶段通过

#### Scenario: responseSize matcher 校验失败
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`，但实际响应为 2 字节
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 kind 为 `mismatch`，phase 为 `responseSize`

#### Scenario: sourceHost matcher 校验
- **WHEN** udp target 配置 `expect.sourceHost: { equals: "127.0.0.1" }`，且 Bun 回调中的来源地址为 `127.0.0.1`
- **THEN** 系统 SHALL 判定 sourceHost 阶段通过

#### Scenario: sourcePort matcher 校验
- **WHEN** udp target 配置 `expect.sourcePort: { equals: 9000 }`，且 Bun 回调中的来源端口为 `9000`
- **THEN** 系统 SHALL 判定 sourcePort 阶段通过

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

#### Scenario: response 断言要求实际有响应
- **WHEN** udp target 配置了 `expect.response` 或 `expect.responseSize`，但同时配置 `expect.responded: false`
- **THEN** 系统 SHALL 在启动期配置校验失败，提示响应内容或大小断言需要 `expect.responded` 为 true

#### Scenario: source 断言要求实际有响应
- **WHEN** udp target 配置了 `expect.sourceHost` 或 `expect.sourcePort`，但同时配置 `expect.responded: false`
- **THEN** 系统 SHALL 在启动期配置校验失败，提示响应来源断言需要 `expect.responded` 为 true

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

### Requirement: udp detail 摘要
系统 SHALL 在 udp API 序列化时从 observation 动态生成简短 detail 摘要，展示关键结果和执行耗时并避免返回过长响应内容。UDP observation SHALL 包含 durationMs 以支持 detail 构造。

#### Scenario: 收到响应的摘要
- **WHEN** udp target 收到 4 字节响应且完整执行耗时为 12ms
- **THEN** detail SHALL 包含 `responded in 12ms` 和 `4 bytes`

#### Scenario: 未收到响应的摘要
- **WHEN** udp target 配置 `expect.responded: false` 且 timeout 内未收到 UDP datagram
- **THEN** detail SHALL 包含 `no response` 和执行耗时

#### Scenario: 响应内容摘要截断
- **WHEN** udp target 收到较长响应内容
- **THEN** detail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要
