refactor: 将 checker normalize 职责下沉到各 runner 目录
- 新增 CheckerDefinition.normalize 必需方法,typecheck 兜底遗漏实现 - 新增 expect/normalize.ts 共享 helper(compactExpect、normalizeValue、 normalizeContent、normalizeKeyed) - 为 HTTP、Cmd、DB、TCP、UDP、ICMP、LLM、WS、DNS 各新增独立 normalize.ts - 简化 normalizer.ts:删除所有 checker type switch,改为 registry 委托 - 修复 DNS authoring 简写 bug:durationMs、valueCount、result 等字段 现可通过完整加载链路 - 新增 DNS 回归测试和 registry 级合同测试 - 更新 docs/development/checker.md:补充 normalize 规范、文件结构、 测试要求和 checklist
This commit is contained in:
@@ -60,7 +60,7 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
||||
throw new Error("配置文件内容为空或格式无效");
|
||||
}
|
||||
|
||||
const normalizeResult = normalizeAuthoringConfig(parsed);
|
||||
const normalizeResult = normalizeAuthoringConfig(parsed, checkerRegistry);
|
||||
if (normalizeResult.issues.length > 0) {
|
||||
throwConfigIssues(dedupeIssues(normalizeResult.issues));
|
||||
}
|
||||
|
||||
50
src/server/checker/expect/normalize.ts
Normal file
50
src/server/checker/expect/normalize.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import { resolveContentExpectations } from "./content";
|
||||
import { CONTENT_EXTRACTOR_KEY_SET, MATCHER_KEY_SET } from "./keys";
|
||||
import { isValueMatcherObject, isValueMatcherPrimitive, resolveValueExpectation } from "./value";
|
||||
|
||||
type ExpectRecord = Record<string, unknown>;
|
||||
|
||||
export function compactExpect(original: ExpectRecord, overrides: ExpectRecord): ExpectRecord {
|
||||
const result: ExpectRecord = {};
|
||||
for (const [key, value] of Object.entries(original)) {
|
||||
if (value !== undefined) result[key] = value;
|
||||
}
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (value !== undefined) result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function normalizeContent(value: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Array.isArray(value)) return value;
|
||||
return (value as unknown[]).map((entry): unknown => {
|
||||
if (!canNormalizeContentEntry(entry)) return entry;
|
||||
const resolved = resolveContentExpectations([entry] as never);
|
||||
return resolved?.[0];
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeKeyed(value: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (!isPlainObject(value)) return value;
|
||||
return Object.entries(value as ExpectRecord).map(([key, item]) => ({ key, matcher: normalizeValue(item) }));
|
||||
}
|
||||
|
||||
export function normalizeValue(value: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (isValueMatcherPrimitive(value) || isValueMatcherObject(value)) return resolveValueExpectation(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
function canNormalizeContentEntry(value: unknown): boolean {
|
||||
if (!isPlainObject(value)) return false;
|
||||
const keys = Object.keys(value);
|
||||
const extractorKeys = keys.filter((key) => CONTENT_EXTRACTOR_KEY_SET.has(key));
|
||||
const matcherKeys = keys.filter((key) => MATCHER_KEY_SET.has(key));
|
||||
if (extractorKeys.length === 0) return matcherKeys.length > 0 && matcherKeys.length === keys.length;
|
||||
if (extractorKeys.length !== 1 || matcherKeys.length > 0 || keys.length !== 1) return false;
|
||||
return isPlainObject((value as ExpectRecord)[extractorKeys[0]!]);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { CheckerRegistry } from "./runner/registry";
|
||||
import type { ConfigValidationIssue } from "./schema/issues";
|
||||
import type { AuthoringProbeConfig, NormalizedProbeConfig } from "./schema/types";
|
||||
import type { RawTargetConfig } from "./types";
|
||||
|
||||
import { resolveContentExpectations } from "./expect/content";
|
||||
import { CONTENT_EXTRACTOR_KEY_SET, MATCHER_KEY_SET } from "./expect/keys";
|
||||
import { isValueMatcherObject, isValueMatcherPrimitive, resolveValueExpectation } from "./expect/value";
|
||||
import { checkerRegistry } from "./runner";
|
||||
import { resolveVariables } from "./variables";
|
||||
|
||||
type ExpectRecord = Record<string, unknown>;
|
||||
|
||||
export function normalizeAuthoringConfig(config: unknown): {
|
||||
export function normalizeAuthoringConfig(
|
||||
config: unknown,
|
||||
registry: CheckerRegistry = checkerRegistry,
|
||||
): {
|
||||
config: unknown;
|
||||
issues: ConfigValidationIssue[];
|
||||
} {
|
||||
@@ -23,177 +23,20 @@ export function normalizeAuthoringConfig(config: unknown): {
|
||||
const normalized = { ...(variableResult.config as Record<string, unknown>) };
|
||||
delete normalized["variables"];
|
||||
if (Array.isArray(normalized["targets"])) {
|
||||
normalized["targets"] = normalized["targets"].map((target) => normalizeTarget(target));
|
||||
normalized["targets"] = normalized["targets"].map((target) => normalizeTarget(target, registry));
|
||||
}
|
||||
|
||||
return { config: normalized, issues: variableResult.issues };
|
||||
}
|
||||
|
||||
function canNormalizeContentEntry(value: unknown): boolean {
|
||||
if (!isPlainObject(value)) return false;
|
||||
const keys = Object.keys(value);
|
||||
const extractorKeys = keys.filter((key) => CONTENT_EXTRACTOR_KEY_SET.has(key));
|
||||
const matcherKeys = keys.filter((key) => MATCHER_KEY_SET.has(key));
|
||||
if (extractorKeys.length === 0) return matcherKeys.length > 0 && matcherKeys.length === keys.length;
|
||||
if (extractorKeys.length !== 1 || matcherKeys.length > 0 || keys.length !== 1) return false;
|
||||
return isPlainObject((value as ExpectRecord)[extractorKeys[0]!]);
|
||||
}
|
||||
|
||||
function compact(original: ExpectRecord, overrides: ExpectRecord): ExpectRecord {
|
||||
const result: ExpectRecord = {};
|
||||
for (const [key, value] of Object.entries(original)) {
|
||||
if (value !== undefined) result[key] = value;
|
||||
}
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (value !== undefined) result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeCommandExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
exitCode: raw["exitCode"],
|
||||
stderr: normalizeContent(raw["stderr"]),
|
||||
stdout: normalizeContent(raw["stdout"]),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeContent(value: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Array.isArray(value)) return value;
|
||||
return (value as unknown[]).map((entry): unknown => {
|
||||
if (!canNormalizeContentEntry(entry)) return entry;
|
||||
const resolved = resolveContentExpectations([entry] as never);
|
||||
return resolved?.[0];
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeDbExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
result: normalizeContent(raw["result"]),
|
||||
rowCount: normalizeValue(raw["rowCount"]),
|
||||
rows: Array.isArray(raw["rows"]) ? raw["rows"].map((row) => normalizeKeyed(row)) : raw["rows"],
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeExpect(type: string, expect: unknown): unknown {
|
||||
if (!isPlainObject(expect)) return expect;
|
||||
const raw = expect as ExpectRecord;
|
||||
switch (type) {
|
||||
case "cmd":
|
||||
return normalizeCommandExpect(raw);
|
||||
case "db":
|
||||
return normalizeDbExpect(raw);
|
||||
case "http":
|
||||
return normalizeHttpExpect(raw);
|
||||
case "icmp":
|
||||
return normalizeIcmpExpect(raw);
|
||||
case "llm":
|
||||
return normalizeLlmExpect(raw);
|
||||
case "tcp":
|
||||
return normalizeTcpExpect(raw);
|
||||
case "udp":
|
||||
return normalizeUdpExpect(raw);
|
||||
case "ws":
|
||||
return normalizeWsExpect(raw);
|
||||
default:
|
||||
return expect;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHttpExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
body: normalizeContent(raw["body"]),
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
headers: normalizeKeyed(raw["headers"]),
|
||||
status: raw["status"],
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeIcmpExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
alive: raw["alive"],
|
||||
avgLatencyMs: normalizeValue(raw["avgLatencyMs"]),
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
maxLatencyMs: normalizeValue(raw["maxLatencyMs"]),
|
||||
packetLossPercent: normalizeValue(raw["packetLossPercent"]),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeKeyed(value: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (!isPlainObject(value)) return value;
|
||||
return Object.entries(value as ExpectRecord).map(([key, item]) => ({ key, matcher: normalizeValue(item) }));
|
||||
}
|
||||
|
||||
function normalizeLlmExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
finishReason: normalizeValue(raw["finishReason"]),
|
||||
headers: normalizeKeyed(raw["headers"]),
|
||||
output: normalizeContent(raw["output"]),
|
||||
rawFinishReason: normalizeValue(raw["rawFinishReason"]),
|
||||
status: raw["status"],
|
||||
stream: isPlainObject(raw["stream"])
|
||||
? compact(raw["stream"] as ExpectRecord, {
|
||||
completed: (raw["stream"] as ExpectRecord)["completed"],
|
||||
firstTokenMs: normalizeValue((raw["stream"] as ExpectRecord)["firstTokenMs"]),
|
||||
})
|
||||
: raw["stream"],
|
||||
usage: isPlainObject(raw["usage"])
|
||||
? compact(raw["usage"] as ExpectRecord, {
|
||||
inputTokens: normalizeValue((raw["usage"] as ExpectRecord)["inputTokens"]),
|
||||
outputTokens: normalizeValue((raw["usage"] as ExpectRecord)["outputTokens"]),
|
||||
totalTokens: normalizeValue((raw["usage"] as ExpectRecord)["totalTokens"]),
|
||||
})
|
||||
: raw["usage"],
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTarget(target: unknown): unknown {
|
||||
function normalizeTarget(target: unknown, registry: CheckerRegistry): unknown {
|
||||
if (!isPlainObject(target)) return target;
|
||||
const result = { ...(target as RawTargetConfig) };
|
||||
if (result.expect !== undefined) {
|
||||
result.expect = normalizeExpect(result.type, result.expect);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeTcpExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
banner: normalizeContent(raw["banner"]),
|
||||
connected: raw["connected"],
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeUdpExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
responded: raw["responded"],
|
||||
response: normalizeContent(raw["response"]),
|
||||
responseSize: normalizeValue(raw["responseSize"]),
|
||||
sourceHost: normalizeValue(raw["sourceHost"]),
|
||||
sourcePort: normalizeValue(raw["sourcePort"]),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeValue(value: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (isValueMatcherPrimitive(value) || isValueMatcherObject(value)) return resolveValueExpectation(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeWsExpect(raw: ExpectRecord): ExpectRecord {
|
||||
return compact(raw, {
|
||||
connected: raw["connected"],
|
||||
connectTimeMs: normalizeValue(raw["connectTimeMs"]),
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
handshakeHeaders: normalizeKeyed(raw["handshakeHeaders"]),
|
||||
message: normalizeContent(raw["message"]),
|
||||
});
|
||||
const type = result.type;
|
||||
if (typeof type !== "string") return result;
|
||||
const checker = registry?.tryGet(type);
|
||||
if (!checker) return result;
|
||||
return checker.normalize(result);
|
||||
}
|
||||
|
||||
export type { AuthoringProbeConfig, NormalizedProbeConfig };
|
||||
|
||||
@@ -10,6 +10,7 @@ import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { parseSize } from "../../utils";
|
||||
import { checkExitCode } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { commandCheckerSchemas } from "./schema";
|
||||
import { validateCommandConfig } from "./validate";
|
||||
|
||||
@@ -202,6 +203,10 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
|
||||
};
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
|
||||
const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" };
|
||||
|
||||
|
||||
19
src/server/checker/runner/cmd/normalize.ts
Normal file
19
src/server/checker/runner/cmd/normalize.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
exitCode: raw["exitCode"],
|
||||
stderr: normalizeContent(raw["stderr"]),
|
||||
stdout: normalizeContent(raw["stdout"]),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { checkContentExpectations } from "../../expect/content";
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { checkRowCount, checkRows } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { dbCheckerSchemas } from "./schema";
|
||||
import { validateDbConfig } from "./validate";
|
||||
|
||||
@@ -223,6 +224,10 @@ export class DbChecker implements CheckerDefinition<ResolvedDbTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedDbTarget {
|
||||
const t = target as RawTargetConfig & { db: DbTargetConfig; type: "db" };
|
||||
|
||||
|
||||
19
src/server/checker/runner/db/normalize.ts
Normal file
19
src/server/checker/runner/db/normalize.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeKeyed, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
result: normalizeContent(raw["result"]),
|
||||
rowCount: normalizeValue(raw["rowCount"]),
|
||||
rows: Array.isArray(raw["rows"]) ? raw["rows"].map((row) => normalizeKeyed(row)) : raw["rows"],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
checkTtlMin,
|
||||
checkValueCount,
|
||||
} from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { dnsCheckerSchemas } from "./schema";
|
||||
import { queryDns } from "./transport";
|
||||
import { validateDnsConfig } from "./validate";
|
||||
@@ -83,6 +84,10 @@ export class DnsChecker implements CheckerDefinition<ResolvedDnsTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedDnsTarget {
|
||||
const dns = target["dns"] as DnsServerConfig | DnsSystemConfig;
|
||||
|
||||
|
||||
28
src/server/checker/runner/dns/normalize.ts
Normal file
28
src/server/checker/runner/dns/normalize.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
answerCount: normalizeValue(raw["answerCount"]),
|
||||
authenticatedData: raw["authenticatedData"],
|
||||
authoritative: raw["authoritative"],
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
rcode: raw["rcode"],
|
||||
recursionAvailable: raw["recursionAvailable"],
|
||||
responded: raw["responded"],
|
||||
result: normalizeContent(raw["result"]),
|
||||
truncated: raw["truncated"],
|
||||
ttlMax: normalizeValue(raw["ttlMax"]),
|
||||
ttlMin: normalizeValue(raw["ttlMin"]),
|
||||
valueCount: normalizeValue(raw["valueCount"]),
|
||||
values: raw["values"],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { checkHeaderExpectations } from "../../expect/headers";
|
||||
import { checkStatusCode } from "../../expect/status";
|
||||
import { checkValueExpectation, displayValueExpectation } from "../../expect/value";
|
||||
import { parseSize } from "../../utils";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { httpCheckerSchemas } from "./schema";
|
||||
import { validateHttpConfig } from "./validate";
|
||||
|
||||
@@ -172,6 +173,10 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedHttpTarget {
|
||||
const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" };
|
||||
|
||||
|
||||
19
src/server/checker/runner/http/normalize.ts
Normal file
19
src/server/checker/runner/http/normalize.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeKeyed, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
body: normalizeContent(raw["body"]),
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
headers: normalizeKeyed(raw["headers"]),
|
||||
status: raw["status"],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { buildPingCommand } from "./command";
|
||||
import { checkAlive, checkAvgLatency, checkMaxLatency, checkPacketLoss } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { parsePingOutput } from "./parse";
|
||||
import { icmpCheckerSchemas } from "./schema";
|
||||
import { validatePingConfig } from "./validate";
|
||||
@@ -153,6 +154,10 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
|
||||
};
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
|
||||
const t = target as RawTargetConfig & { icmp: PingTargetConfig; type: "icmp" };
|
||||
|
||||
|
||||
20
src/server/checker/runner/icmp/normalize.ts
Normal file
20
src/server/checker/runner/icmp/normalize.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
alive: raw["alive"],
|
||||
avgLatencyMs: normalizeValue(raw["avgLatencyMs"]),
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
maxLatencyMs: normalizeValue(raw["maxLatencyMs"]),
|
||||
packetLossPercent: normalizeValue(raw["packetLossPercent"]),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type { LlmTargetConfig, ResolvedLlmExpectConfig, ResolvedLlmTarget } from
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { runExpects } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import {
|
||||
buildObservationFromApiCallError,
|
||||
buildObservationFromGenerateText,
|
||||
@@ -127,6 +128,10 @@ export class LlmChecker implements CheckerDefinition<ResolvedLlmTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedLlmTarget {
|
||||
const t = target as RawTargetConfig & { llm: LlmTargetConfig; type: "llm" };
|
||||
|
||||
|
||||
34
src/server/checker/runner/llm/normalize.ts
Normal file
34
src/server/checker/runner/llm/normalize.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeKeyed, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
finishReason: normalizeValue(raw["finishReason"]),
|
||||
headers: normalizeKeyed(raw["headers"]),
|
||||
output: normalizeContent(raw["output"]),
|
||||
rawFinishReason: normalizeValue(raw["rawFinishReason"]),
|
||||
status: raw["status"],
|
||||
stream: isPlainObject(raw["stream"])
|
||||
? compactExpect(raw["stream"] as Record<string, unknown>, {
|
||||
completed: (raw["stream"] as Record<string, unknown>)["completed"],
|
||||
firstTokenMs: normalizeValue((raw["stream"] as Record<string, unknown>)["firstTokenMs"]),
|
||||
})
|
||||
: raw["stream"],
|
||||
usage: isPlainObject(raw["usage"])
|
||||
? compactExpect(raw["usage"] as Record<string, unknown>, {
|
||||
inputTokens: normalizeValue((raw["usage"] as Record<string, unknown>)["inputTokens"]),
|
||||
outputTokens: normalizeValue((raw["usage"] as Record<string, unknown>)["outputTokens"]),
|
||||
totalTokens: normalizeValue((raw["usage"] as Record<string, unknown>)["totalTokens"]),
|
||||
})
|
||||
: raw["usage"],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { parseSize } from "../../utils";
|
||||
import { checkBanner, checkConnected } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { tcpCheckerSchemas } from "./schema";
|
||||
import { validateTcpConfig } from "./validate";
|
||||
|
||||
@@ -203,6 +204,10 @@ export class TcpChecker implements CheckerDefinition<ResolvedTcpTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTcpTarget {
|
||||
const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
|
||||
|
||||
|
||||
18
src/server/checker/runner/tcp/normalize.ts
Normal file
18
src/server/checker/runner/tcp/normalize.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
banner: normalizeContent(raw["banner"]),
|
||||
connected: raw["connected"],
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export interface CheckerDefinition<TResolved extends ResolvedTargetBase = Resolv
|
||||
buildDetail(observation: Record<string, unknown>): null | string;
|
||||
readonly configKey: string;
|
||||
execute(target: TResolved, ctx: CheckerContext): Promise<CheckResult>;
|
||||
normalize(target: RawTargetConfig): RawTargetConfig;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): TResolved;
|
||||
readonly schemas: CheckerSchemas;
|
||||
serialize(target: TResolved): { config: string; target: string };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { checkValueExpectation } from "../../expect/value";
|
||||
import { parseSize } from "../../utils";
|
||||
import { decodePayload, encodeResponse } from "./encoding";
|
||||
import { checkResponded, checkResponseSize, checkResponseText, checkSourceHost, checkSourcePort } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { udpCheckerSchemas } from "./schema";
|
||||
import { validateUdpConfig } from "./validate";
|
||||
|
||||
@@ -295,6 +296,10 @@ export class UdpChecker implements CheckerDefinition<ResolvedUdpTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedUdpTarget {
|
||||
const t = target as RawTargetConfig & { type: "udp"; udp: UdpTargetConfig };
|
||||
|
||||
|
||||
21
src/server/checker/runner/udp/normalize.ts
Normal file
21
src/server/checker/runner/udp/normalize.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
responded: raw["responded"],
|
||||
response: normalizeContent(raw["response"]),
|
||||
responseSize: normalizeValue(raw["responseSize"]),
|
||||
sourceHost: normalizeValue(raw["sourceHost"]),
|
||||
sourcePort: normalizeValue(raw["sourcePort"]),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { parseSize } from "../../utils";
|
||||
import { checkConnected, checkHandshakeHeaders, checkMessage } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { wsCheckerSchemas } from "./schema";
|
||||
import { validateWsConfig } from "./validate";
|
||||
|
||||
@@ -281,6 +282,10 @@ export class WsChecker implements CheckerDefinition<ResolvedWsTarget> {
|
||||
}
|
||||
}
|
||||
|
||||
normalize(target: RawTargetConfig): RawTargetConfig {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedWsTarget {
|
||||
const t = target as RawTargetConfig & { type: "ws"; ws: WsTargetConfig };
|
||||
|
||||
|
||||
20
src/server/checker/runner/ws/normalize.ts
Normal file
20
src/server/checker/runner/ws/normalize.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { RawTargetConfig } from "../../types";
|
||||
|
||||
import { compactExpect, normalizeContent, normalizeKeyed, normalizeValue } from "../../expect/normalize";
|
||||
|
||||
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
|
||||
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
|
||||
const raw = target.expect as Record<string, unknown>;
|
||||
return {
|
||||
...target,
|
||||
expect: compactExpect(raw, {
|
||||
connected: raw["connected"],
|
||||
connectTimeMs: normalizeValue(raw["connectTimeMs"]),
|
||||
durationMs: normalizeValue(raw["durationMs"]),
|
||||
handshakeHeaders: normalizeKeyed(raw["handshakeHeaders"]),
|
||||
message: normalizeContent(raw["message"]),
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user