- 引入 typed target 判别联合,支持 http 与 command 两种 checker - expect 重构为有序规则数组,按配置顺序快速失败并生成结构化 failure - 新增 command runner,支持 exec + args 本地命令执行 - 引入全局并发限制 maxConcurrentChecks 和 size 解析 (KB/MB/GB) - HTTP/command 各自独立 expect pipeline,应用领域默认成功语义 - SQLite schema、API、Dashboard 全链路调整为 checker 通用契约 - 补充完整测试覆盖(192 tests),更新 README 与示例配置
123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
import type { BodyRule, CheckFailure, HeaderExpect, HttpExpectConfig } from "../types";
|
|
import { checkBodyExpect } from "./body";
|
|
import { applyOperator } from "./body";
|
|
import { mismatchFailure, errorFailure } from "./failure";
|
|
|
|
export interface HttpObservation {
|
|
statusCode: number;
|
|
headers: Record<string, string>;
|
|
body: string | null;
|
|
durationMs: number;
|
|
}
|
|
|
|
function checkStatus(obs: HttpObservation, allowed: number[]): { matched: boolean; failure: CheckFailure | null } {
|
|
if (!allowed.includes(obs.statusCode)) {
|
|
return {
|
|
matched: false,
|
|
failure: mismatchFailure(
|
|
"status",
|
|
"status",
|
|
allowed,
|
|
obs.statusCode,
|
|
`status ${obs.statusCode} not in [${allowed}]`,
|
|
),
|
|
};
|
|
}
|
|
return { matched: true, failure: null };
|
|
}
|
|
|
|
function checkDuration(
|
|
obs: HttpObservation,
|
|
maxDurationMs?: number,
|
|
): { matched: boolean; failure: CheckFailure | null } {
|
|
if (maxDurationMs === undefined) return { matched: true, failure: null };
|
|
if (obs.durationMs > maxDurationMs) {
|
|
return {
|
|
matched: false,
|
|
failure: mismatchFailure(
|
|
"duration",
|
|
"duration",
|
|
`<=${maxDurationMs}ms`,
|
|
obs.durationMs,
|
|
`duration ${obs.durationMs}ms > ${maxDurationMs}ms`,
|
|
),
|
|
};
|
|
}
|
|
return { matched: true, failure: null };
|
|
}
|
|
|
|
function checkHeaders(
|
|
obs: HttpObservation,
|
|
headerExpects?: Record<string, HeaderExpect>,
|
|
): { matched: boolean; failure: CheckFailure | null } {
|
|
if (!headerExpects) return { matched: true, failure: null };
|
|
|
|
for (const [key, expected] of Object.entries(headerExpects)) {
|
|
const actualValue = obs.headers[key.toLowerCase()];
|
|
const path = `headers.${key}`;
|
|
|
|
if (typeof expected === "string") {
|
|
if (actualValue !== expected) {
|
|
return {
|
|
matched: false,
|
|
failure: mismatchFailure("headers", path, expected, actualValue, `header ${key} mismatch`),
|
|
};
|
|
}
|
|
} else {
|
|
if (actualValue === undefined) {
|
|
if (expected.exists !== false) {
|
|
return {
|
|
matched: false,
|
|
failure: mismatchFailure("headers", path, "defined", undefined, `header ${key} not found`),
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
if (!applyOperator(actualValue, expected)) {
|
|
return {
|
|
matched: false,
|
|
failure: mismatchFailure("headers", path, expected, actualValue, `header ${key} mismatch`),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return { matched: true, failure: null };
|
|
}
|
|
|
|
function checkBody(obs: HttpObservation, bodyRules?: BodyRule[]): { matched: boolean; failure: CheckFailure | null } {
|
|
if (!bodyRules || bodyRules.length === 0) return { matched: true, failure: null };
|
|
|
|
if (obs.body === null) {
|
|
return {
|
|
matched: false,
|
|
failure: errorFailure("body", "body", "body is null but body rules are configured"),
|
|
};
|
|
}
|
|
|
|
return checkBodyExpect(obs.body, bodyRules);
|
|
}
|
|
|
|
export function checkHttpExpect(
|
|
obs: HttpObservation,
|
|
expect?: HttpExpectConfig,
|
|
): { matched: boolean; failure: CheckFailure | null } {
|
|
if (!expect) {
|
|
return checkStatus(obs, [200]);
|
|
}
|
|
|
|
const statusResult = checkStatus(obs, expect.status ?? [200]);
|
|
if (!statusResult.matched) return statusResult;
|
|
|
|
const durationResult = checkDuration(obs, expect.maxDurationMs);
|
|
if (!durationResult.matched) return durationResult;
|
|
|
|
const headersResult = checkHeaders(obs, expect.headers);
|
|
if (!headersResult.matched) return headersResult;
|
|
|
|
const bodyResult = checkBody(obs, expect.body);
|
|
if (!bodyResult.matched) return bodyResult;
|
|
|
|
return { matched: true, failure: null };
|
|
}
|