1
0
Files
DiAL/openspec/specs/udp-checker/spec.md
lanyuanxiaoyao 52262a31f6 feat: 新增 UDP checker,支持自定义 payload 请求-响应探测与断言
基于 Bun connected UDP socket 实现通用 UDP 拨测能力:
- 支持 text/hex/base64 payload 编码与独立 responseEncoding 响应视图
- 支持 responded、response、responseSize、sourceHost、sourcePort、maxDurationMs 专属 expect
- 单 datagram 发送,仅断言首个 UDP 响应 datagram
- 通过 maxResponseBytes 和 flags.truncated 进行响应大小限制与截断保护
- payload 可选,省略时发送空 datagram
- 自包含模块结构(types/schema/validate/expect/encoding/execute)
- 新增 741 tests(含 unit、execute 集成、expect 和编码 roundtrip),全部通过
2026-05-18 17:23:17 +08:00

204 lines
12 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.
## 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记录完整执行耗时并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
#### Scenario: UDP 请求响应成功
- **WHEN** udp target 指向会返回 `PONG` 的 UDP 服务,且未配置 expect 或 `expect.responded``true`
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` 和包含响应大小的 `statusDetail`,并关闭 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`failure 的 kind 为 `error`phase 为 `response`message 包含超时或未响应信息
#### Scenario: 期望无响应且实际无响应
- **WHEN** udp target 配置 `expect.responded: false`,且 timeout 内未收到 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=true`statusDetail SHALL 表示未收到响应
#### Scenario: 期望无响应但实际收到响应
- **WHEN** udp target 配置 `expect.responded: false`,但收到了 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `mismatch`phase 为 `responded`
#### Scenario: UDP socket 底层错误
- **WHEN** Bun UDP socket 在发送或接收过程中触发 error 事件
- **THEN** 系统 SHALL best-effort 关闭 socket并记录 `matched=false`、failure.kind=`error` 和可读错误信息
#### 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``maxDurationMs`,并按 responded、responseSize、response、sourceHost、sourcePort、duration 的阶段顺序快速失败。
#### Scenario: 默认 responded 成功语义
- **WHEN** udp target 未显式配置 `expect.responded`
- **THEN** 系统 SHALL 使用默认 `expect.responded: true` 进行校验
#### Scenario: response text rules 校验通过
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`,且按 `responseEncoding` 转换后的响应文本包含 `PONG`
- **THEN** 系统 SHALL 判定 response 阶段通过
#### Scenario: response text rules 校验失败
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`,但按 `responseEncoding` 转换后的响应文本不包含 `PONG`
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `mismatch`phase 为 `response`path 指向失败的 response 规则
#### Scenario: responseEncoding 为 hex
- **WHEN** udp target 配置 `udp.responseEncoding: "hex"` 且收到字节内容 `PONG`
- **THEN** 系统 SHALL 将响应转换为小写 hex 字符串 `504f4e47` 后执行 `expect.response` 规则
#### Scenario: responseEncoding 为 base64
- **WHEN** udp target 配置 `udp.responseEncoding: "base64"` 且收到字节内容 `PONG`
- **THEN** 系统 SHALL 将响应转换为 base64 字符串 `UE9ORw==` 后执行 `expect.response` 规则
#### Scenario: responseSize operator 校验通过
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`,且实际响应为 4 字节
- **THEN** 系统 SHALL 判定 responseSize 阶段通过
#### Scenario: responseSize operator 校验失败
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`,但实际响应为 2 字节
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `mismatch`phase 为 `responseSize`
#### Scenario: sourceHost operator 校验
- **WHEN** udp target 配置 `expect.sourceHost: { equals: "127.0.0.1" }`,且 Bun 回调中的来源地址为 `127.0.0.1`
- **THEN** 系统 SHALL 判定 sourceHost 阶段通过
#### Scenario: sourcePort operator 校验
- **WHEN** udp target 配置 `expect.sourcePort: { equals: 9000 }`,且 Bun 回调中的来源端口为 `9000`
- **THEN** 系统 SHALL 判定 sourcePort 阶段通过
#### Scenario: maxDurationMs 校验
- **WHEN** udp target 配置 `expect.maxDurationMs: 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]` 或其他非 udp expect 字段
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
### Requirement: udp statusDetail 摘要
系统 SHALL 在 udp 执行后生成简短 statusDetail 摘要,展示关键结果并避免写入过长响应内容。
#### Scenario: 收到响应的摘要
- **WHEN** udp target 收到 4 字节响应且完整执行耗时为 12ms
- **THEN** statusDetail SHALL 包含 `responded in 12ms``4 bytes`
#### Scenario: 未收到响应的摘要
- **WHEN** udp target 配置 `expect.responded: false` 且 timeout 内未收到 UDP datagram
- **THEN** statusDetail SHALL 包含 `no response` 和执行耗时
#### Scenario: 响应内容摘要截断
- **WHEN** udp target 收到较长响应内容
- **THEN** statusDetail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要