feat: 新增 memory checker,支持系统级内存和交换空间检测
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -22,6 +22,7 @@
|
|||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
|
"systeminformation": "^5.31.6",
|
||||||
"tdesign-icons-react": "^0.6.4",
|
"tdesign-icons-react": "^0.6.4",
|
||||||
"tdesign-react": "^1.16.9",
|
"tdesign-react": "^1.16.9",
|
||||||
"xpath": "^0.0.34",
|
"xpath": "^0.0.34",
|
||||||
@@ -498,7 +499,7 @@
|
|||||||
|
|
||||||
"cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="],
|
"cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="],
|
||||||
|
|
||||||
"croner": ["croner@10.0.1", "", {}, "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g=="],
|
"croner": ["croner@10.0.1", "https://registry.npmmirror.com/croner/-/croner-10.0.1.tgz", {}, "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
@@ -1134,6 +1135,8 @@
|
|||||||
|
|
||||||
"synckit": ["synckit@0.11.12", "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
|
"synckit": ["synckit@0.11.12", "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
|
||||||
|
|
||||||
|
"systeminformation": ["systeminformation@5.31.6", "https://registry.npmmirror.com/systeminformation/-/systeminformation-5.31.6.tgz", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA=="],
|
||||||
|
|
||||||
"tar-stream": ["tar-stream@3.2.0", "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.2.0.tgz", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
|
"tar-stream": ["tar-stream@3.2.0", "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.2.0.tgz", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
|
||||||
|
|
||||||
"tdesign-icons-react": ["tdesign-icons-react@0.6.4", "https://registry.npmmirror.com/tdesign-icons-react/-/tdesign-icons-react-0.6.4.tgz", { "dependencies": { "@babel/runtime": "^7.16.5", "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-USAoi9vBWcwcJT45VqR3dRqX1MeAsn/RhHVx4bLwplhrlvE80ZQ1N9V+6F3HqE1Qe9mMDbtRM8Ul80+lALScww=="],
|
"tdesign-icons-react": ["tdesign-icons-react@0.6.4", "https://registry.npmmirror.com/tdesign-icons-react/-/tdesign-icons-react-0.6.4.tgz", { "dependencies": { "@babel/runtime": "^7.16.5", "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-USAoi9vBWcwcJT45VqR3dRqX1MeAsn/RhHVx4bLwplhrlvE80ZQ1N9V+6F3HqE1Qe9mMDbtRM8Ul80+lALScww=="],
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ Checker 是 DiAL 的拨测执行单元。每个 target 通过 `type` 选择一
|
|||||||
|
|
||||||
## 支持的类型
|
## 支持的类型
|
||||||
|
|
||||||
| 类型 | 用途 | 文档 |
|
| 类型 | 用途 | 文档 |
|
||||||
| ------ | -------------------------------------- | --------------- |
|
| -------- | -------------------------------------- | ------------------- |
|
||||||
| `http` | HTTP/HTTPS 应用层健康检查 | [HTTP](http.md) |
|
| `http` | HTTP/HTTPS 应用层健康检查 | [HTTP](http.md) |
|
||||||
| `cmd` | 执行本地命令或脚本 | [Cmd](cmd.md) |
|
| `cmd` | 执行本地命令或脚本 | [Cmd](cmd.md) |
|
||||||
| `db` | PostgreSQL/MySQL/SQLite 连接和查询检查 | [DB](db.md) |
|
| `db` | PostgreSQL/MySQL/SQLite 连接和查询检查 | [DB](db.md) |
|
||||||
| `tcp` | TCP 端口可达性和 banner 探测 | [TCP](tcp.md) |
|
| `tcp` | TCP 端口可达性和 banner 探测 | [TCP](tcp.md) |
|
||||||
| `udp` | UDP payload 请求-响应检查 | [UDP](udp.md) |
|
| `udp` | UDP payload 请求-响应检查 | [UDP](udp.md) |
|
||||||
| `icmp` | 基于系统 `ping` 的存活、延迟、丢包检查 | [ICMP](icmp.md) |
|
| `icmp` | 基于系统 `ping` 的存活、延迟、丢包检查 | [ICMP](icmp.md) |
|
||||||
| `dns` | 本机解析或指定 DNS server 协议级检查 | [DNS](dns.md) |
|
| `dns` | 本机解析或指定 DNS server 协议级检查 | [DNS](dns.md) |
|
||||||
| `llm` | 大模型服务应用层健康检查 | [LLM](llm.md) |
|
| `llm` | 大模型服务应用层健康检查 | [LLM](llm.md) |
|
||||||
| `ws` | WebSocket 可达性和消息交互检查 | [WS](ws.md) |
|
| `ws` | WebSocket 可达性和消息交互检查 | [WS](ws.md) |
|
||||||
| `cpu` | 本机 CPU 使用率健康检查 | [CPU](cpu.md) |
|
| `cpu` | 本机 CPU 使用率健康检查 | [CPU](cpu.md) |
|
||||||
|
| `memory` | 本机系统内存使用状况检查 | [Memory](memory.md) |
|
||||||
|
|
||||||
## 选择建议
|
## 选择建议
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ Checker 是 DiAL 的拨测执行单元。每个 target 通过 `type` 选择一
|
|||||||
| LLM API 是否可用、输出是否符合预期 | `llm` |
|
| LLM API 是否可用、输出是否符合预期 | `llm` |
|
||||||
| WebSocket 可达性或消息交互验证 | `ws` |
|
| WebSocket 可达性或消息交互验证 | `ws` |
|
||||||
| 本机 CPU 使用率健康检查 | `cpu` |
|
| 本机 CPU 使用率健康检查 | `cpu` |
|
||||||
|
| 本机系统内存使用状况检查 | `memory` |
|
||||||
|
|
||||||
## 通用字段
|
## 通用字段
|
||||||
|
|
||||||
|
|||||||
119
docs/user/checkers/memory.md
Normal file
119
docs/user/checkers/memory.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Memory Checker
|
||||||
|
|
||||||
|
`type: memory` 用于检查本机系统级内存使用状况,包括物理内存和交换空间的使用率及字节数。
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
Memory checker 配置为空对象,无需额外参数:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
memory: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## expect 校验项
|
||||||
|
|
||||||
|
### 百分比字段
|
||||||
|
|
||||||
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
|
| ------------------ | -------------------------------------------------------------------------- | ---- | ------ |
|
||||||
|
| `usagePercent` | 真实内存使用率 = `activeBytes / totalBytes × 100`,不含 buffers/cache 假象 | 否 | 无 |
|
||||||
|
| `usedPercent` | 原始已用百分比 = `usedBytes / totalBytes × 100`,包含 buffers/cache | 否 | 无 |
|
||||||
|
| `freePercent` | 空闲百分比 = `freeBytes / totalBytes × 100` | 否 | 无 |
|
||||||
|
| `activePercent` | 活跃内存百分比 = `activeBytes / totalBytes × 100` | 否 | 无 |
|
||||||
|
| `availablePercent` | 可用内存百分比 = `availableBytes / totalBytes × 100` | 否 | 无 |
|
||||||
|
| `swapUsagePercent` | 交换空间使用率,当系统无交换分区时为 `null` | 否 | 无 |
|
||||||
|
|
||||||
|
所有百分比字段范围为 `0-100`,使用 `ValueMatcher`。
|
||||||
|
|
||||||
|
### 字节字段
|
||||||
|
|
||||||
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
|
| ---------------- | ----------------------------------------- | ---- | ------ |
|
||||||
|
| `activeBytes` | 活跃内存字节数 | 否 | 无 |
|
||||||
|
| `usedBytes` | 已用内存字节数(含 buffers/cache) | 否 | 无 |
|
||||||
|
| `freeBytes` | 空闲内存字节数 | 否 | 无 |
|
||||||
|
| `availableBytes` | 可用内存字节数 | 否 | 无 |
|
||||||
|
| `totalBytes` | 物理内存总字节数 | 否 | 无 |
|
||||||
|
| `swapUsedBytes` | 交换空间已用字节数,无交换分区时为 `null` | 否 | 无 |
|
||||||
|
| `swapFreeBytes` | 交换空间空闲字节数,无交换分区时为 `null` | 否 | 无 |
|
||||||
|
| `swapTotalBytes` | 交换空间总字节数,无交换分区时为 `0` | 否 | 无 |
|
||||||
|
| `buffcacheBytes` | 缓冲缓存字节数,部分平台可能为 `null` | 否 | 无 |
|
||||||
|
|
||||||
|
字节字段支持数字(字节数)或大小字符串(如 `"512MB"`、`"1GB"`),使用 `ValueMatcher`。
|
||||||
|
|
||||||
|
### 通用字段
|
||||||
|
|
||||||
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
|
| ------------ | ------------------------------------- | ---- | ------ |
|
||||||
|
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
检查内存使用率不超过 85%:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: "local-memory"
|
||||||
|
name: "本机内存"
|
||||||
|
type: memory
|
||||||
|
interval: "30s"
|
||||||
|
timeout: "5s"
|
||||||
|
memory: {}
|
||||||
|
expect:
|
||||||
|
usagePercent:
|
||||||
|
lte: 85
|
||||||
|
```
|
||||||
|
|
||||||
|
检查可用内存不低于 4GB:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: "local-memory-available"
|
||||||
|
name: "可用内存检查"
|
||||||
|
type: memory
|
||||||
|
memory: {}
|
||||||
|
expect:
|
||||||
|
availableBytes:
|
||||||
|
gte: "4GB"
|
||||||
|
```
|
||||||
|
|
||||||
|
同时检查内存和交换空间:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: "local-memory-swap"
|
||||||
|
name: "内存和交换空间"
|
||||||
|
type: memory
|
||||||
|
memory: {}
|
||||||
|
expect:
|
||||||
|
usagePercent:
|
||||||
|
lte: 80
|
||||||
|
swapUsagePercent:
|
||||||
|
lte: 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## 语义说明
|
||||||
|
|
||||||
|
Memory checker 通过 `systeminformation` 库读取系统内存数据,在 Linux、macOS 和 Windows 上均可运行。
|
||||||
|
|
||||||
|
- **`usagePercent`** 使用 `activeBytes / totalBytes` 计算,反映真实的内存压力,不受 Linux buffers/cache 缓存影响。推荐使用此字段进行内存健康检查。
|
||||||
|
- **`usedPercent`** 使用 `usedBytes / totalBytes` 计算,包含 buffers/cache。在 Linux 上此值通常高于 `usagePercent`。
|
||||||
|
- **Swap 字段**:当系统未配置交换分区时,`swapTotalBytes` 为 `0`,`swapUsagePercent` 为 `null`(非 `0`)。
|
||||||
|
- **`buffcacheBytes`**:反映 Linux 的 buffers + cache 用量,在其他平台上可能为 `null`。
|
||||||
|
|
||||||
|
Memory checker 是即时读取(非采样),无需 `sampleDuration`,执行速度远快于 CPU checker。
|
||||||
|
|
||||||
|
## 跨平台注意事项
|
||||||
|
|
||||||
|
- Windows 环境依赖 PowerShell 5+ 获取部分内存指标
|
||||||
|
- `buffcacheBytes` 在非 Linux 平台上可能返回 `null`
|
||||||
|
- 容器环境中内存数据可能不反映 cgroup 内存限制
|
||||||
|
|
||||||
|
## 不支持的功能
|
||||||
|
|
||||||
|
- 进程级内存使用(如 RSS、VSZ)
|
||||||
|
- cgroup/container 内存限制精度
|
||||||
|
- 内存趋势采样和历史记录
|
||||||
|
- 内存条物理布局信息
|
||||||
|
- 详细内存分类(slab、reclaimable、dirty 等)作为 expect 字段
|
||||||
|
|
||||||
|
## 更新触发条件
|
||||||
|
|
||||||
|
修改 Memory checker 配置、expect 字段、行为或语义时,必须更新本文档。
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
|
"systeminformation": "^5.31.6",
|
||||||
"tdesign-icons-react": "^0.6.4",
|
"tdesign-icons-react": "^0.6.4",
|
||||||
"tdesign-react": "^1.16.9",
|
"tdesign-react": "^1.16.9",
|
||||||
"xpath": "^0.0.34"
|
"xpath": "^0.0.34"
|
||||||
|
|||||||
@@ -6850,6 +6850,781 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"memory"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 500,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"activeBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"activePercent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"availableBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"availablePercent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"buffcacheBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"durationMs": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"freeBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"freePercent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"swapFreeBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"swapTotalBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"swapUsagePercent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"swapUsedBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"totalBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"usagePercent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"usedBytes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"usedPercent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contains": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"lte": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"maxLength": 30,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 30,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^\\$\\{[^}]+\\}$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"const": "memory",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,3 +367,14 @@ targets:
|
|||||||
lte: 85
|
lte: 85
|
||||||
maxCoreUsagePercent:
|
maxCoreUsagePercent:
|
||||||
lte: 95
|
lte: 95
|
||||||
|
|
||||||
|
- id: "local-memory"
|
||||||
|
name: "本机内存"
|
||||||
|
type: memory
|
||||||
|
group: "基础设施"
|
||||||
|
interval: "30s"
|
||||||
|
timeout: "5s"
|
||||||
|
memory: {}
|
||||||
|
expect:
|
||||||
|
usagePercent:
|
||||||
|
lte: 85
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DnsChecker } from "./dns";
|
|||||||
import { HttpChecker } from "./http";
|
import { HttpChecker } from "./http";
|
||||||
import { IcmpChecker } from "./icmp";
|
import { IcmpChecker } from "./icmp";
|
||||||
import { LlmChecker } from "./llm";
|
import { LlmChecker } from "./llm";
|
||||||
|
import { MemoryChecker } from "./memory";
|
||||||
import { CheckerRegistry } from "./registry";
|
import { CheckerRegistry } from "./registry";
|
||||||
import { TcpChecker } from "./tcp";
|
import { TcpChecker } from "./tcp";
|
||||||
import { UdpChecker } from "./udp";
|
import { UdpChecker } from "./udp";
|
||||||
@@ -21,6 +22,7 @@ const checkers = [
|
|||||||
new DnsChecker(),
|
new DnsChecker(),
|
||||||
new WsChecker(),
|
new WsChecker(),
|
||||||
new CpuChecker(),
|
new CpuChecker(),
|
||||||
|
new MemoryChecker(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export function createDefaultCheckerRegistry(): CheckerRegistry {
|
export function createDefaultCheckerRegistry(): CheckerRegistry {
|
||||||
|
|||||||
60
src/server/checker/runner/memory/calculate.ts
Normal file
60
src/server/checker/runner/memory/calculate.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { Systeminformation } from "systeminformation";
|
||||||
|
|
||||||
|
import type { MemoryStats } from "./types";
|
||||||
|
|
||||||
|
export function calculateMemoryStats(data: Systeminformation.MemData): MemoryStats {
|
||||||
|
const totalBytes = data.total;
|
||||||
|
const usedBytes = data.used;
|
||||||
|
const activeBytes = data.active;
|
||||||
|
const availableBytes = data.available;
|
||||||
|
const freeBytes = data.free;
|
||||||
|
|
||||||
|
const usagePercent = totalBytes > 0 ? round1((activeBytes / totalBytes) * 100) : 0;
|
||||||
|
const usedPercent = totalBytes > 0 ? round1((usedBytes / totalBytes) * 100) : 0;
|
||||||
|
const freePercent = totalBytes > 0 ? round1((freeBytes / totalBytes) * 100) : 0;
|
||||||
|
const activePercent = totalBytes > 0 ? round1((activeBytes / totalBytes) * 100) : 0;
|
||||||
|
const availablePercent = totalBytes > 0 ? round1((availableBytes / totalBytes) * 100) : 0;
|
||||||
|
|
||||||
|
const swapTotalBytes = data.swaptotal;
|
||||||
|
const swapUsedBytes = data.swapused;
|
||||||
|
const swapFreeBytes = data.swapfree;
|
||||||
|
|
||||||
|
const swapUsagePercent = resolveSwapUsagePercent(swapTotalBytes, swapUsedBytes);
|
||||||
|
const buffcacheBytes = resolveNullableNumber(data.buffcache);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeBytes,
|
||||||
|
activePercent,
|
||||||
|
availableBytes,
|
||||||
|
availablePercent,
|
||||||
|
buffcacheBytes,
|
||||||
|
freeBytes,
|
||||||
|
freePercent,
|
||||||
|
swapFreeBytes: resolveNullableNumber(swapFreeBytes),
|
||||||
|
swapTotalBytes: resolveNullableNumber(swapTotalBytes),
|
||||||
|
swapUsagePercent,
|
||||||
|
swapUsedBytes: resolveNullableNumber(swapUsedBytes),
|
||||||
|
totalBytes,
|
||||||
|
usagePercent,
|
||||||
|
usedBytes,
|
||||||
|
usedPercent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readMemoryData(): Promise<Systeminformation.MemData> {
|
||||||
|
const si = await import("systeminformation");
|
||||||
|
return si.mem();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNullableNumber(value: number): null | number {
|
||||||
|
return value > 0 ? value : value === 0 ? 0 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSwapUsagePercent(swapTotal: number, swapUsed: number): null | number {
|
||||||
|
if (swapTotal === 0) return null;
|
||||||
|
return round1((swapUsed / swapTotal) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function round1(value: number): number {
|
||||||
|
return Math.round(value * 10) / 10;
|
||||||
|
}
|
||||||
183
src/server/checker/runner/memory/execute.ts
Normal file
183
src/server/checker/runner/memory/execute.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import type { Systeminformation } from "systeminformation";
|
||||||
|
|
||||||
|
import type { CheckResult, RawTargetConfig } from "../../types";
|
||||||
|
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
||||||
|
import type { MemoryStats, ResolvedMemoryExpectConfig, ResolvedMemoryTarget } from "./types";
|
||||||
|
|
||||||
|
import { errorFailure } from "../../expect/failure";
|
||||||
|
import { checkValueExpectation } from "../../expect/value";
|
||||||
|
import { calculateMemoryStats, readMemoryData } from "./calculate";
|
||||||
|
import {
|
||||||
|
checkActiveBytes,
|
||||||
|
checkActivePercent,
|
||||||
|
checkAvailableBytes,
|
||||||
|
checkAvailablePercent,
|
||||||
|
checkBuffcacheBytes,
|
||||||
|
checkFreeBytes,
|
||||||
|
checkFreePercent,
|
||||||
|
checkSwapFreeBytes,
|
||||||
|
checkSwapTotalBytes,
|
||||||
|
checkSwapUsagePercent,
|
||||||
|
checkSwapUsedBytes,
|
||||||
|
checkTotalBytes,
|
||||||
|
checkUsagePercent,
|
||||||
|
checkUsedBytes,
|
||||||
|
checkUsedPercent,
|
||||||
|
} from "./expect";
|
||||||
|
import { normalizeTargetExpect } from "./normalize";
|
||||||
|
import { memoryCheckerSchemas } from "./schema";
|
||||||
|
import { validateMemoryConfig } from "./validate";
|
||||||
|
|
||||||
|
export class MemoryChecker implements CheckerDefinition<ResolvedMemoryTarget> {
|
||||||
|
readonly configKey = "memory";
|
||||||
|
|
||||||
|
readonly schemas = memoryCheckerSchemas;
|
||||||
|
|
||||||
|
readonly type = "memory";
|
||||||
|
|
||||||
|
constructor(private readonly reader: () => Promise<Systeminformation.MemData> = readMemoryData) {}
|
||||||
|
|
||||||
|
buildDetail(observation: Record<string, unknown>): null | string {
|
||||||
|
const usage = observation["usagePercent"];
|
||||||
|
const usageStr = typeof usage === "number" ? formatNumber(usage) : "n/a";
|
||||||
|
const total = observation["totalBytes"];
|
||||||
|
const totalStr = typeof total === "number" ? formatBytes(total) : "n/a";
|
||||||
|
return `usage ${usageStr}%, total ${totalStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(t: ResolvedMemoryTarget, _ctx: CheckerContext): Promise<CheckResult> {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
let data: Systeminformation.MemData;
|
||||||
|
try {
|
||||||
|
data = await this.reader();
|
||||||
|
} catch (error) {
|
||||||
|
const durationMs = Math.round(performance.now() - start);
|
||||||
|
return {
|
||||||
|
detail: null,
|
||||||
|
durationMs,
|
||||||
|
failure: errorFailure(
|
||||||
|
"memory",
|
||||||
|
"snapshot",
|
||||||
|
`内存数据读取失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
),
|
||||||
|
matched: false,
|
||||||
|
observation: null,
|
||||||
|
targetId: t.id,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationMs = Math.round(performance.now() - start);
|
||||||
|
const stats = calculateMemoryStats(data);
|
||||||
|
const result = checkStats(stats, t.expect, durationMs);
|
||||||
|
|
||||||
|
const observation: Record<string, unknown> = {
|
||||||
|
activeBytes: stats.activeBytes,
|
||||||
|
activePercent: stats.activePercent,
|
||||||
|
availableBytes: stats.availableBytes,
|
||||||
|
availablePercent: stats.availablePercent,
|
||||||
|
buffcacheBytes: stats.buffcacheBytes,
|
||||||
|
error: null,
|
||||||
|
freeBytes: stats.freeBytes,
|
||||||
|
freePercent: stats.freePercent,
|
||||||
|
swapFreeBytes: stats.swapFreeBytes,
|
||||||
|
swapTotalBytes: stats.swapTotalBytes,
|
||||||
|
swapUsagePercent: stats.swapUsagePercent,
|
||||||
|
swapUsedBytes: stats.swapUsedBytes,
|
||||||
|
totalBytes: stats.totalBytes,
|
||||||
|
usagePercent: stats.usagePercent,
|
||||||
|
usedBytes: stats.usedBytes,
|
||||||
|
usedPercent: stats.usedPercent,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
detail: null,
|
||||||
|
durationMs,
|
||||||
|
failure: result.failure,
|
||||||
|
matched: result.matched,
|
||||||
|
observation,
|
||||||
|
targetId: t.id,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||||
|
return normalizeTargetExpect(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedMemoryTarget {
|
||||||
|
return {
|
||||||
|
description: null,
|
||||||
|
expect: target.expect as ResolvedMemoryExpectConfig | undefined,
|
||||||
|
group: target.group ?? "default",
|
||||||
|
id: target.id,
|
||||||
|
intervalMs: context.defaultIntervalMs,
|
||||||
|
memory: {},
|
||||||
|
name: target.name ?? null,
|
||||||
|
timeoutMs: context.defaultTimeoutMs,
|
||||||
|
type: "memory",
|
||||||
|
} satisfies ResolvedMemoryTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(t: ResolvedMemoryTarget): { config: string; target: string } {
|
||||||
|
return {
|
||||||
|
config: JSON.stringify(t.memory),
|
||||||
|
target: `memory`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(input: CheckerValidationInput) {
|
||||||
|
return validateMemoryConfig(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStats(stats: MemoryStats, expect: ResolvedMemoryExpectConfig | undefined, durationMs: number) {
|
||||||
|
let result = checkUsagePercent(stats.usagePercent, expect?.usagePercent);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkUsedPercent(stats.usedPercent, expect?.usedPercent);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkFreePercent(stats.freePercent, expect?.freePercent);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkActivePercent(stats.activePercent, expect?.activePercent);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkAvailablePercent(stats.availablePercent, expect?.availablePercent);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkActiveBytes(stats.activeBytes, expect?.activeBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkUsedBytes(stats.usedBytes, expect?.usedBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkFreeBytes(stats.freeBytes, expect?.freeBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkAvailableBytes(stats.availableBytes, expect?.availableBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkTotalBytes(stats.totalBytes, expect?.totalBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkSwapUsagePercent(stats.swapUsagePercent, expect?.swapUsagePercent);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkSwapUsedBytes(stats.swapUsedBytes, expect?.swapUsedBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkSwapFreeBytes(stats.swapFreeBytes, expect?.swapFreeBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkSwapTotalBytes(stats.swapTotalBytes, expect?.swapTotalBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
result = checkBuffcacheBytes(stats.buffcacheBytes, expect?.buffcacheBytes);
|
||||||
|
if (!result.matched) return result;
|
||||||
|
return checkValueExpectation(durationMs, expect?.durationMs, {
|
||||||
|
message: "durationMs mismatch",
|
||||||
|
path: "durationMs",
|
||||||
|
phase: "duration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes >= 1073741824) return `${formatNumber(bytes / 1073741824)}GB`;
|
||||||
|
if (bytes >= 1048576) return `${formatNumber(bytes / 1048576)}MB`;
|
||||||
|
if (bytes >= 1024) return `${formatNumber(bytes / 1024)}KB`;
|
||||||
|
return `${bytes}B`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value: number): string {
|
||||||
|
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(1)));
|
||||||
|
}
|
||||||
123
src/server/checker/runner/memory/expect.ts
Normal file
123
src/server/checker/runner/memory/expect.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { ExpectationResult, ValueExpectation } from "../../expect/types";
|
||||||
|
|
||||||
|
import { checkValueExpectation } from "../../expect/value";
|
||||||
|
|
||||||
|
export function checkActiveBytes(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存活跃字节数不满足条件",
|
||||||
|
path: "activeBytes",
|
||||||
|
phase: "activeBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkActivePercent(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存活跃百分比不满足条件",
|
||||||
|
path: "activePercent",
|
||||||
|
phase: "active",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkAvailableBytes(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存可用字节数不满足条件",
|
||||||
|
path: "availableBytes",
|
||||||
|
phase: "availableBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkAvailablePercent(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存可用百分比不满足条件",
|
||||||
|
path: "availablePercent",
|
||||||
|
phase: "available",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkBuffcacheBytes(actual: null | number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "缓冲缓存字节数不满足条件",
|
||||||
|
path: "buffcacheBytes",
|
||||||
|
phase: "buffcacheBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkFreeBytes(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存空闲字节数不满足条件",
|
||||||
|
path: "freeBytes",
|
||||||
|
phase: "freeBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkFreePercent(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存空闲百分比不满足条件",
|
||||||
|
path: "freePercent",
|
||||||
|
phase: "free",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkSwapFreeBytes(actual: null | number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "交换空间空闲字节数不满足条件",
|
||||||
|
path: "swapFreeBytes",
|
||||||
|
phase: "swapFreeBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkSwapTotalBytes(actual: null | number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "交换空间总字节数不满足条件",
|
||||||
|
path: "swapTotalBytes",
|
||||||
|
phase: "swapTotalBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkSwapUsagePercent(actual: null | number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "交换空间使用率不满足条件",
|
||||||
|
path: "swapUsagePercent",
|
||||||
|
phase: "swapUsage",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkSwapUsedBytes(actual: null | number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "交换空间已用字节数不满足条件",
|
||||||
|
path: "swapUsedBytes",
|
||||||
|
phase: "swapUsedBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkTotalBytes(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存总字节数不满足条件",
|
||||||
|
path: "totalBytes",
|
||||||
|
phase: "totalBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkUsagePercent(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存使用率不满足条件",
|
||||||
|
path: "usagePercent",
|
||||||
|
phase: "usage",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkUsedBytes(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存已用字节数不满足条件",
|
||||||
|
path: "usedBytes",
|
||||||
|
phase: "usedBytes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkUsedPercent(actual: number, matcher: undefined | ValueExpectation): ExpectationResult {
|
||||||
|
return checkValueExpectation(actual, matcher, {
|
||||||
|
message: "内存已用百分比不满足条件",
|
||||||
|
path: "usedPercent",
|
||||||
|
phase: "used",
|
||||||
|
});
|
||||||
|
}
|
||||||
1
src/server/checker/runner/memory/index.ts
Normal file
1
src/server/checker/runner/memory/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { MemoryChecker } from "./execute";
|
||||||
72
src/server/checker/runner/memory/normalize.ts
Normal file
72
src/server/checker/runner/memory/normalize.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { isNumber, isPlainObject, isString } from "es-toolkit";
|
||||||
|
|
||||||
|
import type { RawTargetConfig } from "../../types";
|
||||||
|
|
||||||
|
import { compactExpect, normalizeValue } from "../../expect/normalize";
|
||||||
|
import { parseSize } from "../../utils";
|
||||||
|
|
||||||
|
const BYTE_FIELDS = new Set([
|
||||||
|
"activeBytes",
|
||||||
|
"availableBytes",
|
||||||
|
"buffcacheBytes",
|
||||||
|
"freeBytes",
|
||||||
|
"swapFreeBytes",
|
||||||
|
"swapTotalBytes",
|
||||||
|
"swapUsedBytes",
|
||||||
|
"totalBytes",
|
||||||
|
"usedBytes",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PERCENT_FIELDS = new Set([
|
||||||
|
"activePercent",
|
||||||
|
"availablePercent",
|
||||||
|
"freePercent",
|
||||||
|
"swapUsagePercent",
|
||||||
|
"usagePercent",
|
||||||
|
"usedPercent",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function normalizeByteValue(value: unknown): unknown {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
if (isString(value)) {
|
||||||
|
const parsed = parseSize(value);
|
||||||
|
return normalizeValue(parsed);
|
||||||
|
}
|
||||||
|
if (isNumber(value)) {
|
||||||
|
return normalizeValue(value);
|
||||||
|
}
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
const obj = value as Record<string, unknown>;
|
||||||
|
const converted: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
converted[k] = isString(v) ? parseSize(v) : v;
|
||||||
|
}
|
||||||
|
return normalizeValue(converted);
|
||||||
|
}
|
||||||
|
return normalizeValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||||
|
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||||
|
const raw = target.expect as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
expect: compactExpect(raw, normalizeAllFields(raw)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAllFields(raw: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of BYTE_FIELDS) {
|
||||||
|
result[key] = normalizeByteValue(raw[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of PERCENT_FIELDS) {
|
||||||
|
result[key] = normalizeValue(raw[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result["durationMs"] = normalizeValue(raw["durationMs"]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
55
src/server/checker/runner/memory/schema.ts
Normal file
55
src/server/checker/runner/memory/schema.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
|
import type { CheckerSchemas } from "../types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAuthoringFieldSchema,
|
||||||
|
createAuthoringValueExpectationSchema,
|
||||||
|
createNormalizedValueExpectationSchema,
|
||||||
|
sizeSchema,
|
||||||
|
} from "../../schema/fragments";
|
||||||
|
|
||||||
|
export const memoryCheckerSchemas: CheckerSchemas = {
|
||||||
|
authoring: {
|
||||||
|
config: createMemoryConfigSchema("authoring"),
|
||||||
|
expect: createMemoryExpectSchema("authoring"),
|
||||||
|
},
|
||||||
|
normalized: {
|
||||||
|
config: createMemoryConfigSchema("normalized"),
|
||||||
|
expect: createMemoryExpectSchema("normalized"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMemoryConfigSchema(_kind: "authoring" | "normalized") {
|
||||||
|
return Type.Object({}, { additionalProperties: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMemoryExpectSchema(kind: "authoring" | "normalized") {
|
||||||
|
const valueSchema =
|
||||||
|
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema();
|
||||||
|
|
||||||
|
const byteValueSchema =
|
||||||
|
kind === "authoring" ? createAuthoringFieldSchema(sizeSchema) : createNormalizedValueExpectationSchema();
|
||||||
|
|
||||||
|
return Type.Object(
|
||||||
|
{
|
||||||
|
activeBytes: Type.Optional(byteValueSchema),
|
||||||
|
activePercent: Type.Optional(valueSchema),
|
||||||
|
availableBytes: Type.Optional(byteValueSchema),
|
||||||
|
availablePercent: Type.Optional(valueSchema),
|
||||||
|
buffcacheBytes: Type.Optional(byteValueSchema),
|
||||||
|
durationMs: Type.Optional(valueSchema),
|
||||||
|
freeBytes: Type.Optional(byteValueSchema),
|
||||||
|
freePercent: Type.Optional(valueSchema),
|
||||||
|
swapFreeBytes: Type.Optional(byteValueSchema),
|
||||||
|
swapTotalBytes: Type.Optional(byteValueSchema),
|
||||||
|
swapUsagePercent: Type.Optional(valueSchema),
|
||||||
|
swapUsedBytes: Type.Optional(byteValueSchema),
|
||||||
|
totalBytes: Type.Optional(byteValueSchema),
|
||||||
|
usagePercent: Type.Optional(valueSchema),
|
||||||
|
usedBytes: Type.Optional(byteValueSchema),
|
||||||
|
usedPercent: Type.Optional(valueSchema),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/server/checker/runner/memory/types.ts
Normal file
75
src/server/checker/runner/memory/types.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { Systeminformation } from "systeminformation";
|
||||||
|
|
||||||
|
import type { RawValueExpectation, ValueExpectation } from "../../expect/types";
|
||||||
|
import type { ResolvedTargetBase } from "../../types";
|
||||||
|
|
||||||
|
export type MemoryDataReader = () => Promise<Systeminformation.MemData>;
|
||||||
|
|
||||||
|
export interface MemoryStats {
|
||||||
|
activeBytes: number;
|
||||||
|
activePercent: number;
|
||||||
|
availableBytes: number;
|
||||||
|
availablePercent: number;
|
||||||
|
buffcacheBytes: null | number;
|
||||||
|
freeBytes: number;
|
||||||
|
freePercent: number;
|
||||||
|
swapFreeBytes: null | number;
|
||||||
|
swapTotalBytes: null | number;
|
||||||
|
swapUsagePercent: null | number;
|
||||||
|
swapUsedBytes: null | number;
|
||||||
|
totalBytes: number;
|
||||||
|
usagePercent: number;
|
||||||
|
usedBytes: number;
|
||||||
|
usedPercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawMemoryExpectConfig {
|
||||||
|
activeBytes?: RawValueExpectation;
|
||||||
|
activePercent?: RawValueExpectation;
|
||||||
|
availableBytes?: RawValueExpectation;
|
||||||
|
availablePercent?: RawValueExpectation;
|
||||||
|
buffcacheBytes?: RawValueExpectation;
|
||||||
|
durationMs?: RawValueExpectation;
|
||||||
|
freeBytes?: RawValueExpectation;
|
||||||
|
freePercent?: RawValueExpectation;
|
||||||
|
swapFreeBytes?: RawValueExpectation;
|
||||||
|
swapTotalBytes?: RawValueExpectation;
|
||||||
|
swapUsagePercent?: RawValueExpectation;
|
||||||
|
swapUsedBytes?: RawValueExpectation;
|
||||||
|
totalBytes?: RawValueExpectation;
|
||||||
|
usagePercent?: RawValueExpectation;
|
||||||
|
usedBytes?: RawValueExpectation;
|
||||||
|
usedPercent?: RawValueExpectation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export interface ResolvedMemoryConfig {}
|
||||||
|
|
||||||
|
export interface ResolvedMemoryExpectConfig {
|
||||||
|
activeBytes?: ValueExpectation;
|
||||||
|
activePercent?: ValueExpectation;
|
||||||
|
availableBytes?: ValueExpectation;
|
||||||
|
availablePercent?: ValueExpectation;
|
||||||
|
buffcacheBytes?: ValueExpectation;
|
||||||
|
durationMs?: ValueExpectation;
|
||||||
|
freeBytes?: ValueExpectation;
|
||||||
|
freePercent?: ValueExpectation;
|
||||||
|
swapFreeBytes?: ValueExpectation;
|
||||||
|
swapTotalBytes?: ValueExpectation;
|
||||||
|
swapUsagePercent?: ValueExpectation;
|
||||||
|
swapUsedBytes?: ValueExpectation;
|
||||||
|
totalBytes?: ValueExpectation;
|
||||||
|
usagePercent?: ValueExpectation;
|
||||||
|
usedBytes?: ValueExpectation;
|
||||||
|
usedPercent?: ValueExpectation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedMemoryTarget extends ResolvedTargetBase {
|
||||||
|
expect?: ResolvedMemoryExpectConfig;
|
||||||
|
group: string;
|
||||||
|
intervalMs: number;
|
||||||
|
memory: ResolvedMemoryConfig;
|
||||||
|
name: null | string;
|
||||||
|
timeoutMs: number;
|
||||||
|
type: "memory";
|
||||||
|
}
|
||||||
111
src/server/checker/runner/memory/validate.ts
Normal file
111
src/server/checker/runner/memory/validate.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { isString } from "es-toolkit";
|
||||||
|
|
||||||
|
import type { ConfigValidationIssue } from "../../schema/issues";
|
||||||
|
import type { CheckerValidationInput } from "../types";
|
||||||
|
|
||||||
|
import { isPlainRecord, validateRawValueExpectation } from "../../expect/validate";
|
||||||
|
import { issue, joinPath } from "../../schema/issues";
|
||||||
|
import { parseSize } from "../../utils";
|
||||||
|
|
||||||
|
const MEMORY_CONFIG_KEYS = new Set<string>([]);
|
||||||
|
|
||||||
|
const MEMORY_EXPECT_FIELDS = [
|
||||||
|
"activeBytes",
|
||||||
|
"activePercent",
|
||||||
|
"availableBytes",
|
||||||
|
"availablePercent",
|
||||||
|
"buffcacheBytes",
|
||||||
|
"durationMs",
|
||||||
|
"freeBytes",
|
||||||
|
"freePercent",
|
||||||
|
"swapFreeBytes",
|
||||||
|
"swapTotalBytes",
|
||||||
|
"swapUsagePercent",
|
||||||
|
"swapUsedBytes",
|
||||||
|
"totalBytes",
|
||||||
|
"usagePercent",
|
||||||
|
"usedBytes",
|
||||||
|
"usedPercent",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const BYTE_EXPECT_FIELDS = new Set([
|
||||||
|
"activeBytes",
|
||||||
|
"availableBytes",
|
||||||
|
"buffcacheBytes",
|
||||||
|
"freeBytes",
|
||||||
|
"swapFreeBytes",
|
||||||
|
"swapTotalBytes",
|
||||||
|
"swapUsedBytes",
|
||||||
|
"totalBytes",
|
||||||
|
"usedBytes",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MEMORY_EXPECT_KEYS = new Set<string>(MEMORY_EXPECT_FIELDS);
|
||||||
|
|
||||||
|
export function validateMemoryConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < input.targets.length; i++) {
|
||||||
|
const target = input.targets[i] as unknown;
|
||||||
|
if (!isPlainRecord(target)) continue;
|
||||||
|
if (target["type"] !== "memory") continue;
|
||||||
|
issues.push(...validateMemoryTarget(target, `targets[${i}]`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetName(target: Record<string, unknown>): string | undefined {
|
||||||
|
if (isString(target["name"])) return target["name"];
|
||||||
|
return isString(target["id"]) ? target["id"] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMemoryExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||||
|
const rawExpect = target["expect"];
|
||||||
|
if (rawExpect === undefined || rawExpect === null || !isPlainRecord(rawExpect)) return [];
|
||||||
|
const expect = rawExpect;
|
||||||
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
const targetName = getTargetName(target);
|
||||||
|
const expectPath = joinPath(path, "expect");
|
||||||
|
|
||||||
|
for (const key of MEMORY_EXPECT_FIELDS) {
|
||||||
|
if (expect[key] !== undefined) {
|
||||||
|
issues.push(...validateRawValueExpectation(expect[key], joinPath(expectPath, key), targetName));
|
||||||
|
if (BYTE_EXPECT_FIELDS.has(key) && isString(expect[key])) {
|
||||||
|
try {
|
||||||
|
parseSize(expect[key]);
|
||||||
|
} catch {
|
||||||
|
issues.push(issue("invalid-value", joinPath(expectPath, key), "不是有效的字节大小格式", targetName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(expect)) {
|
||||||
|
if (!MEMORY_EXPECT_KEYS.has(key)) {
|
||||||
|
issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMemoryTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||||
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
const targetName = getTargetName(target);
|
||||||
|
|
||||||
|
const rawMemory = target["memory"];
|
||||||
|
if (!isPlainRecord(rawMemory)) {
|
||||||
|
issues.push(issue("required", joinPath(path, "memory"), "缺少 memory 配置分组", targetName));
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(rawMemory)) {
|
||||||
|
if (!MEMORY_CONFIG_KEYS.has(key)) {
|
||||||
|
issues.push(issue("unknown-field", joinPath(joinPath(path, "memory"), key), "是未知字段", targetName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issues.push(...validateMemoryExpect(target, path));
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
141
tests/server/checker/runner/memory/calculate.test.ts
Normal file
141
tests/server/checker/runner/memory/calculate.test.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import type { Systeminformation } from "systeminformation";
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { calculateMemoryStats } from "../../../../../src/server/checker/runner/memory/calculate";
|
||||||
|
|
||||||
|
function makeMemData(overrides: Partial<Systeminformation.MemData> = {}): Systeminformation.MemData {
|
||||||
|
return {
|
||||||
|
active: 4294967296,
|
||||||
|
available: 8589934592,
|
||||||
|
buffcache: 1073741824,
|
||||||
|
buffers: 536870912,
|
||||||
|
cached: 536870912,
|
||||||
|
dirty: null,
|
||||||
|
free: 4294967296,
|
||||||
|
reclaimable: 0,
|
||||||
|
slab: 0,
|
||||||
|
swapfree: 0,
|
||||||
|
swaptotal: 0,
|
||||||
|
swapused: 0,
|
||||||
|
total: 17179869184,
|
||||||
|
used: 8589934592,
|
||||||
|
writeback: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("calculateMemoryStats", () => {
|
||||||
|
test("usagePercent = activeBytes / totalBytes * 100", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ active: 4294967296, total: 8589934592 }));
|
||||||
|
expect(stats.usagePercent).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("usedPercent = usedBytes / totalBytes * 100", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ total: 8589934592, used: 6442450944 }));
|
||||||
|
expect(stats.usedPercent).toBe(75);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("freePercent = freeBytes / totalBytes * 100", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ free: 2147483648, total: 8589934592 }));
|
||||||
|
expect(stats.freePercent).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("activePercent = activeBytes / totalBytes * 100", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ active: 3221225472, total: 8589934592 }));
|
||||||
|
expect(stats.activePercent).toBe(37.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("availablePercent = availableBytes / totalBytes * 100", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ available: 6442450944, total: 8589934592 }));
|
||||||
|
expect(stats.availablePercent).toBe(75);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("保留 1 位小数", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ active: 3000000000, total: 8000000000 }));
|
||||||
|
expect(stats.usagePercent).toBe(37.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("round1 处理需要四舍五入的情况", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ active: 3333333333, total: 10000000000 }));
|
||||||
|
expect(stats.usagePercent).toBe(33.3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("total 为 0 时百分比为 0", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ active: 0, available: 0, free: 0, total: 0, used: 0 }));
|
||||||
|
expect(stats.usagePercent).toBe(0);
|
||||||
|
expect(stats.usedPercent).toBe(0);
|
||||||
|
expect(stats.freePercent).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buffcacheBytes 为 null 映射", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ buffcache: 0 }));
|
||||||
|
expect(stats.buffcacheBytes).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buffcacheBytes 为正数时保留", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ buffcache: 1073741824 }));
|
||||||
|
expect(stats.buffcacheBytes).toBe(1073741824);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("所有字节字段正确映射", () => {
|
||||||
|
const data = makeMemData({
|
||||||
|
active: 1000,
|
||||||
|
available: 2000,
|
||||||
|
free: 3000,
|
||||||
|
total: 4000,
|
||||||
|
used: 3500,
|
||||||
|
});
|
||||||
|
const stats = calculateMemoryStats(data);
|
||||||
|
expect(stats.activeBytes).toBe(1000);
|
||||||
|
expect(stats.availableBytes).toBe(2000);
|
||||||
|
expect(stats.freeBytes).toBe(3000);
|
||||||
|
expect(stats.totalBytes).toBe(4000);
|
||||||
|
expect(stats.usedBytes).toBe(3500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("calculateMemoryStats swap", () => {
|
||||||
|
test("swap 不可用:swaptotal=0 时 swapUsagePercent=null", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ swapfree: 0, swaptotal: 0, swapused: 0 }));
|
||||||
|
expect(stats.swapUsagePercent).toBe(null);
|
||||||
|
expect(stats.swapTotalBytes).toBe(0);
|
||||||
|
expect(stats.swapUsedBytes).toBe(0);
|
||||||
|
expect(stats.swapFreeBytes).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swap 总量为 0,swapUsagePercent 为 null(不是 0)", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ swaptotal: 0 }));
|
||||||
|
expect(stats.swapUsagePercent).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swap 已使用", () => {
|
||||||
|
const stats = calculateMemoryStats(
|
||||||
|
makeMemData({ swapfree: 1073741824, swaptotal: 4294967296, swapused: 3221225472 }),
|
||||||
|
);
|
||||||
|
expect(stats.swapUsagePercent).toBe(75);
|
||||||
|
expect(stats.swapTotalBytes).toBe(4294967296);
|
||||||
|
expect(stats.swapUsedBytes).toBe(3221225472);
|
||||||
|
expect(stats.swapFreeBytes).toBe(1073741824);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swap 未使用:swapUsagePercent=0(不是 null)", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ swapfree: 4294967296, swaptotal: 4294967296, swapused: 0 }));
|
||||||
|
expect(stats.swapUsagePercent).toBe(0);
|
||||||
|
expect(stats.swapUsedBytes).toBe(0);
|
||||||
|
expect(stats.swapFreeBytes).toBe(4294967296);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swap 部分使用保留 1 位小数", () => {
|
||||||
|
const stats = calculateMemoryStats(
|
||||||
|
makeMemData({ swapfree: 3000000000, swaptotal: 10000000000, swapused: 7000000000 }),
|
||||||
|
);
|
||||||
|
expect(stats.swapUsagePercent).toBe(70);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swap 合法 0 不被转换为 null", () => {
|
||||||
|
const stats = calculateMemoryStats(makeMemData({ swapfree: 4294967296, swaptotal: 4294967296, swapused: 0 }));
|
||||||
|
expect(stats.swapUsedBytes).toBe(0);
|
||||||
|
expect(stats.swapUsagePercent).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
184
tests/server/checker/runner/memory/execute.test.ts
Normal file
184
tests/server/checker/runner/memory/execute.test.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import type { Systeminformation } from "systeminformation";
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
||||||
|
|
||||||
|
import { MemoryChecker } from "../../../../../src/server/checker/runner/memory/execute";
|
||||||
|
|
||||||
|
function makeMemData(overrides: Partial<Systeminformation.MemData> = {}): Systeminformation.MemData {
|
||||||
|
return {
|
||||||
|
active: 4294967296,
|
||||||
|
available: 8589934592,
|
||||||
|
buffcache: 1073741824,
|
||||||
|
buffers: 536870912,
|
||||||
|
cached: 536870912,
|
||||||
|
dirty: null,
|
||||||
|
free: 4294967296,
|
||||||
|
reclaimable: 0,
|
||||||
|
slab: 0,
|
||||||
|
swapfree: 0,
|
||||||
|
swaptotal: 0,
|
||||||
|
swapused: 0,
|
||||||
|
total: 17179869184,
|
||||||
|
used: 8589934592,
|
||||||
|
writeback: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeResolveContext(
|
||||||
|
overrides: Partial<{ configDir: string; defaultIntervalMs: number; defaultTimeoutMs: number }> = {},
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
configDir: "/test",
|
||||||
|
defaultIntervalMs: 30000,
|
||||||
|
defaultTimeoutMs: 10000,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("MemoryChecker resolve", () => {
|
||||||
|
const checker = new MemoryChecker();
|
||||||
|
|
||||||
|
test("默认值:memory 为空对象", () => {
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
expect(resolved.memory).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("无 expect 时 expect 为 undefined", () => {
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
expect(resolved.expect).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("保留 expect 字段", () => {
|
||||||
|
const target: RawTargetConfig = {
|
||||||
|
expect: { usagePercent: { lte: 85 } },
|
||||||
|
id: "mem-test",
|
||||||
|
memory: {},
|
||||||
|
type: "memory",
|
||||||
|
};
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
expect(resolved.expect).toEqual({ usagePercent: { lte: 85 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("type 为 memory", () => {
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
expect(resolved.type).toBe("memory");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MemoryChecker execute", () => {
|
||||||
|
test("成功匹配", async () => {
|
||||||
|
const data = makeMemData({ active: 4294967296, total: 8589934592 });
|
||||||
|
const reader = () => Promise.resolve(data);
|
||||||
|
const checker = new MemoryChecker(reader);
|
||||||
|
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
resolved.expect = { usagePercent: { lte: 85 } };
|
||||||
|
|
||||||
|
const ctx = { signal: new AbortController().signal };
|
||||||
|
const result = await checker.execute(resolved, ctx);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.failure).toBeNull();
|
||||||
|
expect(result.observation).toMatchObject({
|
||||||
|
totalBytes: 8589934592,
|
||||||
|
usagePercent: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("usagePercent mismatch", async () => {
|
||||||
|
const data = makeMemData({ active: 7730941132, total: 8589934592 });
|
||||||
|
const reader = () => Promise.resolve(data);
|
||||||
|
const checker = new MemoryChecker(reader);
|
||||||
|
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
resolved.expect = { usagePercent: { lte: 50 } };
|
||||||
|
|
||||||
|
const ctx = { signal: new AbortController().signal };
|
||||||
|
const result = await checker.execute(resolved, ctx);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("usage");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("observation 包含所有字段", async () => {
|
||||||
|
const data = makeMemData();
|
||||||
|
const reader = () => Promise.resolve(data);
|
||||||
|
const checker = new MemoryChecker(reader);
|
||||||
|
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
|
||||||
|
const ctx = { signal: new AbortController().signal };
|
||||||
|
const result = await checker.execute(resolved, ctx);
|
||||||
|
|
||||||
|
const obs = result.observation!;
|
||||||
|
expect(obs).toHaveProperty("activeBytes");
|
||||||
|
expect(obs).toHaveProperty("activePercent");
|
||||||
|
expect(obs).toHaveProperty("availableBytes");
|
||||||
|
expect(obs).toHaveProperty("availablePercent");
|
||||||
|
expect(obs).toHaveProperty("buffcacheBytes");
|
||||||
|
expect(obs).toHaveProperty("freeBytes");
|
||||||
|
expect(obs).toHaveProperty("freePercent");
|
||||||
|
expect(obs).toHaveProperty("swapFreeBytes");
|
||||||
|
expect(obs).toHaveProperty("swapTotalBytes");
|
||||||
|
expect(obs).toHaveProperty("swapUsagePercent");
|
||||||
|
expect(obs).toHaveProperty("swapUsedBytes");
|
||||||
|
expect(obs).toHaveProperty("totalBytes");
|
||||||
|
expect(obs).toHaveProperty("usagePercent");
|
||||||
|
expect(obs).toHaveProperty("usedBytes");
|
||||||
|
expect(obs).toHaveProperty("usedPercent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reader reject 返回失败结果", async () => {
|
||||||
|
const reader = () => Promise.reject(new Error("read error"));
|
||||||
|
const checker = new MemoryChecker(reader);
|
||||||
|
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
|
||||||
|
const ctx = { signal: new AbortController().signal };
|
||||||
|
const result = await checker.execute(resolved, ctx);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("memory");
|
||||||
|
expect(result.failure?.path).toBe("snapshot");
|
||||||
|
expect(result.observation).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detail 格式", async () => {
|
||||||
|
const data = makeMemData({ active: 4294967296, total: 8589934592 });
|
||||||
|
const reader = () => Promise.resolve(data);
|
||||||
|
const checker = new MemoryChecker(reader);
|
||||||
|
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
|
||||||
|
const ctx = { signal: new AbortController().signal };
|
||||||
|
const result = await checker.execute(resolved, ctx);
|
||||||
|
|
||||||
|
const detail = checker.buildDetail(result.observation!);
|
||||||
|
expect(detail).toContain("usage");
|
||||||
|
expect(detail).toContain("%");
|
||||||
|
expect(detail).toContain("total");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MemoryChecker serialize", () => {
|
||||||
|
test("序列化输出", () => {
|
||||||
|
const checker = new MemoryChecker();
|
||||||
|
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||||
|
const resolved = checker.resolve(target, makeResolveContext());
|
||||||
|
const result = checker.serialize(resolved);
|
||||||
|
expect(result.target).toBe("memory");
|
||||||
|
const config = JSON.parse(result.config) as Record<string, unknown>;
|
||||||
|
expect(config).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
149
tests/server/checker/runner/memory/expect.test.ts
Normal file
149
tests/server/checker/runner/memory/expect.test.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkActiveBytes,
|
||||||
|
checkActivePercent,
|
||||||
|
checkAvailableBytes,
|
||||||
|
checkAvailablePercent,
|
||||||
|
checkBuffcacheBytes,
|
||||||
|
checkFreeBytes,
|
||||||
|
checkFreePercent,
|
||||||
|
checkSwapFreeBytes,
|
||||||
|
checkSwapTotalBytes,
|
||||||
|
checkSwapUsagePercent,
|
||||||
|
checkSwapUsedBytes,
|
||||||
|
checkTotalBytes,
|
||||||
|
checkUsagePercent,
|
||||||
|
checkUsedBytes,
|
||||||
|
checkUsedPercent,
|
||||||
|
} from "../../../../../src/server/checker/runner/memory/expect";
|
||||||
|
|
||||||
|
describe("Memory expect checks - 百分比字段", () => {
|
||||||
|
test("checkUsagePercent 匹配", () => {
|
||||||
|
expect(checkUsagePercent(50, { lte: 85 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkUsagePercent 不匹配", () => {
|
||||||
|
const result = checkUsagePercent(90, { lte: 85 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("usage");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkUsedPercent 匹配", () => {
|
||||||
|
expect(checkUsedPercent(75, { lte: 80 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkUsedPercent 不匹配", () => {
|
||||||
|
const result = checkUsedPercent(85, { lte: 80 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("used");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkFreePercent 匹配", () => {
|
||||||
|
expect(checkFreePercent(25, { gte: 15 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkFreePercent 不匹配", () => {
|
||||||
|
const result = checkFreePercent(10, { gte: 15 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("free");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkActivePercent 匹配", () => {
|
||||||
|
expect(checkActivePercent(50, { lte: 85 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkAvailablePercent 匹配", () => {
|
||||||
|
expect(checkAvailablePercent(50, { gte: 20 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("undefined matcher 直接通过", () => {
|
||||||
|
expect(checkUsagePercent(99.9, undefined).matched).toBe(true);
|
||||||
|
expect(checkFreePercent(0, undefined).matched).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Memory expect checks - 字节字段", () => {
|
||||||
|
test("checkActiveBytes 匹配", () => {
|
||||||
|
expect(checkActiveBytes(4294967296, { lte: 8589934592 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkUsedBytes 不匹配", () => {
|
||||||
|
const result = checkUsedBytes(10737418240, { lte: 8589934592 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("usedBytes");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkFreeBytes 匹配", () => {
|
||||||
|
expect(checkFreeBytes(4294967296, { gte: 2147483648 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkAvailableBytes 匹配", () => {
|
||||||
|
expect(checkAvailableBytes(6442450944, { gte: 4294967296 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkTotalBytes 匹配", () => {
|
||||||
|
expect(checkTotalBytes(17179869184, { gte: 8589934592 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Memory expect checks - swap 字段", () => {
|
||||||
|
test("checkSwapUsagePercent null 通过 gte 检查 (Number(null)=0)", () => {
|
||||||
|
expect(checkSwapUsagePercent(null, { gte: 0 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapUsagePercent null 不匹配大于 0 的 gte", () => {
|
||||||
|
const result = checkSwapUsagePercent(null, { gte: 1 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("swapUsage");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapUsagePercent 有值时正常匹配", () => {
|
||||||
|
expect(checkSwapUsagePercent(50, { lte: 80 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapUsagePercent 有值时不匹配", () => {
|
||||||
|
const result = checkSwapUsagePercent(90, { lte: 80 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapUsedBytes null 通过 gte:0 (Number(null)=0)", () => {
|
||||||
|
expect(checkSwapUsedBytes(null, { gte: 0 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapUsedBytes 0 通过 gte:0", () => {
|
||||||
|
expect(checkSwapUsedBytes(0, { gte: 0 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapFreeBytes null 通过 gte:0", () => {
|
||||||
|
expect(checkSwapFreeBytes(null, { gte: 0 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapTotalBytes null 匹配 equals:null", () => {
|
||||||
|
expect(checkSwapTotalBytes(null, { equals: null }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapTotalBytes 0 匹配 equals:0", () => {
|
||||||
|
expect(checkSwapTotalBytes(0, { equals: 0 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkSwapTotalBytes null 不匹配 equals:0", () => {
|
||||||
|
expect(checkSwapTotalBytes(null, { equals: 0 }).matched).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Memory expect checks - buffcacheBytes", () => {
|
||||||
|
test("checkBuffcacheBytes 有值时匹配", () => {
|
||||||
|
expect(checkBuffcacheBytes(1073741824, { lte: 2147483648 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkBuffcacheBytes null 通过 gte:0 (Number(null)=0)", () => {
|
||||||
|
expect(checkBuffcacheBytes(null, { gte: 0 }).matched).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkBuffcacheBytes null 不匹配 gte:1", () => {
|
||||||
|
const result = checkBuffcacheBytes(null, { gte: 1 });
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.failure?.phase).toBe("buffcacheBytes");
|
||||||
|
});
|
||||||
|
});
|
||||||
72
tests/server/checker/runner/memory/normalize.test.ts
Normal file
72
tests/server/checker/runner/memory/normalize.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { normalizeTargetExpect } from "../../../../../src/server/checker/runner/memory/normalize";
|
||||||
|
|
||||||
|
describe("normalizeTargetExpect (memory)", () => {
|
||||||
|
test("无 expect 直接返回", () => {
|
||||||
|
const target = { id: "test", memory: {}, type: "memory" };
|
||||||
|
expect(normalizeTargetExpect(target)).toEqual(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 为非对象直接返回", () => {
|
||||||
|
const target = { expect: "not-an-object", id: "test", memory: {}, type: "memory" };
|
||||||
|
expect(normalizeTargetExpect(target)).toEqual(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("字节大小字符串 512MB 转换为数字", () => {
|
||||||
|
const target = { expect: { usedBytes: "512MB" }, id: "test", memory: {}, type: "memory" };
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
expect((result.expect as Record<string, unknown>)["usedBytes"]).toEqual({ equals: 536870912 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("字节大小字符串 1GB 转换为数字", () => {
|
||||||
|
const target = { expect: { totalBytes: "1GB" }, id: "test", memory: {}, type: "memory" };
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
expect((result.expect as Record<string, unknown>)["totalBytes"]).toEqual({ equals: 1073741824 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("数字字节 matcher 保持不变", () => {
|
||||||
|
const target = { expect: { usedBytes: 1073741824 }, id: "test", memory: {}, type: "memory" };
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
expect((result.expect as Record<string, unknown>)["usedBytes"]).toEqual({ equals: 1073741824 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("百分比 matcher 正常展开", () => {
|
||||||
|
const target = { expect: { usagePercent: 85 }, id: "test", memory: {}, type: "memory" };
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
expect((result.expect as Record<string, unknown>)["usagePercent"]).toEqual({ equals: 85 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matcher 对象保持不变", () => {
|
||||||
|
const target = { expect: { usagePercent: { lte: 85 } }, id: "test", memory: {}, type: "memory" };
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
expect((result.expect as Record<string, unknown>)["usagePercent"]).toEqual({ lte: 85 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("字节 matcher 对象内字符串转换", () => {
|
||||||
|
const target = { expect: { usedBytes: { gte: "512MB" } }, id: "test", memory: {}, type: "memory" };
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
expect((result.expect as Record<string, unknown>)["usedBytes"]).toEqual({ gte: 536870912 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("多个字段同时处理", () => {
|
||||||
|
const target = {
|
||||||
|
expect: { freePercent: 25, totalBytes: "16GB", usagePercent: { lte: 85 } },
|
||||||
|
id: "test",
|
||||||
|
memory: {},
|
||||||
|
type: "memory",
|
||||||
|
};
|
||||||
|
const result = normalizeTargetExpect(target);
|
||||||
|
const expectObj = result.expect as Record<string, unknown>;
|
||||||
|
expect(expectObj["freePercent"]).toEqual({ equals: 25 });
|
||||||
|
expect(expectObj["totalBytes"]).toEqual({ equals: 17179869184 });
|
||||||
|
expect(expectObj["usagePercent"]).toEqual({ lte: 85 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeTargetExpect (memory) 错误", () => {
|
||||||
|
test("非法大小字符串抛出", () => {
|
||||||
|
const target = { expect: { usedBytes: "abc" }, id: "test", memory: {}, type: "memory" };
|
||||||
|
expect(() => normalizeTargetExpect(target)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
95
tests/server/checker/runner/memory/schema.test.ts
Normal file
95
tests/server/checker/runner/memory/schema.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import Ajv from "ajv";
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { memoryCheckerSchemas } from "../../../../../src/server/checker/runner/memory/schema";
|
||||||
|
|
||||||
|
const ajv = new Ajv({ strict: false });
|
||||||
|
|
||||||
|
describe("Memory checker schema", () => {
|
||||||
|
test("authoring config 空配置通过", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.authoring.config);
|
||||||
|
expect(validate({})).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalized config 空配置通过", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.config);
|
||||||
|
expect(validate({})).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("config 拒绝额外字段", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.authoring.config);
|
||||||
|
expect(validate({ extraField: true })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authoring expect 允许百分比 ValueMatcher 简写", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||||
|
expect(validate({ usagePercent: 85 })).toBe(true);
|
||||||
|
expect(validate({ usagePercent: { lte: 85 } })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authoring expect 允许字节字段字符串", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||||
|
expect(validate({ usedBytes: "512MB" })).toBe(true);
|
||||||
|
expect(validate({ totalBytes: "1GB" })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authoring expect 允许字节字段数字", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||||
|
expect(validate({ usedBytes: 536870912 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalized expect 允许 matcher 对象", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||||
|
expect(validate({ freePercent: { gte: 15 }, usagePercent: { lte: 85 } })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 拒绝未知字段", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||||
|
expect(validate({ unknownField: 1 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 空对象通过", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||||
|
expect(validate({})).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 允许所有合法百分比字段", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||||
|
expect(
|
||||||
|
validate({
|
||||||
|
activePercent: { lte: 80 },
|
||||||
|
availablePercent: { gte: 20 },
|
||||||
|
freePercent: { gte: 15 },
|
||||||
|
swapUsagePercent: { lte: 50 },
|
||||||
|
usagePercent: { lte: 85 },
|
||||||
|
usedPercent: { lte: 90 },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 允许所有合法字节字段", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||||
|
expect(
|
||||||
|
validate({
|
||||||
|
activeBytes: { lte: 8589934592 },
|
||||||
|
availableBytes: { gte: 4294967296 },
|
||||||
|
freeBytes: { gte: 2147483648 },
|
||||||
|
swapFreeBytes: { gte: 0 },
|
||||||
|
swapTotalBytes: { lte: 4294967296 },
|
||||||
|
swapUsedBytes: { lte: 2147483648 },
|
||||||
|
totalBytes: { equals: 17179869184 },
|
||||||
|
usedBytes: { lte: 8589934592 },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 允许 durationMs 字段", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||||
|
expect(validate({ durationMs: { lte: 5000 } })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 允许 buffcacheBytes 字段", () => {
|
||||||
|
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||||
|
expect(validate({ buffcacheBytes: { lte: 2147483648 } })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
87
tests/server/checker/runner/memory/validate.test.ts
Normal file
87
tests/server/checker/runner/memory/validate.test.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
||||||
|
|
||||||
|
import { validateMemoryConfig } from "../../../../../src/server/checker/runner/memory/validate";
|
||||||
|
|
||||||
|
function validate(target: RawTargetConfig) {
|
||||||
|
return validateMemoryConfig({ targets: [target] });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("validateMemoryConfig", () => {
|
||||||
|
test("有效配置无错误", () => {
|
||||||
|
expect(validate({ id: "mem-test", memory: {}, type: "memory" })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("缺少 memory 配置分组", () => {
|
||||||
|
const issues = validate({ id: "mem-test", type: "memory" });
|
||||||
|
expect(issues.some((i) => i.path.endsWith("memory") && i.code === "required")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("memory 未知字段报错", () => {
|
||||||
|
const issues = validate({ id: "mem-test", memory: { extra: true }, type: "memory" });
|
||||||
|
expect(issues.some((i) => i.path.endsWith("extra") && i.code === "unknown-field")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 未知字段报错", () => {
|
||||||
|
const issues = validate({ expect: { logicalCoreCount: { gte: 4 } }, id: "mem-test", memory: {}, type: "memory" });
|
||||||
|
expect(issues.some((i) => i.path.endsWith("logicalCoreCount") && i.code === "unknown-field")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 合法 ValueMatcher 通过", () => {
|
||||||
|
const issues = validate({
|
||||||
|
expect: { usagePercent: { lte: 85 }, usedBytes: { lte: 8589934592 } },
|
||||||
|
id: "mem-test",
|
||||||
|
memory: {},
|
||||||
|
type: "memory",
|
||||||
|
});
|
||||||
|
expect(issues.filter((i) => i.path.includes("expect"))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 非法 ValueMatcher 报错", () => {
|
||||||
|
const issues = validate({ expect: { usagePercent: [1, 2] }, id: "mem-test", memory: {}, type: "memory" });
|
||||||
|
expect(issues.some((i) => i.path.includes("usagePercent"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 合法字节大小字符串通过", () => {
|
||||||
|
const issues = validate({ expect: { usedBytes: "512MB" }, id: "mem-test", memory: {}, type: "memory" });
|
||||||
|
expect(issues.filter((i) => i.path.includes("usedBytes"))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 非法字节大小字符串报错", () => {
|
||||||
|
const issues = validate({ expect: { usedBytes: "abc" }, id: "mem-test", memory: {}, type: "memory" });
|
||||||
|
expect(issues.some((i) => i.path.includes("usedBytes") && i.message.includes("字节大小"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("expect 所有合法字段通过", () => {
|
||||||
|
const issues = validate({
|
||||||
|
expect: {
|
||||||
|
activeBytes: { lte: 8589934592 },
|
||||||
|
activePercent: { lte: 80 },
|
||||||
|
availableBytes: { gte: 4294967296 },
|
||||||
|
availablePercent: { gte: 20 },
|
||||||
|
buffcacheBytes: { lte: 2147483648 },
|
||||||
|
durationMs: { lte: 5000 },
|
||||||
|
freeBytes: { gte: 2147483648 },
|
||||||
|
freePercent: { gte: 15 },
|
||||||
|
swapFreeBytes: { gte: 0 },
|
||||||
|
swapTotalBytes: { lte: 4294967296 },
|
||||||
|
swapUsagePercent: { lte: 50 },
|
||||||
|
swapUsedBytes: { lte: 2147483648 },
|
||||||
|
totalBytes: { equals: 17179869184 },
|
||||||
|
usagePercent: { lte: 85 },
|
||||||
|
usedBytes: { lte: 8589934592 },
|
||||||
|
usedPercent: { lte: 90 },
|
||||||
|
},
|
||||||
|
id: "mem-test",
|
||||||
|
memory: {},
|
||||||
|
type: "memory",
|
||||||
|
});
|
||||||
|
expect(issues).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("非 memory type 的 target 不校验", () => {
|
||||||
|
const issues = validate({ id: "other-test", type: "http" });
|
||||||
|
expect(issues).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -84,9 +84,22 @@ describe("CheckerRegistry", () => {
|
|||||||
"dns",
|
"dns",
|
||||||
"ws",
|
"ws",
|
||||||
"cpu",
|
"cpu",
|
||||||
|
"memory",
|
||||||
"custom",
|
"custom",
|
||||||
]);
|
]);
|
||||||
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "dns", "ws", "cpu"]);
|
expect(second.supportedTypes).toEqual([
|
||||||
|
"http",
|
||||||
|
"cmd",
|
||||||
|
"db",
|
||||||
|
"tcp",
|
||||||
|
"icmp",
|
||||||
|
"udp",
|
||||||
|
"llm",
|
||||||
|
"dns",
|
||||||
|
"ws",
|
||||||
|
"cpu",
|
||||||
|
"memory",
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
first.definitions.every((checker) => checker.schemas.authoring.config && checker.schemas.normalized.expect),
|
first.definitions.every((checker) => checker.schemas.authoring.config && checker.schemas.normalized.expect),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user