refactor: expect 类型模型重构,Raw/Resolved 双层分离与断言基础设施内聚
- 重命名 ContentRules→ContentExpectations, KeyValueExpect→KeyedExpectations - 新增 Raw/Resolved 双层模型:resolve 阶段物化为执行计划,store 持久化 Raw 快照 - HTTP body 按需读取:status/headers 失败或无 body expectation 时不读取 body - 新增 displayValueExpectation() 解包 failure.expected 用户可读展示 - 修复 checkEarlyTimeout 独立 lte/lt 检查,修复 KeyedExpectations JSON Schema - 新增 expect/value.ts(resolve/check/display)、keyed.ts、content.ts、headers.ts、status.ts - 删除旧 normalize.ts/matcher.ts/validate-matcher.ts/key-value.ts - 更新 DEVELOPMENT.md:expect 五层管线表、displayValueExpectation、1.7↔1.10 交叉引用 - 同步 13 个 main specs,归档 refactor-expect-type-model 变更(62/62 tasks)
This commit is contained in:
178
DEVELOPMENT.md
178
DEVELOPMENT.md
@@ -39,7 +39,7 @@ src/
|
||||
variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成
|
||||
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||||
builder.ts 全量 JSON Schema 组装(遍历 registry 生成)
|
||||
fragments.ts 共享 TypeBox schema 片段(duration、size、ValueMatcher、ContentRules、KeyValueExpect 等)
|
||||
fragments.ts 共享 TypeBox schema 片段(duration、size、ValueMatcher、ContentExpectations、KeyedExpectations 等)
|
||||
validate.ts Ajv 契约校验入口
|
||||
issues.ts 校验问题类型与渲染
|
||||
types.ts schema 层类型
|
||||
@@ -48,12 +48,14 @@ src/
|
||||
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制 + 数据清理)
|
||||
utils.ts 共享工具函数(parseSize、parseDuration)
|
||||
expect/ 共享 expect 断言基础设施(跨 checker 复用)
|
||||
types.ts ExpectResult、ValueMatcher、ContentRules、KeyValueExpect 类型
|
||||
types.ts Raw/Resolved ValueExpectation、ContentExpectations、KeyedExpectations、ExpectationResult 类型
|
||||
failure.ts 失败信息构造(errorFailure、mismatchFailure、truncateActual)
|
||||
matcher.ts ValueMatcher 执行、JSONPath 提取、字面量 equals 快捷语义
|
||||
content.ts ContentRules 执行(direct/json/css/xpath)
|
||||
key-value.ts KeyValueExpect 执行(动态键与 key 规范化)
|
||||
validate-matcher.ts matcher/content/key-value 语义校验
|
||||
value.ts ValueExpectation resolve(primitive→equals)和执行、JSONPath 提取
|
||||
content.ts Resolved ContentExpectations 执行(kind=value/json/css/xpath)和 Raw resolve
|
||||
keyed.ts Resolved KeyedExpectations 执行(顺序 + key 规范化)和 Raw resolve
|
||||
headers.ts HTTP/LLM 共享 header keyed expectation 包装(大小写不敏感)
|
||||
status.ts HTTP/LLM 共享 status code 断言(精确数值与 1xx-5xx 范围)
|
||||
validate.ts Raw value/content/keyed expectation 语义校验(不修改输入)
|
||||
redos.ts regex ReDoS 风险检测
|
||||
runner/ Checker 统一抽象与注册机制
|
||||
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
|
||||
@@ -265,22 +267,22 @@ checkerRegistry(单例)
|
||||
|
||||
每个 checker 目录的标准文件结构:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ------------- | ------------------------------------------------------------------------------------- |
|
||||
| `index.ts` | 模块入口,re-export Checker 类 |
|
||||
| `types.ts` | Checker 专属类型(ResolvedXxxTarget、XxxTargetConfig、XxxExpectConfig 等) |
|
||||
| `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
||||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||||
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||
| `expect.ts` | Checker 专用断言函数 |
|
||||
| `*.ts` | 其他 checker 专属逻辑(如协议解析、编码、provider 适配、平台命令封装) |
|
||||
| 文件 | 职责 |
|
||||
| ------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `index.ts` | 模块入口,re-export Checker 类 |
|
||||
| `types.ts` | Checker 专属类型(RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) |
|
||||
| `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
||||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||||
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||
| `expect.ts` | Checker 专用断言函数 |
|
||||
| `*.ts` | 其他 checker 专属逻辑(如协议解析、编码、provider 适配、平台命令封装) |
|
||||
|
||||
#### 1.7.2 步骤一:创建 Checker 目录与类型
|
||||
|
||||
在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts`、`cmd/types.ts`):
|
||||
|
||||
- `XxxTargetConfig` — YAML 原始配置类型
|
||||
- `XxxExpectConfig` — expect 字段类型
|
||||
- `RawXxxTargetConfig` — YAML 原始配置类型
|
||||
- `RawXxxExpectConfig` / `ResolvedXxxExpectConfig` — Raw expect 字段类型与运行期 Resolved expect 执行计划类型
|
||||
- `XxxDefaultsConfig` — defaults 专属字段类型
|
||||
- `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量
|
||||
|
||||
@@ -292,16 +294,16 @@ checkerRegistry(单例)
|
||||
|
||||
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
||||
|
||||
| Fragment | 用途 |
|
||||
| ------------------------------ | -------------------------------------------------------- |
|
||||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"2h"`、`"7d"`、`"500ms"`) |
|
||||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||||
| `createValueMatcherSchema()` | `ValueMatcher` 对象(equals/contains/regex/数值比较等) |
|
||||
| `createContentRulesSchema()` | `ContentRules` 数组(direct/json/css/xpath 内容规则) |
|
||||
| `createKeyValueExpectSchema()` | 动态键 `KeyValueExpect`(headers、DB rows 列值) |
|
||||
| `matcherProperties()` | matcher 字段 Record,供 extractor schema 复用 |
|
||||
| Fragment | 用途 |
|
||||
| ----------------------------------- | ----------------------------------------------------------- |
|
||||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"2h"`、`"7d"`、`"500ms"`) |
|
||||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||||
| `createValueMatcherSchema()` | `ValueMatcher` 对象(equals/contains/regex/数值比较等) |
|
||||
| `createContentExpectationsSchema()` | `ContentExpectations` 数组(value/json/css/xpath 内容断言) |
|
||||
| `createKeyedExpectationsSchema()` | 动态键 `KeyedExpectations`(headers、DB rows 列值) |
|
||||
| `matcherProperties()` | matcher 字段 Record,供 extractor schema 复用 |
|
||||
|
||||
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`cmd.env`)可以开放任意键名。
|
||||
|
||||
@@ -313,15 +315,15 @@ checkerRegistry(单例)
|
||||
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
|
||||
```
|
||||
|
||||
**共享校验工具**(`expect/validate-matcher.ts`):
|
||||
**共享校验工具**(`expect/validate.ts`):
|
||||
|
||||
| 函数 | 用途 |
|
||||
| ------------------------------------------------------ | ----------------------------------------- |
|
||||
| `validateValueMatcher(value, path, targetName, opts?)` | 校验 matcher 字段、类型、regex 和组合语义 |
|
||||
| `validateContentRules(rules, path, targetName)` | 校验 ContentRules 数组、extractor 互斥性 |
|
||||
| `validateKeyValueExpect(value, path, targetName)` | 校验动态键值断言 |
|
||||
| `validateJsonPath(path, rulePath, targetName)` | 校验项目支持的 JSONPath 子集 |
|
||||
| `isJsonValue(value)` | 判断是否为合法 JSON value |
|
||||
| 函数 | 用途 |
|
||||
| -------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| `validateRawValueExpectation(value, path, targetName, opts?)` | 校验 Raw `ValueExpectation`(primitive 或 matcher) |
|
||||
| `validateRawContentExpectations(value, path, targetName)` | 校验 Raw `ContentExpectations` 数组、extractor 互斥 |
|
||||
| `validateRawKeyedExpectations(value, path, targetName, opts?)` | 校验 Raw `KeyedExpectations`,可选大小写不敏感重复 |
|
||||
| `validateJsonPath(path, rulePath, targetName)` | 校验项目支持的 JSONPath 子集 |
|
||||
| `isJsonValue(value)` | 判断是否为合法 JSON value |
|
||||
|
||||
#### 1.7.5 步骤四:实现 Checker 类
|
||||
|
||||
@@ -342,9 +344,47 @@ TcpChecker implements Checker
|
||||
**`resolve()` 规范**:
|
||||
|
||||
- 只做默认值合并、路径解析、单位转换,**不执行校验**
|
||||
- 若 checker 支持 expect,必须保留 `rawExpect` 为变量替换后的 Raw 配置快照,并生成只供 `execute()` 使用的 Resolved `expect`
|
||||
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
|
||||
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型)
|
||||
|
||||
**expect 五层管线**:每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数:
|
||||
|
||||
| 断言模型 | 类型层(Raw) | Schema 层 | Validate 层 | Resolve 层 | Execute 层 |
|
||||
| --------------------- | ----------------------------------- | ----------------------------------- | ---------------------------------- | ------------------------------ | ---------------------------- |
|
||||
| `ValueExpectation` | `number \| ValueMatcher` | `createValueMatcherSchema()` | `validateRawValueExpectation()` | `resolveValueExpectation()` | `checkValueExpectation()` |
|
||||
| `ContentExpectations` | `(ValueMatcher \| ExtractorRule)[]` | `createContentExpectationsSchema()` | `validateRawContentExpectations()` | `resolveContentExpectations()` | `checkContentExpectations()` |
|
||||
| `KeyedExpectations` | `Record<string, ValueExpectation>` | `createKeyedExpectationsSchema()` | `validateRawKeyedExpectations()` | `resolveKeyedExpectations()` | `checkKeyedExpectations()` |
|
||||
|
||||
选择哪种模型参考 [1.10 expect 字段选择规范](#110-expect-断言系统)的决策树。
|
||||
|
||||
**resolve 中的标准模式**:
|
||||
|
||||
```typescript
|
||||
// resolve() 内:逐字段调用对应的 resolve 函数,未配置的字段保持 undefined
|
||||
const rawExpect = raw.expect ?? {};
|
||||
expect: {
|
||||
durationMs: rawExpect.durationMs != null ? resolveValueExpectation(rawExpect.durationMs) : undefined,
|
||||
body: rawExpect.body != null ? resolveContentExpectations(rawExpect.body) : undefined,
|
||||
headers: rawExpect.headers != null ? resolveKeyedExpectations(rawExpect.headers) : undefined,
|
||||
}
|
||||
```
|
||||
|
||||
**execute 中的标准模式**:
|
||||
|
||||
```typescript
|
||||
// execute() 内:按快速失败顺序依次检查,首个失败即返回
|
||||
const r = resolved.expect;
|
||||
if (r.durationMs) {
|
||||
const result = checkValueExpectation(elapsed, r.durationMs, { phase: "duration", path: "durationMs" });
|
||||
if (!result.matched) return { ..., failure: result.failure, matched: false };
|
||||
}
|
||||
if (r.body) {
|
||||
const result = checkContentExpectations(bodyText, r.body, { phase: "body", path: "body" });
|
||||
if (!result.matched) return { ..., failure: result.failure, matched: false };
|
||||
}
|
||||
```
|
||||
|
||||
**`execute()` 规范**:
|
||||
|
||||
- 始终记录 `timestamp`(ISO 字符串)和 `start = performance.now()`
|
||||
@@ -353,23 +393,29 @@ TcpChecker implements Checker
|
||||
- 成功时 `failure: null, matched: true`
|
||||
- 异常时使用 `errorFailure(phase, path, message)` 构造 failure
|
||||
- 不匹配时使用 `mismatchFailure(phase, path, expected, actual, message)` 构造 failure
|
||||
- `mismatchFailure` 的 `expected` 参数应传用户可读值,使用 `displayValueExpectation(matcher)` 解包单字段 `{ equals: x }` 为 `x`
|
||||
|
||||
**可用的共享断言工具**(`checker/expect/`):
|
||||
|
||||
| 模块 | 函数 | 用途 |
|
||||
| --------------------- | ----------------------------------------------------- | ------------------------------------- |
|
||||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||||
| `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) |
|
||||
| `matcher.ts` | `applyMatcher(actual, matcher, options?)` | 执行 ValueMatcher AND 匹配 |
|
||||
| `matcher.ts` | `checkValueMatcher(actual, matcher, options)` | 执行 matcher 并返回 `ExpectResult` |
|
||||
| `matcher.ts` | `checkExpectValue(actual, expected)` | 执行字面量 equals 或 matcher |
|
||||
| `matcher.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||
| `content.ts` | `checkContentRules(source, rules, options)` | 执行 ContentRules 数组 |
|
||||
| `key-value.ts` | `checkKeyValueExpect(actual, expected, options)` | 执行动态键值断言 |
|
||||
| `validate-matcher.ts` | `validateValueMatcher/ContentRules/KeyValueExpect` | matcher/content/key-value 语义校验 |
|
||||
| 模块 | 函数 | 用途 |
|
||||
| ------------- | ------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||||
| `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) |
|
||||
| `value.ts` | `applyValueMatcher(actual, matcher, options?)` | 执行 Resolved `ValueMatcher` AND 匹配 |
|
||||
| `value.ts` | `checkValueExpectation(actual, matcher, options)` | 执行 matcher 并返回 `ExpectationResult` |
|
||||
| `value.ts` | `resolveValueExpectation(raw)` | Raw `ValueExpectation` → Resolved `ValueExpectation` |
|
||||
| `value.ts` | `displayValueExpectation(matcher)` | 解包单字段 `{ equals: x }` 为 `x`,用于 failure 展示 |
|
||||
| `value.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||
| `content.ts` | `checkContentExpectations(source, expectations, options)` | 执行 Resolved `ContentExpectations` |
|
||||
| `content.ts` | `resolveContentExpectations(raw)` | Raw → Resolved `ContentExpectations` |
|
||||
| `keyed.ts` | `checkKeyedExpectations(actual, expectations, options)` | 执行 Resolved `KeyedExpectations` |
|
||||
| `keyed.ts` | `resolveKeyedExpectations(raw)` | Raw Record → Resolved 有序数组 |
|
||||
| `headers.ts` | `checkHeaderExpectations(headers, expectations, options?)` | HTTP/LLM headers 大小写不敏感包装 |
|
||||
| `status.ts` | `checkStatusCode(actual, expected, phase, path)` | HTTP/LLM status code(精确数值与 1xx-5xx 范围) |
|
||||
| `validate.ts` | `validateRawValueExpectation/ContentExpectations/KeyedExpectations` | Raw expectation 语义校验(不修改输入) |
|
||||
|
||||
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `cmd/expect.ts`(checkExitCode)。
|
||||
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `cmd/expect.ts`(checkExitCode)、`tcp/expect.ts`(checkConnected)、`udp/expect.ts`(checkResponded)和 `icmp/expect.ts`(checkAlive)。HTTP/LLM 复用的 status 与 headers 断言放在共享 expect 模块。
|
||||
|
||||
#### 1.7.6 步骤五:创建模块入口并注册
|
||||
|
||||
@@ -496,19 +542,21 @@ TcpChecker implements Checker
|
||||
|
||||
两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属状态断言位于各自目录。
|
||||
|
||||
**Raw vs Resolved**:用户 YAML 写的是 Raw 形态(primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record<string, value>` 键值表),`config-loader` 的 resolve 阶段将其转换为 Resolved 形态供运行期执行(`{ equals: primitive }`、`{ kind, matcher, ... }` content 联合、`{ key, matcher }[]` 有序数组)。Store 持久化 Raw 快照(`rawExpect`),checker.execute 消费 Resolved `expect`。
|
||||
|
||||
**共享模型**:
|
||||
|
||||
| 模型 | 用途 | 典型字段 |
|
||||
| ---------------- | ---------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `ValueMatcher` | 单个值、数字指标和字符串元数据断言 | `durationMs`、`rowCount`、`usage.totalTokens`、`finishReason` |
|
||||
| `ContentRules` | 返回内容或半结构化内容断言,必须是数组 | `body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` |
|
||||
| `KeyValueExpect` | 动态键值断言,字面量等价于 `{ equals: value }` | `headers`、DB `rows[]` 中的列值 |
|
||||
| 模型 | 用途 | 典型字段 |
|
||||
| --------------------- | ---------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `ValueExpectation` | 单个值、数字指标和字符串元数据断言 | `durationMs`、`rowCount`、`usage.totalTokens`、`finishReason` |
|
||||
| `ContentExpectations` | 返回内容或半结构化内容断言,必须是数组 | `body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` |
|
||||
| `KeyedExpectations` | 动态键值断言,字面量等价于 `{ equals: value }` | `headers`、DB `rows[]` 中的列值 |
|
||||
|
||||
`ValueMatcher` 支持 `equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。一个 matcher 对象内多个字段为 AND 语义;`exists: false` 不能和其他 matcher 组合;`equals` 使用 `es-toolkit/isEqual` 做 JSON 深度相等;`regex` 固定为无 flags 的 `new RegExp(pattern).test(String(actual))`。ValueMatcher expect 字段输入可使用 string、number、boolean 或 null 简写,语义校验入口会归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`。
|
||||
`ValueMatcher` 支持 `equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。一个 matcher 对象内多个字段为 AND 语义;`exists: false` 不能和其他 matcher 组合;`equals` 使用 `es-toolkit/isEqual` 做 JSON 深度相等;`regex` 固定为无 flags 的 `new RegExp(pattern).test(String(actual))`。ValueExpectation Raw 输入可使用 string、number、boolean 或 null 简写,resolve 阶段归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`。
|
||||
|
||||
`ContentRules` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
|
||||
`ContentExpectations` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
|
||||
|
||||
启动期语义校验统一由 `expect/validate-matcher.ts` 负责,会校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentRules 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性和 ReDoS 风险。旧字段 `match`、`maxDurationMs`、ICMP 的 `max*` 阈值字段不再支持。
|
||||
启动期语义校验统一由 `expect/validate.ts` 负责,会校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改 Raw 输入。旧字段 `match`、`maxDurationMs`、ICMP 的 `max*` 阈值字段不再支持。
|
||||
|
||||
**快速失败顺序**:
|
||||
|
||||
@@ -523,11 +571,11 @@ TcpChecker implements Checker
|
||||
| LLM http | `status → headers → output → finishReason → rawFinishReason → usage → durationMs` |
|
||||
| LLM stream | `status → headers → stream.completed → stream.firstTokenMs → output → finishReason → rawFinishReason → usage → durationMs` |
|
||||
|
||||
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;有 body 规则时,在读取 body 前可先检查是否已超过 `durationMs` 阈值,避免无意义读取。
|
||||
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、按需响应体读取、解码和 expect 校验)。未配置 body expectation、status 失败或 headers 失败时不读取 body;有 body expectation 时,在读取 body 前可先检查 `durationMs` 上界 matcher 是否已不可能通过,避免无意义读取。
|
||||
|
||||
**expect 字段选择规范**:
|
||||
|
||||
新增或修改 checker 的 expect 字段时,按以下决策树选择合适的断言模型:
|
||||
新增或修改 checker 的 expect 字段时,按以下决策树选择合适的断言模型(选定后,各层的具体函数映射参考 [1.7.5 的五层管线表](#175-步骤四实现-checker-类)):
|
||||
|
||||
```
|
||||
expect 字段
|
||||
@@ -544,11 +592,11 @@ expect 字段
|
||||
│ finishReason、rawFinishReason、usage.*、stream.firstTokenMs
|
||||
│
|
||||
└─ 返回内容 / 半结构化内容 / 不完全确定的值
|
||||
├─ 内容断言 → ContentRules(数组)
|
||||
├─ 内容断言 → ContentExpectations(数组)
|
||||
│ HTTP body、Cmd stdout/stderr、TCP banner、
|
||||
│ UDP response、LLM output、DB result
|
||||
│
|
||||
└─ 键值断言 → KeyValueExpect(动态键对象)
|
||||
└─ 键值断言 → KeyedExpectations(动态键对象)
|
||||
HTTP/LLM headers、DB rows[] 中的列值
|
||||
```
|
||||
|
||||
@@ -558,14 +606,16 @@ expect 字段
|
||||
|
||||
2. **单值数字指标和字符串元数据使用 ValueMatcher**。观测值是一个明确的标量(耗时、行数、丢包率、finish reason),但阈值不确定时,使用 `{ lte: 100 }` 或 `{ regex: "^(stop|end)$" }` 等 matcher 表达;精确匹配 primitive 可直接写 `100` 或 `"stop"`。
|
||||
|
||||
3. **返回内容使用 ContentRules 数组**。观测值是文本、JSON、HTML 或 XML 内容,且可能需要多步提取或多条规则时,使用 ContentRules。即使只有一条规则也必须写成数组形式(`[{ contains: "ok" }]`),不支持对象快捷写法。
|
||||
3. **返回内容使用 ContentExpectations 数组**。观测值是文本、JSON、HTML 或 XML 内容,且可能需要多步提取或多条规则时,使用 ContentExpectations。即使只有一条规则也必须写成数组形式(`[{ contains: "ok" }]`),不支持对象快捷写法。
|
||||
|
||||
4. **键值对使用 KeyValueExpect**。观测值是动态键值表(如 headers),且需要对每个键独立断言时使用。字面量值自动等价于 `{ equals: value }`。
|
||||
4. **键值对使用 KeyedExpectations**。观测值是动态键值表(如 headers),且需要对每个键独立断言时使用。字面量值自动等价于 `{ equals: value }`。
|
||||
|
||||
5. **不要混用模型**。一个 expect 字段只能对应一种断言模型。例如 `finishReason` 是单值字符串元数据,用 ValueMatcher 而非 ContentRules(ContentRules 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。
|
||||
5. **不要混用模型**。一个 expect 字段只能对应一种断言模型。例如 `finishReason` 是单值字符串元数据,用 ValueMatcher 而非 ContentExpectations(ContentExpectations 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。
|
||||
|
||||
6. **failure phase 命名遵循去单位后缀规则**。数字指标字段的 phase 去掉单位后缀(`durationMs` → `duration`、`packetLossPercent` → `packetLoss`、`avgLatencyMs` → `avgLatency`),不带单位后缀的字段直接使用字段名(`rowCount` → `rowCount`、`finishReason` → `finishReason`)。
|
||||
|
||||
7. **实现时参考 [1.7.5 五层管线](#175-步骤四实现-checker-类) 中的对应表**。决策树解决"选哪种模型",五层管线表解决"每种模型从类型定义到执行分别调哪个函数"。
|
||||
|
||||
### 1.11 错误模式
|
||||
|
||||
- **API 错误**:`{ error: "描述", status: <code> }`,状态码 400/404/503
|
||||
@@ -575,7 +625,7 @@ expect 字段
|
||||
|
||||
### 1.12 测试规范
|
||||
|
||||
- 测试目录 `tests/` 镜像 `src/` 目录结构,但共享 expect 模块的测试集中放在 `tests/server/checker/runner/shared/` 下,覆盖 `failure.ts`、`matcher.ts`、`content.ts`、`key-value.ts`、`validate-matcher.ts` 和 `redos.ts`
|
||||
- 测试目录 `tests/` 镜像 `src/` 目录结构,但共享 expect 模块的测试集中放在 `tests/server/checker/runner/shared/` 下,覆盖 `failure.ts`、`value.ts`(operator)、`content.ts`(body/text)、`keyed.ts`(headers/duplicate-key)、`validate.ts`(shorthand)和 `redos.ts`
|
||||
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
||||
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
|
||||
- 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })`
|
||||
|
||||
Reference in New Issue
Block a user