refactor: ICMP checker type 从 ping 统一改为 icmp,修复前端 UI 细节
- ICMP checker 的 type/configKey/YAML 配置键/接口属性名从 ping 改为 icmp - IcmpChecker 添加 platform 构造函数注入,修复 Windows 测试兼容性 - 前端 target 表格延迟列优化:标题简化为「延迟」,单位下移到单元格,宽度 80px - Drawer 概览页 Descriptions 添加 tableLayout=auto 收窄 label 宽度 - 同步更新 README.md、DEVELOPMENT.md、probes.example.yaml、JSON Schema 和全部测试
This commit is contained in:
@@ -63,7 +63,7 @@ src/
|
|||||||
cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate)
|
cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||||
db/ DB Checker(自包含模块,含 types/schema/execute/expect/validate)
|
db/ DB Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||||
tcp/ TCP Checker(自包含模块,含 types/schema/execute/expect/validate)
|
tcp/ TCP Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||||
icmp/ Ping Checker(自包含模块,含 types/schema/execute/expect/validate/parse)
|
icmp/ ICMP Checker(自包含模块,含 types/schema/execute/expect/validate/parse)
|
||||||
udp/ UDP Checker(自包含模块,含 types/schema/execute/expect/validate/encoding)
|
udp/ UDP Checker(自包含模块,含 types/schema/execute/expect/validate/encoding)
|
||||||
llm/ LLM Checker(自包含模块,含 types/schema/execute/expect/validate/provider/observation)
|
llm/ LLM Checker(自包含模块,含 types/schema/execute/expect/validate/provider/observation)
|
||||||
shared/
|
shared/
|
||||||
@@ -486,7 +486,7 @@ TcpChecker implements Checker
|
|||||||
- **调度**:`ProbeEngine` 用 `es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
|
- **调度**:`ProbeEngine` 用 `es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
|
||||||
- **并发控制**:`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks`,默认 20),`acquire()` 阻塞等待
|
- **并发控制**:`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks`,默认 20),`acquire()` 阻塞等待
|
||||||
- **Runner 选择**:`engine.runCheck()` 通过 `checkerRegistry.get(target.type)` 获取 checker,并调用 `checker.execute(target, { signal })`
|
- **Runner 选择**:`engine.runCheck()` 通过 `checkerRegistry.get(target.type)` 获取 checker,并调用 `checker.execute(target, { signal })`
|
||||||
- **超时控制**:`ProbeEngine` 为每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abort;checker 必须使用 `CheckerContext.signal` 感知超时,HTTP 将 signal 传给 `fetch()`,Cmd 和 Ping 在 signal abort 时 `proc.kill()`
|
- **超时控制**:`ProbeEngine` 为每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abort;checker 必须使用 `CheckerContext.signal` 感知超时,HTTP 将 signal 传给 `fetch()`,Cmd 和 ICMP 在 signal abort 时 `proc.kill()`
|
||||||
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 基于配置 target id 确认目标仍存在;detail 为 API 层从 observation 派生,不进入存储层
|
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 基于配置 target id 确认目标仍存在;detail 为 API 层从 observation 派生,不进入存储层
|
||||||
- **异常可观测**:`probeGroup()` 对 `Promise.allSettled` 的 rejected 结果通过索引关联 target,并写入 `phase:"internal"` 的失败记录
|
- **异常可观测**:`probeGroup()` 对 `Promise.allSettled` 的 rejected 结果通过索引关联 target,并写入 `phase:"internal"` 的失败记录
|
||||||
- **数据清理**:当 `retentionMs > 0` 时,engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据
|
- **数据清理**:当 `retentionMs > 0` 时,engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据
|
||||||
@@ -508,7 +508,7 @@ TcpChecker implements Checker
|
|||||||
|
|
||||||
`ContentRules` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
|
`ContentRules` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
|
||||||
|
|
||||||
启动期语义校验统一由 `expect/validate-matcher.ts` 负责,会校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentRules 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性和 ReDoS 风险。旧字段 `match`、`maxDurationMs`、Ping 的 `max*` 阈值字段不再支持。
|
启动期语义校验统一由 `expect/validate-matcher.ts` 负责,会校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentRules 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性和 ReDoS 风险。旧字段 `match`、`maxDurationMs`、ICMP 的 `max*` 阈值字段不再支持。
|
||||||
|
|
||||||
**快速失败顺序**:
|
**快速失败顺序**:
|
||||||
|
|
||||||
@@ -519,7 +519,7 @@ TcpChecker implements Checker
|
|||||||
| DB | `durationMs → rowCount → rows → result` |
|
| DB | `durationMs → rowCount → rows → result` |
|
||||||
| TCP | `connected → banner → durationMs` |
|
| TCP | `connected → banner → durationMs` |
|
||||||
| UDP | `responded → responseSize → response → sourceHost → sourcePort → durationMs` |
|
| UDP | `responded → responseSize → response → sourceHost → sourcePort → durationMs` |
|
||||||
| Ping | `alive → packetLossPercent → avgLatencyMs → maxLatencyMs → durationMs` |
|
| ICMP | `alive → packetLossPercent → avgLatencyMs → maxLatencyMs → durationMs` |
|
||||||
| LLM http | `status → headers → output → finishReason → rawFinishReason → usage → durationMs` |
|
| LLM http | `status → headers → output → finishReason → rawFinishReason → usage → durationMs` |
|
||||||
| LLM stream | `status → headers → stream.completed → stream.firstTokenMs → output → finishReason → rawFinishReason → usage → durationMs` |
|
| LLM stream | `status → headers → stream.completed → stream.firstTokenMs → output → finishReason → rawFinishReason → usage → durationMs` |
|
||||||
|
|
||||||
@@ -535,7 +535,7 @@ expect 字段
|
|||||||
├─ 状态类结果,结果集合小且稳定
|
├─ 状态类结果,结果集合小且稳定
|
||||||
│ └─ enum / boolean
|
│ └─ enum / boolean
|
||||||
│ HTTP/LLM status、Cmd exitCode、TCP connected、
|
│ HTTP/LLM status、Cmd exitCode、TCP connected、
|
||||||
│ UDP responded、Ping alive
|
│ UDP responded、ICMP alive
|
||||||
│
|
│
|
||||||
├─ 数字指标 / 字符串元数据
|
├─ 数字指标 / 字符串元数据
|
||||||
│ └─ ValueMatcher
|
│ └─ ValueMatcher
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
DiAL 是一个自托管的拨测监控工具,支持 **HTTP**、**命令行**、**数据库**、**TCP**、**UDP**、**Ping** 和 **LLM** 多种拨测类型。通过 YAML 配置文件定义拨测目标,后端定时并发执行拨测并将结果持久化到本地 SQLite,前端 Dashboard 展示各目标的实时状态、可用率和耗时趋势。
|
DiAL 是一个自托管的拨测监控工具,支持 **HTTP**、**命令行**、**数据库**、**TCP**、**UDP**、**ICMP** 和 **LLM** 多种拨测类型。通过 YAML 配置文件定义拨测目标,后端定时并发执行拨测并将结果持久化到本地 SQLite,前端 Dashboard 展示各目标的实时状态、可用率和耗时趋势。
|
||||||
|
|
||||||
**功能亮点:**
|
**功能亮点:**
|
||||||
|
|
||||||
- 多种拨测类型:HTTP(GET/POST/PUT 等)、Cmd(命令行执行)、DB(PostgreSQL/MySQL/SQLite)、TCP(端口可达性 + Banner 探测)、UDP(自定义 payload 请求-响应)、Ping(ICMP 存活、延迟、丢包率)、LLM(大模型服务应用层健康检查)
|
- 多种拨测类型:HTTP(GET/POST/PUT 等)、Cmd(命令行执行)、DB(PostgreSQL/MySQL/SQLite)、TCP(端口可达性 + Banner 探测)、UDP(自定义 payload 请求-响应)、ICMP(存活检测、延迟、丢包率)、LLM(大模型服务应用层健康检查)
|
||||||
- 丰富的校验规则:状态码、响应头、JSONPath、CSS 选择器、XPath、正则匹配、数值比较等
|
- 丰富的校验规则:状态码、响应头、JSONPath、CSS 选择器、XPath、正则匹配、数值比较等
|
||||||
- 结构化观测数据:检查结果保留 HTTP body 预览、TCP/UDP 响应摘要、Ping 丢包率、CMD 输出预览、LLM token 用量等 observation,便于排障和后续分析
|
- 结构化观测数据:检查结果保留 HTTP body 预览、TCP/UDP 响应摘要、ICMP 丢包率、CMD 输出预览、LLM token 用量等 observation,便于排障和后续分析
|
||||||
- 响应式 Dashboard:实时状态、可用率统计、耗时趋势图、手动/自动刷新
|
- 响应式 Dashboard:实时状态、可用率统计、耗时趋势图、手动/自动刷新
|
||||||
- 多主题支持:系统、明亮、黑暗三种主题模式
|
- 多主题支持:系统、明亮、黑暗三种主题模式
|
||||||
- 零外部依赖:数据存储使用 SQLite,无需额外数据库服务
|
- 零外部依赖:数据存储使用 SQLite,无需额外数据库服务
|
||||||
@@ -25,7 +25,7 @@ DiAL 是一个自托管的拨测监控工具,支持 **HTTP**、**命令行**
|
|||||||
|
|
||||||
**前置条件:** [Bun](https://bun.sh/) >= 1.0
|
**前置条件:** [Bun](https://bun.sh/) >= 1.0
|
||||||
|
|
||||||
Ping checker 依赖系统 `ping` 命令。精简容器镜像需额外安装,例如 Alpine 可安装 `iputils-ping`。
|
ICMP checker 依赖系统 `ping` 命令。精简容器镜像需额外安装,例如 Alpine 可安装 `iputils-ping`。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
# 克隆仓库
|
||||||
@@ -161,10 +161,10 @@ targets:
|
|||||||
durationMs:
|
durationMs:
|
||||||
lte: 100
|
lte: 100
|
||||||
|
|
||||||
- id: "gateway-ping"
|
- id: "gateway-icmp"
|
||||||
name: "网关 ICMP 可达"
|
name: "网关 ICMP 可达"
|
||||||
type: ping
|
type: icmp
|
||||||
ping:
|
icmp:
|
||||||
host: "10.0.0.1"
|
host: "10.0.0.1"
|
||||||
count: 3
|
count: 3
|
||||||
packetSize: 56
|
packetSize: 56
|
||||||
@@ -227,7 +227,7 @@ targets:
|
|||||||
| `id` | 目标唯一标识,最长 30 字符,支持字母数字、下划线、连字符,不参与变量替换 | 是 |
|
| `id` | 目标唯一标识,最长 30 字符,支持字母数字、下划线、连字符,不参与变量替换 | 是 |
|
||||||
| `name` | 展示名称,最长 30 字符,支持变量替换,可省略或显式 null;前端展示时 null 回退到 `id` | 否 |
|
| `name` | 展示名称,最长 30 字符,支持变量替换,可省略或显式 null;前端展示时 null 回退到 `id` | 否 |
|
||||||
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 |
|
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 |
|
||||||
| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`ping`、`llm` | 是 |
|
| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`icmp`、`llm` | 是 |
|
||||||
| `group` | 分组名称 | 否,默认 `"default"` |
|
| `group` | 分组名称 | 否,默认 `"default"` |
|
||||||
| `interval` | 覆盖全局拨测间隔 | 否 |
|
| `interval` | 覆盖全局拨测间隔 | 否 |
|
||||||
| `timeout` | 覆盖全局超时时间 | 否 |
|
| `timeout` | 覆盖全局超时时间 | 否 |
|
||||||
@@ -269,15 +269,15 @@ targets:
|
|||||||
| `tcp.bannerReadTimeout` | banner 读取超时(毫秒),默认 `2000` |
|
| `tcp.bannerReadTimeout` | banner 读取超时(毫秒),默认 `2000` |
|
||||||
| `tcp.maxBannerBytes` | banner 最大字节数,支持 `KB`/`MB`/`GB` 单位,默认 `4KB` |
|
| `tcp.maxBannerBytes` | banner 最大字节数,支持 `KB`/`MB`/`GB` 单位,默认 `4KB` |
|
||||||
|
|
||||||
**Ping 类型** (`type: ping`)
|
**ICMP 类型** (`type: icmp`)
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
| ----------------- | ----------------------------------- |
|
| ----------------- | ----------------------------------- |
|
||||||
| `ping.host` | 目标主机地址 |
|
| `icmp.host` | 目标主机地址 |
|
||||||
| `ping.count` | ICMP 包数量,默认 `3`,范围 `1-100` |
|
| `icmp.count` | ICMP 包数量,默认 `3`,范围 `1-100` |
|
||||||
| `ping.packetSize` | ICMP 包大小(bytes),默认 `56` |
|
| `icmp.packetSize` | ICMP 包大小(bytes),默认 `56` |
|
||||||
|
|
||||||
Ping checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS 和 Windows 输出解析。
|
ICMP checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS 和 Windows 输出解析。
|
||||||
|
|
||||||
**LLM 类型** (`type: llm`)
|
**LLM 类型** (`type: llm`)
|
||||||
|
|
||||||
@@ -324,10 +324,10 @@ Ping checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS
|
|||||||
| `responseSize` | UDP | 响应字节数校验,使用 `ValueMatcher` |
|
| `responseSize` | UDP | 响应字节数校验,使用 `ValueMatcher` |
|
||||||
| `sourceHost` | UDP | 响应来源地址校验,使用 `ValueMatcher` |
|
| `sourceHost` | UDP | 响应来源地址校验,使用 `ValueMatcher` |
|
||||||
| `sourcePort` | UDP | 响应来源端口校验,使用 `ValueMatcher` |
|
| `sourcePort` | UDP | 响应来源端口校验,使用 `ValueMatcher` |
|
||||||
| `alive` | Ping | 期望主机可达性,默认 `true` |
|
| `alive` | ICMP | 期望主机可达性,默认 `true` |
|
||||||
| `packetLossPercent` | Ping | 丢包率百分比校验,范围 `0-100`,使用 `ValueMatcher` |
|
| `packetLossPercent` | ICMP | 丢包率百分比校验,范围 `0-100`,使用 `ValueMatcher` |
|
||||||
| `avgLatencyMs` | Ping | 平均延迟校验,使用 `ValueMatcher` |
|
| `avgLatencyMs` | ICMP | 平均延迟校验,使用 `ValueMatcher` |
|
||||||
| `maxLatencyMs` | Ping | 最大单次延迟校验,使用 `ValueMatcher` |
|
| `maxLatencyMs` | ICMP | 最大单次延迟校验,使用 `ValueMatcher` |
|
||||||
|
|
||||||
**ContentRules 校验项**(`body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` 均使用数组):
|
**ContentRules 校验项**(`body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` 均使用数组):
|
||||||
|
|
||||||
@@ -339,7 +339,7 @@ Ping checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS
|
|||||||
|
|
||||||
**ValueMatcher 字段**:`equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。`equals` 支持 JSON 深度相等;`regex` 固定使用无 flags 正则;提取器未配置 matcher 时等价于 `exists: true`。ValueMatcher expect 字段也可直接写 string、number、boolean 或 null,等价于 `{ equals: value }`;数组和对象必须显式写成 `{ equals: ... }`。
|
**ValueMatcher 字段**:`equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。`equals` 支持 JSON 深度相等;`regex` 固定使用无 flags 正则;提取器未配置 matcher 时等价于 `exists: true`。ValueMatcher expect 字段也可直接写 string、number、boolean 或 null,等价于 `{ equals: value }`;数组和对象必须显式写成 `{ equals: ... }`。
|
||||||
|
|
||||||
旧字段 `maxDurationMs`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs` 和旧正则字段 `match` 已移除,请分别改用 `durationMs`、Ping matcher 字段和 `regex`。
|
旧字段 `maxDurationMs`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs` 和旧正则字段 `match` 已移除,请分别改用 `durationMs`、ICMP matcher 字段和 `regex`。
|
||||||
|
|
||||||
**大小说明**:`maxBodyBytes` 和 `maxOutputBytes` 支持 `KB`、`MB`、`GB` 单位,也可直接使用数字。
|
**大小说明**:`maxBodyBytes` 和 `maxOutputBytes` 支持 `KB`、`MB`、`GB` 单位,也可直接使用数字。
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +1,88 @@
|
|||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
定义 ICMP/Ping checker 的配置格式、命令执行、跨平台输出解析、expect 校验、失败结构和状态摘要。
|
定义 ICMP checker 的配置格式、命令执行、跨平台输出解析、expect 校验、失败结构和状态摘要。
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Requirement: ping target 配置
|
### Requirement: icmp target 配置
|
||||||
系统 SHALL 支持 `type: ping` 的 target 配置,通过 `ping.host` 描述目标主机地址,并通过可选字段控制探测行为。
|
系统 SHALL 支持 `type: icmp` 的 target 配置,通过 `icmp.host` 描述目标主机地址,并通过可选字段控制探测行为。
|
||||||
|
|
||||||
#### Scenario: 解析最简 ping target
|
#### Scenario: 解析最简 icmp target
|
||||||
- **WHEN** YAML 中 target 配置 `type: ping` 和 `ping.host: "10.0.0.1"`
|
- **WHEN** YAML 中 target 配置 `type: icmp` 和 `icmp.host: "10.0.0.1"`
|
||||||
- **THEN** 系统 SHALL 将其解析为 ping checker,并填充 `count=3`、`packetSize=56`、interval、timeout、group 和 expect 配置
|
- **THEN** 系统 SHALL 将其解析为 icmp checker,并填充 `count=3`、`packetSize=56`、interval、timeout、group 和 expect 配置
|
||||||
|
|
||||||
#### Scenario: ping target 缺少 host
|
#### Scenario: icmp target 缺少 host
|
||||||
- **WHEN** YAML 中 target 配置 `type: ping` 但缺少 `ping.host`
|
- **WHEN** YAML 中 target 配置 `type: icmp` 但缺少 `icmp.host`
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 ping.host 字段
|
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 icmp.host 字段
|
||||||
|
|
||||||
#### Scenario: ping host 类型非法
|
#### Scenario: icmp host 类型非法
|
||||||
- **WHEN** YAML 中 ping target 的 `ping.host` 不是非空字符串
|
- **WHEN** YAML 中 icmp target 的 `icmp.host` 不是非空字符串
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 ping.host 必须为非空字符串
|
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp.host 必须为非空字符串
|
||||||
|
|
||||||
#### Scenario: ping count 配置
|
#### Scenario: icmp count 配置
|
||||||
- **WHEN** YAML 中 ping target 配置 `ping.count: 5`
|
- **WHEN** YAML 中 icmp target 配置 `icmp.count: 5`
|
||||||
- **THEN** 系统 SHALL 使用 5 作为 ICMP 包发送数量
|
- **THEN** 系统 SHALL 使用 5 作为 ICMP 包发送数量
|
||||||
|
|
||||||
#### Scenario: ping count 非法
|
#### Scenario: icmp count 非法
|
||||||
- **WHEN** YAML 中 ping target 的 `ping.count` 不是 1 到 100 之间的正整数
|
- **WHEN** YAML 中 icmp target 的 `icmp.count` 不是 1 到 100 之间的正整数
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 ping.count 必须为 1-100 的正整数
|
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp.count 必须为 1-100 的正整数
|
||||||
|
|
||||||
#### Scenario: ping packetSize 配置
|
#### Scenario: icmp packetSize 配置
|
||||||
- **WHEN** YAML 中 ping target 配置 `ping.packetSize: 1472`
|
- **WHEN** YAML 中 icmp target 配置 `icmp.packetSize: 1472`
|
||||||
- **THEN** 系统 SHALL 使用 1472 作为 ICMP 包大小(bytes)
|
- **THEN** 系统 SHALL 使用 1472 作为 ICMP 包大小(bytes)
|
||||||
|
|
||||||
#### Scenario: ping packetSize 非法
|
#### Scenario: icmp packetSize 非法
|
||||||
- **WHEN** YAML 中 ping target 的 `ping.packetSize` 不是 1 到 65500 之间的正整数
|
- **WHEN** YAML 中 icmp target 的 `icmp.packetSize` 不是 1 到 65500 之间的正整数
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 ping.packetSize 必须为 1-65500 的正整数
|
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp.packetSize 必须为 1-65500 的正整数
|
||||||
|
|
||||||
#### Scenario: ping 分组未知字段失败
|
#### Scenario: icmp 分组未知字段失败
|
||||||
- **WHEN** YAML 中 ping target 的 `ping` 分组包含 `timeout: 5` 等未知字段
|
- **WHEN** YAML 中 icmp target 的 `icmp` 分组包含 `timeout: 5` 等未知字段
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 ping 分组包含未知字段
|
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp 分组包含未知字段
|
||||||
|
|
||||||
#### Scenario: ping 序列化展示摘要
|
#### Scenario: icmp 序列化展示摘要
|
||||||
- **WHEN** 系统同步 ping target 到 targets 表
|
- **WHEN** 系统同步 icmp target 到 targets 表
|
||||||
- **THEN** `target` 展示摘要 SHALL 为 `ping <host>`,`config` JSON SHALL 包含 resolved 后的 host、count 和 packetSize
|
- **THEN** `target` 展示摘要 SHALL 为 `icmp <host>`,`config` JSON SHALL 包含 resolved 后的 host、count 和 packetSize
|
||||||
|
|
||||||
### Requirement: ping checker 执行
|
### Requirement: icmp checker 执行
|
||||||
系统 SHALL 通过调用系统 `ping` 命令执行 ICMP 探测,记录完整执行耗时,并在命令不可用、超时或解析失败时产生结构化失败信息。
|
系统 SHALL 通过调用系统 `ping` 命令执行 ICMP 探测,记录完整执行耗时,并在命令不可用、超时或解析失败时产生结构化失败信息。`IcmpChecker` SHALL 通过构造函数参数支持 platform 注入,默认使用 `process.platform`。
|
||||||
|
|
||||||
#### Scenario: ping 命令构建(Linux)
|
#### Scenario: ping 命令构建(Linux)
|
||||||
- **WHEN** 系统平台为 linux,ping target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
- **WHEN** 系统平台为 linux,icmp target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
||||||
- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10 10.0.0.1`(-W 单位为秒,向上取整)
|
- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10 10.0.0.1`(-W 单位为秒,向上取整)
|
||||||
|
|
||||||
#### Scenario: ping 命令构建(macOS)
|
#### Scenario: ping 命令构建(macOS)
|
||||||
- **WHEN** 系统平台为 darwin,ping target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
- **WHEN** 系统平台为 darwin,icmp target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
||||||
- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10000 10.0.0.1`(-W 单位为毫秒)
|
- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10000 10.0.0.1`(-W 单位为毫秒)
|
||||||
|
|
||||||
#### Scenario: ping 命令构建(Windows)
|
#### Scenario: ping 命令构建(Windows)
|
||||||
- **WHEN** 系统平台为 win32,ping target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
- **WHEN** 系统平台为 win32,icmp target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
||||||
- **THEN** 系统 SHALL 执行 `ping -n 3 -l 56 -w 10000 10.0.0.1`(-w 单位为毫秒)
|
- **THEN** 系统 SHALL 执行 `ping -n 3 -l 56 -w 10000 10.0.0.1`(-w 单位为毫秒)
|
||||||
|
|
||||||
#### Scenario: ping 命令不存在
|
#### Scenario: ping 命令不存在
|
||||||
- **WHEN** 系统未安装 `ping` 命令(spawn 抛出 ENOENT)
|
- **WHEN** 系统未安装 `ping` 命令(spawn 抛出 ENOENT)
|
||||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `ping`,path 为 `spawn`,message 包含 "ping 命令不可用" 和原始错误信息
|
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `icmp`,path 为 `spawn`,message 包含 "icmp 命令不可用" 和原始错误信息
|
||||||
|
|
||||||
#### Scenario: ping 执行超时
|
#### Scenario: ping 执行超时
|
||||||
- **WHEN** 引擎注入的 `ctx.signal` 在 ping 命令执行过程中 abort
|
- **WHEN** 引擎注入的 `ctx.signal` 在 ping 命令执行过程中 abort
|
||||||
- **THEN** 系统 SHALL 调用 `proc.kill()` 终止子进程,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `ping`,message 包含超时信息
|
- **THEN** 系统 SHALL 调用 `proc.kill()` 终止子进程,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `icmp`,message 包含超时信息
|
||||||
|
|
||||||
#### Scenario: ping 目标可达
|
#### Scenario: ping 目标可达
|
||||||
- **WHEN** ping target 指向可达主机,且 ping 命令正常返回
|
- **WHEN** icmp target 指向可达主机,且 ping 命令正常返回
|
||||||
- **THEN** 系统 SHALL 解析 stdout 获取统计数据,并按断言链执行 expect 校验
|
- **THEN** 系统 SHALL 解析 stdout 获取统计数据,并按断言链执行 expect 校验
|
||||||
|
|
||||||
#### Scenario: ping 目标不可达
|
#### Scenario: ping 目标不可达
|
||||||
- **WHEN** ping target 指向不可达主机,且 ping 命令返回 100% packet loss
|
- **WHEN** icmp target 指向不可达主机,且 ping 命令返回 100% packet loss
|
||||||
- **THEN** 系统 SHALL 解析 stdout 获取统计数据,`alive` 为 false,延迟字段为 null
|
- **THEN** 系统 SHALL 解析 stdout 获取统计数据,`alive` 为 false,延迟字段为 null
|
||||||
|
|
||||||
#### Scenario: duration 覆盖完整执行
|
#### Scenario: duration 覆盖完整执行
|
||||||
- **WHEN** ping 命令执行完成
|
- **WHEN** ping 命令执行完成
|
||||||
- **THEN** 结果中的 `durationMs` SHALL 覆盖从 spawn 到进程退出的完整耗时
|
- **THEN** 结果中的 `durationMs` SHALL 覆盖从 spawn 到进程退出的完整耗时
|
||||||
|
|
||||||
### Requirement: 跨平台 ping 输出解析
|
#### Scenario: platform 注入用于测试
|
||||||
|
- **WHEN** 构造 `new IcmpChecker("linux")`
|
||||||
|
- **THEN** execute 方法 SHALL 使用注入的 "linux" 作为平台参数,而非 `process.platform`
|
||||||
|
|
||||||
|
### Requirement: 跨平台 icmp 输出解析
|
||||||
系统 SHALL 实现跨平台 ping 输出解析器,支持 Linux、macOS 和 Windows(含多语言 locale),从 stdout 中提取 transmitted、received、packetLoss、minLatencyMs、avgLatencyMs、maxLatencyMs。
|
系统 SHALL 实现跨平台 ping 输出解析器,支持 Linux、macOS 和 Windows(含多语言 locale),从 stdout 中提取 transmitted、received、packetLoss、minLatencyMs、avgLatencyMs、maxLatencyMs。
|
||||||
|
|
||||||
#### Scenario: 解析 Linux ping 输出
|
#### Scenario: 解析 Linux ping 输出
|
||||||
@@ -103,86 +107,86 @@
|
|||||||
|
|
||||||
#### Scenario: 输出无法解析
|
#### Scenario: 输出无法解析
|
||||||
- **WHEN** stdout 不匹配任何已知的统计行格式
|
- **WHEN** stdout 不匹配任何已知的统计行格式
|
||||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `ping`,path 为 `parse`,message 包含 "无法解析 ping 输出"
|
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `icmp`,path 为 `parse`,message 包含 "无法解析 icmp 输出"
|
||||||
|
|
||||||
### Requirement: ping expect 校验
|
### Requirement: icmp expect 校验
|
||||||
系统 SHALL 支持 ping 专属 expect,包括 `alive`、`packetLossPercent`、`avgLatencyMs`、`maxLatencyMs` 和 `durationMs`,并按 alive、packetLossPercent、avgLatencyMs、maxLatencyMs、durationMs 的阶段顺序快速失败。`alive` SHALL 保持布尔状态语义,未配置时默认 `true`。`packetLossPercent` SHALL 表示 0 到 100 的丢包率百分比,并使用共享 `ValueMatcher`。`avgLatencyMs`、`maxLatencyMs` 和 `durationMs` SHALL 使用共享 `ValueMatcher`。
|
系统 SHALL 支持 icmp 专属 expect,包括 `alive`、`packetLossPercent`、`avgLatencyMs`、`maxLatencyMs` 和 `durationMs`,并按 alive、packetLossPercent、avgLatencyMs、maxLatencyMs、durationMs 的阶段顺序快速失败。`alive` SHALL 保持布尔状态语义,未配置时默认 `true`。`packetLossPercent` SHALL 表示 0 到 100 的丢包率百分比,并使用共享 `ValueMatcher`。`avgLatencyMs`、`maxLatencyMs` 和 `durationMs` SHALL 使用共享 `ValueMatcher`。
|
||||||
|
|
||||||
#### Scenario: 默认 alive 成功语义
|
#### Scenario: 默认 alive 成功语义
|
||||||
- **WHEN** ping target 未显式配置 `expect.alive`
|
- **WHEN** icmp target 未显式配置 `expect.alive`
|
||||||
- **THEN** 系统 SHALL 使用默认 `expect.alive: true` 进行校验
|
- **THEN** 系统 SHALL 使用默认 `expect.alive: true` 进行校验
|
||||||
|
|
||||||
#### Scenario: alive 校验通过
|
#### Scenario: alive 校验通过
|
||||||
- **WHEN** ping target 配置 `expect.alive: true`,且目标主机可达
|
- **WHEN** icmp target 配置 `expect.alive: true`,且目标主机可达
|
||||||
- **THEN** 系统 SHALL 判定 alive 阶段通过
|
- **THEN** 系统 SHALL 判定 alive 阶段通过
|
||||||
|
|
||||||
#### Scenario: alive 校验失败
|
#### Scenario: alive 校验失败
|
||||||
- **WHEN** ping target 配置 `expect.alive: true`,且目标主机不可达
|
- **WHEN** icmp target 配置 `expect.alive: true`,且目标主机不可达
|
||||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `alive`
|
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `alive`
|
||||||
|
|
||||||
#### Scenario: 反向 alive 断言
|
#### Scenario: 反向 alive 断言
|
||||||
- **WHEN** ping target 配置 `expect.alive: false`,且目标主机不可达
|
- **WHEN** icmp target 配置 `expect.alive: false`,且目标主机不可达
|
||||||
- **THEN** 系统 SHALL 判定 alive 阶段通过(`matched=true`)
|
- **THEN** 系统 SHALL 判定 alive 阶段通过(`matched=true`)
|
||||||
|
|
||||||
#### Scenario: packetLossPercent 校验通过
|
#### Scenario: packetLossPercent 校验通过
|
||||||
- **WHEN** ping target 配置 `expect.packetLossPercent: {lte: 10}`,且实际丢包率为 0%
|
- **WHEN** icmp target 配置 `expect.packetLossPercent: {lte: 10}`,且实际丢包率为 0%
|
||||||
- **THEN** 系统 SHALL 判定 packetLossPercent 阶段通过
|
- **THEN** 系统 SHALL 判定 packetLossPercent 阶段通过
|
||||||
|
|
||||||
#### Scenario: packetLossPercent 校验失败
|
#### Scenario: packetLossPercent 校验失败
|
||||||
- **WHEN** ping target 配置 `expect.packetLossPercent: {lte: 10}`,且实际丢包率为 33%
|
- **WHEN** icmp target 配置 `expect.packetLossPercent: {lte: 10}`,且实际丢包率为 33%
|
||||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `packetLoss`
|
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `packetLoss`
|
||||||
|
|
||||||
#### Scenario: avgLatencyMs 校验通过
|
#### Scenario: avgLatencyMs 校验通过
|
||||||
- **WHEN** ping target 配置 `expect.avgLatencyMs: {lte: 200}`,且实际平均延迟为 12ms
|
- **WHEN** icmp target 配置 `expect.avgLatencyMs: {lte: 200}`,且实际平均延迟为 12ms
|
||||||
- **THEN** 系统 SHALL 判定 avgLatency 阶段通过
|
- **THEN** 系统 SHALL 判定 avgLatency 阶段通过
|
||||||
|
|
||||||
#### Scenario: avgLatencyMs 校验失败
|
#### Scenario: avgLatencyMs 校验失败
|
||||||
- **WHEN** ping target 配置 `expect.avgLatencyMs: {lte: 100}`,且实际平均延迟为 156ms
|
- **WHEN** icmp target 配置 `expect.avgLatencyMs: {lte: 100}`,且实际平均延迟为 156ms
|
||||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `avgLatency`
|
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `avgLatency`
|
||||||
|
|
||||||
#### Scenario: maxLatencyMs 校验通过
|
#### Scenario: maxLatencyMs 校验通过
|
||||||
- **WHEN** ping target 配置 `expect.maxLatencyMs: {lte: 500}`,且实际最大延迟为 340ms
|
- **WHEN** icmp target 配置 `expect.maxLatencyMs: {lte: 500}`,且实际最大延迟为 340ms
|
||||||
- **THEN** 系统 SHALL 判定 maxLatency 阶段通过
|
- **THEN** 系统 SHALL 判定 maxLatency 阶段通过
|
||||||
|
|
||||||
#### Scenario: maxLatencyMs 校验失败
|
#### Scenario: maxLatencyMs 校验失败
|
||||||
- **WHEN** ping target 配置 `expect.maxLatencyMs: {lte: 200}`,且实际最大延迟为 340ms
|
- **WHEN** icmp target 配置 `expect.maxLatencyMs: {lte: 200}`,且实际最大延迟为 340ms
|
||||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `maxLatency`
|
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `maxLatency`
|
||||||
|
|
||||||
#### Scenario: durationMs 校验
|
#### Scenario: durationMs 校验
|
||||||
- **WHEN** ping target 配置 `expect.durationMs: {lte: 5000}`,且完整执行耗时超过 5000ms
|
- **WHEN** icmp target 配置 `expect.durationMs: {lte: 5000}`,且完整执行耗时超过 5000ms
|
||||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||||
|
|
||||||
#### Scenario: alive=false 时跳过延迟断言
|
#### Scenario: alive=false 时跳过延迟断言
|
||||||
- **WHEN** ping target 配置 `expect.alive: true` 和 `expect.avgLatencyMs: {lte: 100}`,且目标不可达
|
- **WHEN** icmp target 配置 `expect.alive: true` 和 `expect.avgLatencyMs: {lte: 100}`,且目标不可达
|
||||||
- **THEN** 系统 SHALL 在 alive 阶段即返回失败,不执行后续延迟断言
|
- **THEN** 系统 SHALL 在 alive 阶段即返回失败,不执行后续延迟断言
|
||||||
|
|
||||||
#### Scenario: ping expect 未知字段失败
|
#### Scenario: icmp expect 未知字段失败
|
||||||
- **WHEN** YAML 中 ping target 的 expect 包含 `status: [200]`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs`、`maxDurationMs` 或其他非 ping expect 字段
|
- **WHEN** YAML 中 icmp target 的 expect 包含 `status: [200]`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs`、`maxDurationMs` 或其他非 icmp expect 字段
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||||
|
|
||||||
#### Scenario: packetLossPercent 类型非法
|
#### Scenario: packetLossPercent 类型非法
|
||||||
- **WHEN** YAML 中 ping target 的 `expect.packetLossPercent` 不是合法 `ValueMatcher`,或其数值范围无法用于 0 到 100 的百分比断言
|
- **WHEN** YAML 中 icmp target 的 `expect.packetLossPercent` 不是合法 `ValueMatcher`,或其数值范围无法用于 0 到 100 的百分比断言
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.packetLossPercent 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.packetLossPercent 格式错误
|
||||||
|
|
||||||
#### Scenario: avgLatencyMs 类型非法
|
#### Scenario: avgLatencyMs 类型非法
|
||||||
- **WHEN** YAML 中 ping target 的 `expect.avgLatencyMs` 不是合法 `ValueMatcher`
|
- **WHEN** YAML 中 icmp target 的 `expect.avgLatencyMs` 不是合法 `ValueMatcher`
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.avgLatencyMs 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.avgLatencyMs 格式错误
|
||||||
|
|
||||||
#### Scenario: maxLatencyMs 类型非法
|
#### Scenario: maxLatencyMs 类型非法
|
||||||
- **WHEN** YAML 中 ping target 的 `expect.maxLatencyMs` 不是合法 `ValueMatcher`
|
- **WHEN** YAML 中 icmp target 的 `expect.maxLatencyMs` 不是合法 `ValueMatcher`
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxLatencyMs 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxLatencyMs 格式错误
|
||||||
|
|
||||||
### Requirement: ping detail 摘要
|
### Requirement: icmp detail 摘要
|
||||||
系统 SHALL 在 ping API 序列化时从 observation 动态生成结构化 detail 摘要,展示关键指标。API registry type SHALL 仍为 `ping`。
|
系统 SHALL 在 icmp API 序列化时从 observation 动态生成结构化 detail 摘要,展示关键指标。API registry type SHALL 仍为 `icmp`。
|
||||||
|
|
||||||
#### Scenario: 目标可达无丢包
|
#### Scenario: 目标可达无丢包
|
||||||
- **WHEN** ping observation 为 alive=true, avgLatencyMs=12, packetLoss=0%, transmitted=3, received=3
|
- **WHEN** icmp observation 为 alive=true, avgLatencyMs=12, packetLoss=0%, transmitted=3, received=3
|
||||||
- **THEN** detail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
|
- **THEN** detail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
|
||||||
|
|
||||||
#### Scenario: 目标可达有丢包
|
#### Scenario: 目标可达有丢包
|
||||||
- **WHEN** ping observation 为 alive=true, avgLatencyMs=156, maxLatencyMs=340, packetLoss=33%, transmitted=3, received=2
|
- **WHEN** icmp observation 为 alive=true, avgLatencyMs=156, maxLatencyMs=340, packetLoss=33%, transmitted=3, received=2
|
||||||
- **THEN** detail SHALL 包含 avg、max 和 loss 信息
|
- **THEN** detail SHALL 包含 avg、max 和 loss 信息
|
||||||
|
|
||||||
#### Scenario: 目标不可达
|
#### Scenario: 目标不可达
|
||||||
- **WHEN** ping observation 为 alive=false, transmitted=3, received=0
|
- **WHEN** icmp observation 为 alive=false, transmitted=3, received=0
|
||||||
- **THEN** detail SHALL 为 `unreachable (0/3 received)`
|
- **THEN** detail SHALL 为 `unreachable (0/3 received)`
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Requirement: YAML 配置文件格式
|
### Requirement: YAML 配置文件格式
|
||||||
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、可选的 variables 段、checker 默认值和 typed target 列表(含可选 group 字段)。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,ping 领域字段 MUST 放在 `ping` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Ping target 的 `ping` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。
|
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、可选的 variables 段、checker 默认值和 typed target 列表(含可选 group 字段)。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。
|
||||||
|
|
||||||
`defaults.http` 分组 SHALL 仅支持 `headers`(可选)和 `maxBodyBytes`(可选)字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。`defaults.udp` 分组 SHALL 仅支持 `encoding`(可选)、`responseEncoding`(可选)和 `maxResponseBytes`(可选)字段。`defaults.llm` 分组 SHALL 仅支持 `mode`(可选)、`headers`(可选)、`ignoreSSL`(可选)、`options`(可选)和 `providerOptions`(可选)字段。
|
`defaults.http` 分组 SHALL 仅支持 `headers`(可选)和 `maxBodyBytes`(可选)字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。`defaults.icmp` 分组 SHALL 仅支持空对象。`defaults.udp` 分组 SHALL 仅支持 `encoding`(可选)、`responseEncoding`(可选)和 `maxResponseBytes`(可选)字段。`defaults.llm` 分组 SHALL 仅支持 `mode`(可选)、`headers`(可选)、`ignoreSSL`(可选)、`options`(可选)和 `providerOptions`(可选)字段。
|
||||||
|
|
||||||
#### Scenario: 完整配置文件解析
|
#### Scenario: 完整配置文件解析
|
||||||
- **WHEN** 系统启动并读取包含 server、runtime、variables、defaults、targets(含 id、group 字段)的 YAML 配置文件
|
- **WHEN** 系统启动并读取包含 server、runtime、variables、defaults、targets(含 id、group 字段)的 YAML 配置文件
|
||||||
@@ -57,9 +57,9 @@
|
|||||||
- **WHEN** HTTP target 未配置 `http.method` 且 defaults.http 中无 method 字段
|
- **WHEN** HTTP target 未配置 `http.method` 且 defaults.http 中无 method 字段
|
||||||
- **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法
|
- **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法
|
||||||
|
|
||||||
#### Scenario: 最简 ping 配置文件解析
|
#### Scenario: 最简 icmp 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: ping` target(含 `id` 和 `ping.host`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: icmp` target(含 `id` 和 `icmp.host`)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", ping.count=3, ping.packetSize=56),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", icmp.count=3, icmp.packetSize=56),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: 最简 udp 配置文件解析
|
#### Scenario: 最简 udp 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: udp` target(含 `id`、`udp.host` 和 `udp.port`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: udp` target(含 `id`、`udp.host` 和 `udp.port`)的 YAML 配置文件
|
||||||
@@ -219,9 +219,9 @@
|
|||||||
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为对象 `{foo: "bar"}`,且 `foo` 不是合法 matcher 字段
|
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为对象 `{foo: "bar"}`,且 `foo` 不是合法 matcher 字段
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示 `foo` 是未知 matcher;如需对象 equals 匹配应写成 `{equals: {foo: "bar"}}`
|
- **THEN** 系统 SHALL 以错误退出,提示 `foo` 是未知 matcher;如需对象 equals 匹配应写成 `{equals: {foo: "bar"}}`
|
||||||
|
|
||||||
#### Scenario: ping target 缺少 host
|
#### Scenario: icmp target 缺少 host
|
||||||
- **WHEN** YAML 中某个 target 配置 `type: ping` 但缺少 `ping.host`
|
- **WHEN** YAML 中某个 target 配置 `type: icmp` 但缺少 `icmp.host`
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 ping.host 字段
|
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 icmp.host 字段
|
||||||
|
|
||||||
#### Scenario: ping expect 未知字段
|
#### Scenario: ping expect 未知字段
|
||||||
- **WHEN** YAML 中 ping target 的 expect 包含非 ping expect 字段
|
- **WHEN** YAML 中 ping target 的 expect 包含非 ping expect 字段
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。Tabs
|
|||||||
|
|
||||||
#### Scenario: 基本信息直接展示
|
#### Scenario: 基本信息直接展示
|
||||||
- **WHEN** 概览面板渲染
|
- **WHEN** 概览面板渲染
|
||||||
- **THEN** 面板 SHALL 在"基本信息"区域直接使用 TDesign Descriptions 组件展示配置信息(不折叠)
|
- **THEN** 面板 SHALL 在"基本信息"区域直接使用 TDesign Descriptions 组件展示配置信息(不折叠),Descriptions SHALL 配置 `tableLayout="auto"` 使 label 宽度自适应内容
|
||||||
|
|
||||||
#### Scenario: 基本信息内容
|
#### Scenario: 基本信息内容
|
||||||
- **WHEN** 概览面板渲染
|
- **WHEN** 概览面板渲染
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Dashboard SHALL 按 group 字段将目标分组,每个分组使用 TDesign Car
|
|||||||
|
|
||||||
#### Scenario: 延迟列
|
#### Scenario: 延迟列
|
||||||
- **WHEN** 表格渲染
|
- **WHEN** 表格渲染
|
||||||
- **THEN** 延迟列标题 SHALL 展示为"延迟(ms)",单元格 SHALL 显示最近一次检查的延迟毫秒数值并右对齐,颜色通过 CSS 类控制;超过 9999ms 时 SHALL 显示为"9999+"
|
- **THEN** 延迟列标题 SHALL 展示为"延迟",宽度 SHALL 为 80px,单元格 SHALL 显示最近一次检查的延迟数值并附加 " ms" 后缀(如 "156 ms"),右对齐,颜色通过 CSS 类控制;超过 9999ms 时 SHALL 显示为"9999+ ms"
|
||||||
|
|
||||||
#### Scenario: 间隔列移除
|
#### Scenario: 间隔列移除
|
||||||
- **WHEN** 表格渲染
|
- **WHEN** 表格渲染
|
||||||
|
|||||||
@@ -52,3 +52,7 @@ probes.example.yaml 中的 cmd 类型示例 SHALL 使用跨平台命令(如 `b
|
|||||||
#### Scenario: 示例命令跨平台可执行
|
#### Scenario: 示例命令跨平台可执行
|
||||||
- **WHEN** 用户在 Windows、macOS 或 Linux 上直接使用 probes.example.yaml 中的 cmd 示例
|
- **WHEN** 用户在 Windows、macOS 或 Linux 上直接使用 probes.example.yaml 中的 cmd 示例
|
||||||
- **THEN** 所有 cmd 示例 SHALL 能正常执行,不依赖平台特定命令
|
- **THEN** 所有 cmd 示例 SHALL 能正常执行,不依赖平台特定命令
|
||||||
|
|
||||||
|
#### Scenario: ICMP checker 测试使用 platform 注入
|
||||||
|
- **WHEN** 在 Windows 上运行 ICMP checker 测试,mock 的 stdout 为 Unix 格式
|
||||||
|
- **THEN** 测试 SHALL 通过 `new IcmpChecker("linux")` 构造 checker 实例,使 parsePingOutput 使用 Unix 解析器,确保测试在所有平台上通过
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ping": {
|
"icmp": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
@@ -2479,7 +2479,7 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"type",
|
"type",
|
||||||
"ping"
|
"icmp"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"description": {
|
"description": {
|
||||||
@@ -2829,10 +2829,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"const": "ping",
|
"const": "icmp",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"ping": {
|
"icmp": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -178,13 +178,13 @@ targets:
|
|||||||
banner:
|
banner:
|
||||||
- contains: "ESMTP"
|
- contains: "ESMTP"
|
||||||
|
|
||||||
# ========== Ping targets ==========
|
# ========== ICMP targets ==========
|
||||||
|
|
||||||
- id: "gateway-ping"
|
- id: "gateway-icmp"
|
||||||
name: "网关 ICMP 可达"
|
name: "网关 ICMP 可达"
|
||||||
type: ping
|
type: icmp
|
||||||
group: "基础设施"
|
group: "基础设施"
|
||||||
ping:
|
icmp:
|
||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
count: 3
|
count: 3
|
||||||
packetSize: 56
|
packetSize: 56
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ export function buildPingCommand(t: ResolvedPingTarget, platform: NodeJS.Platfor
|
|||||||
return [
|
return [
|
||||||
"ping",
|
"ping",
|
||||||
"-n",
|
"-n",
|
||||||
String(t.ping.count),
|
String(t.icmp.count),
|
||||||
"-l",
|
"-l",
|
||||||
String(t.ping.packetSize),
|
String(t.icmp.packetSize),
|
||||||
"-w",
|
"-w",
|
||||||
String(t.timeoutMs),
|
String(t.timeoutMs),
|
||||||
t.ping.host,
|
t.icmp.host,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
const timeout = platform === "linux" ? String(Math.ceil(t.timeoutMs / 1000)) : String(t.timeoutMs);
|
const timeout = platform === "linux" ? String(Math.ceil(t.timeoutMs / 1000)) : String(t.timeoutMs);
|
||||||
return ["ping", "-c", String(t.ping.count), "-s", String(t.ping.packetSize), "-W", timeout, t.ping.host];
|
return ["ping", "-c", String(t.icmp.count), "-s", String(t.icmp.packetSize), "-W", timeout, t.icmp.host];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,17 @@ const DEFAULT_COUNT = 3;
|
|||||||
const DEFAULT_PACKET_SIZE = 56;
|
const DEFAULT_PACKET_SIZE = 56;
|
||||||
|
|
||||||
export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||||
readonly configKey = "ping";
|
readonly configKey = "icmp";
|
||||||
|
|
||||||
|
readonly platform: NodeJS.Platform;
|
||||||
|
|
||||||
readonly schemas = icmpCheckerSchemas;
|
readonly schemas = icmpCheckerSchemas;
|
||||||
|
|
||||||
readonly type = "ping";
|
readonly type = "icmp";
|
||||||
|
|
||||||
|
constructor(platform: NodeJS.Platform = process.platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
buildDetail(observation: Record<string, unknown>): null | string {
|
buildDetail(observation: Record<string, unknown>): null | string {
|
||||||
const alive = observation["alive"];
|
const alive = observation["alive"];
|
||||||
@@ -67,7 +73,7 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
|||||||
return {
|
return {
|
||||||
detail: null,
|
detail: null,
|
||||||
durationMs,
|
durationMs,
|
||||||
failure: errorFailure("ping", "spawn", `ping 命令不可用: ${isError(error) ? error.message : String(error)}`),
|
failure: errorFailure("icmp", "spawn", `icmp 命令不可用: ${isError(error) ? error.message : String(error)}`),
|
||||||
matched: false,
|
matched: false,
|
||||||
observation: null,
|
observation: null,
|
||||||
targetId: t.id,
|
targetId: t.id,
|
||||||
@@ -95,7 +101,7 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
|||||||
return {
|
return {
|
||||||
detail: null,
|
detail: null,
|
||||||
durationMs,
|
durationMs,
|
||||||
failure: errorFailure("ping", "timeout", `ping 执行超时 (${t.timeoutMs}ms)`),
|
failure: errorFailure("icmp", "timeout", `icmp 执行超时 (${t.timeoutMs}ms)`),
|
||||||
matched: false,
|
matched: false,
|
||||||
observation: null,
|
observation: null,
|
||||||
targetId: t.id,
|
targetId: t.id,
|
||||||
@@ -103,12 +109,12 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = parsePingOutput(stdout, process.platform);
|
const stats = parsePingOutput(stdout, this.platform);
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return {
|
return {
|
||||||
detail: null,
|
detail: null,
|
||||||
durationMs,
|
durationMs,
|
||||||
failure: errorFailure("ping", "parse", "无法解析 ping 输出"),
|
failure: errorFailure("icmp", "parse", "无法解析 icmp 输出"),
|
||||||
matched: false,
|
matched: false,
|
||||||
observation: {
|
observation: {
|
||||||
alive: false,
|
alive: false,
|
||||||
@@ -148,28 +154,28 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
|
||||||
const t = target as RawTargetConfig & { ping: PingTargetConfig; type: "ping" };
|
const t = target as RawTargetConfig & { icmp: PingTargetConfig; type: "icmp" };
|
||||||
return {
|
return {
|
||||||
description: null,
|
description: null,
|
||||||
expect: target.expect as PingExpectConfig | undefined,
|
expect: target.expect as PingExpectConfig | undefined,
|
||||||
group: target.group ?? "default",
|
group: target.group ?? "default",
|
||||||
|
icmp: {
|
||||||
|
count: t.icmp.count ?? DEFAULT_COUNT,
|
||||||
|
host: t.icmp.host,
|
||||||
|
packetSize: t.icmp.packetSize ?? DEFAULT_PACKET_SIZE,
|
||||||
|
},
|
||||||
id: t.id,
|
id: t.id,
|
||||||
intervalMs: context.defaultIntervalMs,
|
intervalMs: context.defaultIntervalMs,
|
||||||
name: t.name ?? null,
|
name: t.name ?? null,
|
||||||
ping: {
|
|
||||||
count: t.ping.count ?? DEFAULT_COUNT,
|
|
||||||
host: t.ping.host,
|
|
||||||
packetSize: t.ping.packetSize ?? DEFAULT_PACKET_SIZE,
|
|
||||||
},
|
|
||||||
timeoutMs: context.defaultTimeoutMs,
|
timeoutMs: context.defaultTimeoutMs,
|
||||||
type: "ping",
|
type: "icmp",
|
||||||
} satisfies ResolvedPingTarget;
|
} satisfies ResolvedPingTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(t: ResolvedPingTarget): { config: string; target: string } {
|
serialize(t: ResolvedPingTarget): { config: string; target: string } {
|
||||||
return {
|
return {
|
||||||
config: JSON.stringify(t.ping),
|
config: JSON.stringify(t.icmp),
|
||||||
target: `ping ${t.ping.host}`,
|
target: `icmp ${t.icmp.host}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function checkAlive(actual: boolean, expected: boolean): ExpectResult {
|
|||||||
"alive",
|
"alive",
|
||||||
expected,
|
expected,
|
||||||
actual,
|
actual,
|
||||||
expected ? "期望主机可达但 ping 不可达" : "期望主机不可达但 ping 可达",
|
expected ? "期望主机可达但 icmp 不可达" : "期望主机不可达但 icmp 可达",
|
||||||
),
|
),
|
||||||
matched: false,
|
matched: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ export interface ResolvedPingConfig {
|
|||||||
export interface ResolvedPingTarget extends ResolvedTargetBase {
|
export interface ResolvedPingTarget extends ResolvedTargetBase {
|
||||||
expect?: PingExpectConfig;
|
expect?: PingExpectConfig;
|
||||||
group: string;
|
group: string;
|
||||||
|
icmp: ResolvedPingConfig;
|
||||||
intervalMs: number;
|
intervalMs: number;
|
||||||
name: null | string;
|
name: null | string;
|
||||||
ping: ResolvedPingConfig;
|
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
type: "ping";
|
type: "icmp";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import { issue, joinPath } from "../../schema/issues";
|
|||||||
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
|
||||||
const defaults = input.defaults["ping"];
|
const defaults = input.defaults["icmp"];
|
||||||
if (defaults !== undefined && defaults !== null) {
|
if (defaults !== undefined && defaults !== null) {
|
||||||
const targetName = "defaults.ping";
|
const targetName = "defaults.icmp";
|
||||||
if (!isPlainObject(defaults)) {
|
if (!isPlainObject(defaults)) {
|
||||||
issues.push(issue("invalid-type", "defaults.ping", "必须为对象", targetName));
|
issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName));
|
||||||
} else {
|
} else {
|
||||||
const pingDefaults = defaults as Record<string, unknown>;
|
const icmpDefaults = defaults as Record<string, unknown>;
|
||||||
for (const key of Object.keys(pingDefaults)) {
|
for (const key of Object.keys(icmpDefaults)) {
|
||||||
issues.push(issue("unknown-field", joinPath("defaults.ping", key), "是未知字段", targetName));
|
issues.push(issue("unknown-field", joinPath("defaults.icmp", key), "是未知字段", targetName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ export function validatePingConfig(input: CheckerValidationInput): ConfigValidat
|
|||||||
const target = input.targets[i] as unknown;
|
const target = input.targets[i] as unknown;
|
||||||
if (!isPlainObject(target)) continue;
|
if (!isPlainObject(target)) continue;
|
||||||
const targetRecord = target as Record<string, unknown>;
|
const targetRecord = target as Record<string, unknown>;
|
||||||
if (targetRecord["type"] !== "ping") continue;
|
if (targetRecord["type"] !== "icmp") continue;
|
||||||
issues.push(...validatePingTarget(targetRecord, `targets[${i}]`));
|
issues.push(...validatePingTarget(targetRecord, `targets[${i}]`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,39 +71,39 @@ function validatePingExpect(target: Record<string, unknown>, path: string): Conf
|
|||||||
function validatePingTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
function validatePingTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
const targetName = getTargetName(target);
|
const targetName = getTargetName(target);
|
||||||
const rawPing = target["ping"];
|
const rawIcmp = target["icmp"];
|
||||||
|
|
||||||
if (!isPlainObject(rawPing)) {
|
if (!isPlainObject(rawIcmp)) {
|
||||||
issues.push(issue("required", joinPath(path, "ping"), "缺少 ping 配置分组", targetName));
|
issues.push(issue("required", joinPath(path, "icmp"), "缺少 icmp 配置分组", targetName));
|
||||||
issues.push(...validatePingExpect(target, path));
|
issues.push(...validatePingExpect(target, path));
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
const ping = rawPing as Record<string, unknown>;
|
const icmp = rawIcmp as Record<string, unknown>;
|
||||||
|
|
||||||
if (!isString(ping["host"]) || ping["host"].trim() === "") {
|
if (!isString(icmp["host"]) || icmp["host"].trim() === "") {
|
||||||
issues.push(issue("required", joinPath(joinPath(path, "ping"), "host"), "缺少 ping.host 字段", targetName));
|
issues.push(issue("required", joinPath(joinPath(path, "icmp"), "host"), "缺少 icmp.host 字段", targetName));
|
||||||
}
|
}
|
||||||
if (ping["count"] !== undefined) {
|
if (icmp["count"] !== undefined) {
|
||||||
const count = ping["count"];
|
const count = icmp["count"];
|
||||||
if (!isNumber(count) || !Number.isInteger(count) || count < 1 || count > 100) {
|
if (!isNumber(count) || !Number.isInteger(count) || count < 1 || count > 100) {
|
||||||
issues.push(
|
issues.push(
|
||||||
issue("invalid-value", joinPath(joinPath(path, "ping"), "count"), "必须为 1-100 的正整数", targetName),
|
issue("invalid-value", joinPath(joinPath(path, "icmp"), "count"), "必须为 1-100 的正整数", targetName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ping["packetSize"] !== undefined) {
|
if (icmp["packetSize"] !== undefined) {
|
||||||
const packetSize = ping["packetSize"];
|
const packetSize = icmp["packetSize"];
|
||||||
if (!isNumber(packetSize) || !Number.isInteger(packetSize) || packetSize < 1 || packetSize > 65500) {
|
if (!isNumber(packetSize) || !Number.isInteger(packetSize) || packetSize < 1 || packetSize > 65500) {
|
||||||
issues.push(
|
issues.push(
|
||||||
issue("invalid-value", joinPath(joinPath(path, "ping"), "packetSize"), "必须为 1-65500 的正整数", targetName),
|
issue("invalid-value", joinPath(joinPath(path, "icmp"), "packetSize"), "必须为 1-65500 的正整数", targetName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedPingKeys = new Set(["count", "host", "packetSize"]);
|
const allowedIcmpKeys = new Set(["count", "host", "packetSize"]);
|
||||||
for (const key of Object.keys(ping)) {
|
for (const key of Object.keys(icmp)) {
|
||||||
if (!allowedPingKeys.has(key)) {
|
if (!allowedIcmpKeys.has(key)) {
|
||||||
issues.push(issue("unknown-field", joinPath(joinPath(path, "ping"), key), "是未知字段", targetName));
|
issues.push(issue("unknown-field", joinPath(joinPath(path, "icmp"), key), "是未知字段", targetName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function OverviewTab({ metricsData, metricsLoading, target }: OverviewTab
|
|||||||
{ content: target.latestCheck?.detail ?? "-", label: "状态详情" },
|
{ content: target.latestCheck?.detail ?? "-", label: "状态详情" },
|
||||||
{ content: target.description ?? "", label: "描述", span: 2 },
|
{ content: target.description ?? "", label: "描述", span: 2 },
|
||||||
]}
|
]}
|
||||||
|
tableLayout="auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider align="left">统计</Divider>
|
<Divider align="left">统计</Divider>
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ export function createTargetTableColumns(checkerTypes: string[]): Array<PrimaryT
|
|||||||
if (ms === null || ms === undefined) return <span className="text-disabled">-</span>;
|
if (ms === null || ms === undefined) return <span className="text-disabled">-</span>;
|
||||||
const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error";
|
const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error";
|
||||||
const latencyText = ms > 9999 ? "9999+" : `${Math.round(ms)}`;
|
const latencyText = ms > 9999 ? "9999+" : `${Math.round(ms)}`;
|
||||||
return <span className={`${colorClass} latency-value tabular-nums`}>{latencyText}</span>;
|
return <span className={`${colorClass} latency-value tabular-nums`}>{latencyText} ms</span>;
|
||||||
},
|
},
|
||||||
colKey: "latestCheck.durationMs",
|
colKey: "latestCheck.durationMs",
|
||||||
sorter: latencySorter,
|
sorter: latencySorter,
|
||||||
sortType: "all",
|
sortType: "all",
|
||||||
title: "延迟(ms)",
|
title: "延迟",
|
||||||
width: 75,
|
width: 80,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2009,14 +2009,14 @@ targets:
|
|||||||
expect(t.expect?.durationMs).toEqual({ lte: 5000 });
|
expect(t.expect?.durationMs).toEqual({ lte: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("解析最简 ping 配置", async () => {
|
test("解析最简 icmp 配置", async () => {
|
||||||
const configPath = join(tempDir, "minimal-ping.yaml");
|
const configPath = join(tempDir, "minimal-icmp.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- id: "gateway"
|
- id: "gateway"
|
||||||
type: ping
|
type: icmp
|
||||||
ping:
|
icmp:
|
||||||
host: "10.0.0.1"
|
host: "10.0.0.1"
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
@@ -2024,21 +2024,21 @@ targets:
|
|||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
expect(config.targets).toHaveLength(1);
|
expect(config.targets).toHaveLength(1);
|
||||||
const t = config.targets[0]! as ResolvedPingTarget;
|
const t = config.targets[0]! as ResolvedPingTarget;
|
||||||
expect(t.type).toBe("ping");
|
expect(t.type).toBe("icmp");
|
||||||
expect(t.ping).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
|
expect(t.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
|
||||||
expect(t.group).toBe("default");
|
expect(t.group).toBe("default");
|
||||||
expect(t.intervalMs).toBe(30000);
|
expect(t.intervalMs).toBe(30000);
|
||||||
expect(t.timeoutMs).toBe(10000);
|
expect(t.timeoutMs).toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("解析 ping expect 配置", async () => {
|
test("解析 icmp expect 配置", async () => {
|
||||||
const configPath = join(tempDir, "ping-expect.yaml");
|
const configPath = join(tempDir, "icmp-expect.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- id: "gateway"
|
- id: "gateway"
|
||||||
type: ping
|
type: icmp
|
||||||
ping:
|
icmp:
|
||||||
host: "10.0.0.1"
|
host: "10.0.0.1"
|
||||||
count: 5
|
count: 5
|
||||||
packetSize: 1472
|
packetSize: 1472
|
||||||
@@ -2057,7 +2057,7 @@ targets:
|
|||||||
|
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
const t = config.targets[0]! as ResolvedPingTarget;
|
const t = config.targets[0]! as ResolvedPingTarget;
|
||||||
expect(t.ping).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
|
expect(t.icmp).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
|
||||||
expect(t.expect).toEqual({
|
expect(t.expect).toEqual({
|
||||||
alive: true,
|
alive: true,
|
||||||
avgLatencyMs: { lte: 200 },
|
avgLatencyMs: { lte: 200 },
|
||||||
@@ -2067,39 +2067,39 @@ targets:
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ping 缺少 host 抛出错误", async () => {
|
test("icmp 缺少 host 抛出错误", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"ping-no-host.yaml",
|
"icmp-no-host.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- id: "gateway"
|
- id: "gateway"
|
||||||
type: ping
|
type: icmp
|
||||||
ping: {}
|
icmp: {}
|
||||||
`,
|
`,
|
||||||
"ping.host",
|
"icmp.host",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ping count 非法抛出错误", async () => {
|
test("icmp count 非法抛出错误", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"ping-bad-count.yaml",
|
"icmp-bad-count.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- id: "gateway"
|
- id: "gateway"
|
||||||
type: ping
|
type: icmp
|
||||||
ping:
|
icmp:
|
||||||
host: "10.0.0.1"
|
host: "10.0.0.1"
|
||||||
count: 0
|
count: 0
|
||||||
`,
|
`,
|
||||||
"ping.count",
|
"icmp.count",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ping expect 未知字段抛出错误", async () => {
|
test("icmp expect 未知字段抛出错误", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"ping-unknown-expect.yaml",
|
"icmp-unknown-expect.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- id: "gateway"
|
- id: "gateway"
|
||||||
type: ping
|
type: icmp
|
||||||
ping:
|
icmp:
|
||||||
host: "10.0.0.1"
|
host: "10.0.0.1"
|
||||||
expect:
|
expect:
|
||||||
status: [200]
|
status: [200]
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ function makeTarget(overrides?: Partial<ResolvedPingTarget>): ResolvedPingTarget
|
|||||||
return {
|
return {
|
||||||
description: null,
|
description: null,
|
||||||
group: "default",
|
group: "default",
|
||||||
|
icmp: { count: 3, host: "10.0.0.1", packetSize: 56 },
|
||||||
id: "test",
|
id: "test",
|
||||||
intervalMs: 30000,
|
intervalMs: 30000,
|
||||||
name: null,
|
name: null,
|
||||||
ping: { count: 3, host: "10.0.0.1", packetSize: 56 },
|
|
||||||
timeoutMs: 10000,
|
timeoutMs: 10000,
|
||||||
type: "ping",
|
type: "icmp",
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ describe("buildPingCommand", () => {
|
|||||||
|
|
||||||
test("自定义 count 和 packetSize", () => {
|
test("自定义 count 和 packetSize", () => {
|
||||||
const cmd = buildPingCommand(
|
const cmd = buildPingCommand(
|
||||||
makeTarget({ ping: { count: 5, host: "10.0.0.1", packetSize: 1472 }, timeoutMs: 5000 }),
|
makeTarget({ icmp: { count: 5, host: "10.0.0.1", packetSize: 1472 }, timeoutMs: 5000 }),
|
||||||
"linux",
|
"linux",
|
||||||
);
|
);
|
||||||
expect(cmd).toEqual(["ping", "-c", "5", "-s", "1472", "-W", "5", "10.0.0.1"]);
|
expect(cmd).toEqual(["ping", "-c", "5", "-s", "1472", "-W", "5", "10.0.0.1"]);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { CheckerContext } from "../../../../../src/server/checker/runner/ty
|
|||||||
|
|
||||||
import { IcmpChecker } from "../../../../../src/server/checker/runner/icmp/execute";
|
import { IcmpChecker } from "../../../../../src/server/checker/runner/icmp/execute";
|
||||||
|
|
||||||
const checker = new IcmpChecker();
|
const checker = new IcmpChecker("linux");
|
||||||
const originalSpawn = Bun.spawn;
|
const originalSpawn = Bun.spawn;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -21,12 +21,12 @@ function makeTarget(overrides?: Partial<ResolvedPingTarget>): ResolvedPingTarget
|
|||||||
return {
|
return {
|
||||||
description: null,
|
description: null,
|
||||||
group: "default",
|
group: "default",
|
||||||
|
icmp: { count: 3, host: "127.0.0.1", packetSize: 56 },
|
||||||
id: "ping-local",
|
id: "ping-local",
|
||||||
intervalMs: 30000,
|
intervalMs: 30000,
|
||||||
name: null,
|
name: null,
|
||||||
ping: { count: 3, host: "127.0.0.1", packetSize: 56 },
|
|
||||||
timeoutMs: 10000,
|
timeoutMs: 10000,
|
||||||
type: "ping",
|
type: "icmp",
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -94,16 +94,16 @@ rtt min/avg/max/mdev = 1.234/156.000/340.000/0.567 ms`);
|
|||||||
mockSpawn("unexpected output");
|
mockSpawn("unexpected output");
|
||||||
const result = await checker.execute(makeTarget(), makeCtx());
|
const result = await checker.execute(makeTarget(), makeCtx());
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
expect(result.failure).toMatchObject({ kind: "error", path: "parse", phase: "ping" });
|
expect(result.failure).toMatchObject({ kind: "error", path: "parse", phase: "icmp" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("spawn 失败返回 ping 命令不可用", async () => {
|
test("spawn 失败返回 icmp 命令不可用", async () => {
|
||||||
Bun.spawn = mock(() => {
|
Bun.spawn = mock(() => {
|
||||||
throw new Error("ENOENT");
|
throw new Error("ENOENT");
|
||||||
});
|
});
|
||||||
const result = await checker.execute(makeTarget(), makeCtx());
|
const result = await checker.execute(makeTarget(), makeCtx());
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
expect(result.failure?.message).toContain("ping 命令不可用");
|
expect(result.failure?.message).toContain("icmp 命令不可用");
|
||||||
expect(result.observation).toBeNull();
|
expect(result.observation).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,23 +113,23 @@ rtt min/avg/max/mdev = 1.234/156.000/340.000/0.567 ms`);
|
|||||||
controller.abort();
|
controller.abort();
|
||||||
const result = await checker.execute(makeTarget(), { signal: controller.signal });
|
const result = await checker.execute(makeTarget(), { signal: controller.signal });
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
expect(result.failure).toMatchObject({ path: "timeout", phase: "ping" });
|
expect(result.failure).toMatchObject({ path: "timeout", phase: "icmp" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("IcmpChecker resolve", () => {
|
describe("IcmpChecker resolve", () => {
|
||||||
test("解析默认值", () => {
|
test("解析默认值", () => {
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ id: "ping", ping: { host: "10.0.0.1" }, type: "ping" },
|
{ icmp: { host: "10.0.0.1" }, id: "ping", type: "icmp" },
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.ping).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
|
expect(target.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
|
||||||
expect(target.group).toBe("default");
|
expect(target.group).toBe("default");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("serialize 返回摘要和配置", () => {
|
test("serialize 返回摘要和配置", () => {
|
||||||
const serialized = checker.serialize(makeTarget({ ping: { count: 5, host: "10.0.0.1", packetSize: 1472 } }));
|
const serialized = checker.serialize(makeTarget({ icmp: { count: 5, host: "10.0.0.1", packetSize: 1472 } }));
|
||||||
expect(serialized.target).toBe("ping 10.0.0.1");
|
expect(serialized.target).toBe("icmp 10.0.0.1");
|
||||||
expect(JSON.parse(serialized.config)).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
|
expect(JSON.parse(serialized.config)).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,61 +10,61 @@ function validate(target: RawTargetConfig) {
|
|||||||
|
|
||||||
describe("validatePingConfig", () => {
|
describe("validatePingConfig", () => {
|
||||||
test("有效配置无错误", () => {
|
test("有效配置无错误", () => {
|
||||||
expect(validate({ id: "ping", ping: { count: 3, host: "127.0.0.1", packetSize: 56 }, type: "ping" })).toEqual([]);
|
expect(validate({ icmp: { count: 3, host: "127.0.0.1", packetSize: 56 }, id: "icmp", type: "icmp" })).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("host 缺失", () => {
|
test("host 缺失", () => {
|
||||||
const issues = validate({ id: "ping", ping: {}, type: "ping" });
|
const issues = validate({ icmp: {}, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("ping.host"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("icmp.host"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("host 类型非法", () => {
|
test("host 类型非法", () => {
|
||||||
const issues = validate({ id: "ping", ping: { host: 123 }, type: "ping" });
|
const issues = validate({ icmp: { host: 123 }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("ping.host"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("icmp.host"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("count 非法", () => {
|
test("count 非法", () => {
|
||||||
const issues = validate({ id: "ping", ping: { count: 0, host: "127.0.0.1" }, type: "ping" });
|
const issues = validate({ icmp: { count: 0, host: "127.0.0.1" }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("ping.count"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("icmp.count"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("packetSize 非法", () => {
|
test("packetSize 非法", () => {
|
||||||
const issues = validate({ id: "ping", ping: { host: "127.0.0.1", packetSize: 65501 }, type: "ping" });
|
const issues = validate({ icmp: { host: "127.0.0.1", packetSize: 65501 }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("ping.packetSize"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("icmp.packetSize"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ping 未知字段", () => {
|
test("icmp 未知字段", () => {
|
||||||
const issues = validate({ id: "ping", ping: { host: "127.0.0.1", timeout: 5 }, type: "ping" });
|
const issues = validate({ icmp: { host: "127.0.0.1", timeout: 5 }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.code === "unknown-field" && item.path.endsWith("ping.timeout"))).toBe(true);
|
expect(issues.some((item) => item.code === "unknown-field" && item.path.endsWith("icmp.timeout"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("expect 未知字段", () => {
|
test("expect 未知字段", () => {
|
||||||
const issues = validate({ expect: { status: [200] }, id: "ping", ping: { host: "127.0.0.1" }, type: "ping" });
|
const issues = validate({ expect: { status: [200] }, icmp: { host: "127.0.0.1" }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("expect.status"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("expect.status"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("expect 数值旧字段非法", () => {
|
test("expect 数值旧字段非法", () => {
|
||||||
const issues = validate({ expect: { maxPacketLoss: 101 }, id: "ping", ping: { host: "127.0.0.1" }, type: "ping" });
|
const issues = validate({ expect: { maxPacketLoss: 101 }, icmp: { host: "127.0.0.1" }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("expect.maxPacketLoss"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("expect.maxPacketLoss"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("durationMs 数组简写非法", () => {
|
test("durationMs 数组简写非法", () => {
|
||||||
const issues = validate({ expect: { durationMs: [1, 2] }, id: "ping", ping: { host: "127.0.0.1" }, type: "ping" });
|
const issues = validate({ expect: { durationMs: [1, 2] }, icmp: { host: "127.0.0.1" }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("expect.durationMs"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("expect.durationMs"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("avgLatencyMs 对象简写非法", () => {
|
test("avgLatencyMs 对象简写非法", () => {
|
||||||
const issues = validate({
|
const issues = validate({
|
||||||
expect: { avgLatencyMs: { foo: "bar" } },
|
expect: { avgLatencyMs: { foo: "bar" } },
|
||||||
id: "ping",
|
icmp: { host: "127.0.0.1" },
|
||||||
ping: { host: "127.0.0.1" },
|
id: "icmp",
|
||||||
type: "ping",
|
type: "icmp",
|
||||||
});
|
});
|
||||||
expect(issues.some((item) => item.path.endsWith("expect.avgLatencyMs.foo"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("expect.avgLatencyMs.foo"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("host 为空字符串", () => {
|
test("host 为空字符串", () => {
|
||||||
const issues = validate({ id: "ping", ping: { host: " " }, type: "ping" });
|
const issues = validate({ icmp: { host: " " }, id: "icmp", type: "icmp" });
|
||||||
expect(issues.some((item) => item.path.endsWith("ping.host"))).toBe(true);
|
expect(issues.some((item) => item.path.endsWith("icmp.host"))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ describe("CheckerRegistry", () => {
|
|||||||
const second = createDefaultCheckerRegistry();
|
const second = createDefaultCheckerRegistry();
|
||||||
first.register(createChecker("custom"));
|
first.register(createChecker("custom"));
|
||||||
|
|
||||||
expect(first.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "ping", "udp", "llm", "custom"]);
|
expect(first.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "custom"]);
|
||||||
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "ping", "udp", "llm"]);
|
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm"]);
|
||||||
expect(
|
expect(
|
||||||
first.definitions.every(
|
first.definitions.every(
|
||||||
(checker) => checker.schemas.config && checker.schemas.defaults && checker.schemas.expect,
|
(checker) => checker.schemas.config && checker.schemas.defaults && checker.schemas.expect,
|
||||||
@@ -76,9 +76,9 @@ describe("CheckerRegistry", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("默认 registry 注册 ping type", () => {
|
test("默认 registry 注册 icmp type", () => {
|
||||||
const registry = createDefaultCheckerRegistry();
|
const registry = createDefaultCheckerRegistry();
|
||||||
expect(registry.supportedTypes).toContain("ping");
|
expect(registry.supportedTypes).toContain("icmp");
|
||||||
expect(registry.get("ping").configKey).toBe("ping");
|
expect(registry.get("icmp").configKey).toBe("icmp");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ describe("ValueMatcher primitive shorthand in checker validators", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expect: { avgLatencyMs: 1, durationMs: 100, maxLatencyMs: 2, packetLossPercent: 0 },
|
expect: { avgLatencyMs: 1, durationMs: 100, maxLatencyMs: 2, packetLossPercent: 0 },
|
||||||
id: "ping",
|
icmp: { host: "127.0.0.1" },
|
||||||
ping: { host: "127.0.0.1" },
|
id: "icmp",
|
||||||
type: "ping",
|
type: "icmp",
|
||||||
validate: validatePingConfig,
|
validate: validatePingConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,16 +121,16 @@ describe("createTargetTableColumns", () => {
|
|||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(element.props.children).toBe("9999+");
|
expect(element.props.children).toEqual(["9999+", " ms"]);
|
||||||
expect(element.props.className).toContain("latency-value");
|
expect(element.props.className).toContain("latency-value");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("延迟列标题为 延迟(ms)", () => {
|
test("延迟列标题为 延迟", () => {
|
||||||
const latencyColumn = getColumn(createTargetTableColumns(["http"]), "latestCheck.durationMs");
|
const latencyColumn = getColumn(createTargetTableColumns(["http"]), "latestCheck.durationMs");
|
||||||
expect(latencyColumn.title).toBe("延迟(ms)");
|
expect(latencyColumn.title).toBe("延迟");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("延迟列正常值不包含 ms 单位", () => {
|
test("延迟列正常值包含 ms 单位", () => {
|
||||||
const latencyColumn = getColumn(createTargetTableColumns(["http"]), "latestCheck.durationMs");
|
const latencyColumn = getColumn(createTargetTableColumns(["http"]), "latestCheck.durationMs");
|
||||||
const renderCell = latencyColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => {
|
const renderCell = latencyColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => {
|
||||||
props: { children: string; className: string };
|
props: { children: string; className: string };
|
||||||
@@ -150,7 +150,7 @@ describe("createTargetTableColumns", () => {
|
|||||||
}),
|
}),
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
});
|
});
|
||||||
expect(element.props.children).toBe("123");
|
expect(element.props.children).toEqual(["123", " ms"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("名称列无排序配置", () => {
|
test("名称列无排序配置", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user