1
0

feat: 重构配置生命周期为 Authoring/Normalized/Resolved 三层

将变量替换和 expect 简写展开统一放入 Normalized 阶段,
运行时 AJV 使用 Normalized schema,导出 schema 面向 Authoring Config。

主要变更:
- 新增 normalizer.ts 实现 normalizeAuthoringConfig()
- 拆分 Authoring/Normalized 双 schema,checker 接口支持 authoring/normalized 片段
- config-loader 流程:normalize → Normalized AJV → semantic → resolve
- validator 兼容层自动分派 raw/normalized expect 形态
- 删除 rawExpect,store.expect 列写入 null
- Authoring schema 对 integer/boolean/enum 字段接受变量引用
- 修复 DB/HTTP validate 入口守卫和 LLM options integer 变量引用
- 优化 compact() 避免 undefined 覆盖隐患
- 移除 content.ts 恒为 true 的前置条件
- 同步 5 个主规范并归档 change
This commit is contained in:
2026-05-22 14:00:47 +08:00
parent 6e53c8130d
commit cf847ccd7a
56 changed files with 1717 additions and 656 deletions

View File

@@ -3,16 +3,11 @@ import { resolve } from "node:path";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type {
CommandTargetConfig,
RawCommandExpectConfig,
ResolvedCommandExpectConfig,
ResolvedCommandTarget,
} from "./types";
import type { CommandTargetConfig, ResolvedCommandExpectConfig, ResolvedCommandTarget } from "./types";
import { checkContentExpectations, resolveContentExpectations } from "../../expect/content";
import { checkContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { checkExitCode } from "./expect";
import { commandCheckerSchemas } from "./schema";
@@ -217,13 +212,11 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record<string, string>;
const rawExpect = target.expect as RawCommandExpectConfig | undefined;
const resolvedExpect: ResolvedCommandExpectConfig = rawExpect
const expect = target.expect as ResolvedCommandExpectConfig | undefined;
const resolvedExpect: ResolvedCommandExpectConfig = expect
? {
durationMs: resolveValueExpectation(rawExpect.durationMs),
exitCode: rawExpect.exitCode ?? [0],
stderr: resolveContentExpectations(rawExpect.stderr),
stdout: resolveContentExpectations(rawExpect.stdout),
...expect,
exitCode: expect.exitCode ?? [0],
}
: { exitCode: [0] };
@@ -241,7 +234,6 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
id: t.id,
intervalMs: context.defaultIntervalMs,
name: t.name ?? null,
rawExpect,
timeoutMs: context.defaultTimeoutMs,
type: "cmd",
} satisfies ResolvedCommandTarget;

View File

@@ -3,14 +3,27 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import {
createRawContentExpectationsSchema,
createRawValueExpectationSchema,
createAuthoringContentExpectationsSchema,
createAuthoringValueExpectationSchema,
createNormalizedContentExpectationsSchema,
createNormalizedValueExpectationSchema,
sizeSchema,
stringMapSchema,
} from "../../schema/fragments";
export const commandCheckerSchemas: CheckerSchemas = {
config: Type.Object(
authoring: {
config: createCommandConfigSchema(),
expect: createCommandExpectSchema("authoring"),
},
normalized: {
config: createCommandConfigSchema(),
expect: createCommandExpectSchema("normalized"),
},
};
function createCommandConfigSchema() {
return Type.Object(
{
args: Type.Optional(Type.Array(Type.String())),
cwd: Type.Optional(Type.String()),
@@ -19,14 +32,23 @@ export const commandCheckerSchemas: CheckerSchemas = {
maxOutputBytes: Type.Optional(sizeSchema),
},
{ additionalProperties: false },
),
expect: Type.Object(
);
}
function createCommandExpectSchema(kind: "authoring" | "normalized") {
return Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),
durationMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
exitCode: Type.Optional(Type.Array(Type.Integer())),
stderr: Type.Optional(createRawContentExpectationsSchema()),
stdout: Type.Optional(createRawContentExpectationsSchema()),
stderr: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
stdout: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
},
{ additionalProperties: false },
),
};
);
}

View File

@@ -42,7 +42,6 @@ export interface ResolvedCommandTarget extends ResolvedTargetBase {
group: string;
intervalMs: number;
name: null | string;
rawExpect?: RawCommandExpectConfig;
timeoutMs: number;
type: "cmd";
}

View File

@@ -3,12 +3,11 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { DbTargetConfig, RawDbExpectConfig, ResolvedDbExpectConfig, ResolvedDbTarget } from "./types";
import type { DbTargetConfig, ResolvedDbExpectConfig, ResolvedDbTarget } from "./types";
import { checkContentExpectations, resolveContentExpectations } from "../../expect/content";
import { checkContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { resolveKeyedExpectations } from "../../expect/keyed";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation } from "../../expect/value";
import { checkRowCount, checkRows } from "./expect";
import { dbCheckerSchemas } from "./schema";
import { validateDbConfig } from "./validate";
@@ -227,15 +226,7 @@ export class DbChecker implements CheckerDefinition<ResolvedDbTarget> {
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedDbTarget {
const t = target as RawTargetConfig & { db: DbTargetConfig; type: "db" };
const rawExpect = target.expect as RawDbExpectConfig | undefined;
const resolvedExpect: ResolvedDbExpectConfig | undefined = rawExpect
? {
durationMs: resolveValueExpectation(rawExpect.durationMs),
result: resolveContentExpectations(rawExpect.result),
rowCount: resolveValueExpectation(rawExpect.rowCount),
rows: rawExpect.rows?.map((r) => resolveKeyedExpectations(r)!),
}
: undefined;
const resolvedExpect = target.expect as ResolvedDbExpectConfig | undefined;
return {
db: {
@@ -248,7 +239,6 @@ export class DbChecker implements CheckerDefinition<ResolvedDbTarget> {
id: t.id,
intervalMs: context.defaultIntervalMs,
name: t.name ?? null,
rawExpect,
timeoutMs: context.defaultTimeoutMs,
type: "db",
} satisfies ResolvedDbTarget;

View File

@@ -3,13 +3,27 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import {
createRawContentExpectationsSchema,
createRawKeyedExpectationsSchema,
createRawValueExpectationSchema,
createAuthoringContentExpectationsSchema,
createAuthoringKeyedExpectationsSchema,
createAuthoringValueExpectationSchema,
createNormalizedContentExpectationsSchema,
createNormalizedKeyedExpectationsSchema,
createNormalizedValueExpectationSchema,
} from "../../schema/fragments";
export const dbCheckerSchemas: CheckerSchemas = {
config: Type.Object(
authoring: {
config: createDbConfigSchema(),
expect: createDbExpectSchema("authoring"),
},
normalized: {
config: createDbConfigSchema(),
expect: createDbExpectSchema("normalized"),
},
};
function createDbConfigSchema() {
return Type.Object(
{
query: Type.Optional(
Type.String({
@@ -19,14 +33,27 @@ export const dbCheckerSchemas: CheckerSchemas = {
url: Type.String({ minLength: 1 }),
},
{ additionalProperties: false },
),
expect: Type.Object(
);
}
function createDbExpectSchema(kind: "authoring" | "normalized") {
return Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),
result: Type.Optional(createRawContentExpectationsSchema()),
rowCount: Type.Optional(createRawValueExpectationSchema()),
rows: Type.Optional(Type.Array(createRawKeyedExpectationsSchema())),
durationMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
result: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
rowCount: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
rows: Type.Optional(
Type.Array(
kind === "authoring" ? createAuthoringKeyedExpectationsSchema() : createNormalizedKeyedExpectationsSchema(),
),
),
},
{ additionalProperties: false },
),
};
);
}

View File

@@ -38,7 +38,6 @@ export interface ResolvedDbTarget extends ResolvedTargetBase {
group: string;
intervalMs: number;
name: null | string;
rawExpect?: RawDbExpectConfig;
timeoutMs: number;
type: "db";
}

View File

@@ -27,12 +27,7 @@ export function validateDbConfig(input: CheckerValidationInput): ConfigValidatio
function collectRowExpects(rows: unknown[], path: string, targetName?: string): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i]!;
if (!isPlainRecord(row)) {
issues.push(issue("invalid-type", `${path}[${i}]`, "必须为对象", targetName));
continue;
}
issues.push(...validateRawKeyedExpectations(row, `${path}[${i}]`, targetName));
issues.push(...validateRawKeyedExpectations(rows[i], `${path}[${i}]`, targetName));
}
return issues;
}

View File

@@ -2,14 +2,13 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { HttpTargetConfig, RawHttpExpectConfig, ResolvedHttpExpectConfig, ResolvedHttpTarget } from "./types";
import type { HttpTargetConfig, ResolvedHttpExpectConfig, ResolvedHttpTarget } from "./types";
import { checkContentExpectations, resolveContentExpectations } from "../../expect/content";
import { checkContentExpectations } from "../../expect/content";
import { errorFailure, mismatchFailure } from "../../expect/failure";
import { checkHeaderExpectations } from "../../expect/headers";
import { resolveKeyedExpectations } from "../../expect/keyed";
import { checkStatusCode } from "../../expect/status";
import { checkValueExpectation, displayValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation, displayValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { httpCheckerSchemas } from "./schema";
import { validateHttpConfig } from "./validate";
@@ -179,13 +178,11 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
const method = t.http.method ?? "GET";
const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? "100MB");
const rawExpect = target.expect as RawHttpExpectConfig | undefined;
const resolvedExpect: ResolvedHttpExpectConfig = rawExpect
const expect = target.expect as ResolvedHttpExpectConfig | undefined;
const resolvedExpect: ResolvedHttpExpectConfig = expect
? {
body: resolveContentExpectations(rawExpect.body),
durationMs: resolveValueExpectation(rawExpect.durationMs),
headers: resolveKeyedExpectations(rawExpect.headers),
status: rawExpect.status ?? [200],
...expect,
status: expect.status ?? [200],
}
: { status: [200] };
@@ -205,7 +202,6 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
id: t.id,
intervalMs: context.defaultIntervalMs,
name: t.name ?? null,
rawExpect,
timeoutMs: context.defaultTimeoutMs,
type: "http",
} satisfies ResolvedHttpTarget;

View File

@@ -3,9 +3,14 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import {
createRawContentExpectationsSchema,
createRawKeyedExpectationsSchema,
createRawValueExpectationSchema,
createAuthoringContentExpectationsSchema,
createAuthoringFieldSchema,
createAuthoringKeyedExpectationsSchema,
createAuthoringStringMapSchema,
createAuthoringValueExpectationSchema,
createNormalizedContentExpectationsSchema,
createNormalizedKeyedExpectationsSchema,
createNormalizedValueExpectationSchema,
httpMethodSchema,
sizeSchema,
statusCodePatternSchema,
@@ -13,25 +18,47 @@ import {
} from "../../schema/fragments";
export const httpCheckerSchemas: CheckerSchemas = {
config: Type.Object(
authoring: {
config: createHttpConfigSchema("authoring"),
expect: createHttpExpectSchema("authoring"),
},
normalized: {
config: createHttpConfigSchema("normalized"),
expect: createHttpExpectSchema("normalized"),
},
};
function createHttpConfigSchema(kind: "authoring" | "normalized") {
const bool = Type.Boolean();
const redirects = Type.Integer({ minimum: 0 });
return Type.Object(
{
body: Type.Optional(Type.String()),
headers: Type.Optional(stringMapSchema),
ignoreSSL: Type.Optional(Type.Boolean()),
headers: Type.Optional(kind === "authoring" ? createAuthoringStringMapSchema() : stringMapSchema),
ignoreSSL: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(bool) : bool),
maxBodyBytes: Type.Optional(sizeSchema),
maxRedirects: Type.Optional(Type.Integer({ minimum: 0 })),
method: Type.Optional(httpMethodSchema),
maxRedirects: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(redirects) : redirects),
method: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(httpMethodSchema) : httpMethodSchema),
url: Type.String({ minLength: 1 }),
},
{ additionalProperties: false },
),
expect: Type.Object(
);
}
function createHttpExpectSchema(kind: "authoring" | "normalized") {
return Type.Object(
{
body: Type.Optional(createRawContentExpectationsSchema()),
durationMs: Type.Optional(createRawValueExpectationSchema()),
headers: Type.Optional(createRawKeyedExpectationsSchema()),
body: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
durationMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
headers: Type.Optional(
kind === "authoring" ? createAuthoringKeyedExpectationsSchema() : createNormalizedKeyedExpectationsSchema(),
),
status: Type.Optional(Type.Array(statusCodePatternSchema)),
},
{ additionalProperties: false },
),
};
);
}

View File

@@ -48,7 +48,6 @@ export interface ResolvedHttpTarget extends ResolvedTargetBase {
http: ResolvedHttpConfig;
intervalMs: number;
name: null | string;
rawExpect?: RawHttpExpectConfig;
timeoutMs: number;
type: "http";
}

View File

@@ -43,7 +43,7 @@ function validateHttpExpect(target: Record<string, unknown>, path: string): Conf
const issues: ConfigValidationIssue[] = [];
const expectPath = joinPath(path, "expect");
if (isPlainRecord(expect["headers"])) {
if (expect["headers"] !== undefined) {
issues.push(
...validateRawKeyedExpectations(expect["headers"], joinPath(expectPath, "headers"), targetName, {
caseInsensitive: true,

View File

@@ -2,16 +2,10 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type {
PingStats,
PingTargetConfig,
RawIcmpExpectConfig,
ResolvedIcmpExpectConfig,
ResolvedPingTarget,
} from "./types";
import type { PingStats, PingTargetConfig, ResolvedIcmpExpectConfig, ResolvedPingTarget } from "./types";
import { errorFailure } from "../../expect/failure";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation } from "../../expect/value";
import { buildPingCommand } from "./command";
import { checkAlive, checkAvgLatency, checkMaxLatency, checkPacketLoss } from "./expect";
import { parsePingOutput } from "./parse";
@@ -162,14 +156,11 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
const t = target as RawTargetConfig & { icmp: PingTargetConfig; type: "icmp" };
const rawExpect = target.expect as RawIcmpExpectConfig | undefined;
const resolvedExpect: ResolvedIcmpExpectConfig = rawExpect
const expect = target.expect as ResolvedIcmpExpectConfig | undefined;
const resolvedExpect: ResolvedIcmpExpectConfig = expect
? {
alive: rawExpect.alive ?? true,
avgLatencyMs: resolveValueExpectation(rawExpect.avgLatencyMs),
durationMs: resolveValueExpectation(rawExpect.durationMs),
maxLatencyMs: resolveValueExpectation(rawExpect.maxLatencyMs),
packetLossPercent: resolveValueExpectation(rawExpect.packetLossPercent),
...expect,
alive: expect.alive ?? true,
}
: { alive: true };
@@ -185,7 +176,6 @@ export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
id: t.id,
intervalMs: context.defaultIntervalMs,
name: t.name ?? null,
rawExpect,
timeoutMs: context.defaultTimeoutMs,
type: "icmp",
} satisfies ResolvedPingTarget;

View File

@@ -2,25 +2,54 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import { createRawValueExpectationSchema } from "../../schema/fragments";
import {
createAuthoringFieldSchema,
createAuthoringValueExpectationSchema,
createNormalizedValueExpectationSchema,
} from "../../schema/fragments";
export const icmpCheckerSchemas: CheckerSchemas = {
config: Type.Object(
{
count: Type.Optional(Type.Integer({ maximum: 100, minimum: 1 })),
host: Type.String({ minLength: 1 }),
packetSize: Type.Optional(Type.Integer({ maximum: 65500, minimum: 1 })),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
alive: Type.Optional(Type.Boolean()),
avgLatencyMs: Type.Optional(createRawValueExpectationSchema()),
durationMs: Type.Optional(createRawValueExpectationSchema()),
maxLatencyMs: Type.Optional(createRawValueExpectationSchema()),
packetLossPercent: Type.Optional(createRawValueExpectationSchema()),
},
{ additionalProperties: false },
),
authoring: {
config: createIcmpConfigSchema("authoring"),
expect: createIcmpExpectSchema("authoring"),
},
normalized: {
config: createIcmpConfigSchema("normalized"),
expect: createIcmpExpectSchema("normalized"),
},
};
function createIcmpConfigSchema(kind: "authoring" | "normalized") {
const count = Type.Integer({ maximum: 100, minimum: 1 });
const packetSize = Type.Integer({ maximum: 65500, minimum: 1 });
return Type.Object(
{
count: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(count) : count),
host: Type.String({ minLength: 1 }),
packetSize: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(packetSize) : packetSize),
},
{ additionalProperties: false },
);
}
function createIcmpExpectSchema(kind: "authoring" | "normalized") {
const bool = Type.Boolean();
return Type.Object(
{
alive: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(bool) : bool),
avgLatencyMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
durationMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
maxLatencyMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
packetLossPercent: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
},
{ additionalProperties: false },
);
}

View File

@@ -45,7 +45,6 @@ export interface ResolvedPingTarget extends ResolvedTargetBase {
icmp: ResolvedPingConfig;
intervalMs: number;
name: null | string;
rawExpect?: RawIcmpExpectConfig;
timeoutMs: number;
type: "icmp";
}

View File

@@ -3,12 +3,10 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { LlmTargetConfig, RawLlmExpectConfig, ResolvedLlmExpectConfig, ResolvedLlmTarget } from "./types";
import type { LlmTargetConfig, ResolvedLlmExpectConfig, ResolvedLlmTarget } from "./types";
import { resolveContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { resolveKeyedExpectations } from "../../expect/keyed";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation } from "../../expect/value";
import { runExpects } from "./expect";
import {
buildObservationFromApiCallError,
@@ -155,26 +153,15 @@ export class LlmChecker implements CheckerDefinition<ResolvedLlmTarget> {
url: t.llm.url,
};
const rawExpect = target.expect as RawLlmExpectConfig | undefined;
const resolvedExpect: ResolvedLlmExpectConfig = rawExpect
const expect = target.expect as ResolvedLlmExpectConfig | undefined;
const resolvedExpect: ResolvedLlmExpectConfig = expect
? {
durationMs: resolveValueExpectation(rawExpect.durationMs),
finishReason: resolveValueExpectation(rawExpect.finishReason),
headers: resolveKeyedExpectations(rawExpect.headers),
output: resolveContentExpectations(rawExpect.output),
rawFinishReason: resolveValueExpectation(rawExpect.rawFinishReason),
status: rawExpect.status ?? [200],
stream: rawExpect.stream
...expect,
status: expect.status ?? [200],
stream: expect.stream
? {
completed: rawExpect.stream.completed ?? true,
firstTokenMs: resolveValueExpectation(rawExpect.stream.firstTokenMs),
}
: undefined,
usage: rawExpect.usage
? {
inputTokens: resolveValueExpectation(rawExpect.usage.inputTokens),
outputTokens: resolveValueExpectation(rawExpect.usage.outputTokens),
totalTokens: resolveValueExpectation(rawExpect.usage.totalTokens),
...expect.stream,
completed: expect.stream.completed ?? true,
}
: undefined,
}
@@ -188,7 +175,6 @@ export class LlmChecker implements CheckerDefinition<ResolvedLlmTarget> {
intervalMs: context.defaultIntervalMs,
llm: resolvedConfig,
name: (target.name as null | string) ?? null,
rawExpect,
timeoutMs: context.defaultTimeoutMs,
type: "llm",
} satisfies ResolvedLlmTarget;

View File

@@ -3,18 +3,26 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import {
createRawContentExpectationsSchema,
createRawKeyedExpectationsSchema,
createRawValueExpectationSchema,
createAuthoringContentExpectationsSchema,
createAuthoringFieldSchema,
createAuthoringKeyedExpectationsSchema,
createAuthoringStringMapSchema,
createAuthoringValueExpectationSchema,
createNormalizedContentExpectationsSchema,
createNormalizedKeyedExpectationsSchema,
createNormalizedValueExpectationSchema,
statusCodePatternSchema,
stringMapSchema,
} from "../../schema/fragments";
function createLlmOptionsSchema() {
function createLlmOptionsSchema(kind: "authoring" | "normalized") {
const maxOutputTokens = Type.Integer({ minimum: 1 });
return Type.Object(
{
frequencyPenalty: Type.Optional(Type.Number()),
maxOutputTokens: Type.Optional(Type.Integer({ minimum: 1 })),
maxOutputTokens: Type.Optional(
kind === "authoring" ? createAuthoringFieldSchema(maxOutputTokens) : maxOutputTokens,
),
presencePenalty: Type.Optional(Type.Number()),
seed: Type.Optional(Type.Number()),
stopSequences: Type.Optional(Type.Array(Type.String())),
@@ -27,35 +35,59 @@ function createLlmOptionsSchema() {
}
export const llmCheckerSchemas: CheckerSchemas = {
config: Type.Object(
authoring: {
config: createLlmConfigSchema("authoring"),
expect: createLlmExpectSchema("authoring"),
},
normalized: {
config: createLlmConfigSchema("normalized"),
expect: createLlmExpectSchema("normalized"),
},
};
function createLlmConfigSchema(kind: "authoring" | "normalized") {
const bool = Type.Boolean();
const mode = createLlmModeSchema();
const provider = createLlmProviderSchema();
return Type.Object(
{
authToken: Type.Optional(Type.String()),
headers: Type.Optional(stringMapSchema),
ignoreSSL: Type.Optional(Type.Boolean()),
headers: Type.Optional(kind === "authoring" ? createAuthoringStringMapSchema() : stringMapSchema),
ignoreSSL: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(bool) : bool),
key: Type.Optional(Type.String()),
mode: Type.Optional(Type.Union([Type.Literal("http"), Type.Literal("stream")])),
mode: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(mode) : mode),
model: Type.String({ minLength: 1 }),
options: Type.Optional(createLlmOptionsSchema()),
options: Type.Optional(createLlmOptionsSchema(kind)),
prompt: Type.String({ minLength: 1 }),
provider: Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]),
provider: kind === "authoring" ? createAuthoringFieldSchema(provider) : provider,
providerOptions: Type.Optional(Type.Record(Type.String(), Type.Object({}, { additionalProperties: true }))),
url: Type.String({ minLength: 1 }),
},
{ additionalProperties: false },
),
expect: Type.Object(
);
}
function createLlmExpectSchema(kind: "authoring" | "normalized") {
const bool = Type.Boolean();
const valueExpectation =
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema();
return Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),
finishReason: Type.Optional(createRawValueExpectationSchema()),
headers: Type.Optional(createRawKeyedExpectationsSchema()),
output: Type.Optional(createRawContentExpectationsSchema()),
rawFinishReason: Type.Optional(createRawValueExpectationSchema()),
durationMs: Type.Optional(valueExpectation),
finishReason: Type.Optional(valueExpectation),
headers: Type.Optional(
kind === "authoring" ? createAuthoringKeyedExpectationsSchema() : createNormalizedKeyedExpectationsSchema(),
),
output: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
rawFinishReason: Type.Optional(valueExpectation),
status: Type.Optional(Type.Array(statusCodePatternSchema)),
stream: Type.Optional(
Type.Object(
{
completed: Type.Optional(Type.Boolean()),
firstTokenMs: Type.Optional(createRawValueExpectationSchema()),
completed: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(bool) : bool),
firstTokenMs: Type.Optional(valueExpectation),
},
{ additionalProperties: false },
),
@@ -63,14 +95,22 @@ export const llmCheckerSchemas: CheckerSchemas = {
usage: Type.Optional(
Type.Object(
{
inputTokens: Type.Optional(createRawValueExpectationSchema()),
outputTokens: Type.Optional(createRawValueExpectationSchema()),
totalTokens: Type.Optional(createRawValueExpectationSchema()),
inputTokens: Type.Optional(valueExpectation),
outputTokens: Type.Optional(valueExpectation),
totalTokens: Type.Optional(valueExpectation),
},
{ additionalProperties: false },
),
),
},
{ additionalProperties: false },
),
};
);
}
function createLlmModeSchema() {
return Type.Union([Type.Literal("http"), Type.Literal("stream")]);
}
function createLlmProviderSchema() {
return Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]);
}

View File

@@ -152,7 +152,6 @@ export interface ResolvedLlmTarget extends ResolvedTargetBase {
intervalMs: number;
llm: ResolvedLlmConfig;
name: null | string;
rawExpect?: RawLlmExpectConfig;
timeoutMs: number;
type: "llm";
}

View File

@@ -2,11 +2,10 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { RawTcpExpectConfig, ResolvedTcpExpectConfig, ResolvedTcpTarget, TcpTargetConfig } from "./types";
import type { ResolvedTcpExpectConfig, ResolvedTcpTarget, TcpTargetConfig } from "./types";
import { resolveContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { checkBanner, checkConnected } from "./expect";
import { tcpCheckerSchemas } from "./schema";
@@ -210,12 +209,11 @@ export class TcpChecker implements CheckerDefinition<ResolvedTcpTarget> {
const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES);
const bannerReadTimeout = t.tcp.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT;
const rawExpect = target.expect as RawTcpExpectConfig | undefined;
const resolvedExpect: ResolvedTcpExpectConfig = rawExpect
const expect = target.expect as ResolvedTcpExpectConfig | undefined;
const resolvedExpect: ResolvedTcpExpectConfig = expect
? {
banner: resolveContentExpectations(rawExpect.banner),
connected: rawExpect.connected ?? true,
durationMs: resolveValueExpectation(rawExpect.durationMs),
...expect,
connected: expect.connected ?? true,
}
: { connected: true };
@@ -226,7 +224,6 @@ export class TcpChecker implements CheckerDefinition<ResolvedTcpTarget> {
id: t.id,
intervalMs: context.defaultIntervalMs,
name: t.name ?? null,
rawExpect,
tcp: {
bannerReadTimeout,
host: t.tcp.host,

View File

@@ -3,28 +3,52 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import {
createRawContentExpectationsSchema,
createRawValueExpectationSchema,
createAuthoringContentExpectationsSchema,
createAuthoringFieldSchema,
createAuthoringValueExpectationSchema,
createNormalizedContentExpectationsSchema,
createNormalizedValueExpectationSchema,
sizeSchema,
} from "../../schema/fragments";
export const tcpCheckerSchemas: CheckerSchemas = {
config: Type.Object(
authoring: {
config: createTcpConfigSchema("authoring"),
expect: createTcpExpectSchema("authoring"),
},
normalized: {
config: createTcpConfigSchema("normalized"),
expect: createTcpExpectSchema("normalized"),
},
};
function createTcpConfigSchema(kind: "authoring" | "normalized") {
const port = Type.Integer({ maximum: 65535, minimum: 1 });
const readBanner = Type.Boolean();
return Type.Object(
{
bannerReadTimeout: Type.Optional(Type.Number({ minimum: 0 })),
host: Type.String({ minLength: 1 }),
maxBannerBytes: Type.Optional(sizeSchema),
port: Type.Integer({ maximum: 65535, minimum: 1 }),
readBanner: Type.Optional(Type.Boolean()),
port: kind === "authoring" ? createAuthoringFieldSchema(port) : port,
readBanner: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(readBanner) : readBanner),
},
{ additionalProperties: false },
),
expect: Type.Object(
);
}
function createTcpExpectSchema(kind: "authoring" | "normalized") {
const connected = Type.Boolean();
return Type.Object(
{
banner: Type.Optional(createRawContentExpectationsSchema()),
connected: Type.Optional(Type.Boolean()),
durationMs: Type.Optional(createRawValueExpectationSchema()),
banner: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
connected: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(connected) : connected),
durationMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
},
{ additionalProperties: false },
),
};
);
}

View File

@@ -31,7 +31,6 @@ export interface ResolvedTcpTarget extends ResolvedTargetBase {
group: string;
intervalMs: number;
name: null | string;
rawExpect?: RawTcpExpectConfig;
tcp: ResolvedTcpConfig;
timeoutMs: number;
type: "tcp";

View File

@@ -20,11 +20,16 @@ export interface CheckerDefinition<TResolved extends ResolvedTargetBase = Resolv
validate(input: CheckerValidationInput): ConfigValidationIssue[];
}
export interface CheckerSchemas {
export interface CheckerSchemaPair {
config: TSchema;
expect: TSchema;
}
export interface CheckerSchemas {
authoring: CheckerSchemaPair;
normalized: CheckerSchemaPair;
}
export interface CheckerValidationInput {
targets: RawTargetConfig[];
}

View File

@@ -2,11 +2,10 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { RawUdpExpectConfig, ResolvedUdpExpectConfig, ResolvedUdpTarget, UdpTargetConfig } from "./types";
import type { ResolvedUdpExpectConfig, ResolvedUdpTarget, UdpTargetConfig } from "./types";
import { resolveContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { checkValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { decodePayload, encodeResponse } from "./encoding";
import { checkResponded, checkResponseSize, checkResponseText, checkSourceHost, checkSourcePort } from "./expect";
@@ -303,15 +302,11 @@ export class UdpChecker implements CheckerDefinition<ResolvedUdpTarget> {
const responseEncoding = t.udp.responseEncoding ?? "text";
const maxResponseBytes = parseSize(t.udp.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES);
const rawExpect = target.expect as RawUdpExpectConfig | undefined;
const resolvedExpect: ResolvedUdpExpectConfig = rawExpect
const expect = target.expect as ResolvedUdpExpectConfig | undefined;
const resolvedExpect: ResolvedUdpExpectConfig = expect
? {
durationMs: resolveValueExpectation(rawExpect.durationMs),
responded: rawExpect.responded ?? true,
response: resolveContentExpectations(rawExpect.response),
responseSize: resolveValueExpectation(rawExpect.responseSize),
sourceHost: resolveValueExpectation(rawExpect.sourceHost),
sourcePort: resolveValueExpectation(rawExpect.sourcePort),
...expect,
responded: expect.responded ?? true,
}
: { responded: true };
@@ -322,7 +317,6 @@ export class UdpChecker implements CheckerDefinition<ResolvedUdpTarget> {
id: t.id,
intervalMs: context.defaultIntervalMs,
name: t.name ?? null,
rawExpect,
timeoutMs: context.defaultTimeoutMs,
type: "udp",
udp: {

View File

@@ -3,32 +3,69 @@ import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import {
createRawContentExpectationsSchema,
createRawValueExpectationSchema,
createAuthoringContentExpectationsSchema,
createAuthoringFieldSchema,
createAuthoringValueExpectationSchema,
createNormalizedContentExpectationsSchema,
createNormalizedValueExpectationSchema,
sizeSchema,
} from "../../schema/fragments";
export const udpCheckerSchemas: CheckerSchemas = {
config: Type.Object(
authoring: {
config: createUdpConfigSchema("authoring"),
expect: createUdpExpectSchema("authoring"),
},
normalized: {
config: createUdpConfigSchema("normalized"),
expect: createUdpExpectSchema("normalized"),
},
};
function createEncodingSchema() {
return Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")]);
}
function createUdpConfigSchema(kind: "authoring" | "normalized") {
const port = Type.Integer({ maximum: 65535, minimum: 1 });
const encoding = createEncodingSchema();
const responseEncoding = createEncodingSchema();
return Type.Object(
{
encoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])),
encoding: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(encoding) : encoding),
host: Type.String({ minLength: 1 }),
maxResponseBytes: Type.Optional(sizeSchema),
payload: Type.Optional(Type.String()),
port: Type.Integer({ maximum: 65535, minimum: 1 }),
responseEncoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])),
port: kind === "authoring" ? createAuthoringFieldSchema(port) : port,
responseEncoding: Type.Optional(
kind === "authoring" ? createAuthoringFieldSchema(responseEncoding) : responseEncoding,
),
},
{ additionalProperties: false },
),
expect: Type.Object(
);
}
function createUdpExpectSchema(kind: "authoring" | "normalized") {
const responded = Type.Boolean();
return Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),
responded: Type.Optional(Type.Boolean()),
response: Type.Optional(createRawContentExpectationsSchema()),
responseSize: Type.Optional(createRawValueExpectationSchema()),
sourceHost: Type.Optional(createRawValueExpectationSchema()),
sourcePort: Type.Optional(createRawValueExpectationSchema()),
durationMs: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
responded: Type.Optional(kind === "authoring" ? createAuthoringFieldSchema(responded) : responded),
response: Type.Optional(
kind === "authoring" ? createAuthoringContentExpectationsSchema() : createNormalizedContentExpectationsSchema(),
),
responseSize: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
sourceHost: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
sourcePort: Type.Optional(
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema(),
),
},
{ additionalProperties: false },
),
};
);
}

View File

@@ -38,7 +38,6 @@ export interface ResolvedUdpTarget extends ResolvedTargetBase {
group: string;
intervalMs: number;
name: null | string;
rawExpect?: RawUdpExpectConfig;
timeoutMs: number;
type: "udp";
udp: ResolvedUdpConfig;