refactor: 重命名 command checker 为 cmd checker 并适配跨平台测试
将 type/configKey 从 "command" 统一为 "cmd",源码目录 runner/command/ → runner/cmd/, spec 目录 command-checker/ → cmd-checker/,测试全部改用 bun -e 替代 Unix 系统命令, 归档 cmd-checker-enhancement 变更并同步 delta spec 到主 spec。
This commit is contained in:
@@ -58,7 +58,7 @@ src/
|
|||||||
registry.ts CheckerRegistry 注册中心
|
registry.ts CheckerRegistry 注册中心
|
||||||
index.ts 注册入口(显式数组 + 循环注册)
|
index.ts 注册入口(显式数组 + 循环注册)
|
||||||
http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate/body)
|
http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate/body)
|
||||||
command/ Command Checker(自包含模块,含 types/schema/execute/expect/validate/text)
|
cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate/text)
|
||||||
shared/
|
shared/
|
||||||
api.ts 前后端共享 TypeScript 类型
|
api.ts 前后端共享 TypeScript 类型
|
||||||
web/ React 前端 Dashboard(通过 Bun HTML import 集成)
|
web/ React 前端 Dashboard(通过 Bun HTML import 集成)
|
||||||
@@ -84,7 +84,7 @@ openspec/ OpenSpec 变更与规格文档
|
|||||||
probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验)
|
probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验)
|
||||||
```
|
```
|
||||||
|
|
||||||
> **说明**:`runner/http/` 和 `runner/command/` 的完整文件结构见 [1.7.1 架构总览](#171-架构总览) 中的标准文件表。
|
> **说明**:`runner/http/` 和 `runner/cmd/` 的完整文件结构见 [1.7.1 架构总览](#171-架构总览) 中的标准文件表。
|
||||||
|
|
||||||
## 前后端边界
|
## 前后端边界
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ export function handleTrend(idStr: string, url: URL, store: ProbeStore, mode: Ru
|
|||||||
|
|
||||||
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||||||
|
|
||||||
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers`、`defaults.http.headers`、`expect.headers`、`command.env`。
|
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers`、`defaults.http.headers`、`expect.headers`、`cmd.env`。
|
||||||
|
|
||||||
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 `defaults`/root 路径。
|
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 `defaults`/root 路径。
|
||||||
|
|
||||||
@@ -266,11 +266,11 @@ checkerRegistry(单例)
|
|||||||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||||||
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||||
| `expect.ts` | Checker 专用断言函数 |
|
| `expect.ts` | Checker 专用断言函数 |
|
||||||
| `*.ts` | 其他 checker 专属逻辑(如 http/body.ts、command/text.ts) |
|
| `*.ts` | 其他 checker 专属逻辑(如 http/body.ts、cmd/text.ts) |
|
||||||
|
|
||||||
#### 1.7.2 步骤一:创建 Checker 目录与类型
|
#### 1.7.2 步骤一:创建 Checker 目录与类型
|
||||||
|
|
||||||
在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts`、`command/types.ts`):
|
在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts`、`cmd/types.ts`):
|
||||||
|
|
||||||
- `XxxTargetConfig` — YAML 原始配置类型
|
- `XxxTargetConfig` — YAML 原始配置类型
|
||||||
- `XxxExpectConfig` — expect 字段类型
|
- `XxxExpectConfig` — expect 字段类型
|
||||||
@@ -281,7 +281,7 @@ checkerRegistry(单例)
|
|||||||
|
|
||||||
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
||||||
|
|
||||||
在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / defaults / expect 三部分)。参考 `http/schema.ts`、`command/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
|
在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / defaults / expect 三部分)。参考 `http/schema.ts`、`cmd/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
|
||||||
|
|
||||||
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
||||||
|
|
||||||
@@ -296,11 +296,11 @@ checkerRegistry(单例)
|
|||||||
| `createPureOperatorSchema()` | 操作符对象 |
|
| `createPureOperatorSchema()` | 操作符对象 |
|
||||||
| `operatorProperties()` | 所有操作符字段的 Record |
|
| `operatorProperties()` | 所有操作符字段的 Record |
|
||||||
|
|
||||||
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`command.env`)可以开放任意键名。
|
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`cmd.env`)可以开放任意键名。
|
||||||
|
|
||||||
#### 1.7.4 步骤三:实现语义校验
|
#### 1.7.4 步骤三:实现语义校验
|
||||||
|
|
||||||
在 `src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则(参考 `http/validate.ts`、`command/validate.ts`)。函数签名统一为:
|
在 `src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则(参考 `http/validate.ts`、`cmd/validate.ts`)。函数签名统一为:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
|
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
|
||||||
@@ -315,7 +315,7 @@ export function validateTcpConfig(input: CheckerValidationInput): ConfigValidati
|
|||||||
|
|
||||||
#### 1.7.5 步骤四:实现 Checker 类
|
#### 1.7.5 步骤四:实现 Checker 类
|
||||||
|
|
||||||
在 `src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员(参考 `http/execute.ts`、`command/execute.ts`):
|
在 `src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员(参考 `http/execute.ts`、`cmd/execute.ts`):
|
||||||
|
|
||||||
```
|
```
|
||||||
TcpChecker implements Checker
|
TcpChecker implements Checker
|
||||||
@@ -356,7 +356,7 @@ TcpChecker implements Checker
|
|||||||
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||||
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 |
|
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 |
|
||||||
|
|
||||||
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `command/expect.ts`(checkExitCode)。
|
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `cmd/expect.ts`(checkExitCode)。
|
||||||
|
|
||||||
#### 1.7.6 步骤五:创建模块入口并注册
|
#### 1.7.6 步骤五:创建模块入口并注册
|
||||||
|
|
||||||
@@ -466,7 +466,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()`,Command 在 signal abort 时 `proc.kill()`
|
- **超时控制**:`ProbeEngine` 为每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abort;checker 必须使用 `CheckerContext.signal` 感知超时,HTTP 将 signal 传给 `fetch()`,Cmd 在 signal abort 时 `proc.kill()`
|
||||||
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `targetNameToId` 缓存 name→id 映射
|
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `targetNameToId` 缓存 name→id 映射
|
||||||
- **异常可观测**:`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` 清理过期数据
|
||||||
@@ -486,7 +486,7 @@ HttpChecker.execute → 收集观测(statusCode/headers)
|
|||||||
|
|
||||||
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;进入 body 前若已超过 `maxDurationMs`,直接返回 duration failure。
|
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;进入 body 前若已超过 `maxDurationMs`,直接返回 duration failure。
|
||||||
|
|
||||||
**Command 校验流程**:
|
**Cmd 校验流程**:
|
||||||
|
|
||||||
```
|
```
|
||||||
CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||||
@@ -502,7 +502,7 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
|||||||
- `css`:cheerio CSS 选择器 + 操作符比较
|
- `css`:cheerio CSS 选择器 + 操作符比较
|
||||||
- `xpath`:XPath 节点提取 + 操作符比较
|
- `xpath`:XPath 节点提取 + 操作符比较
|
||||||
|
|
||||||
**文本规则**(`runner/command/text.ts`):stdout/stderr 文本匹配,支持 `contains`、`match`(正则)、操作符比较
|
**文本规则**(`runner/cmd/text.ts`):stdout/stderr 文本匹配,支持 `contains`、`match`(正则)、操作符比较
|
||||||
|
|
||||||
**操作符**(`expect/operator.ts`):`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则,启动期通过 `expect/redos.ts` 拒绝 ReDoS 风险模式)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
**操作符**(`expect/operator.ts`):`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则,启动期通过 `expect/redos.ts` 拒绝 ReDoS 风险模式)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||||||
|
|
||||||
@@ -522,7 +522,7 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
|||||||
- `tests/server/checker/runner/shared/duration.test.ts` ↔ `src/server/checker/expect/duration.ts`
|
- `tests/server/checker/runner/shared/duration.test.ts` ↔ `src/server/checker/expect/duration.ts`
|
||||||
- `tests/server/checker/runner/shared/operator.test.ts` ↔ `src/server/checker/expect/operator.ts`
|
- `tests/server/checker/runner/shared/operator.test.ts` ↔ `src/server/checker/expect/operator.ts`
|
||||||
- `tests/server/checker/runner/shared/body.test.ts` ↔ `src/server/checker/runner/http/body.ts`
|
- `tests/server/checker/runner/shared/body.test.ts` ↔ `src/server/checker/runner/http/body.ts`
|
||||||
- `tests/server/checker/runner/shared/text.test.ts` ↔ `src/server/checker/runner/command/text.ts`
|
- `tests/server/checker/runner/shared/text.test.ts` ↔ `src/server/checker/runner/cmd/text.ts`
|
||||||
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
||||||
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
|
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
|
||||||
- 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })`
|
- 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })`
|
||||||
@@ -983,4 +983,4 @@ bun run verify # 完整验证(check + 构建)
|
|||||||
|
|
||||||
## 已知限制
|
## 已知限制
|
||||||
|
|
||||||
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。
|
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -43,7 +43,7 @@ defaults:
|
|||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
maxBodyBytes: "10MB"
|
maxBodyBytes: "10MB"
|
||||||
command:
|
cmd:
|
||||||
maxOutputBytes: "1MB"
|
maxOutputBytes: "1MB"
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
@@ -82,15 +82,15 @@ targets:
|
|||||||
path: "/html/body/h1/text()"
|
path: "/html/body/h1/text()"
|
||||||
equals: "Herman Melville - Moby-Dick"
|
equals: "Herman Melville - Moby-Dick"
|
||||||
|
|
||||||
- name: "Nginx 进程检查"
|
- name: "Bun 脚本检查"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "pgrep"
|
exec: "bun"
|
||||||
args: ["nginx"]
|
args: ["-e", "console.log('ok')"]
|
||||||
expect:
|
expect:
|
||||||
exitCode: [0]
|
exitCode: [0]
|
||||||
stdout:
|
stdout:
|
||||||
- match: "\\d+"
|
- contains: "ok"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置说明
|
### 配置说明
|
||||||
@@ -109,19 +109,19 @@ targets:
|
|||||||
- `method`: HTTP 方法,默认 `GET`,必须使用大写枚举值,支持 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`、`OPTIONS`
|
- `method`: HTTP 方法,默认 `GET`,必须使用大写枚举值,支持 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`、`OPTIONS`
|
||||||
- `maxBodyBytes`: 响应体最大字节数,默认 `100MB`
|
- `maxBodyBytes`: 响应体最大字节数,默认 `100MB`
|
||||||
- `headers`: 默认请求头(target 中的 headers 会合并覆盖 defaults 中的同名头)
|
- `headers`: 默认请求头(target 中的 headers 会合并覆盖 defaults 中的同名头)
|
||||||
- `command`: Command 类型默认值
|
- `cmd`: Cmd 类型默认值
|
||||||
- `maxOutputBytes`: 输出最大字节数,默认 `100MB`
|
- `maxOutputBytes`: 输出最大字节数,默认 `100MB`
|
||||||
- `cwd`: 默认工作目录(相对于配置文件所在目录解析,默认 `.`)
|
- `cwd`: 默认工作目录(相对于配置文件所在目录解析,默认 `.`)
|
||||||
- **targets**: 拨测目标列表(必填)
|
- **targets**: 拨测目标列表(必填)
|
||||||
- `name`: 目标名称(必填,唯一)
|
- `name`: 目标名称(必填,唯一)
|
||||||
- `type`: 目标类型,`http` 或 `command`(必填)
|
- `type`: 目标类型,`http` 或 `cmd`(必填)
|
||||||
- `group`: 分组名称(可选,默认 `"default"`)
|
- `group`: 分组名称(可选,默认 `"default"`)
|
||||||
- `http`: HTTP 拨测配置(type 为 http 时必填)
|
- `http`: HTTP 拨测配置(type 为 http 时必填)
|
||||||
- `url`: 目标 URL
|
- `url`: 目标 URL
|
||||||
- `method`、`headers`、`body`: 请求参数(`headers` 会与 `defaults.http.headers` 合并,target 优先)
|
- `method`、`headers`、`body`: 请求参数(`headers` 会与 `defaults.http.headers` 合并,target 优先)
|
||||||
- `ignoreSSL`: 是否忽略 HTTPS 证书校验,默认 `false`,用于自签名或私有证书服务
|
- `ignoreSSL`: 是否忽略 HTTPS 证书校验,默认 `false`,用于自签名或私有证书服务
|
||||||
- `maxRedirects`: 最大重定向跟随次数,默认 `0`(不跟随重定向)
|
- `maxRedirects`: 最大重定向跟随次数,默认 `0`(不跟随重定向)
|
||||||
- `command`: 命令行拨测配置(type 为 command 时必填)
|
- `cmd`: 命令行拨测配置(type 为 cmd 时必填)
|
||||||
- `exec`: 可执行文件名或路径
|
- `exec`: 可执行文件名或路径
|
||||||
- `args`: 命令行参数列表
|
- `args`: 命令行参数列表
|
||||||
- `env`: 环境变量覆盖(可选,继承进程环境变量并合并覆盖)
|
- `env`: 环境变量覆盖(可选,继承进程环境变量并合并覆盖)
|
||||||
@@ -129,11 +129,11 @@ targets:
|
|||||||
- `interval`、`timeout`: 覆盖全局默认值
|
- `interval`、`timeout`: 覆盖全局默认值
|
||||||
- `expect`: 期望校验
|
- `expect`: 期望校验
|
||||||
- `status`: 可接受的状态码列表(HTTP),支持精确状态码和范围模式(如 `"2xx"`)混合配置;未指定时默认 `[200]`
|
- `status`: 可接受的状态码列表(HTTP),支持精确状态码和范围模式(如 `"2xx"`)混合配置;未指定时默认 `[200]`
|
||||||
- `exitCode`: 可接受的退出码列表(Command);未指定时不校验退出码
|
- `exitCode`: 可接受的退出码列表(Cmd);未指定时不校验退出码
|
||||||
- `headers`: 响应头校验(HTTP,支持字符串精确匹配或操作符对象)
|
- `headers`: 响应头校验(HTTP,支持字符串精确匹配或操作符对象)
|
||||||
- `maxDurationMs`: 最大耗时阈值(毫秒)
|
- `maxDurationMs`: 最大耗时阈值(毫秒)
|
||||||
- HTTP:覆盖完整执行(含重定向、响应体读取和 expect 校验)
|
- HTTP:覆盖完整执行(含重定向、响应体读取和 expect 校验)
|
||||||
- Command:覆盖命令执行耗时(含 stdout/stderr 读取)
|
- Cmd:覆盖命令执行耗时(含 stdout/stderr 读取)
|
||||||
- `body`: HTTP 响应体校验(数组,可组合使用)
|
- `body`: HTTP 响应体校验(数组,可组合使用)
|
||||||
- `contains`: 响应体包含的文本
|
- `contains`: 响应体包含的文本
|
||||||
- `regex`: 响应体匹配的正则表达式(启动期会拒绝嵌套量词等存在 ReDoS 风险的模式)
|
- `regex`: 响应体匹配的正则表达式(启动期会拒绝嵌套量词等存在 ReDoS 风险的模式)
|
||||||
@@ -147,14 +147,14 @@ targets:
|
|||||||
- `xpath`: XPath 提取 XML/HTML 节点比较
|
- `xpath`: XPath 提取 XML/HTML 节点比较
|
||||||
- `path`: XPath 表达式(必填,如 `/html/body/h1/text()`)
|
- `path`: XPath 表达式(必填,如 `/html/body/h1/text()`)
|
||||||
- 比较操作符(可选,无操作符时仅检查节点是否存在)
|
- 比较操作符(可选,无操作符时仅检查节点是否存在)
|
||||||
- `stdout` / `stderr`: Command 输出校验(数组,每项为一个操作符对象)
|
- `stdout` / `stderr`: Cmd 输出校验(数组,每项为一个操作符对象)
|
||||||
- 比较操作符:`equals`(默认)、`contains`、`match`(正则,启动期会拒绝存在 ReDoS 风险的模式)、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`
|
- 比较操作符:`equals`(默认)、`contains`、`match`(正则,启动期会拒绝存在 ReDoS 风险的模式)、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`
|
||||||
|
|
||||||
大小说明:`maxBodyBytes` 和 `maxOutputBytes` 支持单位 `KB`、`MB`、`GB`,也可直接使用数字(非负安全整数字节数)。
|
大小说明:`maxBodyBytes` 和 `maxOutputBytes` 支持单位 `KB`、`MB`、`GB`,也可直接使用数字(非负安全整数字节数)。
|
||||||
|
|
||||||
配置校验:系统启动时会先用 TypeBox 生成的 JSON Schema 契约校验字段类型、必填字段、枚举、数组/对象形状和未知字段,再执行语义 validator 校验 target name 唯一性、URL、正则、JSONPath、XPath、size/duration 解析等规则。非法配置会阻止启动并输出中文错误信息。
|
配置校验:系统启动时会先用 TypeBox 生成的 JSON Schema 契约校验字段类型、必填字段、枚举、数组/对象形状和未知字段,再执行语义 validator 校验 target name 唯一性、URL、正则、JSONPath、XPath、size/duration 解析等规则。非法配置会阻止启动并输出中文错误信息。
|
||||||
|
|
||||||
未知字段:除 `http.headers`、`defaults.http.headers`、`expect.headers`、`command.env` 等动态键值表外,未知字段会导致启动失败。配置备注请使用 YAML 注释,不要添加 `note`、`comment` 等未声明字段。
|
未知字段:除 `http.headers`、`defaults.http.headers`、`expect.headers`、`cmd.env` 等动态键值表外,未知字段会导致启动失败。配置备注请使用 YAML 注释,不要添加 `note`、`comment` 等未声明字段。
|
||||||
|
|
||||||
JSON Schema:仓库根目录导出 `probe-config.schema.json`,可在 YAML 文件顶部添加 `# yaml-language-server: $schema=./probe-config.schema.json` 获取编辑器提示和静态校验。该 schema 由运行期契约 fragments 生成,提交前可用 `bun run schema:check` 检查同步。
|
JSON Schema:仓库根目录导出 `probe-config.schema.json`,可在 YAML 文件顶部添加 `# yaml-language-server: $schema=./probe-config.schema.json` 获取编辑器提示和静态校验。该 schema 由运行期契约 fragments 生成,提交前可用 `bun run schema:check` 检查同步。
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ JSON Schema:仓库根目录导出 `probe-config.schema.json`,可在 YAML 文
|
|||||||
|
|
||||||
**MetaResponse**: `checkerTypes`(已注册 checker 类型标识符列表)
|
**MetaResponse**: `checkerTypes`(已注册 checker 类型标识符列表)
|
||||||
|
|
||||||
**TargetStatus**: `id`、`name`、`type`(checker 类型,如 http/command)、`target`(URL 或命令摘要)、`group`、`interval`、`latestCheck`、`stats`、`recentSamples`
|
**TargetStatus**: `id`、`name`、`type`(checker 类型,如 http/cmd)、`target`(URL 或命令摘要)、`group`、`interval`、`latestCheck`、`stats`、`recentSamples`
|
||||||
|
|
||||||
**RecentSample**: `timestamp`、`durationMs`、`up`
|
**RecentSample**: `timestamp`、`durationMs`、`up`
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ CLI 只接受一个参数:YAML 配置文件路径。
|
|||||||
|
|
||||||
## 目标状态判定
|
## 目标状态判定
|
||||||
|
|
||||||
单层判定模型,适用于 HTTP 和 Command 两种类型:
|
单层判定模型,适用于 HTTP 和 Cmd 两种类型:
|
||||||
|
|
||||||
- **matched**: 是否符合 expect 规则(HTTP 未指定 `expect.status` 时默认检查 `[200]`)
|
- **matched**: 是否符合 expect 规则(HTTP 未指定 `expect.status` 时默认检查 `[200]`)
|
||||||
- **UP** = matched
|
- **UP** = matched
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
schema: spec-driven
|
|
||||||
created: 2026-05-13
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
## Context
|
|
||||||
|
|
||||||
当前 command checker 使用 `"command"` 作为 type 和 configKey,对应源码目录 `src/server/checker/runner/command/`、测试目录 `tests/server/checker/runner/command/`、spec 目录 `openspec/specs/command-checker/`。
|
|
||||||
|
|
||||||
测试中使用了 `true`、`false`、`sleep`、`bash`、`yes | head` 等 Unix 系统命令,在纯 Windows 环境(无 Git Bash)下无法运行。probes.example.yaml 中的示例命令(`uname -a`、`ls /tmp`、`date`)同样不跨平台。
|
|
||||||
|
|
||||||
项目未上线,无向前兼容负担。
|
|
||||||
|
|
||||||
## Goals / Non-Goals
|
|
||||||
|
|
||||||
**Goals:**
|
|
||||||
|
|
||||||
- 将 type/configKey 从 `"command"` 统一重命名为 `"cmd"`,包括源码目录、测试目录、spec 目录、YAML 配置键名
|
|
||||||
- 测试改用 `bun -e "..."` 替代系统命令,确保 Windows/macOS/Linux 三平台通过
|
|
||||||
- probes.example.yaml 提供跨平台示例
|
|
||||||
|
|
||||||
**Non-Goals:**
|
|
||||||
|
|
||||||
- 不加 shell 模式(现有 exec + args 已覆盖所有 shell 场景)
|
|
||||||
- 不加重试机制(失败是拨测指标)
|
|
||||||
- 不精简 resolve() 中 intervalMs/timeoutMs(收益小,接口改动大)
|
|
||||||
- 不加 successExitCodes 别名(已有 expect.exitCode)
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
### D1: type 与 configKey 统一为 `cmd`
|
|
||||||
|
|
||||||
YAML 配置形态变为:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
defaults:
|
|
||||||
cmd:
|
|
||||||
maxOutputBytes: "100MB"
|
|
||||||
|
|
||||||
targets:
|
|
||||||
- name: "test"
|
|
||||||
type: cmd
|
|
||||||
cmd:
|
|
||||||
exec: "bun"
|
|
||||||
args: ["-e", "console.log('hello')"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**理由:** `cmd` 简洁,且 type 与 configKey 保持一致(与 HTTP checker 的 `http`/`http` 对称)。
|
|
||||||
|
|
||||||
**替代方案:** 只改 type 不改 configKey → 会出现 `type: cmd` + `command: {...}` 的不一致,否决。
|
|
||||||
|
|
||||||
### D2: 内部属性名统一为 `cmd`
|
|
||||||
|
|
||||||
`ResolvedCommandTarget` 接口中的 `command` 属性名也改为 `cmd`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before
|
|
||||||
interface ResolvedCommandTarget {
|
|
||||||
command: ResolvedCommandConfig;
|
|
||||||
type: "command";
|
|
||||||
}
|
|
||||||
// t.command.exec
|
|
||||||
|
|
||||||
// After
|
|
||||||
interface ResolvedCommandTarget {
|
|
||||||
cmd: ResolvedCommandConfig;
|
|
||||||
type: "cmd";
|
|
||||||
}
|
|
||||||
// t.cmd.exec
|
|
||||||
```
|
|
||||||
|
|
||||||
**理由:** 内外一致,避免 configKey 是 `cmd` 但内部属性是 `command` 的割裂。
|
|
||||||
|
|
||||||
### D3: 源码目录重命名 `runner/command/` → `runner/cmd/`
|
|
||||||
|
|
||||||
所有 import 路径同步更新。测试目录 `tests/server/checker/runner/command/` → `tests/server/checker/runner/cmd/`。
|
|
||||||
|
|
||||||
**理由:** 目录名与 type/configKey 保持一致,降低认知负担。
|
|
||||||
|
|
||||||
### D3: 跨平台测试命令替换表
|
|
||||||
|
|
||||||
| 原命令 | 替换为 |
|
|
||||||
|---|---|
|
|
||||||
| `true` | `bun -e "process.exit(0)"` |
|
|
||||||
| `false` | `bun -e "process.exit(1)"` |
|
|
||||||
| `echo hello` | `bun -e "console.log('hello')"` |
|
|
||||||
| `sleep 10` | `bun -e "await Bun.sleep(10000)"` |
|
|
||||||
| `bash -c "echo error >&2"` | `bun -e "process.stderr.write('error\n')"` |
|
|
||||||
| `bash -c "yes \| head -1000"` | `bun -e "process.stdout.write('y\n'.repeat(1000))"` |
|
|
||||||
|
|
||||||
**理由:** `bun` 是项目唯一运行时依赖,三平台均可用,无需额外安装。
|
|
||||||
|
|
||||||
### D4: probes.example.yaml 示例策略
|
|
||||||
|
|
||||||
示例命令改用 `bun -e "..."` 或跨平台命令(如 `bun --version`),不再使用 `uname`、`ls /tmp` 等 Unix 专属命令。
|
|
||||||
|
|
||||||
### D5: spec 目录重命名
|
|
||||||
|
|
||||||
`openspec/specs/command-checker/` → `openspec/specs/cmd-checker/`,与 type 名称对齐。
|
|
||||||
|
|
||||||
### D6: 不加 shell 模式
|
|
||||||
|
|
||||||
用户需要管道/重定向时,用现有参数即可:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
cmd:
|
|
||||||
exec: "/bin/bash"
|
|
||||||
args: ["-c", "df -h | grep /dev/sda1"]
|
|
||||||
```
|
|
||||||
|
|
||||||
shell 模式本质是语法糖——内部仍然是 `Bun.spawn([shell, "-c", exec])`。增加代码复杂度(shell 检测、参数推断、互斥校验)但收益有限。
|
|
||||||
|
|
||||||
## Risks / Trade-offs
|
|
||||||
|
|
||||||
- [全量重命名可能遗漏引用] → 通过全局搜索 `"command"` 字面量确保无遗漏,CI 类型检查兜底
|
|
||||||
- [测试中 `bun -e` 启动开销比原生命令大] → 拨测场景不敏感,测试可接受毫秒级差异
|
|
||||||
- [probes.example.yaml 示例不如 Unix 命令直观] → 加注释说明用途,保持可读性
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
## Why
|
|
||||||
|
|
||||||
`command` 作为 checker type 名称过长,且测试依赖 Unix 系统命令导致 Windows 环境无法运行。需要统一重命名为 `cmd` 并实现跨平台测试适配。
|
|
||||||
|
|
||||||
## What Changes
|
|
||||||
|
|
||||||
- **BREAKING** type 字面量 `"command"` → `"cmd"`,configKey `"command"` → `"cmd"`
|
|
||||||
- **BREAKING** YAML 配置中 `type: command` → `type: cmd`,`command:` 块 → `cmd:` 块
|
|
||||||
- **BREAKING** `defaults.command` → `defaults.cmd`
|
|
||||||
- 源码目录 `runner/command/` → `runner/cmd/`
|
|
||||||
- spec 目录 `command-checker/` → `cmd-checker/`
|
|
||||||
- 测试全部改用 `bun -e "..."` 替代系统命令(true/false/sleep/bash)
|
|
||||||
- probes.example.yaml 更新为跨平台示例
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
|
|
||||||
### New Capabilities
|
|
||||||
|
|
||||||
(无)
|
|
||||||
|
|
||||||
### Modified Capabilities
|
|
||||||
|
|
||||||
- `probe-config`: `type: command` → `type: cmd`,`command` 分组 → `cmd` 分组,`defaults.command` → `defaults.cmd`,所有校验中的 `"command"` 字面量更新
|
|
||||||
- `command-checker`: type/configKey 重命名为 `cmd`,spec 目录重命名为 `cmd-checker`
|
|
||||||
- `checker-runner-abstraction`: registry 注册的 type 从 `"command"` 变为 `"cmd"`,`supportedTypes` 返回 `["http", "cmd"]`
|
|
||||||
- `windows-test-compat`: 测试命令全面改用 `bun -e "..."`,probes.example.yaml 使用跨平台示例
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
|
|
||||||
- 后端:`src/server/checker/runner/command/` 整个目录重命名及内部所有 `"command"` 字面量
|
|
||||||
- 配置:probes.example.yaml、probe-config.schema.json 中的 type 枚举和分组名
|
|
||||||
- 测试:`tests/server/checker/runner/command/` 目录重命名及测试命令替换
|
|
||||||
- 前端:无影响(动态显示 type 值)
|
|
||||||
- 数据库:stored_targets.type 列值变更(项目未上线,无迁移负担)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: CheckerRegistry 注册中心
|
|
||||||
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)`、`get(type)` 和 `supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。
|
|
||||||
|
|
||||||
#### Scenario: 查询支持的 type 列表
|
|
||||||
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
|
|
||||||
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`(按注册顺序)
|
|
||||||
|
|
||||||
### Requirement: Command checker 提供契约片段
|
|
||||||
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 defaults 分组、target 领域分组和 expect 分组。
|
|
||||||
|
|
||||||
#### Scenario: Cmd checker 提供契约片段
|
|
||||||
- **WHEN** Cmd checker 被注册
|
|
||||||
- **THEN** registry SHALL 能提供 Cmd defaults、Cmd target 和 Cmd expect 的 TypeBox 契约片段
|
|
||||||
|
|
||||||
### Requirement: 配置解析通过 registry 委托 checker
|
|
||||||
系统 SHALL 在 `config-loader.ts` 的配置加载流程中通过 `checkerRegistry` 发现已注册 checker,组合公共 TypeBox 契约与 checker 契约,并将 checker 专属语义校验和解析委托给对应 checker。
|
|
||||||
|
|
||||||
#### Scenario: 配置解析委托 checker
|
|
||||||
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
|
|
||||||
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker,并委托该 checker 执行语义校验和 resolve
|
|
||||||
|
|
||||||
### Requirement: Command text 断言位于 Cmd 目录
|
|
||||||
系统 SHALL 在 checker 专用目录中提供 text 断言函数。
|
|
||||||
|
|
||||||
#### Scenario: Command text 断言位于 Cmd 目录
|
|
||||||
- **WHEN** Cmd checker 需要对 stdout/stderr 执行文本规则校验
|
|
||||||
- **THEN** SHALL 调用 `runner/cmd/text.ts` 中的 `checkTextRules(text, rules, phase)`
|
|
||||||
|
|
||||||
### Requirement: Command 专用 expect
|
|
||||||
系统 SHALL 在 checker 专用目录中提供 exitCode 断言函数。
|
|
||||||
|
|
||||||
#### Scenario: Command 专用 expect
|
|
||||||
- **WHEN** Cmd checker 需要校验退出码
|
|
||||||
- **THEN** SHALL 调用 `runner/cmd/expect.ts` 中的 `checkExitCode()`
|
|
||||||
|
|
||||||
### Requirement: Checker 接口定义
|
|
||||||
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type`、`configKey`、TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize` 成员。
|
|
||||||
|
|
||||||
#### Scenario: type 与 configKey 默认一致
|
|
||||||
- **WHEN** checker 定义 `type: "cmd"`
|
|
||||||
- **THEN** checker 的 `configKey` SHALL 默认使用 `"cmd"`,对应 target 的 `cmd` 分组和 defaults.cmd 分组
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: command target 配置
|
|
||||||
系统 SHALL 支持 `type: cmd` 的 target 配置,通过 `cmd.exec` 和 `cmd.args` 描述本地命令,并使用 cmd 专用字段配置工作目录、环境变量和输出限制。
|
|
||||||
|
|
||||||
#### Scenario: 解析 cmd target
|
|
||||||
- **WHEN** YAML 中 target 配置 `type: cmd`、`cmd.exec: "pgrep"` 和 `cmd.args: ["nginx"]`
|
|
||||||
- **THEN** 系统 SHALL 将其解析为 cmd checker,并保留 exec、args、cwd、env、maxOutputBytes、interval、timeout 和 expect 配置
|
|
||||||
|
|
||||||
#### Scenario: cmd target 缺少 exec
|
|
||||||
- **WHEN** YAML 中 target 配置 `type: cmd` 但缺少 `cmd.exec`
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 cmd.exec 字段
|
|
||||||
|
|
||||||
#### Scenario: cwd 相对配置文件目录解析
|
|
||||||
- **WHEN** cmd target 配置 `cmd.cwd: "scripts"` 且配置文件位于 `/opt/checker/probes.yaml`
|
|
||||||
- **THEN** 系统 SHALL 将 cwd 解析为 `/opt/checker/scripts`
|
|
||||||
|
|
||||||
#### Scenario: cmd 不使用 shell
|
|
||||||
- **WHEN** cmd target 配置 `exec` 和 `args`
|
|
||||||
- **THEN** 系统 MUST 直接执行该程序和参数,不通过 shell 解释整段命令字符串
|
|
||||||
|
|
||||||
#### Scenario: env 默认继承并允许覆盖
|
|
||||||
- **WHEN** cmd target 配置 `cmd.env: {LANG: "C"}` 且当前进程环境包含 `PATH`
|
|
||||||
- **THEN** 系统 SHALL 继承当前进程的全部环境变量,并将 `LANG` 覆盖为 `"C"`
|
|
||||||
|
|
||||||
### Requirement: command checker 执行
|
|
||||||
系统 SHALL 按 cmd target 配置执行本地命令,记录执行耗时、退出码、stdout 和 stderr,并在执行失败时产生结构化错误信息。
|
|
||||||
|
|
||||||
### Requirement: command expect 校验
|
|
||||||
系统 SHALL 支持 cmd 专用 expect,包括 `exitCode`、`stdout` 和 `stderr`,并按 exitCode、duration、stdout、stderr 的阶段顺序快速失败。
|
|
||||||
|
|
||||||
### Requirement: command checker 启动期配置校验
|
|
||||||
系统 SHALL 在启动期对 cmd checker 的配置契约和语义执行严格校验。Cmd target 的 `cmd` 分组 SHALL 只允许 `exec`、`args`、`cwd`、`env`、`maxOutputBytes` 字段;Cmd expect SHALL 只允许 `exitCode`、`maxDurationMs`、`stdout`、`stderr` 字段。未知字段、非法类型和不可编译正则 MUST 导致启动期配置错误。`expect.exitCode` SHALL 保留原有有限整数数组语义,不限制到特定平台范围。
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: YAML 配置文件格式
|
|
||||||
target MUST 使用 `type` 字段声明 checker 类型,HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组。
|
|
||||||
|
|
||||||
#### Scenario: 最简 command 配置文件解析
|
|
||||||
- **WHEN** 系统读取只包含一个 `type: cmd` target 和 `cmd.exec` 的 YAML 配置文件
|
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB)
|
|
||||||
|
|
||||||
### Requirement: 配置校验
|
|
||||||
|
|
||||||
#### Scenario: command target 缺少 exec
|
|
||||||
- **WHEN** YAML 中某个 target 配置 `type: cmd` 但缺少 `cmd.exec`
|
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 cmd.exec 字段
|
|
||||||
|
|
||||||
#### Scenario: 动态 env 字段允许
|
|
||||||
- **WHEN** YAML 中 `cmd.env` 包含任意环境变量名称,且对应值为字符串
|
|
||||||
- **THEN** 系统 SHALL 接受这些动态 env 名称
|
|
||||||
|
|
||||||
### Requirement: expect 配置增强
|
|
||||||
系统 SHALL 支持 typed target 的领域专用 expect 配置,包括 HTTP 的 `status`、`headers`、`body` 和 cmd 的 `exitCode`、`stdout`、`stderr`。
|
|
||||||
|
|
||||||
#### Scenario: 解析 command expect 配置
|
|
||||||
- **WHEN** YAML 配置文件中 cmd target 的 expect 包含 exitCode、stdout 和 stderr 规则数组
|
|
||||||
- **THEN** 系统 SHALL 正确解析并存储为 cmd target 的 expect 字段
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: 命令检测器测试 SHALL 使用跨平台命令
|
|
||||||
命令检测器的测试 SHALL 使用 `bun -e` 脚本替代所有系统命令(包括 `true`、`false`、`sleep`、`bash`、`echo`、`yes | head`),确保测试在 Windows、macOS、Linux 三平台上行为一致。
|
|
||||||
|
|
||||||
#### Scenario: 进程退出码 0
|
|
||||||
- **WHEN** 测试需要一个正常退出的命令
|
|
||||||
- **THEN** 测试 SHALL 使用 `bun -e "process.exit(0)"` 替代 `true`
|
|
||||||
|
|
||||||
#### Scenario: 进程退出码非零
|
|
||||||
- **WHEN** 测试需要一个失败退出的命令
|
|
||||||
- **THEN** 测试 SHALL 使用 `bun -e "process.exit(1)"` 替代 `false`
|
|
||||||
|
|
||||||
#### Scenario: stdout 输出
|
|
||||||
- **WHEN** 测试需要一个输出文本到 stdout 的命令
|
|
||||||
- **THEN** 测试 SHALL 使用 `bun -e "console.log('text')"` 替代 `echo text`
|
|
||||||
|
|
||||||
#### Scenario: stderr 输出
|
|
||||||
- **WHEN** 测试需要一个输出文本到 stderr 的命令
|
|
||||||
- **THEN** 测试 SHALL 使用 `bun -e "process.stderr.write('error\n')"` 替代 `bash -c "echo error >&2"`
|
|
||||||
|
|
||||||
#### Scenario: 长时间运行命令
|
|
||||||
- **WHEN** 测试需要一个超时场景的长时间运行命令
|
|
||||||
- **THEN** 测试 SHALL 使用 `bun -e "await Bun.sleep(10000)"` 替代 `sleep 10`
|
|
||||||
|
|
||||||
#### Scenario: 大量输出
|
|
||||||
- **WHEN** 测试需要一个产生大量输出的命令
|
|
||||||
- **THEN** 测试 SHALL 使用 `bun -e "process.stdout.write('y\n'.repeat(N))"` 替代 `bash -c "yes | head -N"`
|
|
||||||
|
|
||||||
#### Scenario: 验证非 shell 模式下特殊字符不被展开
|
|
||||||
- **WHEN** 通过 `Bun.spawn` 执行 `bun -e "console.log('*')"` 并检查 stdout 包含 `*`
|
|
||||||
- **THEN** 测试 SHALL 在 Windows、macOS 和 Linux 上均返回 `matched: true`
|
|
||||||
|
|
||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: probes.example.yaml 使用跨平台示例
|
|
||||||
probes.example.yaml 中的 cmd 类型示例 SHALL 使用跨平台命令(如 `bun -e "..."`、`bun --version`),不使用 Unix 专属命令(如 `uname`、`ls /tmp`、`date`)。
|
|
||||||
|
|
||||||
#### Scenario: 示例命令跨平台可执行
|
|
||||||
- **WHEN** 用户在 Windows、macOS 或 Linux 上直接使用 probes.example.yaml 中的 cmd 示例
|
|
||||||
- **THEN** 所有 cmd 示例 SHALL 能正常执行,不依赖平台特定命令
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
## 1. 源码目录重命名
|
|
||||||
|
|
||||||
- [ ] 1.1 重命名 `src/server/checker/runner/command/` → `src/server/checker/runner/cmd/`,更新目录内所有文件的 type/configKey 字面量为 `"cmd"`
|
|
||||||
- [ ] 1.2 重命名 `tests/server/checker/runner/command/` → `tests/server/checker/runner/cmd/`
|
|
||||||
- [ ] 1.3 更新所有 import 路径中的 `runner/command` → `runner/cmd`(包括 runner/index.ts 等)
|
|
||||||
|
|
||||||
## 2. 类型与配置重命名
|
|
||||||
|
|
||||||
- [ ] 2.1 更新 `src/server/checker/runner/cmd/execute.ts` 中 `type = "cmd"`、`configKey = "cmd"`、`context.defaults["cmd"]`、所有 `t.command.xxx` → `t.cmd.xxx`
|
|
||||||
- [ ] 2.2 更新 `src/server/checker/runner/cmd/types.ts` 中 `ResolvedCommandTarget.command` 属性名改为 `cmd`、`type: "command"` 改为 `type: "cmd"`
|
|
||||||
- [ ] 2.3 更新 `src/server/checker/runner/cmd/validate.ts` 中所有 `"command"` → `"cmd"` 字面量
|
|
||||||
- [ ] 2.4 更新 `src/server/checker/runner/cmd/schema.ts` 中 TypeBox 契约的分组名(如有 `"command"` 字面量)
|
|
||||||
- [ ] 2.5 更新 `probes.example.yaml` 中 `type: command` → `type: cmd`、`command:` → `cmd:`,示例命令改为跨平台命令
|
|
||||||
- [ ] 2.6 更新 `tests/server/app.test.ts`、`tests/server/bootstrap.test.ts`、`tests/server/checker/config-loader.test.ts`、`tests/server/checker/engine.test.ts` 中所有 `"command"` 字面量为 `"cmd"`
|
|
||||||
- [ ] 2.7 重新生成 `probe-config.schema.json`(执行 schema 生成脚本或手动更新)
|
|
||||||
|
|
||||||
## 3. 跨平台测试改造
|
|
||||||
|
|
||||||
- [ ] 3.1 更新 `tests/server/checker/runner/cmd/runner.test.ts` 中所有系统命令为 `bun -e "..."` 形式
|
|
||||||
- [ ] 3.2 更新 `tests/server/checker/runner/cmd/expect.test.ts` 中所有系统命令为 `bun -e "..."` 形式
|
|
||||||
|
|
||||||
## 4. Spec 文档与质量保障
|
|
||||||
|
|
||||||
- [ ] 4.1 重命名 `openspec/specs/command-checker/` → `openspec/specs/cmd-checker/`,更新 spec 内容中的 `command` → `cmd`
|
|
||||||
- [ ] 4.2 执行完整测试套件 `bun test`,确保所有测试通过
|
|
||||||
- [ ] 4.3 执行类型检查 `bunx tsc --noEmit`,确保无类型错误
|
|
||||||
- [ ] 4.4 更新 README.md 中涉及 command checker 的描述和配置示例(包括 defaults.command 段、type 枚举、配置字段说明)
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
- **THEN** `isEqual(actual, expected)` SHALL 递归比较所有属性值,而非引用比较
|
- **THEN** `isEqual(actual, expected)` SHALL 递归比较所有属性值,而非引用比较
|
||||||
|
|
||||||
### Requirement: 使用 es-toolkit 进行错误类型判断
|
### Requirement: 使用 es-toolkit 进行错误类型判断
|
||||||
系统 SHALL 使用 es-toolkit 的 `isError` 替代 `error instanceof Error`,用于 HTTP runner 和 command runner 中的错误类型判断。
|
系统 SHALL 使用 es-toolkit 的 `isError` 替代 `error instanceof Error`,用于 HTTP runner 和 cmd runner 中的错误类型判断。
|
||||||
|
|
||||||
#### Scenario: Error 实例识别
|
#### Scenario: Error 实例识别
|
||||||
- **WHEN** 错误对象为 `new Error("msg")`
|
- **WHEN** 错误对象为 `new Error("msg")`
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
- **WHEN** 开发者查看 `src/server/checker/runner/http/` 目录
|
- **WHEN** 开发者查看 `src/server/checker/runner/http/` 目录
|
||||||
- **THEN** 该目录 SHALL 包含 `index.ts`、`types.ts`、`schema.ts`、`execute.ts`、`expect.ts`、`body.ts`、`validate.ts`
|
- **THEN** 该目录 SHALL 包含 `index.ts`、`types.ts`、`schema.ts`、`execute.ts`、`expect.ts`、`body.ts`、`validate.ts`
|
||||||
|
|
||||||
#### Scenario: Command checker 目录完整性
|
#### Scenario: Cmd checker 目录完整性
|
||||||
- **WHEN** 开发者查看 `src/server/checker/runner/command/` 目录
|
- **WHEN** 开发者查看 `src/server/checker/runner/cmd/` 目录
|
||||||
- **THEN** 该目录 SHALL 包含 `index.ts`、`types.ts`、`schema.ts`、`execute.ts`、`expect.ts`、`text.ts`、`validate.ts`
|
- **THEN** 该目录 SHALL 包含 `index.ts`、`types.ts`、`schema.ts`、`execute.ts`、`expect.ts`、`text.ts`、`validate.ts`
|
||||||
|
|
||||||
#### Scenario: 新增 checker 最小改动
|
#### Scenario: 新增 checker 最小改动
|
||||||
@@ -146,7 +146,7 @@ checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
|
|||||||
|
|
||||||
#### Scenario: DefaultsConfig 为宽松 base 形式
|
#### Scenario: DefaultsConfig 为宽松 base 形式
|
||||||
- **WHEN** 开发者查看顶层 `types.ts` 中的 `DefaultsConfig`
|
- **WHEN** 开发者查看顶层 `types.ts` 中的 `DefaultsConfig`
|
||||||
- **THEN** 该 interface SHALL 仅包含公共字段(`interval?`、`timeout?`)和 index signature(`[checkerKey: string]: unknown`),SHALL NOT 包含 `command?`、`http?` 等 checker 专属字段
|
- **THEN** 该 interface SHALL 仅包含公共字段(`interval?`、`timeout?`)和 index signature(`[checkerKey: string]: unknown`),SHALL NOT 包含 `cmd?`、`http?` 等 checker 专属字段
|
||||||
|
|
||||||
#### Scenario: 各 checker validate 自行 narrow defaults
|
#### Scenario: 各 checker validate 自行 narrow defaults
|
||||||
- **WHEN** checker 的 `validate()` 方法需要访问自身的 defaults 配置
|
- **WHEN** checker 的 `validate()` 方法需要访问自身的 defaults 配置
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
- **WHEN** HTTP checker 被注册
|
- **WHEN** HTTP checker 被注册
|
||||||
- **THEN** registry SHALL 能提供 HTTP defaults、HTTP target 和 HTTP expect 的 TypeBox 契约片段
|
- **THEN** registry SHALL 能提供 HTTP defaults、HTTP target 和 HTTP expect 的 TypeBox 契约片段
|
||||||
|
|
||||||
#### Scenario: Command checker 提供契约片段
|
#### Scenario: Cmd checker 提供契约片段
|
||||||
- **WHEN** Command checker 被注册
|
- **WHEN** Cmd checker 被注册
|
||||||
- **THEN** registry SHALL 能提供 Command defaults、Command target 和 Command expect 的 TypeBox 契约片段
|
- **THEN** registry SHALL 能提供 Cmd defaults、Cmd target 和 Cmd expect 的 TypeBox 契约片段
|
||||||
|
|
||||||
#### Scenario: 新 checker 只维护自身契约
|
#### Scenario: 新 checker 只维护自身契约
|
||||||
- **WHEN** 开发者新增一个 checker 类型
|
- **WHEN** 开发者新增一个 checker 类型
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
|
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
|
||||||
|
|
||||||
#### Scenario: 查询支持的 type 列表
|
#### Scenario: 查询支持的 type 列表
|
||||||
- **WHEN** 注册了 "http" 和 "command" 两个 checker 后查询 `registry.supportedTypes`
|
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
|
||||||
- **THEN** 返回的数组 SHALL 包含 `["http", "command"]`(按注册顺序)
|
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`(按注册顺序)
|
||||||
|
|
||||||
### Requirement: 引擎通过 registry 调度 checker
|
### Requirement: 引擎通过 registry 调度 checker
|
||||||
系统 SHALL 在 `ProbeEngine.runCheck()` 中通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
|
系统 SHALL 在 `ProbeEngine.runCheck()` 中通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
|
||||||
@@ -118,8 +118,8 @@
|
|||||||
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
|
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
|
||||||
|
|
||||||
#### Scenario: 配置解析委托 checker
|
#### Scenario: 配置解析委托 checker
|
||||||
- **WHEN** config-loader 解析一个 type 为 "command" 的 target
|
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
|
||||||
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("command")` 获取对应 checker,并委托该 checker 执行语义校验和 resolve
|
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker,并委托该 checker 执行语义校验和 resolve
|
||||||
|
|
||||||
#### Scenario: 通用字段校验保留在 config-loader
|
#### Scenario: 通用字段校验保留在 config-loader
|
||||||
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
|
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
|
||||||
@@ -163,26 +163,26 @@
|
|||||||
- **WHEN** HTTP checker 需要对响应体执行 contains/regex/json/css/xpath 规则校验
|
- **WHEN** HTTP checker 需要对响应体执行 contains/regex/json/css/xpath 规则校验
|
||||||
- **THEN** SHALL 调用 `runner/http/body.ts` 中的 `checkBodyExpect(body, rules)`
|
- **THEN** SHALL 调用 `runner/http/body.ts` 中的 `checkBodyExpect(body, rules)`
|
||||||
|
|
||||||
#### Scenario: Command text 断言位于 Command 目录
|
#### Scenario: Cmd text 断言位于 Cmd 目录
|
||||||
- **WHEN** Command checker 需要对 stdout/stderr 执行文本规则校验
|
- **WHEN** Cmd checker 需要对 stdout/stderr 执行文本规则校验
|
||||||
- **THEN** SHALL 调用 `runner/command/text.ts` 中的 `checkTextRules(text, rules, phase)`
|
- **THEN** SHALL 调用 `runner/cmd/text.ts` 中的 `checkTextRules(text, rules, phase)`
|
||||||
|
|
||||||
#### Scenario: HTTP 专用 expect
|
#### Scenario: HTTP 专用 expect
|
||||||
- **WHEN** HTTP checker 需要校验响应状态码和响应头
|
- **WHEN** HTTP checker 需要校验响应状态码和响应头
|
||||||
- **THEN** SHALL 调用 `runner/http/expect.ts` 中的 `checkStatus()` 和 `checkHeaders()`
|
- **THEN** SHALL 调用 `runner/http/expect.ts` 中的 `checkStatus()` 和 `checkHeaders()`
|
||||||
|
|
||||||
#### Scenario: Command 专用 expect
|
#### Scenario: Cmd 专用 expect
|
||||||
- **WHEN** Command checker 需要校验退出码
|
- **WHEN** Cmd checker 需要校验退出码
|
||||||
- **THEN** SHALL 调用 `runner/command/expect.ts` 中的 `checkExitCode()`
|
- **THEN** SHALL 调用 `runner/cmd/expect.ts` 中的 `checkExitCode()`
|
||||||
|
|
||||||
### Requirement: 超时控制由引擎注入 signal
|
### Requirement: 超时控制由引擎注入 signal
|
||||||
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController` 或 `setTimeout` 用于超时控制。仅 command checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
|
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController` 或 `setTimeout` 用于超时控制。仅 cmd checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
|
||||||
|
|
||||||
#### Scenario: HTTP checker 使用 signal
|
#### Scenario: HTTP checker 使用 signal
|
||||||
- **WHEN** HttpChecker 执行 HTTP 请求
|
- **WHEN** HttpChecker 执行 HTTP 请求
|
||||||
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()` 的 `signal` 选项,不自行创建 `AbortController`
|
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()` 的 `signal` 选项,不自行创建 `AbortController`
|
||||||
|
|
||||||
#### Scenario: Command checker 响应 signal
|
#### Scenario: Cmd checker 响应 signal
|
||||||
- **WHEN** CommandChecker 执行命令且 signal 被 abort
|
- **WHEN** CommandChecker 执行命令且 signal 被 abort
|
||||||
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
|
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自
|
|||||||
`shared/api.ts` 中 `CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`。
|
`shared/api.ts` 中 `CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`。
|
||||||
|
|
||||||
#### Scenario: phase 支持 checker 专用值
|
#### Scenario: phase 支持 checker 专用值
|
||||||
- **WHEN** command checker 在执行失败(spawn error)时生成 failure
|
- **WHEN** cmd checker 在执行失败(spawn error)时生成 failure
|
||||||
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
|
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
|
||||||
|
|
||||||
#### Scenario: 前端展示 phase 不依赖硬编码类型
|
#### Scenario: 前端展示 phase 不依赖硬编码类型
|
||||||
|
|||||||
141
openspec/specs/cmd-checker/spec.md
Normal file
141
openspec/specs/cmd-checker/spec.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
定义 Cmd 类型拨测目标:通过 `type: cmd` 配置执行本地命令(如进程检查、脚本健康检测),捕获 exit code、stdout、stderr,按 expect 规则校验并生成 matched 判定。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: cmd target 配置
|
||||||
|
系统 SHALL 支持 `type: cmd` 的 target 配置,通过 `cmd.exec` 和 `cmd.args` 描述本地命令,并使用 cmd 专用字段配置工作目录、环境变量和输出限制。
|
||||||
|
|
||||||
|
#### Scenario: 解析 cmd target
|
||||||
|
- **WHEN** YAML 中 target 配置 `type: cmd`、`cmd.exec: "pgrep"` 和 `cmd.args: ["nginx"]`
|
||||||
|
- **THEN** 系统 SHALL 将其解析为 cmd checker,并保留 exec、args、cwd、env、maxOutputBytes、interval、timeout 和 expect 配置
|
||||||
|
|
||||||
|
#### Scenario: cmd target 缺少 exec
|
||||||
|
- **WHEN** YAML 中 target 配置 `type: cmd` 但缺少 `cmd.exec`
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 cmd.exec 字段
|
||||||
|
|
||||||
|
#### Scenario: cwd 相对配置文件目录解析
|
||||||
|
- **WHEN** cmd target 配置 `cmd.cwd: "scripts"` 且配置文件位于 `/opt/checker/probes.yaml`
|
||||||
|
- **THEN** 系统 SHALL 将 cwd 解析为 `/opt/checker/scripts`
|
||||||
|
|
||||||
|
#### Scenario: cmd 不使用 shell
|
||||||
|
- **WHEN** cmd target 配置 `exec` 和 `args`
|
||||||
|
- **THEN** 系统 MUST 直接执行该程序和参数,不通过 shell 解释整段命令字符串
|
||||||
|
|
||||||
|
#### Scenario: env 默认继承并允许覆盖
|
||||||
|
- **WHEN** cmd target 配置 `cmd.env: {LANG: "C"}` 且当前进程环境包含 `PATH`
|
||||||
|
- **THEN** 系统 SHALL 继承当前进程的全部环境变量,并将 `LANG` 覆盖为 `"C"`
|
||||||
|
|
||||||
|
#### Scenario: 不支持 stdin
|
||||||
|
- **WHEN** cmd target 配置并执行命令
|
||||||
|
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
|
||||||
|
|
||||||
|
### Requirement: cmd checker 执行
|
||||||
|
系统 SHALL 按 cmd target 配置执行本地命令,记录执行耗时、退出码、stdout 和 stderr,并在执行失败时产生结构化错误信息。
|
||||||
|
|
||||||
|
#### Scenario: 命令正常退出
|
||||||
|
- **WHEN** cmd target 执行的进程正常退出且 exit code 为 0
|
||||||
|
- **THEN** 系统 SHALL 记录 `durationMs`、`statusDetail="exitCode=0"`,并进入 expect 校验
|
||||||
|
|
||||||
|
#### Scenario: 命令非零退出
|
||||||
|
- **WHEN** cmd target 执行的进程正常退出但 exit code 为 1
|
||||||
|
- **THEN** 系统 SHALL 记录 `statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
|
||||||
|
|
||||||
|
#### Scenario: 命令启动失败
|
||||||
|
- **WHEN** cmd target 的 exec 不存在或无法启动
|
||||||
|
- **THEN** 系统 SHALL 记录 `matched=false`,并在 failure 中写入 kind=`error` 和可读错误信息
|
||||||
|
|
||||||
|
#### Scenario: 命令超时
|
||||||
|
- **WHEN** cmd target 在 timeout 时间内未结束
|
||||||
|
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息
|
||||||
|
|
||||||
|
#### Scenario: 命令输出超限
|
||||||
|
- **WHEN** cmd target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
|
||||||
|
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息
|
||||||
|
|
||||||
|
### Requirement: cmd expect 校验
|
||||||
|
系统 SHALL 支持 cmd 专用 expect,包括 `exitCode`、`stdout` 和 `stderr`,并按 exitCode、duration、stdout、stderr 的阶段顺序快速失败。
|
||||||
|
|
||||||
|
#### Scenario: 默认 exitCode 成功语义
|
||||||
|
- **WHEN** cmd target 未显式配置 `expect.exitCode`
|
||||||
|
- **THEN** 系统 SHALL 使用默认 `expect.exitCode: [0]` 进行校验
|
||||||
|
|
||||||
|
#### Scenario: 显式 exitCode 校验
|
||||||
|
- **WHEN** cmd target 配置 `expect.exitCode: [0, 2]` 且实际 exit code 为 2
|
||||||
|
- **THEN** 系统 SHALL 判定 exitCode 阶段通过,并继续后续 expect 阶段
|
||||||
|
|
||||||
|
#### Scenario: exitCode 不匹配快速失败
|
||||||
|
- **WHEN** cmd target 配置 `expect.exitCode: [0]` 且实际 exit code 为 1
|
||||||
|
- **THEN** 系统 SHALL 立即返回 `matched=false`,并在 failure 中写入 phase=`exitCode`、path=`expect.exitCode`、expected 和 actual
|
||||||
|
|
||||||
|
#### Scenario: stdout 按配置顺序校验
|
||||||
|
- **WHEN** cmd target 配置 `expect.stdout` 为两个规则,第一条通过且第二条失败
|
||||||
|
- **THEN** 系统 SHALL 先执行第一条 stdout 规则,再执行第二条,并将 failure.path 指向失败的 `expect.stdout[1]`
|
||||||
|
|
||||||
|
#### Scenario: stderr 校验为空
|
||||||
|
- **WHEN** cmd target 配置 `expect.stderr: [{empty: true}]` 且实际 stderr 为空字符串
|
||||||
|
- **THEN** 系统 SHALL 判定 stderr 阶段通过
|
||||||
|
|
||||||
|
#### Scenario: stdout 失败后不检查 stderr
|
||||||
|
- **WHEN** cmd target 同时配置 stdout 和 stderr 规则,且 stdout 规则失败
|
||||||
|
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr 规则
|
||||||
|
|
||||||
|
### Requirement: cmd checker 启动期配置校验
|
||||||
|
系统 SHALL 在启动期对 cmd checker 的配置契约和语义执行严格校验。Cmd target 的 `cmd` 分组 SHALL 只允许 `exec`、`args`、`cwd`、`env`、`maxOutputBytes` 字段;Cmd expect SHALL 只允许 `exitCode`、`maxDurationMs`、`stdout`、`stderr` 字段。未知字段、非法类型和不可编译正则 MUST 导致启动期配置错误。`expect.exitCode` SHALL 保留原有有限整数数组语义,不限制到特定平台范围。
|
||||||
|
|
||||||
|
#### Scenario: cmd args 类型非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `cmd.args` 不是字符串数组
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 cmd.args 格式错误
|
||||||
|
|
||||||
|
#### Scenario: cmd cwd 类型非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `cmd.cwd` 不是字符串
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 cmd.cwd 必须为字符串
|
||||||
|
|
||||||
|
#### Scenario: cmd env 值类型非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `cmd.env`,且任一环境变量值不是字符串
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 cmd.env 对应变量值必须为字符串
|
||||||
|
|
||||||
|
#### Scenario: cmd maxOutputBytes 非法
|
||||||
|
- **WHEN** YAML 中 cmd target 或 defaults.cmd 配置的 `maxOutputBytes` 不是合法 size 值
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 maxOutputBytes 格式错误
|
||||||
|
|
||||||
|
#### Scenario: cmd 分组未知字段失败
|
||||||
|
- **WHEN** YAML 中 cmd target 的 `cmd` 分组包含 `shell: true` 等未知字段
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 cmd 分组包含未知字段
|
||||||
|
|
||||||
|
#### Scenario: cmd expect exitCode 类型非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.exitCode` 不是整数数组
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.exitCode 必须为整数数组
|
||||||
|
|
||||||
|
#### Scenario: cmd expect exitCode 不限制平台范围
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.exitCode` 为有限整数数组
|
||||||
|
- **THEN** 系统 SHALL 接受该数组,不额外限制为 0-255 等平台相关范围
|
||||||
|
|
||||||
|
#### Scenario: cmd expect maxDurationMs 非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.maxDurationMs` 不是非负有限数字
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxDurationMs 格式错误
|
||||||
|
|
||||||
|
#### Scenario: stdout 必须为规则数组
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.stdout` 但其值不是数组
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stdout 必须为数组
|
||||||
|
|
||||||
|
#### Scenario: stderr 必须为规则数组
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.stderr` 但其值不是数组
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stderr 必须为数组
|
||||||
|
|
||||||
|
#### Scenario: stdout text rule 空对象非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.stdout: [{}]`
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 stdout 规则必须包含至少一个合法 operator
|
||||||
|
|
||||||
|
#### Scenario: stderr text rule 未知字段非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.stderr: [{foo: "bar"}]`
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 stderr 规则包含未知 operator
|
||||||
|
|
||||||
|
#### Scenario: stdout match 正则非法
|
||||||
|
- **WHEN** YAML 中 cmd target 配置 `expect.stdout: [{match: "[invalid"}]`
|
||||||
|
- **THEN** 系统 SHALL 在启动期配置校验失败,而不是延迟到运行期抛错
|
||||||
|
|
||||||
|
#### Scenario: cmd expect 未知字段失败
|
||||||
|
- **WHEN** YAML 中 cmd target 的 expect 包含 `status: [200]` 或其他非 cmd expect 字段
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
## Purpose
|
|
||||||
|
|
||||||
定义 Command 类型拨测目标:通过 `type: command` 配置执行本地命令(如进程检查、脚本健康检测),捕获 exit code、stdout、stderr,按 expect 规则校验并生成 matched 判定。
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
### Requirement: command target 配置
|
|
||||||
系统 SHALL 支持 `type: command` 的 target 配置,通过 `command.exec` 和 `command.args` 描述本地命令,并使用 command 专用字段配置工作目录、环境变量和输出限制。
|
|
||||||
|
|
||||||
#### Scenario: 解析 command target
|
|
||||||
- **WHEN** YAML 中 target 配置 `type: command`、`command.exec: "pgrep"` 和 `command.args: ["nginx"]`
|
|
||||||
- **THEN** 系统 SHALL 将其解析为 command checker,并保留 exec、args、cwd、env、maxOutputBytes、interval、timeout 和 expect 配置
|
|
||||||
|
|
||||||
#### Scenario: command target 缺少 exec
|
|
||||||
- **WHEN** YAML 中 target 配置 `type: command` 但缺少 `command.exec`
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 command.exec 字段
|
|
||||||
|
|
||||||
#### Scenario: cwd 相对配置文件目录解析
|
|
||||||
- **WHEN** command target 配置 `command.cwd: "scripts"` 且配置文件位于 `/opt/checker/probes.yaml`
|
|
||||||
- **THEN** 系统 SHALL 将 cwd 解析为 `/opt/checker/scripts`
|
|
||||||
|
|
||||||
#### Scenario: command 不使用 shell
|
|
||||||
- **WHEN** command target 配置 `exec` 和 `args`
|
|
||||||
- **THEN** 系统 MUST 直接执行该程序和参数,不通过 shell 解释整段命令字符串
|
|
||||||
|
|
||||||
#### Scenario: env 默认继承并允许覆盖
|
|
||||||
- **WHEN** command target 配置 `command.env: {LANG: "C"}` 且当前进程环境包含 `PATH`
|
|
||||||
- **THEN** 系统 SHALL 继承当前进程的全部环境变量,并将 `LANG` 覆盖为 `"C"`
|
|
||||||
|
|
||||||
#### Scenario: 不支持 stdin
|
|
||||||
- **WHEN** command target 配置并执行命令
|
|
||||||
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
|
|
||||||
|
|
||||||
### Requirement: command checker 执行
|
|
||||||
系统 SHALL 按 command target 配置执行本地命令,记录执行耗时、退出码、stdout 和 stderr,并在执行失败时产生结构化错误信息。
|
|
||||||
|
|
||||||
#### Scenario: 命令正常退出
|
|
||||||
- **WHEN** command target 执行的进程正常退出且 exit code 为 0
|
|
||||||
- **THEN** 系统 SHALL 记录 `durationMs`、`statusDetail="exitCode=0"`,并进入 expect 校验
|
|
||||||
|
|
||||||
#### Scenario: 命令非零退出
|
|
||||||
- **WHEN** command target 执行的进程正常退出但 exit code 为 1
|
|
||||||
- **THEN** 系统 SHALL 记录 `statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
|
|
||||||
|
|
||||||
#### Scenario: 命令启动失败
|
|
||||||
- **WHEN** command target 的 exec 不存在或无法启动
|
|
||||||
- **THEN** 系统 SHALL 记录 `matched=false`,并在 failure 中写入 kind=`error` 和可读错误信息
|
|
||||||
|
|
||||||
#### Scenario: 命令超时
|
|
||||||
- **WHEN** command target 在 timeout 时间内未结束
|
|
||||||
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息
|
|
||||||
|
|
||||||
#### Scenario: 命令输出超限
|
|
||||||
- **WHEN** command target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
|
|
||||||
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息
|
|
||||||
|
|
||||||
### Requirement: command expect 校验
|
|
||||||
系统 SHALL 支持 command 专用 expect,包括 `exitCode`、`stdout` 和 `stderr`,并按 exitCode、duration、stdout、stderr 的阶段顺序快速失败。
|
|
||||||
|
|
||||||
#### Scenario: 默认 exitCode 成功语义
|
|
||||||
- **WHEN** command target 未显式配置 `expect.exitCode`
|
|
||||||
- **THEN** 系统 SHALL 使用默认 `expect.exitCode: [0]` 进行校验
|
|
||||||
|
|
||||||
#### Scenario: 显式 exitCode 校验
|
|
||||||
- **WHEN** command target 配置 `expect.exitCode: [0, 2]` 且实际 exit code 为 2
|
|
||||||
- **THEN** 系统 SHALL 判定 exitCode 阶段通过,并继续后续 expect 阶段
|
|
||||||
|
|
||||||
#### Scenario: exitCode 不匹配快速失败
|
|
||||||
- **WHEN** command target 配置 `expect.exitCode: [0]` 且实际 exit code 为 1
|
|
||||||
- **THEN** 系统 SHALL 立即返回 `matched=false`,并在 failure 中写入 phase=`exitCode`、path=`expect.exitCode`、expected 和 actual
|
|
||||||
|
|
||||||
#### Scenario: stdout 按配置顺序校验
|
|
||||||
- **WHEN** command target 配置 `expect.stdout` 为两个规则,第一条通过且第二条失败
|
|
||||||
- **THEN** 系统 SHALL 先执行第一条 stdout 规则,再执行第二条,并将 failure.path 指向失败的 `expect.stdout[1]`
|
|
||||||
|
|
||||||
#### Scenario: stderr 校验为空
|
|
||||||
- **WHEN** command target 配置 `expect.stderr: [{empty: true}]` 且实际 stderr 为空字符串
|
|
||||||
- **THEN** 系统 SHALL 判定 stderr 阶段通过
|
|
||||||
|
|
||||||
#### Scenario: stdout 失败后不检查 stderr
|
|
||||||
- **WHEN** command target 同时配置 stdout 和 stderr 规则,且 stdout 规则失败
|
|
||||||
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr 规则
|
|
||||||
|
|
||||||
### Requirement: command checker 启动期配置校验
|
|
||||||
系统 SHALL 在启动期对 command checker 的配置契约和语义执行严格校验。Command target 的 `command` 分组 SHALL 只允许 `exec`、`args`、`cwd`、`env`、`maxOutputBytes` 字段;Command expect SHALL 只允许 `exitCode`、`maxDurationMs`、`stdout`、`stderr` 字段。未知字段、非法类型和不可编译正则 MUST 导致启动期配置错误。`expect.exitCode` SHALL 保留原有有限整数数组语义,不限制到特定平台范围。
|
|
||||||
|
|
||||||
#### Scenario: command args 类型非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `command.args` 不是字符串数组
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 command.args 格式错误
|
|
||||||
|
|
||||||
#### Scenario: command cwd 类型非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `command.cwd` 不是字符串
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 command.cwd 必须为字符串
|
|
||||||
|
|
||||||
#### Scenario: command env 值类型非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `command.env`,且任一环境变量值不是字符串
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 command.env 对应变量值必须为字符串
|
|
||||||
|
|
||||||
#### Scenario: command maxOutputBytes 非法
|
|
||||||
- **WHEN** YAML 中 command target 或 defaults.command 配置的 `maxOutputBytes` 不是合法 size 值
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 maxOutputBytes 格式错误
|
|
||||||
|
|
||||||
#### Scenario: command 分组未知字段失败
|
|
||||||
- **WHEN** YAML 中 command target 的 `command` 分组包含 `shell: true` 等未知字段
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 command 分组包含未知字段
|
|
||||||
|
|
||||||
#### Scenario: command expect exitCode 类型非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.exitCode` 不是整数数组
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.exitCode 必须为整数数组
|
|
||||||
|
|
||||||
#### Scenario: command expect exitCode 不限制平台范围
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.exitCode` 为有限整数数组
|
|
||||||
- **THEN** 系统 SHALL 接受该数组,不额外限制为 0-255 等平台相关范围
|
|
||||||
|
|
||||||
#### Scenario: command expect maxDurationMs 非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.maxDurationMs` 不是非负有限数字
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxDurationMs 格式错误
|
|
||||||
|
|
||||||
#### Scenario: stdout 必须为规则数组
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.stdout` 但其值不是数组
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stdout 必须为数组
|
|
||||||
|
|
||||||
#### Scenario: stderr 必须为规则数组
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.stderr` 但其值不是数组
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stderr 必须为数组
|
|
||||||
|
|
||||||
#### Scenario: stdout text rule 空对象非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.stdout: [{}]`
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 stdout 规则必须包含至少一个合法 operator
|
|
||||||
|
|
||||||
#### Scenario: stderr text rule 未知字段非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.stderr: [{foo: "bar"}]`
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 stderr 规则包含未知 operator
|
|
||||||
|
|
||||||
#### Scenario: stdout match 正则非法
|
|
||||||
- **WHEN** YAML 中 command target 配置 `expect.stdout: [{match: "[invalid"}]`
|
|
||||||
- **THEN** 系统 SHALL 在启动期配置校验失败,而不是延迟到运行期抛错
|
|
||||||
|
|
||||||
#### Scenario: command expect 未知字段失败
|
|
||||||
- **WHEN** YAML 中 command target 的 expect 包含 `status: [200]` 或其他非 command expect 字段
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#### Scenario: 获取 checker 类型列表
|
#### Scenario: 获取 checker 类型列表
|
||||||
- **WHEN** 客户端请求 `GET /api/meta`
|
- **WHEN** 客户端请求 `GET /api/meta`
|
||||||
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符(如 `["http", "command"]`)
|
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符(如 `["http", "cmd"]`)
|
||||||
|
|
||||||
#### Scenario: 类型列表来源
|
#### Scenario: 类型列表来源
|
||||||
- **WHEN** 系统启动并注册了 checker
|
- **WHEN** 系统启动并注册了 checker
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Requirement: YAML 配置文件格式
|
### Requirement: YAML 配置文件格式
|
||||||
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、checker 默认值和 typed target 列表(含可选 group 字段)。target MUST 使用 `type` 字段声明 checker 类型,HTTP 领域字段 MUST 放在 `http` 分组,command 领域字段 MUST 放在 `command` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。
|
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、checker 默认值和 typed target 列表(含可选 group 字段)。target MUST 使用 `type` 字段声明 checker 类型,HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。
|
||||||
|
|
||||||
#### Scenario: 完整配置文件解析
|
#### Scenario: 完整配置文件解析
|
||||||
- **WHEN** 系统启动并读取包含 server、runtime、defaults、targets(含 group 字段)的 YAML 配置文件
|
- **WHEN** 系统启动并读取包含 server、runtime、defaults、targets(含 group 字段)的 YAML 配置文件
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
- **WHEN** 系统读取只包含一个 `type: http` target 和 `http.url` 的 YAML 配置文件(省略 server、runtime、defaults 和 expect)
|
- **WHEN** 系统读取只包含一个 `type: http` target 和 `http.url` 的 YAML 配置文件(省略 server、runtime、defaults 和 expect)
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, runtime.maxConcurrentChecks=20, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default")
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, runtime.maxConcurrentChecks=20, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default")
|
||||||
|
|
||||||
#### Scenario: 最简 command 配置文件解析
|
#### Scenario: 最简 cmd 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: command` target 和 `command.exec` 的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: cmd` target 和 `cmd.exec` 的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, command.cwd 为配置文件所在目录, command.maxOutputBytes=100MB)
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB)
|
||||||
|
|
||||||
#### Scenario: per-target 配置覆盖全局默认值
|
#### Scenario: per-target 配置覆盖全局默认值
|
||||||
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
|
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
|
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
|
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
|
||||||
|
|
||||||
#### Scenario: command target 缺少 exec
|
#### Scenario: cmd target 缺少 exec
|
||||||
- **WHEN** YAML 中某个 target 配置 `type: command` 但缺少 `command.exec`
|
- **WHEN** YAML 中某个 target 配置 `type: cmd` 但缺少 `cmd.exec`
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 command.exec 字段
|
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 cmd.exec 字段
|
||||||
|
|
||||||
#### Scenario: target type 非法
|
#### Scenario: target type 非法
|
||||||
- **WHEN** YAML 中某个 target 的 type 不是已注册 checker 类型
|
- **WHEN** YAML 中某个 target 的 type 不是已注册 checker 类型
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
||||||
|
|
||||||
#### Scenario: 动态 env 字段允许
|
#### Scenario: 动态 env 字段允许
|
||||||
- **WHEN** YAML 中 `command.env` 包含任意环境变量名称,且对应值为字符串
|
- **WHEN** YAML 中 `cmd.env` 包含任意环境变量名称,且对应值为字符串
|
||||||
- **THEN** 系统 SHALL 接受这些动态 env 名称
|
- **THEN** 系统 SHALL 接受这些动态 env 名称
|
||||||
|
|
||||||
#### Scenario: JSON Schema 不修改输入
|
#### Scenario: JSON Schema 不修改输入
|
||||||
@@ -243,15 +243,15 @@
|
|||||||
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象
|
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象
|
||||||
|
|
||||||
### Requirement: expect 配置增强
|
### Requirement: expect 配置增强
|
||||||
系统 SHALL 支持 typed target 的领域专用 expect 配置,包括 HTTP 的 `status`(支持精确数字和范围模式)、`headers`、`body` 和 command 的 `exitCode`、`stdout`、`stderr`。内容类 expect MUST 使用数组表达配置顺序。
|
系统 SHALL 支持 typed target 的领域专用 expect 配置,包括 HTTP 的 `status`(支持精确数字和范围模式)、`headers`、`body` 和 cmd 的 `exitCode`、`stdout`、`stderr`。内容类 expect MUST 使用数组表达配置顺序。
|
||||||
|
|
||||||
#### Scenario: 解析 HTTP expect 配置
|
#### Scenario: 解析 HTTP expect 配置
|
||||||
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body 规则数组及内部方法
|
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body 规则数组及内部方法
|
||||||
- **THEN** 系统 SHALL 正确解析并存储为 HTTP target 的 expect 字段
|
- **THEN** 系统 SHALL 正确解析并存储为 HTTP target 的 expect 字段
|
||||||
|
|
||||||
#### Scenario: 解析 command expect 配置
|
#### Scenario: 解析 cmd expect 配置
|
||||||
- **WHEN** YAML 配置文件中 command target 的 expect 包含 exitCode、stdout 和 stderr 规则数组
|
- **WHEN** YAML 配置文件中 cmd target 的 expect 包含 exitCode、stdout 和 stderr 规则数组
|
||||||
- **THEN** 系统 SHALL 正确解析并存储为 command target 的 expect 字段
|
- **THEN** 系统 SHALL 正确解析并存储为 cmd target 的 expect 字段
|
||||||
|
|
||||||
#### Scenario: 解析 body 有序规则数组
|
#### Scenario: 解析 body 有序规则数组
|
||||||
- **WHEN** YAML 中 HTTP target 配置 `expect.body` 为 contains、json、regex 三个数组项
|
- **WHEN** YAML 中 HTTP target 配置 `expect.body` 为 contains、json、regex 三个数组项
|
||||||
@@ -269,8 +269,8 @@
|
|||||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`
|
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`
|
||||||
- **THEN** 系统 SHALL 在执行 expect 时匹配所有 200-299 状态码或精确匹配 301
|
- **THEN** 系统 SHALL 在执行 expect 时匹配所有 200-299 状态码或精确匹配 301
|
||||||
|
|
||||||
#### Scenario: 不配置 command exitCode
|
#### Scenario: 不配置 cmd exitCode
|
||||||
- **WHEN** command target 未配置 `expect.exitCode`
|
- **WHEN** cmd target 未配置 `expect.exitCode`
|
||||||
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `exitCode: [0]` 语义
|
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `exitCode: [0]` 语义
|
||||||
|
|
||||||
#### Scenario: 不配置 expect
|
#### Scenario: 不配置 expect
|
||||||
|
|||||||
@@ -115,16 +115,16 @@
|
|||||||
- **WHEN** 同步 HTTP target
|
- **WHEN** 同步 HTTP target
|
||||||
- **THEN** targets.target SHALL 存储该 target 的 URL
|
- **THEN** targets.target SHALL 存储该 target 的 URL
|
||||||
|
|
||||||
#### Scenario: command target 展示摘要
|
#### Scenario: cmd target 展示摘要
|
||||||
- **WHEN** 同步 command target
|
- **WHEN** 同步 cmd target
|
||||||
- **THEN** targets.target SHALL 存储由 exec 和 args 组成的命令摘要
|
- **THEN** targets.target SHALL 存储由 exec 和 args 组成的命令摘要
|
||||||
|
|
||||||
#### Scenario: HTTP target config 序列化
|
#### Scenario: HTTP target config 序列化
|
||||||
- **WHEN** 同步 HTTP target
|
- **WHEN** 同步 HTTP target
|
||||||
- **THEN** targets.config SHALL 存储 JSON,包含 url、method、headers、body、maxBodyBytes、ignoreSSL、maxRedirects
|
- **THEN** targets.config SHALL 存储 JSON,包含 url、method、headers、body、maxBodyBytes、ignoreSSL、maxRedirects
|
||||||
|
|
||||||
#### Scenario: command target config 序列化
|
#### Scenario: cmd target config 序列化
|
||||||
- **WHEN** 同步 command target
|
- **WHEN** 同步 cmd target
|
||||||
- **THEN** targets.config SHALL 存储 JSON,包含 exec、args、cwd、env、maxOutputBytes
|
- **THEN** targets.config SHALL 存储 JSON,包含 exec、args、cwd、env、maxOutputBytes
|
||||||
|
|
||||||
### Requirement: 数据清理方法
|
### Requirement: 数据清理方法
|
||||||
|
|||||||
@@ -112,8 +112,8 @@
|
|||||||
- **WHEN** HTTP 请求在 timeout 时间内未收到响应
|
- **WHEN** HTTP 请求在 timeout 时间内未收到响应
|
||||||
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
|
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
|
||||||
|
|
||||||
#### Scenario: command 执行超时
|
#### Scenario: cmd 执行超时
|
||||||
- **WHEN** command 进程在 timeout 时间内未退出
|
- **WHEN** cmd 进程在 timeout 时间内未退出
|
||||||
- **THEN** 系统 MUST 终止该子进程,记录为失败并标注超时错误
|
- **THEN** 系统 MUST 终止该子进程,记录为失败并标注超时错误
|
||||||
|
|
||||||
#### Scenario: 请求在超时前完成
|
#### Scenario: 请求在超时前完成
|
||||||
@@ -167,12 +167,12 @@
|
|||||||
- **WHEN** 目标同时配置状态、duration、元数据和内容规则
|
- **WHEN** 目标同时配置状态、duration、元数据和内容规则
|
||||||
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true,任一不通过则为 false 并记录首个失败原因
|
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true,任一不通过则为 false 并记录首个失败原因
|
||||||
|
|
||||||
#### Scenario: command 默认 exitCode
|
#### Scenario: cmd 默认 exitCode
|
||||||
- **WHEN** command target 未配置 `expect.exitCode`
|
- **WHEN** cmd target 未配置 `expect.exitCode`
|
||||||
- **THEN** 系统 SHALL 按默认 `exitCode: [0]` 校验命令退出码
|
- **THEN** 系统 SHALL 按默认 `exitCode: [0]` 校验命令退出码
|
||||||
|
|
||||||
#### Scenario: 校验 command stdout
|
#### Scenario: 校验 cmd stdout
|
||||||
- **WHEN** command target 配置了有序 `expect.stdout` 规则数组
|
- **WHEN** cmd target 配置了有序 `expect.stdout` 规则数组
|
||||||
- **THEN** 系统 SHALL 按数组顺序执行 stdout 规则,任一失败立即记录 failure 并停止后续规则
|
- **THEN** 系统 SHALL 按数组顺序执行 stdout 规则,任一失败立即记录 failure 并停止后续规则
|
||||||
|
|
||||||
### Requirement: Body 校验按需解析
|
### Requirement: Body 校验按需解析
|
||||||
@@ -235,9 +235,9 @@ HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网
|
|||||||
- **WHEN** target.type 为 `http`
|
- **WHEN** target.type 为 `http`
|
||||||
- **THEN** 系统 SHALL 使用 HTTP runner 执行该目标
|
- **THEN** 系统 SHALL 使用 HTTP runner 执行该目标
|
||||||
|
|
||||||
#### Scenario: 选择 command runner
|
#### Scenario: 选择 cmd runner
|
||||||
- **WHEN** target.type 为 `command`
|
- **WHEN** target.type 为 `cmd`
|
||||||
- **THEN** 系统 SHALL 使用 command runner 执行该目标
|
- **THEN** 系统 SHALL 使用 cmd runner 执行该目标
|
||||||
|
|
||||||
### Requirement: 定期数据清理
|
### Requirement: 定期数据清理
|
||||||
ProbeEngine SHALL 在启动时注册数据清理定时器,定期调用 ProbeStore.prune() 清理过期数据。
|
ProbeEngine SHALL 在启动时注册数据清理定时器,定期调用 ProbeStore.prune() 清理过期数据。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
确保测试在 Windows 平台上的兼容性,包括文件句柄释放后的目录清理重试机制和跨平台命令测试约定。
|
确保测试在 Windows、macOS、Linux 平台上的兼容性,包括文件句柄释放后的目录清理重试机制和跨平台命令测试约定。
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -16,10 +16,39 @@
|
|||||||
- **THEN** 删除操作 SHALL 自动重试(最多 3 次,间隔 200ms),直到成功或耗尽重试次数
|
- **THEN** 删除操作 SHALL 自动重试(最多 3 次,间隔 200ms),直到成功或耗尽重试次数
|
||||||
|
|
||||||
### Requirement: 命令检测器测试 SHALL 使用跨平台命令
|
### Requirement: 命令检测器测试 SHALL 使用跨平台命令
|
||||||
|
命令检测器的测试 SHALL 使用 `bun -e` 脚本替代所有系统命令(包括 `true`、`false`、`sleep`、`bash`、`echo`、`yes | head`),确保测试在 Windows、macOS、Linux 三平台上行为一致。
|
||||||
|
|
||||||
命令检测器的测试 SHALL 使用 `bun -e` 脚本替代系统 `echo` 命令,确保测试断言在所有平台上行为一致。
|
#### Scenario: 进程退出码 0
|
||||||
|
- **WHEN** 测试需要一个正常退出的命令
|
||||||
|
- **THEN** 测试 SHALL 使用 `bun -e "process.exit(0)"` 替代 `true`
|
||||||
|
|
||||||
|
#### Scenario: 进程退出码非零
|
||||||
|
- **WHEN** 测试需要一个失败退出的命令
|
||||||
|
- **THEN** 测试 SHALL 使用 `bun -e "process.exit(1)"` 替代 `false`
|
||||||
|
|
||||||
|
#### Scenario: stdout 输出
|
||||||
|
- **WHEN** 测试需要一个输出文本到 stdout 的命令
|
||||||
|
- **THEN** 测试 SHALL 使用 `bun -e "console.log('text')"` 替代 `echo text`
|
||||||
|
|
||||||
|
#### Scenario: stderr 输出
|
||||||
|
- **WHEN** 测试需要一个输出文本到 stderr 的命令
|
||||||
|
- **THEN** 测试 SHALL 使用 `bun -e "process.stderr.write('error\n')"` 替代 `bash -c "echo error >&2"`
|
||||||
|
|
||||||
|
#### Scenario: 长时间运行命令
|
||||||
|
- **WHEN** 测试需要一个超时场景的长时间运行命令
|
||||||
|
- **THEN** 测试 SHALL 使用 `bun -e "await Bun.sleep(10000)"` 替代 `sleep 10`
|
||||||
|
|
||||||
|
#### Scenario: 大量输出
|
||||||
|
- **WHEN** 测试需要一个产生大量输出的命令
|
||||||
|
- **THEN** 测试 SHALL 使用 `bun -e "process.stdout.write('y\n'.repeat(N))"` 替代 `bash -c "yes | head -N"`
|
||||||
|
|
||||||
#### Scenario: 验证非 shell 模式下特殊字符不被展开
|
#### Scenario: 验证非 shell 模式下特殊字符不被展开
|
||||||
|
|
||||||
- **WHEN** 通过 `Bun.spawn` 执行 `bun -e "console.log('*')"` 并检查 stdout 包含 `*`
|
- **WHEN** 通过 `Bun.spawn` 执行 `bun -e "console.log('*')"` 并检查 stdout 包含 `*`
|
||||||
- **THEN** 测试 SHALL 在 Windows 和 Linux 上均返回 `matched: true`
|
- **THEN** 测试 SHALL 在 Windows、macOS 和 Linux 上均返回 `matched: true`
|
||||||
|
|
||||||
|
### Requirement: probes.example.yaml 使用跨平台示例
|
||||||
|
probes.example.yaml 中的 cmd 类型示例 SHALL 使用跨平台命令(如 `bun -e "..."`、`bun --version`),不使用 Unix 专属命令(如 `uname`、`ls /tmp`、`date`)。
|
||||||
|
|
||||||
|
#### Scenario: 示例命令跨平台可执行
|
||||||
|
- **WHEN** 用户在 Windows、macOS 或 Linux 上直接使用 probes.example.yaml 中的 cmd 示例
|
||||||
|
- **THEN** 所有 cmd 示例 SHALL 能正常执行,不依赖平台特定命令
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"command": {
|
"cmd": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"type",
|
"type",
|
||||||
"command"
|
"cmd"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"expect": {
|
"expect": {
|
||||||
@@ -673,10 +673,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"const": "command",
|
"const": "cmd",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"command": {
|
"cmd": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ defaults:
|
|||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
maxBodyBytes: "10MB"
|
maxBodyBytes: "10MB"
|
||||||
command:
|
cmd:
|
||||||
maxOutputBytes: "1MB"
|
maxOutputBytes: "1MB"
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
@@ -149,75 +149,74 @@ targets:
|
|||||||
expect:
|
expect:
|
||||||
status: [200]
|
status: [200]
|
||||||
|
|
||||||
# ========== Command targets ==========
|
# ========== Cmd targets ==========
|
||||||
|
|
||||||
- name: "uname 输出匹配"
|
- name: "Bun 版本输出匹配"
|
||||||
type: command
|
type: cmd
|
||||||
group: "系统检查"
|
group: "系统检查"
|
||||||
command:
|
cmd:
|
||||||
exec: "uname"
|
exec: "bun"
|
||||||
args: ["-s"]
|
args: ["--version"]
|
||||||
expect:
|
expect:
|
||||||
exitCode: [0]
|
exitCode: [0]
|
||||||
stdout:
|
stdout:
|
||||||
- match: "^[A-Z][a-z]+$"
|
- match: "^\\d+\\.\\d+\\.\\d+"
|
||||||
|
|
||||||
- name: "echo 自定义文本输出"
|
- name: "自定义文本输出"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "bun"
|
||||||
args: ["check ok"]
|
args: ["-e", "console.log('check ok')"]
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
- equals: "check ok\n"
|
- equals: "check ok\n"
|
||||||
maxDurationMs: 3000
|
maxDurationMs: 3000
|
||||||
|
|
||||||
- name: "ls 目录无 stderr"
|
- name: "脚本执行无 stderr"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "ls"
|
exec: "bun"
|
||||||
args: ["/tmp"]
|
args: ["-e", "process.stdout.write('ok')"]
|
||||||
cwd: "/"
|
|
||||||
expect:
|
expect:
|
||||||
exitCode: [0]
|
exitCode: [0]
|
||||||
stderr:
|
stderr:
|
||||||
- empty: true
|
- empty: true
|
||||||
|
|
||||||
- name: "date 输出包含年份"
|
- name: "日期脚本输出包含年份"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "date"
|
exec: "bun"
|
||||||
args: ["+%Y"]
|
args: ["-e", "console.log(new Date().getFullYear())"]
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
- match: "^20\\d{2}\n?$"
|
- match: "^20\\d{2}\n?$"
|
||||||
|
|
||||||
- name: "wc 行数计数"
|
- name: "环境变量覆盖"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "wc"
|
exec: "bun"
|
||||||
args: ["-l"]
|
args: ["-e", "console.log(process.env.LANG ?? '')"]
|
||||||
cwd: "/etc"
|
|
||||||
env:
|
env:
|
||||||
LANG: "C"
|
LANG: "C"
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
- match: "\\d+"
|
- contains: "C"
|
||||||
|
|
||||||
- name: "hostname 非空输出"
|
- name: "运行平台非空输出"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "hostname"
|
exec: "bun"
|
||||||
|
args: ["-e", "console.log(process.platform)"]
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
- match: ".+"
|
- match: ".+"
|
||||||
|
|
||||||
- name: "多规则 stdout 顺序校验"
|
- name: "多规则 stdout 顺序校验"
|
||||||
type: command
|
type: cmd
|
||||||
interval: "5m"
|
interval: "5m"
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "bun"
|
||||||
args: ["version: 2.0.1, status: healthy"]
|
args: ["-e", "console.log('version: 2.0.1, status: healthy')"]
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
- contains: "version:"
|
- contains: "version:"
|
||||||
@@ -225,11 +224,11 @@ targets:
|
|||||||
- contains: "healthy"
|
- contains: "healthy"
|
||||||
|
|
||||||
- name: "stderr 内容检查"
|
- name: "stderr 内容检查"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "ls"
|
exec: "bun"
|
||||||
args: ["/nonexistent-path-checker-test"]
|
args: ["-e", "process.stderr.write('simulated error\\n'); process.exit(1)"]
|
||||||
expect:
|
expect:
|
||||||
exitCode: [0, 1, 2]
|
exitCode: [1]
|
||||||
stderr:
|
stderr:
|
||||||
- contains: "No such file"
|
- contains: "simulated error"
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import { checkTextRules } from "./text";
|
|||||||
import { validateCommandConfig } from "./validate";
|
import { validateCommandConfig } from "./validate";
|
||||||
|
|
||||||
export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget> {
|
export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget> {
|
||||||
readonly configKey = "command";
|
readonly configKey = "cmd";
|
||||||
|
|
||||||
readonly schemas = commandCheckerSchemas;
|
readonly schemas = commandCheckerSchemas;
|
||||||
|
|
||||||
readonly type = "command";
|
readonly type = "cmd";
|
||||||
|
|
||||||
async execute(t: ResolvedCommandTarget, ctx: CheckerContext): Promise<CheckResult> {
|
async execute(t: ResolvedCommandTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
@@ -27,9 +27,9 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
|||||||
let proc: ReturnType<typeof Bun.spawn>;
|
let proc: ReturnType<typeof Bun.spawn>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
proc = Bun.spawn([t.command.exec, ...t.command.args], {
|
proc = Bun.spawn([t.cmd.exec, ...t.cmd.args], {
|
||||||
cwd: t.command.cwd,
|
cwd: t.cmd.cwd,
|
||||||
env: t.command.env,
|
env: t.cmd.env,
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
stdin: "ignore",
|
stdin: "ignore",
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
@@ -65,7 +65,7 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
|||||||
proc.stdout as ReadableStream<Uint8Array>,
|
proc.stdout as ReadableStream<Uint8Array>,
|
||||||
proc.stderr as ReadableStream<Uint8Array>,
|
proc.stderr as ReadableStream<Uint8Array>,
|
||||||
() => proc.kill(),
|
() => proc.kill(),
|
||||||
t.command.maxOutputBytes,
|
t.cmd.maxOutputBytes,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
const durationMs = Math.round(performance.now() - start);
|
const durationMs = Math.round(performance.now() - start);
|
||||||
@@ -87,7 +87,7 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
|||||||
if (outputResult.exceeded) {
|
if (outputResult.exceeded) {
|
||||||
return {
|
return {
|
||||||
durationMs,
|
durationMs,
|
||||||
failure: errorFailure("exitCode", "output", `输出超过限制 ${t.command.maxOutputBytes} 字节`),
|
failure: errorFailure("exitCode", "output", `输出超过限制 ${t.cmd.maxOutputBytes} 字节`),
|
||||||
matched: false,
|
matched: false,
|
||||||
statusDetail: `exitCode=${exitCode}`,
|
statusDetail: `exitCode=${exitCode}`,
|
||||||
targetName: t.name,
|
targetName: t.name,
|
||||||
@@ -169,22 +169,22 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
|
||||||
const t = target as RawTargetConfig & { command: CommandTargetConfig; type: "command" };
|
const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" };
|
||||||
const commandDefaults = context.defaults["command"] as undefined | { cwd?: string; maxOutputBytes?: string };
|
const cmdDefaults = context.defaults["cmd"] as undefined | { cwd?: string; maxOutputBytes?: string };
|
||||||
|
|
||||||
const cwd = t.command.cwd ?? commandDefaults?.cwd ?? ".";
|
const cwd = t.cmd.cwd ?? cmdDefaults?.cwd ?? ".";
|
||||||
const resolvedCwd = resolve(context.configDir, cwd);
|
const resolvedCwd = resolve(context.configDir, cwd);
|
||||||
|
|
||||||
const maxOutputBytes = parseSize(t.command.maxOutputBytes ?? commandDefaults?.maxOutputBytes ?? "100MB");
|
const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? cmdDefaults?.maxOutputBytes ?? "100MB");
|
||||||
|
|
||||||
const env = { ...process.env, ...(t.command.env ?? {}) } as Record<string, string>;
|
const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record<string, string>;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: {
|
cmd: {
|
||||||
args: t.command.args ?? [],
|
args: t.cmd.args ?? [],
|
||||||
cwd: resolvedCwd,
|
cwd: resolvedCwd,
|
||||||
env,
|
env,
|
||||||
exec: t.command.exec,
|
exec: t.cmd.exec,
|
||||||
maxOutputBytes,
|
maxOutputBytes,
|
||||||
},
|
},
|
||||||
expect: target.expect as CommandExpectConfig | undefined,
|
expect: target.expect as CommandExpectConfig | undefined,
|
||||||
@@ -192,19 +192,19 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
|||||||
intervalMs: context.defaultIntervalMs,
|
intervalMs: context.defaultIntervalMs,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
timeoutMs: context.defaultTimeoutMs,
|
timeoutMs: context.defaultTimeoutMs,
|
||||||
type: "command",
|
type: "cmd",
|
||||||
} satisfies ResolvedCommandTarget;
|
} satisfies ResolvedCommandTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(t: ResolvedCommandTarget): { config: string; target: string } {
|
serialize(t: ResolvedCommandTarget): { config: string; target: string } {
|
||||||
const parts = [t.command.exec, ...t.command.args];
|
const parts = [t.cmd.exec, ...t.cmd.args];
|
||||||
return {
|
return {
|
||||||
config: JSON.stringify({
|
config: JSON.stringify({
|
||||||
args: t.command.args,
|
args: t.cmd.args,
|
||||||
cwd: t.command.cwd,
|
cwd: t.cmd.cwd,
|
||||||
env: t.command.env,
|
env: t.cmd.env,
|
||||||
exec: t.command.exec,
|
exec: t.cmd.exec,
|
||||||
maxOutputBytes: t.command.maxOutputBytes,
|
maxOutputBytes: t.cmd.maxOutputBytes,
|
||||||
}),
|
}),
|
||||||
target: `exec ${parts.join(" ")}`,
|
target: `exec ${parts.join(" ")}`,
|
||||||
};
|
};
|
||||||
@@ -29,13 +29,13 @@ export interface ResolvedCommandConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolvedCommandTarget extends ResolvedTargetBase {
|
export interface ResolvedCommandTarget extends ResolvedTargetBase {
|
||||||
command: ResolvedCommandConfig;
|
cmd: ResolvedCommandConfig;
|
||||||
expect?: CommandExpectConfig;
|
expect?: CommandExpectConfig;
|
||||||
group: string;
|
group: string;
|
||||||
intervalMs: number;
|
intervalMs: number;
|
||||||
name: string;
|
name: string;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
type: "command";
|
type: "cmd";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextRule = ExpectOperator;
|
export type TextRule = ExpectOperator;
|
||||||
@@ -7,17 +7,16 @@ import { parseSize } from "../../utils";
|
|||||||
|
|
||||||
export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
const defaults =
|
const defaults = isRecord(input.defaults) && isRecord(input.defaults["cmd"]) ? input.defaults["cmd"] : undefined;
|
||||||
isRecord(input.defaults) && isRecord(input.defaults["command"]) ? input.defaults["command"] : undefined;
|
|
||||||
|
|
||||||
if (isSizeInput(defaults?.["maxOutputBytes"])) {
|
if (isSizeInput(defaults?.["maxOutputBytes"])) {
|
||||||
issues.push(...validateSizeValue(defaults["maxOutputBytes"], "defaults.command.maxOutputBytes"));
|
issues.push(...validateSizeValue(defaults["maxOutputBytes"], "defaults.cmd.maxOutputBytes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < input.targets.length; i++) {
|
for (let i = 0; i < input.targets.length; i++) {
|
||||||
const target = input.targets[i] as unknown;
|
const target = input.targets[i] as unknown;
|
||||||
if (!isRecord(target)) continue;
|
if (!isRecord(target)) continue;
|
||||||
if (target["type"] !== "command") continue;
|
if (target["type"] !== "cmd") continue;
|
||||||
issues.push(...validateCommandTarget(target, `targets[${i}]`));
|
issues.push(...validateCommandTarget(target, `targets[${i}]`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,22 +60,18 @@ function validateCommandExpect(target: Record<string, unknown>, path: string): C
|
|||||||
function validateCommandTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
function validateCommandTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
const targetName = getTargetName(target);
|
const targetName = getTargetName(target);
|
||||||
const command = target["command"];
|
const cmd = target["cmd"];
|
||||||
if (!isRecord(command)) {
|
if (!isRecord(cmd)) {
|
||||||
issues.push(issue("required", joinPath(path, "command"), "缺少 command.exec 字段", targetName));
|
issues.push(issue("required", joinPath(path, "cmd"), "缺少 cmd.exec 字段", targetName));
|
||||||
issues.push(...validateCommandExpect(target, path));
|
issues.push(...validateCommandExpect(target, path));
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
if (typeof command["exec"] !== "string" || command["exec"].trim() === "") {
|
if (typeof cmd["exec"] !== "string" || cmd["exec"].trim() === "") {
|
||||||
issues.push(issue("required", joinPath(joinPath(path, "command"), "exec"), "缺少 command.exec 字段", targetName));
|
issues.push(issue("required", joinPath(joinPath(path, "cmd"), "exec"), "缺少 cmd.exec 字段", targetName));
|
||||||
}
|
}
|
||||||
if (isSizeInput(command["maxOutputBytes"])) {
|
if (isSizeInput(cmd["maxOutputBytes"])) {
|
||||||
issues.push(
|
issues.push(
|
||||||
...validateSizeValue(
|
...validateSizeValue(cmd["maxOutputBytes"], joinPath(joinPath(path, "cmd"), "maxOutputBytes"), targetName),
|
||||||
command["maxOutputBytes"],
|
|
||||||
joinPath(joinPath(path, "command"), "maxOutputBytes"),
|
|
||||||
targetName,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
issues.push(...validateCommandExpect(target, path));
|
issues.push(...validateCommandExpect(target, path));
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandChecker } from "./command";
|
import { CommandChecker } from "./cmd";
|
||||||
import { HttpChecker } from "./http";
|
import { HttpChecker } from "./http";
|
||||||
import { CheckerRegistry } from "./registry";
|
import { CheckerRegistry } from "./registry";
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
} from "../../src/shared/api";
|
} from "../../src/shared/api";
|
||||||
|
|
||||||
import { checkerRegistry } from "../../src/server/checker/runner";
|
import { checkerRegistry } from "../../src/server/checker/runner";
|
||||||
import { CommandChecker } from "../../src/server/checker/runner/command/execute";
|
import { CommandChecker } from "../../src/server/checker/runner/cmd/execute";
|
||||||
import { HttpChecker } from "../../src/server/checker/runner/http/execute";
|
import { HttpChecker } from "../../src/server/checker/runner/http/execute";
|
||||||
import { ProbeStore } from "../../src/server/checker/store";
|
import { ProbeStore } from "../../src/server/checker/store";
|
||||||
import { startServer } from "../../src/server/server";
|
import { startServer } from "../../src/server/server";
|
||||||
@@ -56,7 +56,7 @@ describe("API 路由", () => {
|
|||||||
type: "http",
|
type: "http",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: {
|
cmd: {
|
||||||
args: ["hello"],
|
args: ["hello"],
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
env: {},
|
env: {},
|
||||||
@@ -67,7 +67,7 @@ describe("API 路由", () => {
|
|||||||
intervalMs: 60000,
|
intervalMs: 60000,
|
||||||
name: "test-b",
|
name: "test-b",
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
type: "command",
|
type: "cmd",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ describe("API 路由", () => {
|
|||||||
expect(tA.stats.availability).toBeDefined();
|
expect(tA.stats.availability).toBeDefined();
|
||||||
|
|
||||||
const tB = body.find((t) => t.name === "test-b")!;
|
const tB = body.find((t) => t.name === "test-b")!;
|
||||||
expect(tB.type).toBe("command");
|
expect(tB.type).toBe("cmd");
|
||||||
expect(tB.target).toBe("exec echo hello");
|
expect(tB.target).toBe("exec echo hello");
|
||||||
expect(tB.latestCheck).toBeNull();
|
expect(tB.latestCheck).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -162,7 +162,7 @@ describe("API 路由", () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(body.checkerTypes).toEqual(checkerRegistry.supportedTypes);
|
expect(body.checkerTypes).toEqual(checkerRegistry.supportedTypes);
|
||||||
expect(body.checkerTypes).toContain("http");
|
expect(body.checkerTypes).toContain("http");
|
||||||
expect(body.checkerTypes).toContain("command");
|
expect(body.checkerTypes).toContain("cmd");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("不支持的 method 在有 API 通配符时返回 404", async () => {
|
test("不支持的 method 在有 API 通配符时返回 404", async () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const target: ResolvedTargetBase = {
|
|||||||
intervalMs: 30000,
|
intervalMs: 30000,
|
||||||
name: "test",
|
name: "test",
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
type: "command",
|
type: "cmd",
|
||||||
};
|
};
|
||||||
|
|
||||||
function createHarness(overrides: BootstrapDependencies = {}) {
|
function createHarness(overrides: BootstrapDependencies = {}) {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/command/types";
|
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types";
|
||||||
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
|
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
|
||||||
|
|
||||||
import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader";
|
import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader";
|
||||||
import { checkerRegistry } from "../../../src/server/checker/runner";
|
import { checkerRegistry } from "../../../src/server/checker/runner";
|
||||||
import { CommandChecker } from "../../../src/server/checker/runner/command/execute";
|
import { CommandChecker } from "../../../src/server/checker/runner/cmd/execute";
|
||||||
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
|
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
|
||||||
import { readRuntimeConfig } from "../../../src/server/config";
|
import { readRuntimeConfig } from "../../../src/server/config";
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ describe("loadConfig", () => {
|
|||||||
expect(t.timeoutMs).toBe(10000);
|
expect(t.timeoutMs).toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("解析最简 command 配置", async () => {
|
test("解析最简 cmd 配置", async () => {
|
||||||
const subdir = join(tempDir, "subdir");
|
const subdir = join(tempDir, "subdir");
|
||||||
await mkdir(subdir, { recursive: true });
|
await mkdir(subdir, { recursive: true });
|
||||||
const configPath = join(subdir, "cmd.yaml");
|
const configPath = join(subdir, "cmd.yaml");
|
||||||
@@ -140,8 +140,8 @@ describe("loadConfig", () => {
|
|||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- name: "check-nginx"
|
- name: "check-nginx"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "pgrep"
|
exec: "pgrep"
|
||||||
args: ["nginx"]
|
args: ["nginx"]
|
||||||
`,
|
`,
|
||||||
@@ -150,13 +150,13 @@ describe("loadConfig", () => {
|
|||||||
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 ResolvedCommandTarget;
|
const t = config.targets[0]! as ResolvedCommandTarget;
|
||||||
expect(t.type).toBe("command");
|
expect(t.type).toBe("cmd");
|
||||||
expect(t.name).toBe("check-nginx");
|
expect(t.name).toBe("check-nginx");
|
||||||
expect(t.command.exec).toBe("pgrep");
|
expect(t.cmd.exec).toBe("pgrep");
|
||||||
expect(t.command.args).toEqual(["nginx"]);
|
expect(t.cmd.args).toEqual(["nginx"]);
|
||||||
expect(t.command.cwd).toBe(subdir);
|
expect(t.cmd.cwd).toBe(subdir);
|
||||||
expect(t.command.maxOutputBytes).toBe(104857600);
|
expect(t.cmd.maxOutputBytes).toBe(104857600);
|
||||||
expect(t.command.env["PATH"]).toBeDefined();
|
expect(t.cmd.env["PATH"]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("解析完整配置", async () => {
|
test("解析完整配置", async () => {
|
||||||
@@ -177,7 +177,7 @@ defaults:
|
|||||||
headers:
|
headers:
|
||||||
Authorization: "Bearer token"
|
Authorization: "Bearer token"
|
||||||
maxBodyBytes: "50MB"
|
maxBodyBytes: "50MB"
|
||||||
command:
|
cmd:
|
||||||
cwd: "/tmp"
|
cwd: "/tmp"
|
||||||
maxOutputBytes: "10MB"
|
maxOutputBytes: "10MB"
|
||||||
targets:
|
targets:
|
||||||
@@ -193,8 +193,8 @@ targets:
|
|||||||
body:
|
body:
|
||||||
- contains: "ok"
|
- contains: "ok"
|
||||||
- name: "cmd-target"
|
- name: "cmd-target"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "ls"
|
exec: "ls"
|
||||||
args: ["/tmp"]
|
args: ["/tmp"]
|
||||||
expect:
|
expect:
|
||||||
@@ -222,10 +222,10 @@ targets:
|
|||||||
expect(http.timeoutMs).toBe(5000);
|
expect(http.timeoutMs).toBe(5000);
|
||||||
|
|
||||||
const cmd = config.targets[1]! as ResolvedCommandTarget;
|
const cmd = config.targets[1]! as ResolvedCommandTarget;
|
||||||
expect(cmd.type).toBe("command");
|
expect(cmd.type).toBe("cmd");
|
||||||
expect(cmd.command.exec).toBe("ls");
|
expect(cmd.cmd.exec).toBe("ls");
|
||||||
expect(cmd.command.args).toEqual(["/tmp"]);
|
expect(cmd.cmd.args).toEqual(["/tmp"]);
|
||||||
expect(cmd.command.maxOutputBytes).toBe(10485760);
|
expect(cmd.cmd.maxOutputBytes).toBe(10485760);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("绝对 dataDir 保持不变", async () => {
|
test("绝对 dataDir 保持不变", async () => {
|
||||||
@@ -386,18 +386,18 @@ targets:
|
|||||||
await expect(loadConfig(configPath)).rejects.toThrow("status 模式");
|
await expect(loadConfig(configPath)).rejects.toThrow("status 模式");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command target 缺少 exec 抛出错误", async () => {
|
test("cmd target 缺少 exec 抛出错误", async () => {
|
||||||
const configPath = join(tempDir, "no-exec.yaml");
|
const configPath = join(tempDir, "no-exec.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- name: "test"
|
- name: "test"
|
||||||
type: command
|
type: cmd
|
||||||
command: {}
|
cmd: {}
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
await expect(loadConfig(configPath)).rejects.toThrow("缺少 command.exec 字段");
|
await expect(loadConfig(configPath)).rejects.toThrow("缺少 cmd.exec 字段");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("非法 target type 抛出错误", async () => {
|
test("非法 target type 抛出错误", async () => {
|
||||||
@@ -538,14 +538,14 @@ targets:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("解析 command expect 配置", async () => {
|
test("解析 cmd expect 配置", async () => {
|
||||||
const configPath = join(tempDir, "cmd-expect.yaml");
|
const configPath = join(tempDir, "cmd-expect.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd-with-expect"
|
- name: "cmd-with-expect"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "mycheck"
|
exec: "mycheck"
|
||||||
expect:
|
expect:
|
||||||
exitCode: [0, 2]
|
exitCode: [0, 2]
|
||||||
@@ -560,7 +560,7 @@ targets:
|
|||||||
|
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
const t = config.targets[0]!;
|
const t = config.targets[0]!;
|
||||||
if (t.type === "command") {
|
if (t.type === "cmd") {
|
||||||
expect(t.expect).toEqual({
|
expect(t.expect).toEqual({
|
||||||
exitCode: [0, 2],
|
exitCode: [0, 2],
|
||||||
maxDurationMs: 5000,
|
maxDurationMs: 5000,
|
||||||
@@ -570,7 +570,7 @@ targets:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command cwd 相对配置文件目录", async () => {
|
test("cmd cwd 相对配置文件目录", async () => {
|
||||||
const subdir = join(tempDir, "cwd-test");
|
const subdir = join(tempDir, "cwd-test");
|
||||||
await mkdir(subdir, { recursive: true });
|
await mkdir(subdir, { recursive: true });
|
||||||
const configPath = join(subdir, "cwd.yaml");
|
const configPath = join(subdir, "cwd.yaml");
|
||||||
@@ -578,8 +578,8 @@ targets:
|
|||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cwd-test"
|
- name: "cwd-test"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "ls"
|
exec: "ls"
|
||||||
cwd: "scripts"
|
cwd: "scripts"
|
||||||
`,
|
`,
|
||||||
@@ -587,17 +587,17 @@ targets:
|
|||||||
|
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
const t = config.targets[0] as ResolvedCommandTarget;
|
const t = config.targets[0] as ResolvedCommandTarget;
|
||||||
expect(t.command.cwd).toBe(join(subdir, "scripts"));
|
expect(t.cmd.cwd).toBe(join(subdir, "scripts"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command env 覆盖", async () => {
|
test("cmd env 覆盖", async () => {
|
||||||
const configPath = join(tempDir, "env.yaml");
|
const configPath = join(tempDir, "env.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`targets:
|
`targets:
|
||||||
- name: "env-test"
|
- name: "env-test"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
env:
|
env:
|
||||||
LANG: "C"
|
LANG: "C"
|
||||||
@@ -607,9 +607,9 @@ targets:
|
|||||||
|
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
const t = config.targets[0] as ResolvedCommandTarget;
|
const t = config.targets[0] as ResolvedCommandTarget;
|
||||||
expect(t.command.env["LANG"]).toBe("C");
|
expect(t.cmd.env["LANG"]).toBe("C");
|
||||||
expect(t.command.env["CUSTOM_VAR"]).toBe("test");
|
expect(t.cmd.env["CUSTOM_VAR"]).toBe("test");
|
||||||
expect(t.command.env["PATH"]).toBeDefined();
|
expect(t.cmd.env["PATH"]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("解析 group 字段", async () => {
|
test("解析 group 字段", async () => {
|
||||||
@@ -1092,8 +1092,8 @@ targets:
|
|||||||
X-Response-Header:
|
X-Response-Header:
|
||||||
contains: "ok"
|
contains: "ok"
|
||||||
- name: "cmd-test"
|
- name: "cmd-test"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "true"
|
exec: "true"
|
||||||
env:
|
env:
|
||||||
CUSTOM_ENV_NAME: "custom"
|
CUSTOM_ENV_NAME: "custom"
|
||||||
@@ -1101,64 +1101,64 @@ targets:
|
|||||||
);
|
);
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
const http = config.targets[0] as ResolvedHttpTarget;
|
const http = config.targets[0] as ResolvedHttpTarget;
|
||||||
const command = config.targets[1] as ResolvedCommandTarget;
|
const cmdTarget = config.targets[1] as ResolvedCommandTarget;
|
||||||
expect(http.type).toBe("http");
|
expect(http.type).toBe("http");
|
||||||
expect(command.type).toBe("command");
|
expect(cmdTarget.type).toBe("cmd");
|
||||||
expect(http.http.headers["X-Default-Header"]).toBe("default");
|
expect(http.http.headers["X-Default-Header"]).toBe("default");
|
||||||
expect(http.http.headers["X-Custom-Header"]).toBe("custom");
|
expect(http.http.headers["X-Custom-Header"]).toBe("custom");
|
||||||
expect(command.command.env["CUSTOM_ENV_NAME"]).toBe("custom");
|
expect(cmdTarget.cmd.env["CUSTOM_ENV_NAME"]).toBe("custom");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command args 类型非法", async () => {
|
test("cmd args 类型非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-args.yaml",
|
"bad-cmd-args.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
args: "hello"
|
args: "hello"
|
||||||
`,
|
`,
|
||||||
"command.args 类型不合法",
|
"cmd.args 类型不合法",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command cwd 类型非法", async () => {
|
test("cmd cwd 类型非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-cwd.yaml",
|
"bad-cmd-cwd.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
cwd: 123
|
cwd: 123
|
||||||
`,
|
`,
|
||||||
"command.cwd 类型不合法",
|
"cmd.cwd 类型不合法",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command env 值类型非法", async () => {
|
test("cmd env 值类型非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-env.yaml",
|
"bad-cmd-env.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
env:
|
env:
|
||||||
COUNT: 123
|
COUNT: 123
|
||||||
`,
|
`,
|
||||||
"command.env.COUNT 类型不合法",
|
"cmd.env.COUNT 类型不合法",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command maxOutputBytes 非法", async () => {
|
test("cmd maxOutputBytes 非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-max-output.yaml",
|
"bad-cmd-max-output.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
maxOutputBytes: "1TB"
|
maxOutputBytes: "1TB"
|
||||||
`,
|
`,
|
||||||
@@ -1166,13 +1166,13 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command expect exitCode 类型非法", async () => {
|
test("cmd expect exitCode 类型非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-exit-code.yaml",
|
"bad-cmd-exit-code.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
expect:
|
expect:
|
||||||
exitCode: [1.5]
|
exitCode: [1.5]
|
||||||
@@ -1181,13 +1181,13 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command stdout 空 text rule 非法", async () => {
|
test("cmd stdout 空 text rule 非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-stdout-empty.yaml",
|
"bad-cmd-stdout-empty.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
@@ -1197,13 +1197,13 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command stderr 未知 operator 非法", async () => {
|
test("cmd stderr 未知 operator 非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-stderr-operator.yaml",
|
"bad-cmd-stderr-operator.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
expect:
|
expect:
|
||||||
stderr:
|
stderr:
|
||||||
@@ -1213,13 +1213,13 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command stdout match 正则非法", async () => {
|
test("cmd stdout match 正则非法", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-stdout-regex.yaml",
|
"bad-cmd-stdout-regex.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
expect:
|
expect:
|
||||||
stdout:
|
stdout:
|
||||||
@@ -1229,13 +1229,13 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command expect 未知字段失败", async () => {
|
test("cmd expect 未知字段失败", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"bad-command-expect-unknown.yaml",
|
"bad-cmd-expect-unknown.yaml",
|
||||||
`targets:
|
`targets:
|
||||||
- name: "cmd"
|
- name: "cmd"
|
||||||
type: command
|
type: cmd
|
||||||
command:
|
cmd:
|
||||||
exec: "echo"
|
exec: "echo"
|
||||||
expect:
|
expect:
|
||||||
status: [200]
|
status: [200]
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/command/types";
|
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types";
|
||||||
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
|
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
|
||||||
import type { ProbeStore } from "../../../src/server/checker/store";
|
import type { ProbeStore } from "../../../src/server/checker/store";
|
||||||
import type { ResolvedTargetBase } from "../../../src/server/checker/types";
|
import type { ResolvedTargetBase } from "../../../src/server/checker/types";
|
||||||
|
|
||||||
import { ProbeEngine } from "../../../src/server/checker/engine";
|
import { ProbeEngine } from "../../../src/server/checker/engine";
|
||||||
import { checkerRegistry } from "../../../src/server/checker/runner";
|
import { checkerRegistry } from "../../../src/server/checker/runner";
|
||||||
import { CommandChecker } from "../../../src/server/checker/runner/command/execute";
|
import { CommandChecker } from "../../../src/server/checker/runner/cmd/execute";
|
||||||
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
|
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
|
||||||
|
|
||||||
|
const processEnv = Object.fromEntries(
|
||||||
|
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
||||||
|
);
|
||||||
|
|
||||||
function createMockStore(targetNames: string[]) {
|
function createMockStore(targetNames: string[]) {
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
const targets = targetNames.map((name) => ({ id: nextId++, name }));
|
const targets = targetNames.map((name) => ({ id: nextId++, name }));
|
||||||
@@ -27,7 +31,7 @@ function createMockStore(targetNames: string[]) {
|
|||||||
name,
|
name,
|
||||||
target: "",
|
target: "",
|
||||||
timeout_ms: 5000,
|
timeout_ms: 5000,
|
||||||
type: "command" as const,
|
type: "cmd" as const,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
insertCheckResult(result: Record<string, unknown>) {
|
insertCheckResult(result: Record<string, unknown>) {
|
||||||
@@ -45,18 +49,18 @@ function ensureRegistered() {
|
|||||||
|
|
||||||
function makeCommandTarget(name: string, overrides?: Partial<ResolvedCommandTarget>): ResolvedCommandTarget {
|
function makeCommandTarget(name: string, overrides?: Partial<ResolvedCommandTarget>): ResolvedCommandTarget {
|
||||||
return {
|
return {
|
||||||
command: {
|
cmd: {
|
||||||
args: ["hello"],
|
args: ["-e", "console.log('hello')"],
|
||||||
cwd: "/tmp",
|
cwd: process.cwd(),
|
||||||
env: {},
|
env: processEnv,
|
||||||
exec: "echo",
|
exec: "bun",
|
||||||
maxOutputBytes: 1024 * 1024,
|
maxOutputBytes: 1024 * 1024,
|
||||||
},
|
},
|
||||||
group: "default",
|
group: "default",
|
||||||
intervalMs: 60000,
|
intervalMs: 60000,
|
||||||
name,
|
name,
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
type: "command",
|
type: "cmd",
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -72,7 +76,7 @@ describe("ProbeEngine", () => {
|
|||||||
expect(true).toBe(true);
|
expect(true).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("单次 probeGroup 执行 command 检查", async () => {
|
test("单次 probeGroup 执行 cmd 检查", async () => {
|
||||||
const target = makeCommandTarget("cmd-echo");
|
const target = makeCommandTarget("cmd-echo");
|
||||||
const mockStore = createMockStore(["cmd-echo"]) as unknown as ProbeStore;
|
const mockStore = createMockStore(["cmd-echo"]) as unknown as ProbeStore;
|
||||||
const engine = new ProbeEngine(mockStore, [target]);
|
const engine = new ProbeEngine(mockStore, [target]);
|
||||||
@@ -90,10 +94,22 @@ describe("ProbeEngine", () => {
|
|||||||
|
|
||||||
test("多个目标并发执行", async () => {
|
test("多个目标并发执行", async () => {
|
||||||
const targetA = makeCommandTarget("echo-a", {
|
const targetA = makeCommandTarget("echo-a", {
|
||||||
command: { args: ["a"], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 },
|
cmd: {
|
||||||
|
args: ["-e", "console.log('a')"],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: processEnv,
|
||||||
|
exec: "bun",
|
||||||
|
maxOutputBytes: 1024 * 1024,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const targetB = makeCommandTarget("echo-b", {
|
const targetB = makeCommandTarget("echo-b", {
|
||||||
command: { args: ["b"], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 },
|
cmd: {
|
||||||
|
args: ["-e", "console.log('b')"],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: processEnv,
|
||||||
|
exec: "bun",
|
||||||
|
maxOutputBytes: 1024 * 1024,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockStore = createMockStore(["echo-a", "echo-b"]) as unknown as ProbeStore;
|
const mockStore = createMockStore(["echo-a", "echo-b"]) as unknown as ProbeStore;
|
||||||
@@ -110,7 +126,13 @@ describe("ProbeEngine", () => {
|
|||||||
|
|
||||||
test("失败目标不阻塞其他目标", async () => {
|
test("失败目标不阻塞其他目标", async () => {
|
||||||
const badTarget = makeCommandTarget("bad-cmd", {
|
const badTarget = makeCommandTarget("bad-cmd", {
|
||||||
command: { args: [], cwd: "/tmp", env: {}, exec: "false", maxOutputBytes: 1024 * 1024 },
|
cmd: {
|
||||||
|
args: ["-e", "process.exit(1)"],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: processEnv,
|
||||||
|
exec: "bun",
|
||||||
|
maxOutputBytes: 1024 * 1024,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const goodTarget = makeCommandTarget("good-cmd");
|
const goodTarget = makeCommandTarget("good-cmd");
|
||||||
|
|
||||||
@@ -133,7 +155,7 @@ describe("ProbeEngine", () => {
|
|||||||
|
|
||||||
test("checker rejected 时写入 internal error 结果", async () => {
|
test("checker rejected 时写入 internal error 结果", async () => {
|
||||||
ensureRegistered();
|
ensureRegistered();
|
||||||
const checker = checkerRegistry.get("command");
|
const checker = checkerRegistry.get("cmd");
|
||||||
const originalExecute = checker.execute.bind(checker);
|
const originalExecute = checker.execute.bind(checker);
|
||||||
checker.execute = async (target, ctx) => {
|
checker.execute = async (target, ctx) => {
|
||||||
if (target.name === "reject-cmd") {
|
if (target.name === "reject-cmd") {
|
||||||
@@ -176,7 +198,13 @@ describe("ProbeEngine", () => {
|
|||||||
test("并发限制 maxConcurrentChecks", async () => {
|
test("并发限制 maxConcurrentChecks", async () => {
|
||||||
const targets = Array.from({ length: 5 }, (_, i) =>
|
const targets = Array.from({ length: 5 }, (_, i) =>
|
||||||
makeCommandTarget(`cmd-${i}`, {
|
makeCommandTarget(`cmd-${i}`, {
|
||||||
command: { args: [String(i)], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 },
|
cmd: {
|
||||||
|
args: ["-e", `console.log('${i}')`],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: processEnv,
|
||||||
|
exec: "bun",
|
||||||
|
maxOutputBytes: 1024 * 1024,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { checkExitCode } from "../../../../../src/server/checker/runner/command/expect";
|
import { checkExitCode } from "../../../../../src/server/checker/runner/cmd/expect";
|
||||||
|
|
||||||
describe("checkExitCode", () => {
|
describe("checkExitCode", () => {
|
||||||
test("exitCode 在允许列表中匹配成功", () => {
|
test("exitCode 在允许列表中匹配成功", () => {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import type { ResolvedCommandTarget } from "../../../../../src/server/checker/runner/command/types";
|
import type { ResolvedCommandTarget } from "../../../../../src/server/checker/runner/cmd/types";
|
||||||
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";
|
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";
|
||||||
|
|
||||||
import { CommandChecker } from "../../../../../src/server/checker/runner/command/execute";
|
import { CommandChecker } from "../../../../../src/server/checker/runner/cmd/execute";
|
||||||
|
|
||||||
const checker = new CommandChecker();
|
const checker = new CommandChecker();
|
||||||
|
|
||||||
@@ -18,37 +18,37 @@ function makeCtx(timeoutMs = 5000): CheckerContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeTarget(
|
function makeTarget(
|
||||||
command: Partial<ResolvedCommandTarget["command"]>,
|
cmd: Partial<ResolvedCommandTarget["cmd"]>,
|
||||||
overrides?: Partial<ResolvedCommandTarget>,
|
overrides?: Partial<ResolvedCommandTarget>,
|
||||||
): ResolvedCommandTarget {
|
): ResolvedCommandTarget {
|
||||||
return {
|
return {
|
||||||
command: {
|
cmd: {
|
||||||
args: ["hello"],
|
args: ["-e", "console.log('hello')"],
|
||||||
cwd: "/tmp",
|
cwd: process.cwd(),
|
||||||
env: processEnv,
|
env: processEnv,
|
||||||
exec: "echo",
|
exec: "bun",
|
||||||
maxOutputBytes: 1024 * 1024,
|
maxOutputBytes: 1024 * 1024,
|
||||||
...command,
|
...cmd,
|
||||||
},
|
},
|
||||||
group: "default",
|
group: "default",
|
||||||
intervalMs: 60000,
|
intervalMs: 60000,
|
||||||
name: "test-cmd",
|
name: "test-cmd",
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
type: "command",
|
type: "cmd",
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("CommandChecker", () => {
|
describe("CommandChecker", () => {
|
||||||
test("exitCode=0 成功", async () => {
|
test("exitCode=0 成功", async () => {
|
||||||
const result = await checker.execute(makeTarget({ args: [], exec: "true" }), makeCtx());
|
const result = await checker.execute(makeTarget({ args: ["-e", "process.exit(0)"], exec: "bun" }), makeCtx());
|
||||||
expect(result.matched).toBe(true);
|
expect(result.matched).toBe(true);
|
||||||
expect(result.statusDetail).toBe("exitCode=0");
|
expect(result.statusDetail).toBe("exitCode=0");
|
||||||
expect(result.failure).toBeNull();
|
expect(result.failure).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("exitCode=1 不匹配默认 [0]", async () => {
|
test("exitCode=1 不匹配默认 [0]", async () => {
|
||||||
const result = await checker.execute(makeTarget({ args: [], exec: "false" }), makeCtx());
|
const result = await checker.execute(makeTarget({ args: ["-e", "process.exit(1)"], exec: "bun" }), makeCtx());
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
expect(result.statusDetail).toBe("exitCode=1");
|
expect(result.statusDetail).toBe("exitCode=1");
|
||||||
expect(result.failure!.phase).toBe("exitCode");
|
expect(result.failure!.phase).toBe("exitCode");
|
||||||
@@ -56,7 +56,7 @@ describe("CommandChecker", () => {
|
|||||||
|
|
||||||
test("exitCode=1 匹配自定义 [1]", async () => {
|
test("exitCode=1 匹配自定义 [1]", async () => {
|
||||||
const result = await checker.execute(
|
const result = await checker.execute(
|
||||||
makeTarget({ args: [], exec: "false" }, { expect: { exitCode: [1] } }),
|
makeTarget({ args: ["-e", "process.exit(1)"], exec: "bun" }, { expect: { exitCode: [1] } }),
|
||||||
makeCtx(),
|
makeCtx(),
|
||||||
);
|
);
|
||||||
expect(result.matched).toBe(true);
|
expect(result.matched).toBe(true);
|
||||||
@@ -64,26 +64,35 @@ describe("CommandChecker", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("命令不存在返回 spawn 错误", async () => {
|
test("命令不存在返回 spawn 错误", async () => {
|
||||||
const result = await checker.execute(makeTarget({ exec: "/nonexistent/command/xyz" }), makeCtx());
|
const result = await checker.execute(makeTarget({ exec: "dial-command-not-found-xyz" }), makeCtx());
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
expect(result.failure!.phase).toBe("exitCode");
|
expect(result.failure!.phase).toBe("exitCode");
|
||||||
expect(result.failure!.message).toBeTruthy();
|
expect(result.failure!.message).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("超时返回错误", async () => {
|
test("超时返回错误", async () => {
|
||||||
const result = await checker.execute(makeTarget({ args: ["10"], exec: "sleep" }, { timeoutMs: 100 }), makeCtx(100));
|
const result = await checker.execute(
|
||||||
|
makeTarget({ args: ["-e", "await Bun.sleep(10000)"], exec: "bun" }, { timeoutMs: 100 }),
|
||||||
|
makeCtx(100),
|
||||||
|
);
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
expect(result.failure!.message).toContain("超时");
|
expect(result.failure!.message).toContain("超时");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stdout 输出捕获", async () => {
|
test("stdout 输出捕获", async () => {
|
||||||
const result = await checker.execute(makeTarget({ args: ["hello world"], exec: "echo" }), makeCtx());
|
const result = await checker.execute(
|
||||||
|
makeTarget({ args: ["-e", "console.log('hello world')"], exec: "bun" }),
|
||||||
|
makeCtx(),
|
||||||
|
);
|
||||||
expect(result.matched).toBe(true);
|
expect(result.matched).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stdout 匹配 expect", async () => {
|
test("stdout 匹配 expect", async () => {
|
||||||
const result = await checker.execute(
|
const result = await checker.execute(
|
||||||
makeTarget({ args: ["hello"], exec: "echo" }, { expect: { stdout: [{ contains: "hello" }] } }),
|
makeTarget(
|
||||||
|
{ args: ["-e", "console.log('hello')"], exec: "bun" },
|
||||||
|
{ expect: { stdout: [{ contains: "hello" }] } },
|
||||||
|
),
|
||||||
makeCtx(),
|
makeCtx(),
|
||||||
);
|
);
|
||||||
expect(result.matched).toBe(true);
|
expect(result.matched).toBe(true);
|
||||||
@@ -91,7 +100,10 @@ describe("CommandChecker", () => {
|
|||||||
|
|
||||||
test("stdout 不匹配 expect", async () => {
|
test("stdout 不匹配 expect", async () => {
|
||||||
const result = await checker.execute(
|
const result = await checker.execute(
|
||||||
makeTarget({ args: ["hello"], exec: "echo" }, { expect: { stdout: [{ contains: "nonexistent" }] } }),
|
makeTarget(
|
||||||
|
{ args: ["-e", "console.log('hello')"], exec: "bun" },
|
||||||
|
{ expect: { stdout: [{ contains: "nonexistent" }] } },
|
||||||
|
),
|
||||||
makeCtx(),
|
makeCtx(),
|
||||||
);
|
);
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
@@ -100,7 +112,10 @@ describe("CommandChecker", () => {
|
|||||||
|
|
||||||
test("stderr 匹配 expect", async () => {
|
test("stderr 匹配 expect", async () => {
|
||||||
const result = await checker.execute(
|
const result = await checker.execute(
|
||||||
makeTarget({ args: ["-c", "echo error >&2"], exec: "bash" }, { expect: { stderr: [{ contains: "error" }] } }),
|
makeTarget(
|
||||||
|
{ args: ["-e", "process.stderr.write('error\\n')"], exec: "bun" },
|
||||||
|
{ expect: { stderr: [{ contains: "error" }] } },
|
||||||
|
),
|
||||||
makeCtx(),
|
makeCtx(),
|
||||||
);
|
);
|
||||||
expect(result.matched).toBe(true);
|
expect(result.matched).toBe(true);
|
||||||
@@ -108,7 +123,7 @@ describe("CommandChecker", () => {
|
|||||||
|
|
||||||
test("输出超过 maxOutputBytes", async () => {
|
test("输出超过 maxOutputBytes", async () => {
|
||||||
const result = await checker.execute(
|
const result = await checker.execute(
|
||||||
makeTarget({ args: ["-c", "yes | head -1000"], exec: "bash", maxOutputBytes: 10 }),
|
makeTarget({ args: ["-e", "process.stdout.write('y\\n'.repeat(1000))"], exec: "bun", maxOutputBytes: 10 }),
|
||||||
makeCtx(),
|
makeCtx(),
|
||||||
);
|
);
|
||||||
expect(result.matched).toBe(false);
|
expect(result.matched).toBe(false);
|
||||||
@@ -116,7 +131,7 @@ describe("CommandChecker", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("durationMs 非空", async () => {
|
test("durationMs 非空", async () => {
|
||||||
const result = await checker.execute(makeTarget({ args: [], exec: "true" }), makeCtx());
|
const result = await checker.execute(makeTarget({ args: ["-e", "process.exit(0)"], exec: "bun" }), makeCtx());
|
||||||
expect(result.durationMs).not.toBeNull();
|
expect(result.durationMs).not.toBeNull();
|
||||||
expect(result.durationMs!).toBeGreaterThanOrEqual(0);
|
expect(result.durationMs!).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
@@ -134,8 +149,8 @@ describe("CommandChecker", () => {
|
|||||||
makeTarget(
|
makeTarget(
|
||||||
{
|
{
|
||||||
args: ["-e", "console.log(process.env.DIAL_TEST_ENV ?? '')"],
|
args: ["-e", "console.log(process.env.DIAL_TEST_ENV ?? '')"],
|
||||||
env: { DIAL_TEST_ENV: "resolved-env" },
|
env: { ...processEnv, DIAL_TEST_ENV: "resolved-env" },
|
||||||
exec: process.execPath,
|
exec: "bun",
|
||||||
},
|
},
|
||||||
{ expect: { stdout: [{ contains: "resolved-env" }] } },
|
{ expect: { stdout: [{ contains: "resolved-env" }] } },
|
||||||
),
|
),
|
||||||
@@ -146,11 +161,11 @@ describe("CommandChecker", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("serialize 返回命令摘要和 config JSON", () => {
|
test("serialize 返回命令摘要和 config JSON", () => {
|
||||||
const target = makeTarget({ args: ["hello"], exec: "echo" });
|
const target = makeTarget({ args: ["-e", "console.log('hello')"], exec: "bun" });
|
||||||
const s = checker.serialize(target);
|
const s = checker.serialize(target);
|
||||||
expect(s.target).toBe("exec echo hello");
|
expect(s.target).toBe("exec bun -e console.log('hello')");
|
||||||
const config = JSON.parse(s.config) as { args: string[]; exec: string };
|
const config = JSON.parse(s.config) as { args: string[]; exec: string };
|
||||||
expect(config.exec).toBe("echo");
|
expect(config.exec).toBe("bun");
|
||||||
expect(config.args).toEqual(["hello"]);
|
expect(config.args).toEqual(["-e", "console.log('hello')"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -45,8 +45,8 @@ describe("CheckerRegistry", () => {
|
|||||||
test("查询支持的 type 列表", () => {
|
test("查询支持的 type 列表", () => {
|
||||||
const registry = new CheckerRegistry();
|
const registry = new CheckerRegistry();
|
||||||
registry.register(createChecker("http"));
|
registry.register(createChecker("http"));
|
||||||
registry.register(createChecker("command"));
|
registry.register(createChecker("cmd"));
|
||||||
expect(registry.supportedTypes).toEqual(["http", "command"]);
|
expect(registry.supportedTypes).toEqual(["http", "cmd"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("definitions 返回注册定义", () => {
|
test("definitions 返回注册定义", () => {
|
||||||
@@ -66,8 +66,8 @@ describe("CheckerRegistry", () => {
|
|||||||
const second = createDefaultCheckerRegistry();
|
const second = createDefaultCheckerRegistry();
|
||||||
first.register(createChecker("custom"));
|
first.register(createChecker("custom"));
|
||||||
|
|
||||||
expect(first.supportedTypes).toEqual(["http", "command", "custom"]);
|
expect(first.supportedTypes).toEqual(["http", "cmd", "custom"]);
|
||||||
expect(second.supportedTypes).toEqual(["http", "command"]);
|
expect(second.supportedTypes).toEqual(["http", "cmd"]);
|
||||||
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,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { checkTextRules } from "../../../../../src/server/checker/runner/command/text";
|
import { checkTextRules } from "../../../../../src/server/checker/runner/cmd/text";
|
||||||
|
|
||||||
describe("checkTextRules", () => {
|
describe("checkTextRules", () => {
|
||||||
test("无规则返回匹配成功", () => {
|
test("无规则返回匹配成功", () => {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { mkdir } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/command/types";
|
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types";
|
||||||
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
|
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
|
||||||
import type { CheckFailure } from "../../../src/server/checker/types";
|
import type { CheckFailure } from "../../../src/server/checker/types";
|
||||||
|
|
||||||
import { checkerRegistry } from "../../../src/server/checker/runner";
|
import { checkerRegistry } from "../../../src/server/checker/runner";
|
||||||
import { CommandChecker } from "../../../src/server/checker/runner/command/execute";
|
import { CommandChecker } from "../../../src/server/checker/runner/cmd/execute";
|
||||||
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
|
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
|
||||||
import { ProbeStore } from "../../../src/server/checker/store";
|
import { ProbeStore } from "../../../src/server/checker/store";
|
||||||
import { rmRetry } from "../../helpers";
|
import { rmRetry } from "../../helpers";
|
||||||
@@ -42,7 +42,7 @@ const httpTarget: ResolvedHttpTarget = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const commandTarget: ResolvedCommandTarget = {
|
const commandTarget: ResolvedCommandTarget = {
|
||||||
command: {
|
cmd: {
|
||||||
args: ["-c", "1", "localhost"],
|
args: ["-c", "1", "localhost"],
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
env: {},
|
env: {},
|
||||||
@@ -53,7 +53,7 @@ const commandTarget: ResolvedCommandTarget = {
|
|||||||
intervalMs: 60000,
|
intervalMs: 60000,
|
||||||
name: "test-cmd",
|
name: "test-cmd",
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
type: "command",
|
type: "cmd",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("ProbeStore", () => {
|
describe("ProbeStore", () => {
|
||||||
@@ -75,7 +75,7 @@ describe("ProbeStore", () => {
|
|||||||
expect(store.getTargets()).toHaveLength(0);
|
expect(store.getTargets()).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("同步 http 和 command targets", () => {
|
test("同步 http 和 cmd targets", () => {
|
||||||
store.syncTargets([httpTarget, commandTarget]);
|
store.syncTargets([httpTarget, commandTarget]);
|
||||||
const targets = store.getTargets();
|
const targets = store.getTargets();
|
||||||
expect(targets).toHaveLength(2);
|
expect(targets).toHaveLength(2);
|
||||||
@@ -106,9 +106,9 @@ describe("ProbeStore", () => {
|
|||||||
expect(JSON.parse(t.expect!)).toEqual({ maxDurationMs: 3000, status: [200] });
|
expect(JSON.parse(t.expect!)).toEqual({ maxDurationMs: 3000, status: [200] });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("command target 字段正确", () => {
|
test("cmd target 字段正确", () => {
|
||||||
const t = store.getTargets().find((t) => t.name === "test-cmd")!;
|
const t = store.getTargets().find((t) => t.name === "test-cmd")!;
|
||||||
expect(t.type).toBe("command");
|
expect(t.type).toBe("cmd");
|
||||||
expect(t.target).toBe("exec ping -c 1 localhost");
|
expect(t.target).toBe("exec ping -c 1 localhost");
|
||||||
const config = JSON.parse(t.config) as { args: string[]; cwd: string; exec: string; maxOutputBytes: number };
|
const config = JSON.parse(t.config) as { args: string[]; cwd: string; exec: string; maxOutputBytes: number };
|
||||||
expect(config.exec).toBe("ping");
|
expect(config.exec).toBe("ping");
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
|
|||||||
|
|
||||||
describe("createTargetTableColumns", () => {
|
describe("createTargetTableColumns", () => {
|
||||||
test("生成 7 个目标表格列", () => {
|
test("生成 7 个目标表格列", () => {
|
||||||
const columns = createTargetTableColumns(["http", "command"]);
|
const columns = createTargetTableColumns(["http", "cmd"]);
|
||||||
|
|
||||||
expect(columns.map((column) => column.colKey)).toEqual([
|
expect(columns.map((column) => column.colKey)).toEqual([
|
||||||
"latestCheck.matched",
|
"latestCheck.matched",
|
||||||
@@ -48,14 +48,14 @@ describe("createTargetTableColumns", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("根据 checkerTypes 生成类型筛选器", () => {
|
test("根据 checkerTypes 生成类型筛选器", () => {
|
||||||
const typeColumn = getColumn(createTargetTableColumns(["http", "command", "tcp"]), "type");
|
const typeColumn = getColumn(createTargetTableColumns(["http", "cmd", "tcp"]), "type");
|
||||||
const filter = typeColumn.filter as TableFilter;
|
const filter = typeColumn.filter as TableFilter;
|
||||||
|
|
||||||
expect(filter.type).toBe("single");
|
expect(filter.type).toBe("single");
|
||||||
expect(filter.list).toEqual([
|
expect(filter.list).toEqual([
|
||||||
{ label: "全部", value: "" },
|
{ label: "全部", value: "" },
|
||||||
{ label: "http", value: "http" },
|
{ label: "http", value: "http" },
|
||||||
{ label: "command", value: "command" },
|
{ label: "cmd", value: "cmd" },
|
||||||
{ label: "tcp", value: "tcp" },
|
{ label: "tcp", value: "tcp" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user