refactor: 移除顶层 defaults 配置段,简化为 target 显式字段 > 代码内置默认值
- 移除 DefaultsConfig 类型、ProbeConfig.defaults 字段 - 移除 CheckerSchemas.defaults、ResolveContext.defaults、CheckerValidationInput.defaults - 更新所有 checker schema/resolve/validate 删除 defaults 合并逻辑 - 更新 config-loader 不再读取传递 defaults - 更新测试、README、DEVELOPMENT、probes.example.yaml - 重新生成 probe-config.schema.json(不含 defaults) - 同步 delta specs 到主规范 - 归档 openspec change
This commit is contained in:
@@ -64,7 +64,7 @@ src/
|
|||||||
metrics.ts GET /api/targets/:id/metrics
|
metrics.ts GET /api/targets/:id/metrics
|
||||||
history.ts GET /api/targets/:id/history
|
history.ts GET /api/targets/:id/history
|
||||||
checker/
|
checker/
|
||||||
types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface)
|
types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、CheckResult 等基础 interface)
|
||||||
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig)
|
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig)
|
||||||
variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成
|
variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成
|
||||||
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||||||
@@ -237,7 +237,7 @@ export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode:
|
|||||||
- **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetId` 等内部字段
|
- **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetId` 等内部字段
|
||||||
- 存储层类型(`StoredTarget`、`StoredCheckResult`)独立定义,与 API 类型分离
|
- 存储层类型(`StoredTarget`、`StoredCheckResult`)独立定义,与 API 类型分离
|
||||||
- **Checker 类型分层**:
|
- **Checker 类型分层**:
|
||||||
- `checker/types.ts` 定义 base interface(`ResolvedTargetBase`、`RawTargetConfig`、`DefaultsConfig`),使用 index signature 支持扩展
|
- `checker/types.ts` 定义 base interface(`ResolvedTargetBase`、`RawTargetConfig`),使用 index signature 支持扩展
|
||||||
- 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget`、`ResolvedCommandTarget`),满足 base interface 约束
|
- 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget`、`ResolvedCommandTarget`),满足 base interface 约束
|
||||||
- 中间层(engine、store、config-loader)只依赖 base interface,不感知具体 checker 类型
|
- 中间层(engine、store、config-loader)只依赖 base interface,不感知具体 checker 类型
|
||||||
- `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>` 使用泛型约束 `resolve` 返回值以及 `execute`、`serialize` 的 target 参数
|
- `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>` 使用泛型约束 `resolve` 返回值以及 `execute`、`serialize` 的 target 参数
|
||||||
@@ -267,9 +267,9 @@ export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode:
|
|||||||
|
|
||||||
契约层使用 `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`。只有明确声明的动态键值表可以开放任意键名,例如 `variables`、`http.headers`、`defaults.http.headers`、`expect.headers`、`cmd.env`。
|
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `variables`、`http.headers`、`expect.headers`、`cmd.env`。
|
||||||
|
|
||||||
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 `defaults`/root 路径。
|
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 root 路径。
|
||||||
|
|
||||||
新增或修改配置字段时必须同步更新:TypeBox schema fragments、`probe-config.schema.json` 导出、对应语义 validator、单元测试和 README/DEVELOPMENT 用户文档。提交前运行 `bun run schema:check` 确认导出 schema 与 fragments 一致。
|
新增或修改配置字段时必须同步更新:TypeBox schema fragments、`probe-config.schema.json` 导出、对应语义 validator、单元测试和 README/DEVELOPMENT 用户文档。提交前运行 `bun run schema:check` 确认导出 schema 与 fragments 一致。
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ checkerRegistry(单例)
|
|||||||
| ------------- | ------------------------------------------------------------------------------------------ |
|
| ------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| `index.ts` | 模块入口,re-export Checker 类 |
|
| `index.ts` | 模块入口,re-export Checker 类 |
|
||||||
| `types.ts` | Checker 专属类型(RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) |
|
| `types.ts` | Checker 专属类型(RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) |
|
||||||
| `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
| `schema.ts` | TypeBox 契约 schema(config / expect 两部分) |
|
||||||
| `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 专用断言函数 |
|
||||||
@@ -315,14 +315,13 @@ checkerRegistry(单例)
|
|||||||
|
|
||||||
- `RawXxxTargetConfig` — YAML 原始配置类型
|
- `RawXxxTargetConfig` — YAML 原始配置类型
|
||||||
- `RawXxxExpectConfig` / `ResolvedXxxExpectConfig` — Raw expect 字段类型与运行期 Resolved expect 执行计划类型
|
- `RawXxxExpectConfig` / `ResolvedXxxExpectConfig` — Raw expect 字段类型与运行期 Resolved expect 执行计划类型
|
||||||
- `XxxDefaultsConfig` — defaults 专属字段类型
|
|
||||||
- `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量
|
- `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量
|
||||||
|
|
||||||
**注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature(`[key: string]: unknown`),checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。
|
**注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature(`[key: string]: unknown`),checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。
|
||||||
|
|
||||||
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
||||||
|
|
||||||
在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / defaults / expect 三部分)。参考 `http/schema.ts`、`cmd/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
|
在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / expect 两部分)。参考 `http/schema.ts`、`cmd/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
|
||||||
|
|
||||||
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
||||||
|
|
||||||
@@ -368,17 +367,16 @@ TcpChecker implements Checker
|
|||||||
readonly schemas ← tcpCheckerSchemas
|
readonly schemas ← tcpCheckerSchemas
|
||||||
|
|
||||||
validate(input) ← 调用 validateTcpConfig(input)
|
validate(input) ← 调用 validateTcpConfig(input)
|
||||||
resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget
|
resolve(target, ctx)← 内置默认值填充 + 解析,返回 satisfies ResolvedTcpTarget
|
||||||
execute(target, ctx)← 执行检查,返回 CheckResult
|
execute(target, ctx)← 执行检查,返回 CheckResult
|
||||||
serialize(target) ← 返回 { config, target } 用于 DB 持久化
|
serialize(target) ← 返回 { config, target } 用于 DB 持久化
|
||||||
```
|
```
|
||||||
|
|
||||||
**`resolve()` 规范**:
|
**`resolve()` 规范**:
|
||||||
|
|
||||||
- 只做默认值合并、路径解析、单位转换,**不执行校验**
|
- 只做内置默认值填充、路径解析、单位转换,**不执行校验**
|
||||||
- 若 checker 支持 expect,必须保留 `rawExpect` 为变量替换后的 Raw 配置快照,并生成只供 `execute()` 使用的 Resolved `expect`
|
- 若 checker 支持 expect,必须保留 `rawExpect` 为变量替换后的 Raw 配置快照,并生成只供 `execute()` 使用的 Resolved `expect`
|
||||||
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
|
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
|
||||||
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型)
|
|
||||||
|
|
||||||
**expect 五层管线**:每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数:
|
**expect 五层管线**:每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数:
|
||||||
|
|
||||||
@@ -457,13 +455,13 @@ if (r.body) {
|
|||||||
|
|
||||||
注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**:
|
注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**:
|
||||||
|
|
||||||
| 模块 | 自动行为 |
|
| 模块 | 自动行为 |
|
||||||
| -------------------- | ------------------------------------------------------------------------ |
|
| -------------------- | -------------------------------------------------------------- |
|
||||||
| `schema/builder.ts` | 遍历 registry 生成全量 JSON Schema(defaults.tcp + target.tcp + expect) |
|
| `schema/builder.ts` | 遍历 registry 生成全量 JSON Schema(target.tcp + expect) |
|
||||||
| `schema/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` |
|
| `schema/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` |
|
||||||
| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` |
|
| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` |
|
||||||
| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` |
|
| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` |
|
||||||
| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` |
|
| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` |
|
||||||
|
|
||||||
注意:自动适配指上述中间层不需要新增 `switch/case` 或类型分支;开发者仍需按后续步骤更新配置示例、文档和测试。
|
注意:自动适配指上述中间层不需要新增 `switch/case` 或类型分支;开发者仍需按后续步骤更新配置示例、文档和测试。
|
||||||
|
|
||||||
|
|||||||
102
README.md
102
README.md
@@ -135,13 +135,8 @@ probes: # 拨测运行时配置(可省略)
|
|||||||
variables: # 配置变量(可省略)
|
variables: # 配置变量(可省略)
|
||||||
env_name: "生产"
|
env_name: "生产"
|
||||||
base_url: "https://api.example.com"
|
base_url: "https://api.example.com"
|
||||||
|
default_interval: "30s" # 通过变量在多个 target 间共享常用值
|
||||||
defaults: # 全局默认值(均可省略)
|
default_timeout: "10s"
|
||||||
interval: "30s"
|
|
||||||
timeout: "10s"
|
|
||||||
# http: ...
|
|
||||||
# cmd: ...
|
|
||||||
# llm: ...
|
|
||||||
|
|
||||||
targets: # 拨测目标列表(必填)
|
targets: # 拨测目标列表(必填)
|
||||||
- id: "baidu-home"
|
- id: "baidu-home"
|
||||||
@@ -197,14 +192,15 @@ targets: # 拨测目标列表(必填)
|
|||||||
|
|
||||||
控制台始终输出(pretty 格式),文件始终输出 JSONL 格式并支持滚动。`rotation.size` 和 `rotation.frequency` 任一条件触发即滚动。
|
控制台始终输出(pretty 格式),文件始终输出 JSONL 格式并支持滚动。`rotation.size` 和 `rotation.frequency` 任一条件触发即滚动。
|
||||||
|
|
||||||
### defaults — 全局默认值
|
### 内置默认值
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
未显式配置时,系统使用以下内置默认值:
|
||||||
| ---------- | -------- | ---- | ------ |
|
|
||||||
| `interval` | 拨测间隔 | 否 | `30s` |
|
|
||||||
| `timeout` | 超时时间 | 否 | `10s` |
|
|
||||||
|
|
||||||
各 checker 专属的默认配置见对应章节。
|
- `interval`:`30s`(拨测间隔)
|
||||||
|
- `timeout`:`10s`(超时时间)
|
||||||
|
- 各 checker 专属默认值见对应章节
|
||||||
|
|
||||||
|
如需在多个 target 间共享相同的配置值,可使用 `variables` 定义变量,然后在 target 中通过 `${var}` 引用。例如在 `variables` 中定义 `default_interval: "30s"`,在多个 target 的 `interval` 字段写 `${default_interval}`。
|
||||||
|
|
||||||
### variables — 配置变量
|
### variables — 配置变量
|
||||||
|
|
||||||
@@ -227,30 +223,23 @@ targets: # 拨测目标列表(必填)
|
|||||||
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | |
|
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | |
|
||||||
| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`icmp`、`llm` | 是 | |
|
| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`icmp`、`llm` | 是 | |
|
||||||
| `group` | 分组名称 | 否 | `default` |
|
| `group` | 分组名称 | 否 | `default` |
|
||||||
| `interval` | 覆盖全局拨测间隔 | 否 | |
|
| `interval` | 拨测间隔,未配置时使用内置默认值 `30s` | 否 | `30s` |
|
||||||
| `timeout` | 覆盖全局超时时间 | 否 | |
|
| `timeout` | 超时时间,未配置时使用内置默认值 `10s` | 否 | `10s` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### HTTP Checker(`type: http`)
|
### HTTP Checker(`type: http`)
|
||||||
|
|
||||||
**全局默认值(`defaults.http`)**
|
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
|
||||||
| -------------- | -------------------------------------------------- | ---- | ------- |
|
|
||||||
| `maxBodyBytes` | 响应体最大字节数 | 否 | `100MB` |
|
|
||||||
| `headers` | 默认请求头(target 中的 headers 会合并覆盖同名头) | 否 | |
|
|
||||||
|
|
||||||
**配置项**
|
**配置项**
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
| ------------------- | --------------------------------------- | ---- | ------- |
|
| ------------------- | ------------------- | ---- | ------- |
|
||||||
| `http.url` | 目标 URL | 是 | |
|
| `http.url` | 目标 URL | 是 | |
|
||||||
| `http.method` | HTTP 方法 | 否 | `GET` |
|
| `http.method` | HTTP 方法 | 否 | `GET` |
|
||||||
| `http.headers` | 请求头(与 defaults.http.headers 合并) | 否 | |
|
| `http.headers` | 请求头 | 否 | |
|
||||||
| `http.body` | 请求体 | 否 | |
|
| `http.body` | 请求体 | 否 | |
|
||||||
| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
|
| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
|
||||||
| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` |
|
| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` |
|
||||||
|
|
||||||
**expect 校验项**
|
**expect 校验项**
|
||||||
|
|
||||||
@@ -288,13 +277,6 @@ targets: # 拨测目标列表(必填)
|
|||||||
|
|
||||||
### Cmd Checker(`type: cmd`)
|
### Cmd Checker(`type: cmd`)
|
||||||
|
|
||||||
**全局默认值(`defaults.cmd`)**
|
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
|
||||||
| ---------------- | -------------------------------------- | ---- | ------- |
|
|
||||||
| `maxOutputBytes` | 输出最大字节数 | 否 | `100MB` |
|
|
||||||
| `cwd` | 默认工作目录(相对于配置文件所在目录) | 否 | `.` |
|
|
||||||
|
|
||||||
**配置项**
|
**配置项**
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
@@ -405,14 +387,6 @@ targets: # 拨测目标列表(必填)
|
|||||||
|
|
||||||
### UDP Checker(`type: udp`)
|
### UDP Checker(`type: udp`)
|
||||||
|
|
||||||
**全局默认值(`defaults.udp`)**
|
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
|
||||||
| ------------------ | ---------------------------------------- | ---- | ------ |
|
|
||||||
| `encoding` | payload 编码 | 否 | `text` |
|
|
||||||
| `responseEncoding` | 响应解码 | 否 | `text` |
|
|
||||||
| `maxResponseBytes` | 响应最大字节数,支持 `KB`/`MB`/`GB` 单位 | 否 | `4KB` |
|
|
||||||
|
|
||||||
**配置项**
|
**配置项**
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
@@ -503,33 +477,21 @@ ICMP checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS
|
|||||||
|
|
||||||
### LLM Checker(`type: llm`)
|
### LLM Checker(`type: llm`)
|
||||||
|
|
||||||
**全局默认值(`defaults.llm`)**
|
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
|
||||||
| ----------------- | ------------------- | ---- | ------- |
|
|
||||||
| `mode` | 调用模式 | 否 | |
|
|
||||||
| `headers` | 默认请求头 | 否 | |
|
|
||||||
| `ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
|
|
||||||
| `options` | 生成选项 | 否 | |
|
|
||||||
| `providerOptions` | Provider 专属选项 | 否 | |
|
|
||||||
|
|
||||||
不支持 `provider`、`url`、`model`、`key`、`authToken`、`prompt`。
|
|
||||||
|
|
||||||
**配置项**
|
**配置项**
|
||||||
|
|
||||||
| 字段 | 说明 | 必填 | 默认值 |
|
| 字段 | 说明 | 必填 | 默认值 |
|
||||||
| --------------------- | ----------------------------------------------------------- | ---- | ------- |
|
| --------------------- | ------------------------------------------------------ | ---- | ------- |
|
||||||
| `llm.provider` | 模型提供方:`openai`、`openai-responses`、`anthropic` | 是 | |
|
| `llm.provider` | 模型提供方:`openai`、`openai-responses`、`anthropic` | 是 | |
|
||||||
| `llm.url` | API base URL | 是 | |
|
| `llm.url` | API base URL | 是 | |
|
||||||
| `llm.model` | 模型名称 | 是 | |
|
| `llm.model` | 模型名称 | 是 | |
|
||||||
| `llm.prompt` | 单轮 prompt | 是 | |
|
| `llm.prompt` | 单轮 prompt | 是 | |
|
||||||
| `llm.mode` | 调用模式:`http`(非流式)或 `stream`(流式) | 否 | `http` |
|
| `llm.mode` | 调用模式:`http`(非流式)或 `stream`(流式) | 否 | `http` |
|
||||||
| `llm.key` | API key,支持 `${VAR}` 变量替换 | 否 | `""` |
|
| `llm.key` | API key,支持 `${VAR}` 变量替换 | 否 | `""` |
|
||||||
| `llm.authToken` | Bearer token(仅 `anthropic` provider,与 `key` 互斥) | 否 | |
|
| `llm.authToken` | Bearer token(仅 `anthropic` provider,与 `key` 互斥) | 否 | |
|
||||||
| `llm.headers` | 附加请求头(与 `defaults.llm.headers` 合并) | 否 | |
|
| `llm.headers` | 附加请求头 | 否 | |
|
||||||
| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
|
| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
|
||||||
| `llm.options` | 生成选项(与 `defaults.llm.options` 合并) | 否 | |
|
| `llm.options` | 生成选项 | 否 | |
|
||||||
| `llm.providerOptions` | Provider 专属选项(与 `defaults.llm.providerOptions` 合并) | 否 | |
|
| `llm.providerOptions` | Provider 专属选项 | 否 | |
|
||||||
|
|
||||||
`llm.options` 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`、`seed`。
|
`llm.options` 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`、`seed`。
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,16 @@
|
|||||||
"type": "local",
|
"type": "local",
|
||||||
"command": ["bunx", "tdesign-mcp-server@latest"]
|
"command": ["bunx", "tdesign-mcp-server@latest"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"bash": {
|
||||||
|
"npm *": "deny",
|
||||||
|
"npx *": "deny",
|
||||||
|
"pnpm *": "deny",
|
||||||
|
"pnpx *": "deny"
|
||||||
|
},
|
||||||
|
"external_directory": {
|
||||||
|
"/tmp/**": "allow"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
- **THEN** 这些类型 SHALL 全部定义在该 checker 目录的 `types.ts` 中,不在顶层 `types.ts` 中
|
- **THEN** 这些类型 SHALL 全部定义在该 checker 目录的 `types.ts` 中,不在顶层 `types.ts` 中
|
||||||
|
|
||||||
#### Scenario: schema.ts 包含 TypeBox schema 定义
|
#### Scenario: schema.ts 包含 TypeBox schema 定义
|
||||||
- **WHEN** 开发者需要该 checker 的 config/defaults/expect schema
|
- **WHEN** 开发者需要该 checker 的 config 或 expect schema
|
||||||
- **THEN** 这些 schema SHALL 定义在该 checker 目录的 `schema.ts` 中
|
- **THEN** 这些 schema SHALL 定义在该 checker 目录的 `schema.ts` 中
|
||||||
|
|
||||||
#### Scenario: execute.ts 包含 Checker 类实现
|
#### Scenario: execute.ts 包含 Checker 类实现
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Requirement: Checker 配置契约片段
|
### Requirement: Checker 配置契约片段
|
||||||
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 defaults 分组、target 领域分组和 expect 分组。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并组合为启动期 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
|
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并组合为启动期 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
|
||||||
|
|
||||||
#### Scenario: HTTP checker 提供契约片段
|
#### Scenario: HTTP checker 提供契约片段
|
||||||
- **WHEN** HTTP checker 被注册
|
- **WHEN** HTTP checker 被注册
|
||||||
- **THEN** registry SHALL 能提供 HTTP defaults、HTTP target 和 HTTP expect 的 TypeBox 契约片段
|
- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段
|
||||||
|
|
||||||
#### Scenario: Cmd checker 提供契约片段
|
#### Scenario: Cmd checker 提供契约片段
|
||||||
- **WHEN** Cmd checker 被注册
|
- **WHEN** Cmd checker 被注册
|
||||||
- **THEN** registry SHALL 能提供 Cmd defaults、Cmd target 和 Cmd expect 的 TypeBox 契约片段
|
- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段
|
||||||
|
|
||||||
#### Scenario: 新 checker 只维护自身契约
|
#### Scenario: 新 checker 只维护自身契约
|
||||||
- **WHEN** 开发者新增一个 checker 类型
|
- **WHEN** 开发者新增一个 checker 类型
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA
|
|||||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的 "localhost"
|
- **THEN** 系统 SHALL 将该字段替换为 string 类型的 "localhost"
|
||||||
|
|
||||||
### Requirement: 替换范围限制
|
### Requirement: 替换范围限制
|
||||||
变量替换 SHALL 仅作用于 targets 段。`id` 和 `type` 字段 MUST NOT 参与变量替换。`server`、`probes`、`defaults` 段 MUST NOT 参与变量替换。系统 SHALL 递归遍历 target 对象树中所有字符串值进行替换(包括嵌套对象和数组元素中的字符串)。
|
变量替换 SHALL 仅作用于 targets 段。`id` 和 `type` 字段 MUST NOT 参与变量替换。`server`、`probes` 段 MUST NOT 参与变量替换。系统 SHALL 递归遍历 target 对象树中所有字符串值进行替换(包括嵌套对象和数组元素中的字符串)。顶层 `defaults` 不再是合法配置段,因此不属于变量替换范围。
|
||||||
|
|
||||||
#### Scenario: target 嵌套对象中的变量替换
|
#### Scenario: target 嵌套对象中的变量替换
|
||||||
- **WHEN** target 配置 `http.headers.Authorization: "${token}"` 且 variables 中定义 `token: "Bearer abc"`
|
- **WHEN** target 配置 `http.headers.Authorization: "${token}"` 且 variables 中定义 `token: "Bearer abc"`
|
||||||
@@ -139,10 +139,6 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA
|
|||||||
- **WHEN** target 配置 `type: "${checker_type}"` 且 variables 中定义 `checker_type: "http"`
|
- **WHEN** target 配置 `type: "${checker_type}"` 且 variables 中定义 `checker_type: "http"`
|
||||||
- **THEN** 系统 SHALL 保持 type 字段值为字面量 `"${checker_type}"`,不进行替换
|
- **THEN** 系统 SHALL 保持 type 字段值为字面量 `"${checker_type}"`,不进行替换
|
||||||
|
|
||||||
#### Scenario: defaults 段不替换
|
|
||||||
- **WHEN** defaults 配置 `interval: "${default_interval}"` 且 variables 中定义 `default_interval: "30s"`
|
|
||||||
- **THEN** 系统 SHALL 保持 defaults.interval 为字面量 `"${default_interval}"`,不进行替换
|
|
||||||
|
|
||||||
#### Scenario: server 段不替换
|
#### Scenario: server 段不替换
|
||||||
- **WHEN** server 配置 `listen.host: "${server_host}"` 且 variables 中定义 `server_host: "0.0.0.0"`
|
- **WHEN** server 配置 `listen.host: "${server_host}"` 且 variables 中定义 `server_host: "0.0.0.0"`
|
||||||
- **THEN** 系统 SHALL 保持 server.listen.host 为字面量 `"${server_host}"`,不进行替换
|
- **THEN** 系统 SHALL 保持 server.listen.host 为字面量 `"${server_host}"`,不进行替换
|
||||||
@@ -151,6 +147,10 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA
|
|||||||
- **WHEN** probes 配置 `execution.maxConcurrentChecks: "${max_checks}"` 且 variables 中定义 `max_checks: 5`
|
- **WHEN** probes 配置 `execution.maxConcurrentChecks: "${max_checks}"` 且 variables 中定义 `max_checks: 5`
|
||||||
- **THEN** 系统 SHALL 保持 probes.execution.maxConcurrentChecks 为字面量 `"${max_checks}"`,不进行替换
|
- **THEN** 系统 SHALL 保持 probes.execution.maxConcurrentChecks 为字面量 `"${max_checks}"`,不进行替换
|
||||||
|
|
||||||
|
#### Scenario: defaults 段被拒绝
|
||||||
|
- **WHEN** 配置文件声明顶层 `defaults`
|
||||||
|
- **THEN** 系统 SHALL 在契约校验阶段拒绝该未知字段,而不是尝试变量替换
|
||||||
|
|
||||||
### Requirement: 变量替换错误报告
|
### Requirement: 变量替换错误报告
|
||||||
变量替换阶段的错误 SHALL 作为 `ConfigValidationIssue` 输出,code 为 `unresolved-variable`。错误信息 SHALL 包含 target 索引、target id、字段路径和变量名。
|
变量替换阶段的错误 SHALL 作为 `ConfigValidationIssue` 输出,code 为 `unresolved-variable`。错误信息 SHALL 包含 target 索引、target id、字段路径和变量名。
|
||||||
|
|
||||||
|
|||||||
32
openspec/specs/http-checker/spec.md
Normal file
32
openspec/specs/http-checker/spec.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
定义 HTTP 类型拨测目标:通过 `type: http` 配置 HTTP/HTTPS 探测,支持请求方法、headers、body、SSL 校验、重定向和响应体大小限制,捕获 HTTP 状态码、响应头、响应体和执行耗时,按 expect 规则校验并生成 matched 判定。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: HTTP target 配置
|
||||||
|
系统 SHALL 支持 `type: http` 的 target 配置,通过 `http.url` 描述目标 HTTP 地址,并通过可选字段控制请求方法、请求体、headers、SSL 校验、重定向和响应体大小限制。
|
||||||
|
|
||||||
|
#### Scenario: 解析最简 HTTP target
|
||||||
|
- **WHEN** YAML 中 target 配置 `type: http` 和 `http.url: "https://example.com"`
|
||||||
|
- **THEN** 系统 SHALL 将其解析为 HTTP checker,并填充 `method=GET`、`maxBodyBytes=100MB`、`ignoreSSL=false`、`maxRedirects=0`、headers、body、interval、timeout、group 和 expect 配置
|
||||||
|
|
||||||
|
#### Scenario: HTTP target 缺少 url
|
||||||
|
- **WHEN** YAML 中 target 配置 `type: http` 但缺少 `http.url`
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 http.url 字段
|
||||||
|
|
||||||
|
#### Scenario: HTTP target 覆盖请求方法
|
||||||
|
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.method: POST`
|
||||||
|
- **THEN** 该 target SHALL 使用自身 method 字段的值,而不是内置默认值 GET
|
||||||
|
|
||||||
|
#### Scenario: HTTP target 覆盖响应体大小限制
|
||||||
|
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.maxBodyBytes`
|
||||||
|
- **THEN** 该 target SHALL 使用自身 maxBodyBytes 字段的值,而不是内置默认值 100MB
|
||||||
|
|
||||||
|
#### Scenario: HTTP target 覆盖 headers
|
||||||
|
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.headers`
|
||||||
|
- **THEN** 该 target SHALL 使用自身 headers 字段的值,不再与规则文件级 defaults 合并
|
||||||
|
|
||||||
|
#### Scenario: HTTP 序列化展示摘要
|
||||||
|
- **WHEN** 系统同步 HTTP target 到 targets 表
|
||||||
|
- **THEN** `target` 展示摘要 SHALL 为 HTTP URL,`config` JSON SHALL 包含 resolved 后的 url、method、headers、body、ignoreSSL、maxBodyBytes 和 maxRedirects
|
||||||
@@ -24,25 +24,27 @@
|
|||||||
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir`
|
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir`
|
||||||
|
|
||||||
### Requirement: YAML 配置文件格式
|
### Requirement: YAML 配置文件格式
|
||||||
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、probes 执行配置、可选的 variables 段、checker 默认值和 typed target 列表(含可选 group 字段)。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组,将本地数据目录和历史数据保留时长放在 `server.storage` 分组,将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。
|
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、probes 执行配置、可选的 variables 段和 typed target 列表(含可选 group 字段)。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组,将本地数据目录和历史数据保留时长放在 `server.storage` 分组,将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。顶层 `defaults` 不再是合法配置段。
|
||||||
|
|
||||||
`defaults.http` 分组 SHALL 仅支持 `headers`(可选)和 `maxBodyBytes`(可选)字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。`defaults.icmp` 分组 SHALL 仅支持空对象。`defaults.udp` 分组 SHALL 仅支持 `encoding`(可选)、`responseEncoding`(可选)和 `maxResponseBytes`(可选)字段。`defaults.llm` 分组 SHALL 仅支持 `mode`(可选)、`headers`(可选)、`ignoreSSL`(可选)、`options`(可选)和 `providerOptions`(可选)字段。
|
|
||||||
|
|
||||||
#### Scenario: 完整配置文件解析
|
#### Scenario: 完整配置文件解析
|
||||||
- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables、defaults、targets(含 id、group 字段)的 YAML 配置文件
|
- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables 和 targets(含 id、group 字段)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
|
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
|
||||||
|
|
||||||
|
#### Scenario: defaults 配置段被拒绝
|
||||||
|
- **WHEN** 配置文件声明顶层 `defaults`
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `defaults`
|
||||||
|
|
||||||
#### Scenario: 最简 HTTP 配置文件解析
|
#### Scenario: 最简 HTTP 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、probes、variables、defaults 和 expect)
|
- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、probes、variables 和 expect)
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default"),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group=default),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: 最简 cmd 配置文件解析
|
#### Scenario: 最简 cmd 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: cmd` target(含 `id` 和 `cmd.exec`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: cmd` target(含 `id` 和 `cmd.exec`)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: per-target 配置覆盖全局默认值
|
#### Scenario: per-target 配置覆盖内置默认值
|
||||||
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
|
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的可选字段
|
||||||
- **THEN** 该 target SHALL 使用其自身的值,不受 defaults 中对应字段影响
|
- **THEN** 该 target SHALL 使用其自身的值,不受对应内置默认值影响
|
||||||
|
|
||||||
#### Scenario: HTTP target 配置 ignoreSSL
|
#### Scenario: HTTP target 配置 ignoreSSL
|
||||||
- **WHEN** YAML 配置中 HTTP target 设置 `http.ignoreSSL: true`
|
- **WHEN** YAML 配置中 HTTP target 设置 `http.ignoreSSL: true`
|
||||||
@@ -58,43 +60,27 @@
|
|||||||
|
|
||||||
#### Scenario: 最简 tcp 配置文件解析
|
#### Scenario: 最简 tcp 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: tcp` target(含 `id`、`tcp.host` 和 `tcp.port`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: tcp` target(含 `id`、`tcp.host` 和 `tcp.port`)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", tcp.readBanner=false, tcp.bannerReadTimeout=2000, tcp.maxBannerBytes=4096),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, tcp.readBanner=false, tcp.bannerReadTimeout=2000, tcp.maxBannerBytes=4096),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: defaults.tcp 配置 banner 默认值
|
|
||||||
- **WHEN** YAML 配置中 defaults.tcp 设置 `bannerReadTimeout` 和 `maxBannerBytes`
|
|
||||||
- **THEN** 未显式覆盖对应字段的 tcp target SHALL 使用 defaults.tcp 中的值
|
|
||||||
|
|
||||||
#### Scenario: defaults.http.method 触发校验错误
|
|
||||||
- **WHEN** 配置文件中出现 `defaults.http.method` 字段
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 defaults.http 中存在未知字段 method
|
|
||||||
|
|
||||||
#### Scenario: per-target http.method 仍然有效
|
#### Scenario: per-target http.method 仍然有效
|
||||||
- **WHEN** HTTP target 配置 `http.method: POST`
|
- **WHEN** HTTP target 配置 `http.method: POST`
|
||||||
- **THEN** 系统 SHALL 使用 POST 作为该 target 的请求方法
|
- **THEN** 系统 SHALL 使用 POST 作为该 target 的请求方法
|
||||||
|
|
||||||
#### Scenario: 未配置 http.method 使用内置默认值
|
#### Scenario: 未配置 http.method 使用内置默认值
|
||||||
- **WHEN** HTTP target 未配置 `http.method` 且 defaults.http 中无 method 字段
|
- **WHEN** HTTP target 未配置 `http.method`
|
||||||
- **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法
|
- **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法
|
||||||
|
|
||||||
#### Scenario: 最简 icmp 配置文件解析
|
#### Scenario: 最简 icmp 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: icmp` target(含 `id` 和 `icmp.host`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: icmp` target(含 `id` 和 `icmp.host`)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", icmp.count=3, icmp.packetSize=56),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, icmp.count=3, icmp.packetSize=56),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: 最简 udp 配置文件解析
|
#### Scenario: 最简 udp 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: udp` target(含 `id`、`udp.host` 和 `udp.port`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: udp` target(含 `id`、`udp.host` 和 `udp.port`)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", udp.payload="", udp.encoding="text", udp.responseEncoding="text", udp.maxResponseBytes=4096),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, udp.payload=空字符串, udp.encoding=text, udp.responseEncoding=text, udp.maxResponseBytes=4096),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: defaults.udp 配置默认值
|
|
||||||
- **WHEN** YAML 配置中 defaults.udp 设置 `encoding`、`responseEncoding` 和 `maxResponseBytes`
|
|
||||||
- **THEN** 未显式覆盖对应字段的 udp target SHALL 使用 defaults.udp 中的值
|
|
||||||
|
|
||||||
#### Scenario: 最简 llm 配置文件解析
|
#### Scenario: 最简 llm 配置文件解析
|
||||||
- **WHEN** 系统读取只包含一个 `type: llm` target(含 `id`、`llm.provider`、`llm.url`、`llm.model` 和 `llm.prompt`)的 YAML 配置文件
|
- **WHEN** 系统读取只包含一个 `type: llm` target(含 `id`、`llm.provider`、`llm.url`、`llm.model` 和 `llm.prompt`)的 YAML 配置文件
|
||||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定字段(interval=30s, timeout=10s, group="default", llm.mode="http", llm.key="", llm.ignoreSSL=false, llm.options.maxOutputTokens=16, llm.options.temperature=0),并保留 name=null、description=null
|
- **THEN** 系统 SHALL 使用内置默认值填充未指定字段(interval=30s, timeout=10s, group=default, llm.mode=http, llm.key=空字符串, llm.ignoreSSL=false, llm.options.maxOutputTokens=16, llm.options.temperature=0),并保留 name=null、description=null
|
||||||
|
|
||||||
#### Scenario: defaults.llm 配置默认值
|
|
||||||
- **WHEN** YAML 配置中 defaults.llm 设置 `mode`、`headers`、`ignoreSSL`、`options` 或 `providerOptions`
|
|
||||||
- **THEN** 未显式覆盖对应字段的 llm target SHALL 使用 defaults.llm 中的值
|
|
||||||
|
|
||||||
### Requirement: CLI 参数
|
### Requirement: CLI 参数
|
||||||
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。
|
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。
|
||||||
@@ -126,6 +112,10 @@
|
|||||||
- **WHEN** YAML 中某个 target 缺少 id 或 type 字段
|
- **WHEN** YAML 中某个 target 缺少 id 或 type 字段
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段
|
- **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段
|
||||||
|
|
||||||
|
#### Scenario: 顶层 defaults 字段非法
|
||||||
|
- **WHEN** YAML 配置文件声明顶层 `defaults`
|
||||||
|
- **THEN** 系统 SHALL 以配置错误退出,提示 `defaults` 是未知字段
|
||||||
|
|
||||||
#### Scenario: HTTP target 缺少 url
|
#### Scenario: HTTP target 缺少 url
|
||||||
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
|
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
|
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
|
||||||
@@ -203,7 +193,7 @@
|
|||||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.body 必须为字符串
|
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.body 必须为字符串
|
||||||
|
|
||||||
#### Scenario: maxBodyBytes 数字非法
|
#### Scenario: maxBodyBytes 数字非法
|
||||||
- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 或 defaults.http.maxBodyBytes 是负数、非整数或非安全整数
|
- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 是负数、非整数或非安全整数
|
||||||
- **THEN** 系统 SHALL 以错误退出,提示 maxBodyBytes 必须为非负安全整数字节数或合法 size 字符串
|
- **THEN** 系统 SHALL 以错误退出,提示 maxBodyBytes 必须为非负安全整数字节数或合法 size 字符串
|
||||||
|
|
||||||
#### Scenario: status 模式非法
|
#### Scenario: status 模式非法
|
||||||
@@ -291,7 +281,7 @@
|
|||||||
- **THEN** 系统 SHALL 以错误退出,提示未知字段所在路径
|
- **THEN** 系统 SHALL 以错误退出,提示未知字段所在路径
|
||||||
|
|
||||||
#### Scenario: 动态 headers 字段允许
|
#### Scenario: 动态 headers 字段允许
|
||||||
- **WHEN** YAML 中 `http.headers`、`defaults.http.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约
|
- **WHEN** YAML 中 `http.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约
|
||||||
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
||||||
|
|
||||||
#### Scenario: 动态 env 字段允许
|
#### Scenario: 动态 env 字段允许
|
||||||
@@ -312,7 +302,7 @@
|
|||||||
|
|
||||||
#### Scenario: 导出配置 JSON Schema
|
#### Scenario: 导出配置 JSON Schema
|
||||||
- **WHEN** 仓库生成或检查配置契约
|
- **WHEN** 仓库生成或检查配置契约
|
||||||
- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前公共 fragments 和已注册 checker fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name 字段)。所有 `RawValueExpectation` 字段的 schema SHALL 声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型,`RawKeyedExpectations` 的 dynamic value schema SHALL 复用 `RawValueExpectation`
|
- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前公共 fragments 和已注册 checker fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name 字段,且不包含顶层 defaults)。所有 `RawValueExpectation` 字段的 schema SHALL 声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型,`RawKeyedExpectations` 的 dynamic value schema SHALL 复用 `RawValueExpectation`
|
||||||
|
|
||||||
#### Scenario: JSON Schema RawValueExpectation 接受原始值
|
#### Scenario: JSON Schema RawValueExpectation 接受原始值
|
||||||
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数字 `5000`
|
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数字 `5000`
|
||||||
@@ -431,7 +421,7 @@
|
|||||||
- **THEN** 系统 SHALL 接受该配置并在运行期使用完整执行耗时进行 matcher 校验
|
- **THEN** 系统 SHALL 接受该配置并在运行期使用完整执行耗时进行 matcher 校验
|
||||||
|
|
||||||
#### Scenario: 动态 headers 字段允许
|
#### Scenario: 动态 headers 字段允许
|
||||||
- **WHEN** YAML 中 `http.headers`、`defaults.http.headers`、`llm.headers`、`defaults.llm.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约
|
- **WHEN** YAML 中 `http.headers`、`llm.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约
|
||||||
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
||||||
|
|
||||||
#### Scenario: ContentExpectations 字段必须为数组
|
#### Scenario: ContentExpectations 字段必须为数组
|
||||||
@@ -546,11 +536,11 @@
|
|||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.readBanner 必须为布尔值
|
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.readBanner 必须为布尔值
|
||||||
|
|
||||||
#### Scenario: tcp bannerReadTimeout 非法
|
#### Scenario: tcp bannerReadTimeout 非法
|
||||||
- **WHEN** YAML 中 tcp target 或 defaults.tcp 的 `bannerReadTimeout` 不是非负有限数字
|
- **WHEN** YAML 中 tcp target 的 `bannerReadTimeout` 不是非负有限数字
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 bannerReadTimeout 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 bannerReadTimeout 格式错误
|
||||||
|
|
||||||
#### Scenario: tcp maxBannerBytes 非法
|
#### Scenario: tcp maxBannerBytes 非法
|
||||||
- **WHEN** YAML 中 tcp target 或 defaults.tcp 的 `maxBannerBytes` 不是合法 size 值
|
- **WHEN** YAML 中 tcp target 的 `maxBannerBytes` 不是合法 size 值
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 maxBannerBytes 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 maxBannerBytes 格式错误
|
||||||
|
|
||||||
#### Scenario: tcp expect connected 类型非法
|
#### Scenario: tcp expect connected 类型非法
|
||||||
@@ -569,12 +559,8 @@
|
|||||||
- **WHEN** YAML 中 tcp target 的 `tcp` 分组包含 `tls: true` 等未知字段
|
- **WHEN** YAML 中 tcp target 的 `tcp` 分组包含 `tls: true` 等未知字段
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp 分组包含未知字段
|
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp 分组包含未知字段
|
||||||
|
|
||||||
#### Scenario: defaults.tcp 未知字段失败
|
|
||||||
- **WHEN** YAML 中 defaults.tcp 包含 `host` 或其他非默认字段
|
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 defaults.tcp 包含未知字段
|
|
||||||
|
|
||||||
### Requirement: LLM 配置校验
|
### Requirement: LLM 配置校验
|
||||||
系统 SHALL 在启动期对 llm checker 的配置契约和语义执行严格校验。LLM target 的 `llm` 分组 SHALL 只允许 `provider`、`url`、`model`、`prompt`、`mode`、`key`、`authToken`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。`defaults.llm` 分组 SHALL 只允许 `mode`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。LLM expect SHALL 只允许 `status`、`headers`、`output`、`finishReason`、`rawFinishReason`、`usage`、`stream` 和 `durationMs` 字段。`expect.output` MUST 为 `RawContentExpectations` 数组。`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用 `RawValueExpectation`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output expectation 和 `mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw llm expect 输入。
|
系统 SHALL 在启动期对 llm checker 的配置契约和语义执行严格校验。LLM target 的 `llm` 分组 SHALL 只允许 `provider`、`url`、`model`、`prompt`、`mode`、`key`、`authToken`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。LLM expect SHALL 只允许 `status`、`headers`、`output`、`finishReason`、`rawFinishReason`、`usage`、`stream` 和 `durationMs` 字段。`expect.output` MUST 为 `RawContentExpectations` 数组。`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用 `RawValueExpectation`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output expectation 和 `mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw llm expect 输入。
|
||||||
|
|
||||||
#### Scenario: llm provider 非法
|
#### Scenario: llm provider 非法
|
||||||
- **WHEN** YAML 中 llm target 的 `llm.provider` 不是 `openai`、`openai-responses` 或 `anthropic`
|
- **WHEN** YAML 中 llm target 的 `llm.provider` 不是 `openai`、`openai-responses` 或 `anthropic`
|
||||||
@@ -593,15 +579,15 @@
|
|||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.prompt 必须为非空字符串
|
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.prompt 必须为非空字符串
|
||||||
|
|
||||||
#### Scenario: llm mode 非法
|
#### Scenario: llm mode 非法
|
||||||
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `mode` 不是 `http` 或 `stream`
|
- **WHEN** YAML 中 llm target 的 `mode` 不是 `http` 或 `stream`
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.mode 不合法
|
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.mode 不合法
|
||||||
|
|
||||||
#### Scenario: llm headers 类型非法
|
#### Scenario: llm headers 类型非法
|
||||||
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `headers` 不是对象,或任一 header 值不是字符串
|
- **WHEN** YAML 中 llm target 的 `headers` 不是对象,或任一 header 值不是字符串
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.headers 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.headers 格式错误
|
||||||
|
|
||||||
#### Scenario: llm ignoreSSL 类型非法
|
#### Scenario: llm ignoreSSL 类型非法
|
||||||
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `ignoreSSL` 不是布尔值
|
- **WHEN** YAML 中 llm target 的 `ignoreSSL` 不是布尔值
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.ignoreSSL 必须为布尔值
|
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.ignoreSSL 必须为布尔值
|
||||||
|
|
||||||
#### Scenario: llm authToken provider 非法
|
#### Scenario: llm authToken provider 非法
|
||||||
@@ -613,11 +599,11 @@
|
|||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 key 与 authToken 不能同时配置
|
- **THEN** 系统 SHALL 以配置错误退出,提示 key 与 authToken 不能同时配置
|
||||||
|
|
||||||
#### Scenario: llm options 非法
|
#### Scenario: llm options 非法
|
||||||
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `options.maxOutputTokens` 不是正整数,`options.temperature`/`topP`/`topK`/`presencePenalty`/`frequencyPenalty`/`seed` 类型不合法,或 `options.stopSequences` 不是字符串数组
|
- **WHEN** YAML 中 llm target 的 `options.maxOutputTokens` 不是正整数,`options.temperature`/`topP`/`topK`/`presencePenalty`/`frequencyPenalty`/`seed` 类型不合法,或 `options.stopSequences` 不是字符串数组
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.options 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.options 格式错误
|
||||||
|
|
||||||
#### Scenario: llm providerOptions 非法
|
#### Scenario: llm providerOptions 非法
|
||||||
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `providerOptions` 不是 JSON object
|
- **WHEN** YAML 中 llm target 的 `providerOptions` 不是 JSON object
|
||||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.providerOptions 格式错误
|
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.providerOptions 格式错误
|
||||||
|
|
||||||
#### Scenario: llm 禁止字段失败
|
#### Scenario: llm 禁止字段失败
|
||||||
|
|||||||
@@ -5,212 +5,6 @@
|
|||||||
"targets"
|
"targets"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"defaults": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"interval": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"headers": {
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"maxBodyBytes": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"minimum": 0,
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cmd": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"cwd": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"maxOutputBytes": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"minimum": 0,
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
},
|
|
||||||
"tcp": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"bannerReadTimeout": {
|
|
||||||
"minimum": 0,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"maxBannerBytes": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"minimum": 0,
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icmp": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
},
|
|
||||||
"udp": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"encoding": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"const": "text",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"const": "hex",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"const": "base64",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"maxResponseBytes": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"minimum": 0,
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"responseEncoding": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"const": "text",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"const": "hex",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"const": "base64",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"llm": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"headers": {
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"ignoreSSL": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"const": "http",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"const": "stream",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"additionalProperties": false,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"frequencyPenalty": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"maxOutputTokens": {
|
|
||||||
"minimum": 1,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"presencePenalty": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"seed": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"stopSequences": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"temperature": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"topK": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"topP": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"providerOptions": {
|
|
||||||
"type": "object",
|
|
||||||
"patternProperties": {
|
|
||||||
"^(.*)$": {
|
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"probes": {
|
"probes": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ probes:
|
|||||||
execution:
|
execution:
|
||||||
maxConcurrentChecks: 20
|
maxConcurrentChecks: 20
|
||||||
|
|
||||||
defaults:
|
|
||||||
interval: "30s"
|
|
||||||
timeout: "10s"
|
|
||||||
http:
|
|
||||||
maxBodyBytes: "10MB"
|
|
||||||
cmd:
|
|
||||||
maxOutputBytes: "1MB"
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
env_name: "演示"
|
env_name: "演示"
|
||||||
httpbin_base: "https://httpbin.org"
|
httpbin_base: "https://httpbin.org"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { dirname, resolve } from "node:path";
|
|||||||
|
|
||||||
import type { ConfigValidationIssue } from "./schema/issues";
|
import type { ConfigValidationIssue } from "./schema/issues";
|
||||||
import type {
|
import type {
|
||||||
DefaultsConfig,
|
|
||||||
ExecutionConfig,
|
ExecutionConfig,
|
||||||
LoggingConfig,
|
LoggingConfig,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
@@ -90,7 +89,6 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
|||||||
const server = validated.server ?? {};
|
const server = validated.server ?? {};
|
||||||
const listen = server.listen ?? {};
|
const listen = server.listen ?? {};
|
||||||
const storage = server.storage ?? {};
|
const storage = server.storage ?? {};
|
||||||
const defaults = validated.defaults ?? {};
|
|
||||||
|
|
||||||
const host = listen.host ?? DEFAULT_HOST;
|
const host = listen.host ?? DEFAULT_HOST;
|
||||||
const port = listen.port ?? DEFAULT_PORT;
|
const port = listen.port ?? DEFAULT_PORT;
|
||||||
@@ -109,11 +107,11 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
|||||||
throwConfigIssues(dedupeIssues(allRuntimeIssues));
|
throwConfigIssues(dedupeIssues(allRuntimeIssues));
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultIntervalMs = parseDuration(defaults.interval ?? DEFAULT_INTERVAL);
|
const defaultIntervalMs = parseDuration(DEFAULT_INTERVAL);
|
||||||
const defaultTimeoutMs = parseDuration(defaults.timeout ?? DEFAULT_TIMEOUT);
|
const defaultTimeoutMs = parseDuration(DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
const targets: ResolvedTargetBase[] = validated.targets.map((target) =>
|
const targets: ResolvedTargetBase[] = validated.targets.map((target) =>
|
||||||
resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir),
|
resolveTarget(target, defaultIntervalMs, defaultTimeoutMs, configDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { configDir, dataDir, host, logging, maxConcurrentChecks, port, retentionMs, targets };
|
return { configDir, dataDir, host, logging, maxConcurrentChecks, port, retentionMs, targets };
|
||||||
@@ -193,16 +191,15 @@ function resolveRetention(storage: ServerStorageConfig): number {
|
|||||||
|
|
||||||
function resolveTarget(
|
function resolveTarget(
|
||||||
target: RawTargetConfig,
|
target: RawTargetConfig,
|
||||||
defaults: DefaultsConfig,
|
|
||||||
defaultIntervalMs: number,
|
defaultIntervalMs: number,
|
||||||
defaultTimeoutMs: number,
|
defaultTimeoutMs: number,
|
||||||
configDir: string,
|
configDir: string,
|
||||||
): ResolvedTargetBase {
|
): ResolvedTargetBase {
|
||||||
const intervalMs = parseDuration(target.interval ?? defaults.interval ?? DEFAULT_INTERVAL);
|
const intervalMs = parseDuration(target.interval ?? DEFAULT_INTERVAL);
|
||||||
const timeoutMs = parseDuration(target.timeout ?? defaults.timeout ?? DEFAULT_TIMEOUT);
|
const timeoutMs = parseDuration(target.timeout ?? DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
const checker = checkerRegistry.get(target.type);
|
const checker = checkerRegistry.get(target.type);
|
||||||
const result = checker.resolve(target, { configDir, defaultIntervalMs, defaults, defaultTimeoutMs });
|
const result = checker.resolve(target, { configDir, defaultIntervalMs, defaultTimeoutMs });
|
||||||
|
|
||||||
result.intervalMs = intervalMs;
|
result.intervalMs = intervalMs;
|
||||||
result.timeoutMs = timeoutMs;
|
result.timeoutMs = timeoutMs;
|
||||||
@@ -275,11 +272,9 @@ function validateConfig(config: RawProbeConfig): ConfigValidationIssue[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const checker of checkerRegistry.definitions) {
|
for (const checker of checkerRegistry.definitions) {
|
||||||
issues.push(...checker.validate({ defaults: config.defaults ?? {}, targets: config.targets }));
|
issues.push(...checker.validate({ targets: config.targets }));
|
||||||
}
|
}
|
||||||
|
|
||||||
validateDurationValue(config.defaults?.interval, "defaults.interval", issues);
|
|
||||||
validateDurationValue(config.defaults?.timeout, "defaults.timeout", issues);
|
|
||||||
validateDurationValue(
|
validateDurationValue(
|
||||||
isString(config.server?.storage?.retention) ? config.server.storage.retention : undefined,
|
isString(config.server?.storage?.retention) ? config.server.storage.retention : undefined,
|
||||||
"server.storage.retention",
|
"server.storage.retention",
|
||||||
|
|||||||
@@ -209,12 +209,11 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
|||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
|
||||||
const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" };
|
const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" };
|
||||||
const cmdDefaults = context.defaults["cmd"] as undefined | { cwd?: string; maxOutputBytes?: string };
|
|
||||||
|
|
||||||
const cwd = t.cmd.cwd ?? cmdDefaults?.cwd ?? ".";
|
const cwd = t.cmd.cwd ?? ".";
|
||||||
const resolvedCwd = resolve(context.configDir, cwd);
|
const resolvedCwd = resolve(context.configDir, cwd);
|
||||||
|
|
||||||
const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? cmdDefaults?.maxOutputBytes ?? "100MB");
|
const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? "100MB");
|
||||||
|
|
||||||
const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record<string, string>;
|
const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record<string, string>;
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,6 @@ export const commandCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object(
|
|
||||||
{
|
|
||||||
cwd: Type.Optional(Type.String()),
|
|
||||||
maxOutputBytes: Type.Optional(sizeSchema),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import type {
|
|||||||
} from "../../expect/types";
|
} from "../../expect/types";
|
||||||
import type { ResolvedTargetBase } from "../../types";
|
import type { ResolvedTargetBase } from "../../types";
|
||||||
|
|
||||||
export interface CommandDefaultsConfig {
|
|
||||||
cwd?: string;
|
|
||||||
maxOutputBytes?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommandTargetConfig {
|
export interface CommandTargetConfig {
|
||||||
args?: string[];
|
args?: string[];
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ import { parseSize } from "../../utils";
|
|||||||
|
|
||||||
export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
const defaults =
|
|
||||||
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["cmd"]) ? input.defaults["cmd"] : undefined;
|
|
||||||
|
|
||||||
if (isSizeInput(defaults?.["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;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export const dbCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object({}, { additionalProperties: false }),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
||||||
|
|||||||
@@ -175,12 +175,9 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
|
|||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedHttpTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedHttpTarget {
|
||||||
const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" };
|
const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" };
|
||||||
const httpDefaults = context.defaults["http"] as
|
|
||||||
| undefined
|
|
||||||
| { headers?: Record<string, string>; maxBodyBytes?: string };
|
|
||||||
|
|
||||||
const method = t.http.method ?? "GET";
|
const method = t.http.method ?? "GET";
|
||||||
const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? httpDefaults?.maxBodyBytes ?? "100MB");
|
const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? "100MB");
|
||||||
|
|
||||||
const rawExpect = target.expect as RawHttpExpectConfig | undefined;
|
const rawExpect = target.expect as RawHttpExpectConfig | undefined;
|
||||||
const resolvedExpect: ResolvedHttpExpectConfig = rawExpect
|
const resolvedExpect: ResolvedHttpExpectConfig = rawExpect
|
||||||
@@ -198,7 +195,7 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
|
|||||||
group: target.group ?? "default",
|
group: target.group ?? "default",
|
||||||
http: {
|
http: {
|
||||||
body: t.http.body,
|
body: t.http.body,
|
||||||
headers: { ...(httpDefaults?.headers ?? {}), ...(t.http.headers ?? {}) },
|
headers: { ...(t.http.headers ?? {}) },
|
||||||
ignoreSSL: t.http.ignoreSSL ?? false,
|
ignoreSSL: t.http.ignoreSSL ?? false,
|
||||||
maxBodyBytes,
|
maxBodyBytes,
|
||||||
maxRedirects: t.http.maxRedirects ?? 0,
|
maxRedirects: t.http.maxRedirects ?? 0,
|
||||||
|
|||||||
@@ -25,13 +25,6 @@ export const httpCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object(
|
|
||||||
{
|
|
||||||
headers: Type.Optional(stringMapSchema),
|
|
||||||
maxBodyBytes: Type.Optional(sizeSchema),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
body: Type.Optional(createRawContentExpectationsSchema()),
|
body: Type.Optional(createRawContentExpectationsSchema()),
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ import type {
|
|||||||
} from "../../expect/types";
|
} from "../../expect/types";
|
||||||
import type { ResolvedTargetBase } from "../../types";
|
import type { ResolvedTargetBase } from "../../types";
|
||||||
|
|
||||||
export interface HttpDefaultsConfig {
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
maxBodyBytes?: string;
|
|
||||||
method?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpTargetConfig {
|
export interface HttpTargetConfig {
|
||||||
body?: string;
|
body?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
|
|||||||
|
|
||||||
export function validateHttpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validateHttpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
const defaults =
|
|
||||||
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["http"]) ? input.defaults["http"] : undefined;
|
|
||||||
|
|
||||||
if (isSizeInput(defaults?.["maxBodyBytes"])) {
|
|
||||||
issues.push(...validateSizeValue(defaults["maxBodyBytes"], "defaults.http.maxBodyBytes"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const icmpCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object({}, { additionalProperties: false }),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
alive: Type.Optional(Type.Boolean()),
|
alive: Type.Optional(Type.Boolean()),
|
||||||
|
|||||||
@@ -9,19 +9,6 @@ import { issue, joinPath } from "../../schema/issues";
|
|||||||
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
|
||||||
const defaults = input.defaults["icmp"];
|
|
||||||
if (defaults !== undefined && defaults !== null) {
|
|
||||||
const targetName = "defaults.icmp";
|
|
||||||
if (!isPlainRecord(defaults)) {
|
|
||||||
issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName));
|
|
||||||
} else {
|
|
||||||
const icmpDefaults = defaults;
|
|
||||||
for (const key of Object.keys(icmpDefaults)) {
|
|
||||||
issues.push(issue("unknown-field", joinPath("defaults.icmp", key), "是未知字段", targetName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!isPlainRecord(target)) continue;
|
if (!isPlainRecord(target)) continue;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { JSONObject } from "@ai-sdk/provider";
|
|
||||||
|
|
||||||
import { APICallError, generateText, streamText } from "ai";
|
import { APICallError, generateText, streamText } from "ai";
|
||||||
import { isError } from "es-toolkit";
|
import { isError } from "es-toolkit";
|
||||||
|
|
||||||
@@ -133,43 +131,27 @@ export class LlmChecker implements CheckerDefinition<ResolvedLlmTarget> {
|
|||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedLlmTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedLlmTarget {
|
||||||
const t = target as RawTargetConfig & { llm: LlmTargetConfig; type: "llm" };
|
const t = target as RawTargetConfig & { llm: LlmTargetConfig; type: "llm" };
|
||||||
const llmDefaults = context.defaults["llm"] as
|
|
||||||
| undefined
|
|
||||||
| {
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
ignoreSSL?: boolean;
|
|
||||||
mode?: string;
|
|
||||||
options?: Record<string, unknown>;
|
|
||||||
providerOptions?: Record<string, Record<string, unknown>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolvedConfig = {
|
const resolvedConfig = {
|
||||||
authToken: t.llm.authToken,
|
authToken: t.llm.authToken,
|
||||||
headers: { ...(llmDefaults?.headers ?? {}), ...(t.llm.headers ?? {}) },
|
headers: { ...(t.llm.headers ?? {}) },
|
||||||
ignoreSSL: t.llm.ignoreSSL ?? llmDefaults?.ignoreSSL ?? false,
|
ignoreSSL: t.llm.ignoreSSL ?? false,
|
||||||
key: t.llm.key ?? "",
|
key: t.llm.key ?? "",
|
||||||
mode: (t.llm.mode ?? llmDefaults?.mode ?? "http") as "http" | "stream",
|
mode: t.llm.mode ?? "http",
|
||||||
model: t.llm.model,
|
model: t.llm.model,
|
||||||
options: {
|
options: {
|
||||||
frequencyPenalty:
|
frequencyPenalty: t.llm.options?.frequencyPenalty,
|
||||||
t.llm.options?.frequencyPenalty ?? (llmDefaults?.options?.["frequencyPenalty"] as number | undefined),
|
maxOutputTokens: t.llm.options?.maxOutputTokens ?? 16,
|
||||||
maxOutputTokens:
|
presencePenalty: t.llm.options?.presencePenalty,
|
||||||
t.llm.options?.maxOutputTokens ?? (llmDefaults?.options?.["maxOutputTokens"] as number | undefined) ?? 16,
|
seed: t.llm.options?.seed,
|
||||||
presencePenalty:
|
stopSequences: t.llm.options?.stopSequences,
|
||||||
t.llm.options?.presencePenalty ?? (llmDefaults?.options?.["presencePenalty"] as number | undefined),
|
temperature: t.llm.options?.temperature ?? 0,
|
||||||
seed: t.llm.options?.seed ?? (llmDefaults?.options?.["seed"] as number | undefined),
|
topK: t.llm.options?.topK,
|
||||||
stopSequences:
|
topP: t.llm.options?.topP,
|
||||||
t.llm.options?.stopSequences ?? (llmDefaults?.options?.["stopSequences"] as string[] | undefined),
|
|
||||||
temperature: t.llm.options?.temperature ?? (llmDefaults?.options?.["temperature"] as number | undefined) ?? 0,
|
|
||||||
topK: t.llm.options?.topK ?? (llmDefaults?.options?.["topK"] as number | undefined),
|
|
||||||
topP: t.llm.options?.topP ?? (llmDefaults?.options?.["topP"] as number | undefined),
|
|
||||||
},
|
},
|
||||||
prompt: t.llm.prompt,
|
prompt: t.llm.prompt,
|
||||||
provider: t.llm.provider,
|
provider: t.llm.provider,
|
||||||
providerOptions: {
|
providerOptions: { ...(t.llm.providerOptions ?? {}) },
|
||||||
...((llmDefaults?.providerOptions ?? {}) as Record<string, JSONObject>),
|
|
||||||
...(t.llm.providerOptions ?? {}),
|
|
||||||
},
|
|
||||||
url: t.llm.url,
|
url: t.llm.url,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,16 +43,6 @@ export const llmCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object(
|
|
||||||
{
|
|
||||||
headers: Type.Optional(stringMapSchema),
|
|
||||||
ignoreSSL: Type.Optional(Type.Boolean()),
|
|
||||||
mode: Type.Optional(Type.Union([Type.Literal("http"), Type.Literal("stream")])),
|
|
||||||
options: Type.Optional(createLlmOptionsSchema()),
|
|
||||||
providerOptions: Type.Optional(Type.Record(Type.String(), Type.Object({}, { additionalProperties: true }))),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
||||||
|
|||||||
@@ -23,14 +23,6 @@ export interface LlmCheckObservation {
|
|||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LlmDefaultsConfig {
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
ignoreSSL?: boolean;
|
|
||||||
mode?: LlmMode;
|
|
||||||
options?: LlmOptions;
|
|
||||||
providerOptions?: Record<string, JSONObject>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LlmHttpMetadata {
|
export interface LlmHttpMetadata {
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
status: number;
|
status: number;
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ const ALLOWED_PROVIDERS = new Set(["anthropic", "openai", "openai-responses"]);
|
|||||||
|
|
||||||
export function validateLlmConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validateLlmConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
const defaults =
|
|
||||||
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["llm"]) ? input.defaults["llm"] : undefined;
|
|
||||||
|
|
||||||
if (defaults) {
|
|
||||||
issues.push(...validateLlmDefaults(defaults, "defaults.llm"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -39,28 +33,6 @@ function getTargetName(target: Record<string, unknown>): string | undefined {
|
|||||||
return isString(target["id"]) ? target["id"] : undefined;
|
return isString(target["id"]) ? target["id"] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateLlmDefaults(defaults: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
|
||||||
const issues: ConfigValidationIssue[] = [];
|
|
||||||
|
|
||||||
if (defaults["mode"] !== undefined && !ALLOWED_MODES.has(defaults["mode"] as string)) {
|
|
||||||
issues.push(issue("invalid-type", joinPath(path, "mode"), "必须为 http 或 stream"));
|
|
||||||
}
|
|
||||||
if (defaults["ignoreSSL"] !== undefined && !isBoolean(defaults["ignoreSSL"])) {
|
|
||||||
issues.push(issue("invalid-type", joinPath(path, "ignoreSSL"), "必须为布尔值"));
|
|
||||||
}
|
|
||||||
if (defaults["headers"] !== undefined) {
|
|
||||||
issues.push(...validateStringMap(defaults["headers"], joinPath(path, "headers")));
|
|
||||||
}
|
|
||||||
if (defaults["options"] !== undefined) {
|
|
||||||
issues.push(...validateLlmOptions(defaults["options"], joinPath(path, "options")));
|
|
||||||
}
|
|
||||||
if (defaults["providerOptions"] !== undefined) {
|
|
||||||
issues.push(...validateProviderOptions(defaults["providerOptions"], joinPath(path, "providerOptions")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateLlmExpect(
|
function validateLlmExpect(
|
||||||
target: Record<string, unknown>,
|
target: Record<string, unknown>,
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
@@ -206,12 +206,9 @@ export class TcpChecker implements CheckerDefinition<ResolvedTcpTarget> {
|
|||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTcpTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTcpTarget {
|
||||||
const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
|
const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
|
||||||
const tcpDefaults = context.defaults["tcp"] as
|
|
||||||
| undefined
|
|
||||||
| { bannerReadTimeout?: number; maxBannerBytes?: number | string };
|
|
||||||
|
|
||||||
const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? tcpDefaults?.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES);
|
const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES);
|
||||||
const bannerReadTimeout = t.tcp.bannerReadTimeout ?? tcpDefaults?.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT;
|
const bannerReadTimeout = t.tcp.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT;
|
||||||
|
|
||||||
const rawExpect = target.expect as RawTcpExpectConfig | undefined;
|
const rawExpect = target.expect as RawTcpExpectConfig | undefined;
|
||||||
const resolvedExpect: ResolvedTcpExpectConfig = rawExpect
|
const resolvedExpect: ResolvedTcpExpectConfig = rawExpect
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ export const tcpCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object(
|
|
||||||
{
|
|
||||||
bannerReadTimeout: Type.Optional(Type.Number({ minimum: 0 })),
|
|
||||||
maxBannerBytes: Type.Optional(sizeSchema),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
banner: Type.Optional(createRawContentExpectationsSchema()),
|
banner: Type.Optional(createRawContentExpectationsSchema()),
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ export interface ResolvedTcpTarget extends ResolvedTargetBase {
|
|||||||
type: "tcp";
|
type: "tcp";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TcpDefaultsConfig {
|
|
||||||
bannerReadTimeout?: number;
|
|
||||||
maxBannerBytes?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TcpTargetConfig {
|
export interface TcpTargetConfig {
|
||||||
bannerReadTimeout?: number;
|
bannerReadTimeout?: number;
|
||||||
host: string;
|
host: string;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import { issue, joinPath } from "../../schema/issues";
|
|||||||
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
|
||||||
issues.push(...validateTcpDefaults(input));
|
|
||||||
|
|
||||||
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 (!isPlainRecord(target)) continue;
|
if (!isPlainRecord(target)) continue;
|
||||||
@@ -30,40 +28,6 @@ function isNonNegativeFiniteNumber(value: unknown): boolean {
|
|||||||
return isNumber(value) && Number.isFinite(value) && value >= 0;
|
return isNumber(value) && Number.isFinite(value) && value >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTcpDefaults(input: CheckerValidationInput): ConfigValidationIssue[] {
|
|
||||||
const issues: ConfigValidationIssue[] = [];
|
|
||||||
const defaults = input.defaults["tcp"];
|
|
||||||
if (defaults === undefined || defaults === null || !isPlainRecord(defaults)) return issues;
|
|
||||||
|
|
||||||
const targetName = "defaults.tcp";
|
|
||||||
|
|
||||||
if (defaults["bannerReadTimeout"] !== undefined && !isNonNegativeFiniteNumber(defaults["bannerReadTimeout"])) {
|
|
||||||
issues.push(issue("invalid-type", "defaults.tcp.bannerReadTimeout", "必须为非负有限数字", targetName));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaults["maxBannerBytes"] !== undefined) {
|
|
||||||
if (
|
|
||||||
!isString(defaults["maxBannerBytes"]) &&
|
|
||||||
!(
|
|
||||||
isNumber(defaults["maxBannerBytes"]) &&
|
|
||||||
Number.isFinite(defaults["maxBannerBytes"]) &&
|
|
||||||
defaults["maxBannerBytes"] >= 0
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
issues.push(issue("invalid-value", "defaults.tcp.maxBannerBytes", "必须为合法 size 值", targetName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedKeys = new Set(["bannerReadTimeout", "maxBannerBytes"]);
|
|
||||||
for (const key of Object.keys(defaults)) {
|
|
||||||
if (!allowedKeys.has(key)) {
|
|
||||||
issues.push(issue("unknown-field", joinPath("defaults.tcp", key), "是未知字段", targetName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateTcpExpect(
|
function validateTcpExpect(
|
||||||
target: Record<string, unknown>,
|
target: Record<string, unknown>,
|
||||||
path: string,
|
path: string,
|
||||||
@@ -159,5 +123,3 @@ function validateTcpTarget(target: Record<string, unknown>, path: string): Confi
|
|||||||
|
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { validateTcpDefaults };
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { TSchema } from "@sinclair/typebox";
|
import type { TSchema } from "@sinclair/typebox";
|
||||||
|
|
||||||
import type { ConfigValidationIssue } from "../schema/issues";
|
import type { ConfigValidationIssue } from "../schema/issues";
|
||||||
import type { CheckResult, DefaultsConfig, RawTargetConfig, ResolvedTargetBase } from "../types";
|
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../types";
|
||||||
|
|
||||||
export type Checker<TResolved extends ResolvedTargetBase = ResolvedTargetBase> = CheckerDefinition<TResolved>;
|
export type Checker<TResolved extends ResolvedTargetBase = ResolvedTargetBase> = CheckerDefinition<TResolved>;
|
||||||
|
|
||||||
@@ -22,18 +22,15 @@ export interface CheckerDefinition<TResolved extends ResolvedTargetBase = Resolv
|
|||||||
|
|
||||||
export interface CheckerSchemas {
|
export interface CheckerSchemas {
|
||||||
config: TSchema;
|
config: TSchema;
|
||||||
defaults: TSchema;
|
|
||||||
expect: TSchema;
|
expect: TSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckerValidationInput {
|
export interface CheckerValidationInput {
|
||||||
defaults: DefaultsConfig;
|
|
||||||
targets: RawTargetConfig[];
|
targets: RawTargetConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolveContext {
|
export interface ResolveContext {
|
||||||
configDir: string;
|
configDir: string;
|
||||||
defaultIntervalMs: number;
|
defaultIntervalMs: number;
|
||||||
defaults: DefaultsConfig;
|
|
||||||
defaultTimeoutMs: number;
|
defaultTimeoutMs: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { isError } from "es-toolkit";
|
|||||||
|
|
||||||
import type { CheckResult, RawTargetConfig } from "../../types";
|
import type { CheckResult, RawTargetConfig } from "../../types";
|
||||||
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
||||||
import type {
|
import type { RawUdpExpectConfig, ResolvedUdpExpectConfig, ResolvedUdpTarget, UdpTargetConfig } from "./types";
|
||||||
RawUdpExpectConfig,
|
|
||||||
ResolvedUdpExpectConfig,
|
|
||||||
ResolvedUdpTarget,
|
|
||||||
UdpDefaultsConfig,
|
|
||||||
UdpTargetConfig,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
import { resolveContentExpectations } from "../../expect/content";
|
import { resolveContentExpectations } from "../../expect/content";
|
||||||
import { errorFailure } from "../../expect/failure";
|
import { errorFailure } from "../../expect/failure";
|
||||||
@@ -304,13 +298,10 @@ export class UdpChecker implements CheckerDefinition<ResolvedUdpTarget> {
|
|||||||
|
|
||||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedUdpTarget {
|
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedUdpTarget {
|
||||||
const t = target as RawTargetConfig & { type: "udp"; udp: UdpTargetConfig };
|
const t = target as RawTargetConfig & { type: "udp"; udp: UdpTargetConfig };
|
||||||
const udpDefaults = context.defaults["udp"] as UdpDefaultsConfig | undefined;
|
|
||||||
|
|
||||||
const encoding = t.udp.encoding ?? udpDefaults?.encoding ?? "text";
|
const encoding = t.udp.encoding ?? "text";
|
||||||
const responseEncoding = t.udp.responseEncoding ?? udpDefaults?.responseEncoding ?? "text";
|
const responseEncoding = t.udp.responseEncoding ?? "text";
|
||||||
const maxResponseBytes = parseSize(
|
const maxResponseBytes = parseSize(t.udp.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES);
|
||||||
t.udp.maxResponseBytes ?? udpDefaults?.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rawExpect = target.expect as RawUdpExpectConfig | undefined;
|
const rawExpect = target.expect as RawUdpExpectConfig | undefined;
|
||||||
const resolvedExpect: ResolvedUdpExpectConfig = rawExpect
|
const resolvedExpect: ResolvedUdpExpectConfig = rawExpect
|
||||||
|
|||||||
@@ -20,14 +20,6 @@ export const udpCheckerSchemas: CheckerSchemas = {
|
|||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
),
|
),
|
||||||
defaults: Type.Object(
|
|
||||||
{
|
|
||||||
encoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])),
|
|
||||||
maxResponseBytes: Type.Optional(sizeSchema),
|
|
||||||
responseEncoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])),
|
|
||||||
},
|
|
||||||
{ additionalProperties: false },
|
|
||||||
),
|
|
||||||
expect: Type.Object(
|
expect: Type.Object(
|
||||||
{
|
{
|
||||||
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
durationMs: Type.Optional(createRawValueExpectationSchema()),
|
||||||
|
|||||||
@@ -44,12 +44,6 @@ export interface ResolvedUdpTarget extends ResolvedTargetBase {
|
|||||||
udp: ResolvedUdpConfig;
|
udp: ResolvedUdpConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UdpDefaultsConfig {
|
|
||||||
encoding?: UdpEncoding;
|
|
||||||
maxResponseBytes?: number | string;
|
|
||||||
responseEncoding?: UdpEncoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UdpEncoding = "base64" | "hex" | "text";
|
export type UdpEncoding = "base64" | "hex" | "text";
|
||||||
|
|
||||||
export interface UdpTargetConfig {
|
export interface UdpTargetConfig {
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ const VALID_ENCODINGS = new Set(["base64", "hex", "text"]);
|
|||||||
export function validateUdpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
export function validateUdpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||||
const issues: ConfigValidationIssue[] = [];
|
const issues: ConfigValidationIssue[] = [];
|
||||||
|
|
||||||
issues.push(...validateUdpDefaults(input));
|
|
||||||
|
|
||||||
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 (!isPlainRecord(target)) continue;
|
if (!isPlainRecord(target)) continue;
|
||||||
@@ -44,29 +42,6 @@ function validateSize(value: unknown, path: string, targetName: string | undefin
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUdpDefaults(input: CheckerValidationInput): ConfigValidationIssue[] {
|
|
||||||
const issues: ConfigValidationIssue[] = [];
|
|
||||||
const defaults = input.defaults["udp"];
|
|
||||||
if (defaults === undefined || defaults === null || !isPlainRecord(defaults)) return issues;
|
|
||||||
|
|
||||||
const targetName = "defaults.udp";
|
|
||||||
|
|
||||||
issues.push(...validateEncoding(defaults["encoding"], joinPath("defaults.udp", "encoding"), targetName));
|
|
||||||
issues.push(
|
|
||||||
...validateEncoding(defaults["responseEncoding"], joinPath("defaults.udp", "responseEncoding"), targetName),
|
|
||||||
);
|
|
||||||
issues.push(...validateSize(defaults["maxResponseBytes"], joinPath("defaults.udp", "maxResponseBytes"), targetName));
|
|
||||||
|
|
||||||
const allowedKeys = new Set(["encoding", "maxResponseBytes", "responseEncoding"]);
|
|
||||||
for (const key of Object.keys(defaults)) {
|
|
||||||
if (!allowedKeys.has(key)) {
|
|
||||||
issues.push(issue("unknown-field", joinPath("defaults.udp", key), "是未知字段", targetName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUdpExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
function validateUdpExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||||
const targetName = getTargetName(target);
|
const targetName = getTargetName(target);
|
||||||
const expect = target["expect"];
|
const expect = target["expect"];
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export function createExternalProbeConfigSchema(checkers: CheckerDefinition[]):
|
|||||||
export function createProbeConfigSchema(checkers: CheckerDefinition[], external = false): TSchema {
|
export function createProbeConfigSchema(checkers: CheckerDefinition[], external = false): TSchema {
|
||||||
return Type.Object(
|
return Type.Object(
|
||||||
{
|
{
|
||||||
defaults: Type.Optional(createDefaultsSchema(checkers)),
|
|
||||||
probes: Type.Optional(createProbesSchema()),
|
probes: Type.Optional(createProbesSchema()),
|
||||||
server: Type.Optional(createServerSchema()),
|
server: Type.Optional(createServerSchema()),
|
||||||
targets: Type.Array(external ? createExternalTargetSchema(checkers) : createBaseTargetSchema(checkers), {
|
targets: Type.Array(external ? createExternalTargetSchema(checkers) : createBaseTargetSchema(checkers), {
|
||||||
@@ -80,17 +79,6 @@ function createBaseTargetSchema(checkers: CheckerDefinition[]): TSchema {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultsSchema(checkers: CheckerDefinition[]): TSchema {
|
|
||||||
const properties: Record<string, TSchema> = {
|
|
||||||
interval: Type.Optional(durationSchema),
|
|
||||||
timeout: Type.Optional(durationSchema),
|
|
||||||
};
|
|
||||||
for (const checker of checkers) {
|
|
||||||
properties[checker.configKey] = Type.Optional(checker.schemas.defaults);
|
|
||||||
}
|
|
||||||
return Type.Object(properties, { additionalProperties: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
function createExternalTargetSchema(checkers: CheckerDefinition[]): TSchema {
|
function createExternalTargetSchema(checkers: CheckerDefinition[]): TSchema {
|
||||||
return Type.Union(checkers.map((checker) => createTargetSchema(checker)) as [TSchema, ...TSchema[]]);
|
return Type.Union(checkers.map((checker) => createTargetSchema(checker)) as [TSchema, ...TSchema[]]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ export interface CheckResult extends ApiCheckResult {
|
|||||||
targetId: string;
|
targetId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultsConfig {
|
|
||||||
[checkerKey: string]: unknown;
|
|
||||||
interval?: string;
|
|
||||||
timeout?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExecutionConfig {
|
export interface ExecutionConfig {
|
||||||
maxConcurrentChecks?: number;
|
maxConcurrentChecks?: number;
|
||||||
}
|
}
|
||||||
@@ -41,7 +35,6 @@ export interface LoggingFileRotationConfig {
|
|||||||
export type LogLevel = "debug" | "error" | "fatal" | "info" | "trace" | "warn";
|
export type LogLevel = "debug" | "error" | "fatal" | "info" | "trace" | "warn";
|
||||||
|
|
||||||
export interface ProbeConfig {
|
export interface ProbeConfig {
|
||||||
defaults?: DefaultsConfig;
|
|
||||||
probes?: ProbesConfig;
|
probes?: ProbesConfig;
|
||||||
server?: ServerConfig;
|
server?: ServerConfig;
|
||||||
targets: RawTargetConfig[];
|
targets: RawTargetConfig[];
|
||||||
|
|||||||
@@ -190,16 +190,6 @@ describe("loadConfig", () => {
|
|||||||
probes:
|
probes:
|
||||||
execution:
|
execution:
|
||||||
maxConcurrentChecks: 5
|
maxConcurrentChecks: 5
|
||||||
defaults:
|
|
||||||
interval: "15s"
|
|
||||||
timeout: "5s"
|
|
||||||
http:
|
|
||||||
headers:
|
|
||||||
Authorization: "Bearer token"
|
|
||||||
maxBodyBytes: "50MB"
|
|
||||||
cmd:
|
|
||||||
cwd: "/tmp"
|
|
||||||
maxOutputBytes: "10MB"
|
|
||||||
targets:
|
targets:
|
||||||
- name: "http-target"
|
- name: "http-target"
|
||||||
id: "http-target"
|
id: "http-target"
|
||||||
@@ -236,19 +226,18 @@ targets:
|
|||||||
expect(http.type).toBe("http");
|
expect(http.type).toBe("http");
|
||||||
expect(http.http.url).toBe("http://example.com");
|
expect(http.http.url).toBe("http://example.com");
|
||||||
expect(http.http.method).toBe("POST");
|
expect(http.http.method).toBe("POST");
|
||||||
expect(http.http.headers).toEqual({ Authorization: "Bearer token" });
|
|
||||||
expect(http.http.ignoreSSL).toBe(true);
|
expect(http.http.ignoreSSL).toBe(true);
|
||||||
expect(http.http.maxBodyBytes).toBe(52428800);
|
expect(http.http.maxBodyBytes).toBe(104857600);
|
||||||
expect(http.http.maxRedirects).toBe(5);
|
expect(http.http.maxRedirects).toBe(5);
|
||||||
expect(http.expect?.status).toEqual(["2xx", 301]);
|
expect(http.expect?.status).toEqual(["2xx", 301]);
|
||||||
expect(http.intervalMs).toBe(60000);
|
expect(http.intervalMs).toBe(60000);
|
||||||
expect(http.timeoutMs).toBe(5000);
|
expect(http.timeoutMs).toBe(10000);
|
||||||
|
|
||||||
const cmd = config.targets[1]! as ResolvedCommandTarget;
|
const cmd = config.targets[1]! as ResolvedCommandTarget;
|
||||||
expect(cmd.type).toBe("cmd");
|
expect(cmd.type).toBe("cmd");
|
||||||
expect(cmd.cmd.exec).toBe("ls");
|
expect(cmd.cmd.exec).toBe("ls");
|
||||||
expect(cmd.cmd.args).toEqual(["/tmp"]);
|
expect(cmd.cmd.args).toEqual(["/tmp"]);
|
||||||
expect(cmd.cmd.maxOutputBytes).toBe(10485760);
|
expect(cmd.cmd.maxOutputBytes).toBe(104857600);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("name 缺省时保留为 null", async () => {
|
test("name 缺省时保留为 null", async () => {
|
||||||
@@ -526,16 +515,11 @@ targets:
|
|||||||
expect(config.dataDir).toBe(dataDir);
|
expect(config.dataDir).toBe(dataDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("per-target 覆盖 defaults", async () => {
|
test("per-target interval 和 timeout 覆盖全局默认", async () => {
|
||||||
const configPath = join(tempDir, "override.yaml");
|
const configPath = join(tempDir, "override.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`defaults:
|
`targets:
|
||||||
interval: "30s"
|
|
||||||
timeout: "10s"
|
|
||||||
http:
|
|
||||||
maxBodyBytes: "10MB"
|
|
||||||
targets:
|
|
||||||
- name: "override-all"
|
- name: "override-all"
|
||||||
id: "override-all"
|
id: "override-all"
|
||||||
type: http
|
type: http
|
||||||
@@ -799,15 +783,13 @@ targets:
|
|||||||
const configPath = join(tempDir, "bad-size.yaml");
|
const configPath = join(tempDir, "bad-size.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`defaults:
|
`targets:
|
||||||
http:
|
|
||||||
maxBodyBytes: "100TB"
|
|
||||||
targets:
|
|
||||||
- name: "t"
|
- name: "t"
|
||||||
id: "t"
|
id: "t"
|
||||||
type: http
|
type: http
|
||||||
http:
|
http:
|
||||||
url: "http://a.com"
|
url: "http://a.com"
|
||||||
|
maxBodyBytes: "100TB"
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
await expectConfigLoadError(configPath, "无效的 size 格式");
|
await expectConfigLoadError(configPath, "无效的 size 格式");
|
||||||
@@ -1388,9 +1370,9 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("defaults.http.method 触发未知字段错误", async () => {
|
test("defaults 顶层字段触发未知字段错误", async () => {
|
||||||
await expectConfigError(
|
await expectConfigError(
|
||||||
"unknown-default-method.yaml",
|
"unknown-defaults.yaml",
|
||||||
`defaults:
|
`defaults:
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
@@ -1401,7 +1383,7 @@ targets:
|
|||||||
http:
|
http:
|
||||||
url: "http://example.com"
|
url: "http://example.com"
|
||||||
`,
|
`,
|
||||||
"defaults.http.method 是未知字段",
|
"defaults 是未知字段",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1428,11 +1410,7 @@ targets:
|
|||||||
const configPath = join(tempDir, "dynamic-maps.yaml");
|
const configPath = join(tempDir, "dynamic-maps.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`defaults:
|
`targets:
|
||||||
http:
|
|
||||||
headers:
|
|
||||||
X-Default-Header: "default"
|
|
||||||
targets:
|
|
||||||
- name: "http-test"
|
- name: "http-test"
|
||||||
id: "http-test"
|
id: "http-test"
|
||||||
type: http
|
type: http
|
||||||
@@ -1458,7 +1436,6 @@ targets:
|
|||||||
const cmdTarget = config.targets[1] as ResolvedCommandTarget;
|
const cmdTarget = config.targets[1] as ResolvedCommandTarget;
|
||||||
expect(http.type).toBe("http");
|
expect(http.type).toBe("http");
|
||||||
expect(cmdTarget.type).toBe("cmd");
|
expect(cmdTarget.type).toBe("cmd");
|
||||||
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(cmdTarget.cmd.env["CUSTOM_ENV_NAME"]).toBe("custom");
|
expect(cmdTarget.cmd.env["CUSTOM_ENV_NAME"]).toBe("custom");
|
||||||
});
|
});
|
||||||
@@ -1924,15 +1901,11 @@ targets:
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tcp defaults 覆盖 banner 参数", async () => {
|
test("tcp per-target banner 参数覆盖", async () => {
|
||||||
const configPath = join(tempDir, "tcp-defaults.yaml");
|
const configPath = join(tempDir, "tcp-defaults.yaml");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
configPath,
|
configPath,
|
||||||
`defaults:
|
`targets:
|
||||||
tcp:
|
|
||||||
bannerReadTimeout: 1000
|
|
||||||
maxBannerBytes: "8KB"
|
|
||||||
targets:
|
|
||||||
- id: "t1"
|
- id: "t1"
|
||||||
type: tcp
|
type: tcp
|
||||||
tcp:
|
tcp:
|
||||||
@@ -1949,12 +1922,12 @@ targets:
|
|||||||
|
|
||||||
const config = await loadConfig(configPath);
|
const config = await loadConfig(configPath);
|
||||||
const t1 = config.targets[0]! as ResolvedTcpTarget;
|
const t1 = config.targets[0]! as ResolvedTcpTarget;
|
||||||
expect(t1.tcp.bannerReadTimeout).toBe(1000);
|
expect(t1.tcp.bannerReadTimeout).toBe(2000);
|
||||||
expect(t1.tcp.maxBannerBytes).toBe(8192);
|
expect(t1.tcp.maxBannerBytes).toBe(4096);
|
||||||
|
|
||||||
const t2 = config.targets[1]! as ResolvedTcpTarget;
|
const t2 = config.targets[1]! as ResolvedTcpTarget;
|
||||||
expect(t2.tcp.bannerReadTimeout).toBe(3000);
|
expect(t2.tcp.bannerReadTimeout).toBe(3000);
|
||||||
expect(t2.tcp.maxBannerBytes).toBe(8192);
|
expect(t2.tcp.maxBannerBytes).toBe(4096);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tcp expect 未知字段抛出错误", async () => {
|
test("tcp expect 未知字段抛出错误", async () => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function makeCtx(timeoutMs = 5000): CheckerContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeResolveContext(): ResolveContext {
|
function makeResolveContext(): ResolveContext {
|
||||||
return { configDir: process.cwd(), defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 };
|
return { configDir: process.cwd(), defaultIntervalMs: 30000, defaultTimeoutMs: 10000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTarget(
|
function makeTarget(
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import { validateDbConfig } from "../../../../../src/server/checker/runner/db/va
|
|||||||
|
|
||||||
describe("validateDbConfig", () => {
|
describe("validateDbConfig", () => {
|
||||||
test("空配置无问题", () => {
|
test("空配置无问题", () => {
|
||||||
const result = validateDbConfig({ defaults: {}, targets: [] });
|
const result = validateDbConfig({ targets: [] });
|
||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("缺少 db.url 返回错误", () => {
|
test("缺少 db.url 返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [{ id: "test", name: "test", type: "db" }],
|
targets: [{ id: "test", name: "test", type: "db" }],
|
||||||
});
|
});
|
||||||
expect(result.length).toBeGreaterThan(0);
|
expect(result.length).toBeGreaterThan(0);
|
||||||
@@ -21,7 +20,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("db.url 为空字符串返回错误", () => {
|
test("db.url 为空字符串返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [{ db: { url: "" }, id: "test", name: "test", type: "db" }],
|
targets: [{ db: { url: "" }, id: "test", name: "test", type: "db" }],
|
||||||
});
|
});
|
||||||
const urlError = result.find((e) => e.path.includes("db.url"));
|
const urlError = result.find((e) => e.path.includes("db.url"));
|
||||||
@@ -31,7 +29,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("db.query 为空字符串返回错误", () => {
|
test("db.query 为空字符串返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [{ db: { query: "", url: "sqlite://:memory:" }, id: "test", name: "test", type: "db" }],
|
targets: [{ db: { query: "", url: "sqlite://:memory:" }, id: "test", name: "test", type: "db" }],
|
||||||
});
|
});
|
||||||
const queryError = result.find((e) => e.path.includes("db.query"));
|
const queryError = result.find((e) => e.path.includes("db.query"));
|
||||||
@@ -41,7 +38,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("db 分组未知字段返回错误", () => {
|
test("db 分组未知字段返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [{ db: { timeout: 5, url: "sqlite://:memory:" }, id: "test", name: "test", type: "db" }],
|
targets: [{ db: { timeout: 5, url: "sqlite://:memory:" }, id: "test", name: "test", type: "db" }],
|
||||||
});
|
});
|
||||||
const unknownError = result.find((e) => e.path.includes("db.timeout"));
|
const unknownError = result.find((e) => e.path.includes("db.timeout"));
|
||||||
@@ -51,7 +47,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("expect.durationMs 数组简写返回错误", () => {
|
test("expect.durationMs 数组简写返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
db: { url: "sqlite://:memory:" },
|
db: { url: "sqlite://:memory:" },
|
||||||
@@ -69,7 +64,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("expect.rowCount 非法 operator 返回错误", () => {
|
test("expect.rowCount 非法 operator 返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{ db: { url: "sqlite://:memory:" }, expect: { rowCount: { foo: 1 } }, id: "test", name: "test", type: "db" },
|
{ db: { url: "sqlite://:memory:" }, expect: { rowCount: { foo: 1 } }, id: "test", name: "test", type: "db" },
|
||||||
],
|
],
|
||||||
@@ -81,7 +75,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("expect.rows 不是数组返回错误", () => {
|
test("expect.rows 不是数组返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{ db: { url: "sqlite://:memory:" }, expect: { rows: "not-array" }, id: "test", name: "test", type: "db" },
|
{ db: { url: "sqlite://:memory:" }, expect: { rows: "not-array" }, id: "test", name: "test", type: "db" },
|
||||||
],
|
],
|
||||||
@@ -93,7 +86,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("expect.rows 元素不是对象返回错误", () => {
|
test("expect.rows 元素不是对象返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{ db: { url: "sqlite://:memory:" }, expect: { rows: ["not-object"] }, id: "test", name: "test", type: "db" },
|
{ db: { url: "sqlite://:memory:" }, expect: { rows: ["not-object"] }, id: "test", name: "test", type: "db" },
|
||||||
],
|
],
|
||||||
@@ -105,7 +97,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("expect.rows 中 regex 正则非法返回错误", () => {
|
test("expect.rows 中 regex 正则非法返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
db: { url: "sqlite://:memory:" },
|
db: { url: "sqlite://:memory:" },
|
||||||
@@ -123,7 +114,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("expect 未知字段返回错误", () => {
|
test("expect 未知字段返回错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [{ db: { url: "sqlite://:memory:" }, expect: { status: [200] }, id: "test", name: "test", type: "db" }],
|
targets: [{ db: { url: "sqlite://:memory:" }, expect: { status: [200] }, id: "test", name: "test", type: "db" }],
|
||||||
});
|
});
|
||||||
const unknownError = result.find((e) => e.path.includes("expect.status"));
|
const unknownError = result.find((e) => e.path.includes("expect.status"));
|
||||||
@@ -133,7 +123,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("有效配置无错误", () => {
|
test("有效配置无错误", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
db: { query: "SELECT 1", url: "sqlite://:memory:" },
|
db: { query: "SELECT 1", url: "sqlite://:memory:" },
|
||||||
@@ -149,7 +138,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("忽略非 db 类型 target", () => {
|
test("忽略非 db 类型 target", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [{ id: "test", name: "test", type: "http" }],
|
targets: [{ id: "test", name: "test", type: "http" }],
|
||||||
});
|
});
|
||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
@@ -157,7 +145,6 @@ describe("validateDbConfig", () => {
|
|||||||
|
|
||||||
test("多个 db target 分别校验", () => {
|
test("多个 db target 分别校验", () => {
|
||||||
const result = validateDbConfig({
|
const result = validateDbConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
{ db: { url: "sqlite://:memory:" }, id: "db1", name: "db1", type: "db" },
|
{ db: { url: "sqlite://:memory:" }, id: "db1", name: "db1", type: "db" },
|
||||||
{ db: { url: "" }, id: "db2", name: "db2", type: "db" },
|
{ db: { url: "" }, id: "db2", name: "db2", type: "db" },
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const SLOW_BODY_DELAY_MS = 1000;
|
|||||||
const FAST_RESPONSE_LIMIT_MS = 500;
|
const FAST_RESPONSE_LIMIT_MS = 500;
|
||||||
|
|
||||||
function validateHttpTarget(target: unknown): string {
|
function validateHttpTarget(target: unknown): string {
|
||||||
return formatConfigIssues(checker.validate({ defaults: {}, targets: [target as never] }));
|
return formatConfigIssues(checker.validate({ targets: [target as never] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const SELF_SIGNED_CERT = `-----BEGIN CERTIFICATE-----
|
const SELF_SIGNED_CERT = `-----BEGIN CERTIFICATE-----
|
||||||
@@ -946,7 +946,6 @@ describe("HttpChecker.resolve", () => {
|
|||||||
return {
|
return {
|
||||||
configDir: ".",
|
configDir: ".",
|
||||||
defaultIntervalMs: 30000,
|
defaultIntervalMs: 30000,
|
||||||
defaults: {},
|
|
||||||
defaultTimeoutMs: 10000,
|
defaultTimeoutMs: 10000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ describe("IcmpChecker resolve", () => {
|
|||||||
test("解析默认值", () => {
|
test("解析默认值", () => {
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ icmp: { host: "10.0.0.1" }, id: "ping", type: "icmp" },
|
{ icmp: { host: "10.0.0.1" }, id: "ping", type: "icmp" },
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
|
expect(target.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
|
||||||
expect(target.group).toBe("default");
|
expect(target.group).toBe("default");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
|||||||
import { validatePingConfig } from "../../../../../src/server/checker/runner/icmp/validate";
|
import { validatePingConfig } from "../../../../../src/server/checker/runner/icmp/validate";
|
||||||
|
|
||||||
function validate(target: RawTargetConfig) {
|
function validate(target: RawTargetConfig) {
|
||||||
return validatePingConfig({ defaults: {}, targets: [target] });
|
return validatePingConfig({ targets: [target] });
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("validatePingConfig", () => {
|
describe("validatePingConfig", () => {
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ describe("LLM registry integration", () => {
|
|||||||
test("llm checker schemas 有效", () => {
|
test("llm checker schemas 有效", () => {
|
||||||
const checker = checkerRegistry.get("llm");
|
const checker = checkerRegistry.get("llm");
|
||||||
expect(checker.schemas.config).toBeDefined();
|
expect(checker.schemas.config).toBeDefined();
|
||||||
expect(checker.schemas.defaults).toBeDefined();
|
|
||||||
expect(checker.schemas.expect).toBeDefined();
|
expect(checker.schemas.expect).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("llm checker validate 方法可用", () => {
|
test("llm checker validate 方法可用", () => {
|
||||||
const checker = checkerRegistry.get("llm");
|
const checker = checkerRegistry.get("llm");
|
||||||
const issues = checker.validate({
|
const issues = checker.validate({
|
||||||
defaults: {},
|
|
||||||
targets: [],
|
targets: [],
|
||||||
});
|
});
|
||||||
expect(issues).toHaveLength(0);
|
expect(issues).toHaveLength(0);
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ function makeResolveContext(overrides?: Partial<ResolveContext>): ResolveContext
|
|||||||
return {
|
return {
|
||||||
configDir: "/tmp",
|
configDir: "/tmp",
|
||||||
defaultIntervalMs: 30000,
|
defaultIntervalMs: 30000,
|
||||||
defaults: {},
|
|
||||||
defaultTimeoutMs: 10000,
|
defaultTimeoutMs: 10000,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@@ -61,16 +60,15 @@ describe("LlmChecker schema", () => {
|
|||||||
expect(checker?.configKey).toBe("llm");
|
expect(checker?.configKey).toBe("llm");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("schemas 包含 config、defaults、expect", () => {
|
test("schemas 包含 config、expect", () => {
|
||||||
expect(checker).toBeDefined();
|
expect(checker).toBeDefined();
|
||||||
expect(Object.keys(checker!.schemas).sort()).toEqual(["config", "defaults", "expect"].sort());
|
expect(Object.keys(checker!.schemas).sort()).toEqual(["config", "expect"].sort());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("LlmChecker validate", () => {
|
describe("LlmChecker validate", () => {
|
||||||
test("合法 LLM target 无校验问题", () => {
|
test("合法 LLM target 无校验问题", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [makeRawTarget()],
|
targets: [makeRawTarget()],
|
||||||
});
|
});
|
||||||
expect(issues).toHaveLength(0);
|
expect(issues).toHaveLength(0);
|
||||||
@@ -78,7 +76,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("provider 非法报错", () => {
|
test("provider 非法报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { model: "m", prompt: "p", provider: "gemini", url: "https://x" },
|
llm: { model: "m", prompt: "p", provider: "gemini", url: "https://x" },
|
||||||
@@ -91,7 +88,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("url 非法报错", () => {
|
test("url 非法报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { model: "m", prompt: "p", provider: "openai", url: "ftp://bad" },
|
llm: { model: "m", prompt: "p", provider: "openai", url: "ftp://bad" },
|
||||||
@@ -104,7 +100,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("model 为空报错", () => {
|
test("model 为空报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { model: "", prompt: "p", provider: "openai", url: "https://x" },
|
llm: { model: "", prompt: "p", provider: "openai", url: "https://x" },
|
||||||
@@ -117,7 +112,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("prompt 为空报错", () => {
|
test("prompt 为空报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { model: "m", prompt: "", provider: "openai", url: "https://x" },
|
llm: { model: "m", prompt: "", provider: "openai", url: "https://x" },
|
||||||
@@ -130,7 +124,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("mode 非法报错", () => {
|
test("mode 非法报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { mode: "batch", model: "m", prompt: "p", provider: "openai", url: "https://x" },
|
llm: { mode: "batch", model: "m", prompt: "p", provider: "openai", url: "https://x" },
|
||||||
@@ -143,7 +136,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("openai provider 不允许 authToken", () => {
|
test("openai provider 不允许 authToken", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { authToken: "tok", model: "m", prompt: "p", provider: "openai", url: "https://x" },
|
llm: { authToken: "tok", model: "m", prompt: "p", provider: "openai", url: "https://x" },
|
||||||
@@ -155,7 +147,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("anthropic 同时配置 key 和 authToken 报错", () => {
|
test("anthropic 同时配置 key 和 authToken 报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { authToken: "tok", key: "k", model: "m", prompt: "p", provider: "anthropic", url: "https://x" },
|
llm: { authToken: "tok", key: "k", model: "m", prompt: "p", provider: "anthropic", url: "https://x" },
|
||||||
@@ -167,7 +158,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("ignoreSSL 非布尔值报错", () => {
|
test("ignoreSSL 非布尔值报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { ignoreSSL: "yes", model: "m", prompt: "p", provider: "openai", url: "https://x" },
|
llm: { ignoreSSL: "yes", model: "m", prompt: "p", provider: "openai", url: "https://x" },
|
||||||
@@ -179,7 +169,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("options.maxOutputTokens 非正整数报错", () => {
|
test("options.maxOutputTokens 非正整数报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { model: "m", options: { maxOutputTokens: -1 }, prompt: "p", provider: "openai", url: "https://x" },
|
llm: { model: "m", options: { maxOutputTokens: -1 }, prompt: "p", provider: "openai", url: "https://x" },
|
||||||
@@ -191,7 +180,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("options.stopSequences 非字符串数组报错", () => {
|
test("options.stopSequences 非字符串数组报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
llm: { model: "m", options: { stopSequences: [123] }, prompt: "p", provider: "openai", url: "https://x" },
|
llm: { model: "m", options: { stopSequences: [123] }, prompt: "p", provider: "openai", url: "https://x" },
|
||||||
@@ -203,7 +191,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("expect.output 缺少规则类型报错", () => {
|
test("expect.output 缺少规则类型报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [makeRawTarget({ expect: { output: [{}] } })],
|
targets: [makeRawTarget({ expect: { output: [{}] } })],
|
||||||
});
|
});
|
||||||
expect(issues.some((i) => i.code === "empty-matcher")).toBe(true);
|
expect(issues.some((i) => i.code === "empty-matcher")).toBe(true);
|
||||||
@@ -211,7 +198,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("expect.output 直接 matcher 混入 extractor 报错", () => {
|
test("expect.output 直接 matcher 混入 extractor 报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [makeRawTarget({ expect: { output: [{ contains: "y", json: { path: "$.status" } }] } })],
|
targets: [makeRawTarget({ expect: { output: [{ contains: "y", json: { path: "$.status" } }] } })],
|
||||||
});
|
});
|
||||||
expect(issues.some((i) => i.code === "invalid-content-expectation")).toBe(true);
|
expect(issues.some((i) => i.code === "invalid-content-expectation")).toBe(true);
|
||||||
@@ -219,7 +205,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("expect.output regex ReDoS 报错", () => {
|
test("expect.output regex ReDoS 报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [makeRawTarget({ expect: { output: [{ regex: "(a+)+" }] } })],
|
targets: [makeRawTarget({ expect: { output: [{ regex: "(a+)+" }] } })],
|
||||||
});
|
});
|
||||||
expect(issues.some((i) => i.code === "unsafe-regex")).toBe(true);
|
expect(issues.some((i) => i.code === "unsafe-regex")).toBe(true);
|
||||||
@@ -227,7 +212,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("expect.stream 在 mode:http 下报错", () => {
|
test("expect.stream 在 mode:http 下报错", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
expect: { stream: { completed: true } },
|
expect: { stream: { completed: true } },
|
||||||
@@ -240,7 +224,6 @@ describe("LlmChecker validate", () => {
|
|||||||
|
|
||||||
test("expect.stream 在 mode:stream 下合法", () => {
|
test("expect.stream 在 mode:stream 下合法", () => {
|
||||||
const issues = validateLlmConfig({
|
const issues = validateLlmConfig({
|
||||||
defaults: {},
|
|
||||||
targets: [
|
targets: [
|
||||||
makeRawTarget({
|
makeRawTarget({
|
||||||
expect: { stream: { completed: true } },
|
expect: { stream: { completed: true } },
|
||||||
@@ -250,24 +233,6 @@ describe("LlmChecker validate", () => {
|
|||||||
});
|
});
|
||||||
expect(issues).toHaveLength(0);
|
expect(issues).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("defaults.llm 合法配置", () => {
|
|
||||||
const issues = validateLlmConfig({
|
|
||||||
defaults: {
|
|
||||||
llm: { headers: { "X-Custom": "val" }, ignoreSSL: false, mode: "http", options: { maxOutputTokens: 32 } },
|
|
||||||
},
|
|
||||||
targets: [makeRawTarget()],
|
|
||||||
});
|
|
||||||
expect(issues).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults.llm mode 非法报错", () => {
|
|
||||||
const issues = validateLlmConfig({
|
|
||||||
defaults: { llm: { mode: "batch" } },
|
|
||||||
targets: [makeRawTarget()],
|
|
||||||
});
|
|
||||||
expect(issues.some((i) => i.path.includes("defaults.llm.mode"))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("LlmChecker resolve", () => {
|
describe("LlmChecker resolve", () => {
|
||||||
@@ -324,61 +289,6 @@ describe("LlmChecker resolve", () => {
|
|||||||
expect(resolved.expect?.stream).toEqual({ completed: true, firstTokenMs: { equals: 100 } });
|
expect(resolved.expect?.stream).toEqual({ completed: true, firstTokenMs: { equals: 100 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("defaults.llm 与 target.llm 浅合并", () => {
|
|
||||||
const raw = makeRawTarget({
|
|
||||||
llm: {
|
|
||||||
headers: { Authorization: "Bearer test" },
|
|
||||||
model: "gpt-4o-mini",
|
|
||||||
prompt: "Say OK",
|
|
||||||
provider: "openai",
|
|
||||||
url: "https://api.openai.com/v1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ctx = makeResolveContext({
|
|
||||||
defaults: {
|
|
||||||
llm: {
|
|
||||||
headers: { "X-Custom": "default" },
|
|
||||||
ignoreSSL: true,
|
|
||||||
mode: "stream",
|
|
||||||
options: { maxOutputTokens: 64, temperature: 0.5 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const resolved = asLlm(checker.resolve(raw, ctx));
|
|
||||||
expect(resolved.llm.mode).toBe("stream");
|
|
||||||
expect(resolved.llm.ignoreSSL).toBe(true);
|
|
||||||
expect(resolved.llm.headers).toEqual({ Authorization: "Bearer test", "X-Custom": "default" });
|
|
||||||
expect(resolved.llm.options.maxOutputTokens).toBe(64);
|
|
||||||
expect(resolved.llm.options.temperature).toBe(0.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("target 字段覆盖 defaults", () => {
|
|
||||||
const raw = makeRawTarget({
|
|
||||||
llm: {
|
|
||||||
ignoreSSL: false,
|
|
||||||
mode: "http",
|
|
||||||
model: "gpt-4o-mini",
|
|
||||||
options: { maxOutputTokens: 8 },
|
|
||||||
prompt: "Say OK",
|
|
||||||
provider: "openai",
|
|
||||||
url: "https://api.openai.com/v1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ctx = makeResolveContext({
|
|
||||||
defaults: {
|
|
||||||
llm: {
|
|
||||||
ignoreSSL: true,
|
|
||||||
mode: "stream",
|
|
||||||
options: { maxOutputTokens: 64 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const resolved = asLlm(checker.resolve(raw, ctx));
|
|
||||||
expect(resolved.llm.mode).toBe("http");
|
|
||||||
expect(resolved.llm.ignoreSSL).toBe(false);
|
|
||||||
expect(resolved.llm.options.maxOutputTokens).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("serialize 返回正确格式", () => {
|
test("serialize 返回正确格式", () => {
|
||||||
const resolved = asLlm(checker.resolve(makeRawTarget(), makeResolveContext()));
|
const resolved = asLlm(checker.resolve(makeRawTarget(), makeResolveContext()));
|
||||||
const serialized = checker.serialize(resolved);
|
const serialized = checker.serialize(resolved);
|
||||||
@@ -398,25 +308,4 @@ describe("LlmChecker resolve", () => {
|
|||||||
const config = parseSerializedConfig(serialized.config);
|
const config = parseSerializedConfig(serialized.config);
|
||||||
expect(config.key).toBe("***");
|
expect(config.key).toBe("***");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("providerOptions 浅合并", () => {
|
|
||||||
const raw = makeRawTarget({
|
|
||||||
llm: {
|
|
||||||
model: "m",
|
|
||||||
prompt: "p",
|
|
||||||
provider: "openai",
|
|
||||||
providerOptions: { openai: { store: true } },
|
|
||||||
url: "https://x",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ctx = makeResolveContext({
|
|
||||||
defaults: {
|
|
||||||
llm: {
|
|
||||||
providerOptions: { openai: { user: "default-user" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const resolved = asLlm(checker.resolve(raw, ctx));
|
|
||||||
expect(resolved.llm.providerOptions).toEqual({ openai: { store: true } });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ function createChecker(type: string): Checker {
|
|||||||
resolve: () => ({}) as unknown as ResolvedTargetBase,
|
resolve: () => ({}) as unknown as ResolvedTargetBase,
|
||||||
schemas: {
|
schemas: {
|
||||||
config: Type.Object({}, { additionalProperties: false }),
|
config: Type.Object({}, { additionalProperties: false }),
|
||||||
defaults: Type.Object({}, { additionalProperties: false }),
|
|
||||||
expect: Type.Object({}, { additionalProperties: false }),
|
expect: Type.Object({}, { additionalProperties: false }),
|
||||||
},
|
},
|
||||||
serialize: () => ({ config: "", target: "" }),
|
serialize: () => ({ config: "", target: "" }),
|
||||||
@@ -69,11 +68,7 @@ describe("CheckerRegistry", () => {
|
|||||||
|
|
||||||
expect(first.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "custom"]);
|
expect(first.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "custom"]);
|
||||||
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm"]);
|
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm"]);
|
||||||
expect(
|
expect(first.definitions.every((checker) => checker.schemas.config && checker.schemas.expect)).toBe(true);
|
||||||
first.definitions.every(
|
|
||||||
(checker) => checker.schemas.config && checker.schemas.defaults && checker.schemas.expect,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("默认 registry 注册 icmp type", () => {
|
test("默认 registry 注册 icmp type", () => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { validateHttpConfig } from "../../../../../src/server/checker/runner/htt
|
|||||||
import { validateLlmConfig } from "../../../../../src/server/checker/runner/llm/validate";
|
import { validateLlmConfig } from "../../../../../src/server/checker/runner/llm/validate";
|
||||||
|
|
||||||
function input(target: Record<string, unknown>): CheckerValidationInput {
|
function input(target: Record<string, unknown>): CheckerValidationInput {
|
||||||
return { defaults: {}, targets: [target as CheckerValidationInput["targets"][number]] };
|
return { targets: [target as CheckerValidationInput["targets"][number]] };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("HTTP/LLM headers reject case-insensitive duplicate keys", () => {
|
describe("HTTP/LLM headers reject case-insensitive duplicate keys", () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/
|
|||||||
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
|
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
|
||||||
|
|
||||||
function input(target: Record<string, unknown>): CheckerValidationInput {
|
function input(target: Record<string, unknown>): CheckerValidationInput {
|
||||||
return { defaults: {}, targets: [target as CheckerValidationInput["targets"][number]] };
|
return { targets: [target as CheckerValidationInput["targets"][number]] };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("ValueMatcher primitive shorthand in checker validators", () => {
|
describe("ValueMatcher primitive shorthand in checker validators", () => {
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ describe("TcpChecker resolve", () => {
|
|||||||
test("最简 tcp 配置解析默认值", () => {
|
test("最简 tcp 配置解析默认值", () => {
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ id: "t", tcp: { host: "127.0.0.1", port: 6379 }, type: "tcp" },
|
{ id: "t", tcp: { host: "127.0.0.1", port: 6379 }, type: "tcp" },
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.tcp.host).toBe("127.0.0.1");
|
expect(target.tcp.host).toBe("127.0.0.1");
|
||||||
expect(target.tcp.port).toBe(6379);
|
expect(target.tcp.port).toBe(6379);
|
||||||
@@ -325,44 +325,17 @@ describe("TcpChecker resolve", () => {
|
|||||||
tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", maxBannerBytes: "1KB", port: 80, readBanner: true },
|
tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", maxBannerBytes: "1KB", port: 80, readBanner: true },
|
||||||
type: "tcp",
|
type: "tcp",
|
||||||
},
|
},
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.tcp.bannerReadTimeout).toBe(5000);
|
expect(target.tcp.bannerReadTimeout).toBe(5000);
|
||||||
expect(target.tcp.maxBannerBytes).toBe(1024);
|
expect(target.tcp.maxBannerBytes).toBe(1024);
|
||||||
expect(target.tcp.readBanner).toBe(true);
|
expect(target.tcp.readBanner).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("defaults.tcp 合并到 target", () => {
|
|
||||||
const target = checker.resolve(
|
|
||||||
{ id: "t", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
|
|
||||||
{
|
|
||||||
configDir: "/tmp",
|
|
||||||
defaultIntervalMs: 30000,
|
|
||||||
defaults: { tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" } },
|
|
||||||
defaultTimeoutMs: 10000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(target.tcp.bannerReadTimeout).toBe(1000);
|
|
||||||
expect(target.tcp.maxBannerBytes).toBe(8192);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("per-target 覆盖 defaults.tcp", () => {
|
|
||||||
const target = checker.resolve(
|
|
||||||
{ id: "t", tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", port: 80 }, type: "tcp" },
|
|
||||||
{
|
|
||||||
configDir: "/tmp",
|
|
||||||
defaultIntervalMs: 30000,
|
|
||||||
defaults: { tcp: { bannerReadTimeout: 1000 } },
|
|
||||||
defaultTimeoutMs: 10000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(target.tcp.bannerReadTimeout).toBe(5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("maxBannerBytes 整数默认值解析", () => {
|
test("maxBannerBytes 整数默认值解析", () => {
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ id: "t", tcp: { host: "127.0.0.1", maxBannerBytes: 2048, port: 80 }, type: "tcp" },
|
{ id: "t", tcp: { host: "127.0.0.1", maxBannerBytes: 2048, port: 80 }, type: "tcp" },
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.tcp.maxBannerBytes).toBe(2048);
|
expect(target.tcp.maxBannerBytes).toBe(2048);
|
||||||
});
|
});
|
||||||
@@ -375,7 +348,7 @@ describe("TcpChecker resolve", () => {
|
|||||||
tcp: { host: "127.0.0.1", port: 80, readBanner: true },
|
tcp: { host: "127.0.0.1", port: 80, readBanner: true },
|
||||||
type: "tcp",
|
type: "tcp",
|
||||||
},
|
},
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.expect).toEqual({
|
expect(target.expect).toEqual({
|
||||||
banner: [{ kind: "value", matcher: { contains: "ESMTP" } }],
|
banner: [{ kind: "value", matcher: { contains: "ESMTP" } }],
|
||||||
@@ -388,7 +361,7 @@ describe("TcpChecker resolve", () => {
|
|||||||
test("name 和 group 解析", () => {
|
test("name 和 group 解析", () => {
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ group: "infra", id: "t", name: "redis", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
|
{ group: "infra", id: "t", name: "redis", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.name).toBe("redis");
|
expect(target.name).toBe("redis");
|
||||||
expect(target.group).toBe("infra");
|
expect(target.group).toBe("infra");
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import type { CheckerValidationInput } from "../../../../../src/server/checker/r
|
|||||||
|
|
||||||
import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/validate";
|
import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/validate";
|
||||||
|
|
||||||
function makeInput(targets: unknown[], defaults?: Record<string, unknown>): CheckerValidationInput {
|
function makeInput(targets: unknown[]): CheckerValidationInput {
|
||||||
return {
|
return {
|
||||||
defaults: defaults ?? {},
|
|
||||||
targets: targets as CheckerValidationInput["targets"],
|
targets: targets as CheckerValidationInput["targets"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -152,40 +151,4 @@ describe("validateTcpConfig", () => {
|
|||||||
const issues = validateTcpConfig(makeInput([{ http: { url: "http://example.com" }, id: "t1", type: "http" }]));
|
const issues = validateTcpConfig(makeInput([{ http: { url: "http://example.com" }, id: "t1", type: "http" }]));
|
||||||
expect(issues).toHaveLength(0);
|
expect(issues).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("defaults.tcp 合法字段无错误", () => {
|
|
||||||
const issues = validateTcpConfig(
|
|
||||||
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
|
|
||||||
tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(issues).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults.tcp 未知字段", () => {
|
|
||||||
const issues = validateTcpConfig(
|
|
||||||
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
|
|
||||||
tcp: { bannerReadTimeout: 1000, host: "127.0.0.1" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(issues.some((i) => i.message.includes("未知字段"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults.tcp bannerReadTimeout 非法", () => {
|
|
||||||
const issues = validateTcpConfig(
|
|
||||||
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
|
|
||||||
tcp: { bannerReadTimeout: "slow" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(issues.some((i) => i.path.includes("bannerReadTimeout"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults.tcp maxBannerBytes 非法", () => {
|
|
||||||
const issues = validateTcpConfig(
|
|
||||||
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
|
|
||||||
tcp: { maxBannerBytes: true },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(issues.some((i) => i.path.includes("maxBannerBytes"))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ describe("UdpChecker resolve", () => {
|
|||||||
const checker = new UdpChecker();
|
const checker = new UdpChecker();
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
|
{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
|
||||||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
);
|
);
|
||||||
expect(target.udp.payload).toBe("");
|
expect(target.udp.payload).toBe("");
|
||||||
expect(target.udp.encoding).toBe("text");
|
expect(target.udp.encoding).toBe("text");
|
||||||
@@ -344,32 +344,11 @@ describe("UdpChecker resolve", () => {
|
|||||||
expect(target.expect).toEqual({ responded: true });
|
expect(target.expect).toEqual({ responded: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use defaults.udp for missing fields", () => {
|
|
||||||
const checker = new UdpChecker();
|
|
||||||
const target = checker.resolve(
|
|
||||||
{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
|
|
||||||
{
|
|
||||||
configDir: "/tmp",
|
|
||||||
defaultIntervalMs: 30000,
|
|
||||||
defaults: { udp: { encoding: "hex", maxResponseBytes: "8KB", responseEncoding: "hex" } },
|
|
||||||
defaultTimeoutMs: 10000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(target.udp.encoding).toBe("hex");
|
|
||||||
expect(target.udp.responseEncoding).toBe("hex");
|
|
||||||
expect(target.udp.maxResponseBytes).toBe(8192);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should override defaults with target-level config", () => {
|
it("should override defaults with target-level config", () => {
|
||||||
const checker = new UdpChecker();
|
const checker = new UdpChecker();
|
||||||
const target = checker.resolve(
|
const target = checker.resolve(
|
||||||
{ id: "test", type: "udp", udp: { encoding: "base64", host: "127.0.0.1", port: 9000 } },
|
{ id: "test", type: "udp", udp: { encoding: "base64", host: "127.0.0.1", port: 9000 } },
|
||||||
{
|
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
|
||||||
configDir: "/tmp",
|
|
||||||
defaultIntervalMs: 30000,
|
|
||||||
defaults: { udp: { encoding: "hex" } },
|
|
||||||
defaultTimeoutMs: 10000,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
expect(target.udp.encoding).toBe("base64");
|
expect(target.udp.encoding).toBe("base64");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import type { CheckerValidationInput } from "../../../../../src/server/checker/r
|
|||||||
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
|
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
|
||||||
|
|
||||||
describe("validateUdpConfig", () => {
|
describe("validateUdpConfig", () => {
|
||||||
const makeInput = (overrides: {
|
const makeInput = (overrides: { targets?: Array<Record<string, unknown>> }): CheckerValidationInput => ({
|
||||||
defaults?: Record<string, unknown>;
|
|
||||||
targets?: Array<Record<string, unknown>>;
|
|
||||||
}): CheckerValidationInput => ({
|
|
||||||
defaults: overrides.defaults ?? {},
|
|
||||||
targets: (overrides.targets ?? []) as CheckerValidationInput["targets"],
|
targets: (overrides.targets ?? []) as CheckerValidationInput["targets"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,29 +63,6 @@ describe("validateUdpConfig", () => {
|
|||||||
expect(issues[0]!.path).toContain("udp");
|
expect(issues[0]!.path).toContain("udp");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts valid defaults.udp with encoding, responseEncoding, maxResponseBytes", () => {
|
|
||||||
const issues = validateUdpConfig(
|
|
||||||
makeInput({
|
|
||||||
defaults: {
|
|
||||||
udp: { encoding: "hex", maxResponseBytes: 1024, responseEncoding: "text" },
|
|
||||||
},
|
|
||||||
targets: [{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 53 } }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(issues).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports unknown-field in defaults.udp", () => {
|
|
||||||
const issues = validateUdpConfig(
|
|
||||||
makeInput({
|
|
||||||
defaults: { udp: { unknownField: true } },
|
|
||||||
targets: [{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 53 } }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(issues).toHaveLength(1);
|
|
||||||
expect(issues[0]!.code).toBe("unknown-field");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports invalid-value for udp.encoding with bad value", () => {
|
it("reports invalid-value for udp.encoding with bad value", () => {
|
||||||
const issues = validateUdpConfig(
|
const issues = validateUdpConfig(
|
||||||
makeInput({
|
makeInput({
|
||||||
|
|||||||
Reference in New Issue
Block a user