1
0

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:
2026-05-20 16:12:48 +08:00
parent 6098be2d9e
commit 60a54b483f
90 changed files with 2487 additions and 1493 deletions

View File

@@ -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 resolveprimitive→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 契约 schemaconfig / defaults / expect 三部分) |
| `validate.ts` | 启动期语义校验JSON Schema 无法表达的规则) |
| `execute.ts` | Checker 类resolve默认值合并 + 解析、execute执行检查、serializeDB 持久化) |
| `expect.ts` | Checker 专用断言函数 |
| `*.ts` | 其他 checker 专属逻辑如协议解析、编码、provider 适配、平台命令封装) |
| 文件 | 职责 |
| ------------- | ------------------------------------------------------------------------------------------ |
| `index.ts` | 模块入口re-export Checker 类 |
| `types.ts` | Checker 专属类型RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) |
| `schema.ts` | TypeBox 契约 schemaconfig / defaults / expect 三部分) |
| `validate.ts` | 启动期语义校验JSON Schema 无法表达的规则) |
| `execute.ts` | Checker 类resolve默认值合并 + 解析、execute执行检查、serializeDB 持久化) |
| `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 而非 ContentRulesContentRules 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。
5. **不要混用模型**。一个 expect 字段只能对应一种断言模型。例如 `finishReason` 是单值字符串元数据,用 ValueMatcher 而非 ContentExpectationsContentExpectations 的 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 })`