1
0

feat: ValueMatcher 支持 primitive 原始值简写,等价于 { equals: value }

This commit is contained in:
2026-05-19 17:07:47 +08:00
parent 8d8549d07f
commit 12cd05b04e
37 changed files with 1836 additions and 1022 deletions

View File

@@ -49,13 +49,13 @@ describe("validateDbConfig", () => {
expect(unknownError!.code).toBe("unknown-field");
});
test("expect.durationMs 非 matcher 返回错误", () => {
test("expect.durationMs 数组简写返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{
db: { url: "sqlite://:memory:" },
expect: { durationMs: "invalid" },
expect: { durationMs: [1, 2] },
id: "test",
name: "test",
type: "db",

View File

@@ -48,19 +48,19 @@ describe("validatePingConfig", () => {
expect(issues.some((item) => item.path.endsWith("expect.maxPacketLoss"))).toBe(true);
});
test("durationMs 类型非法", () => {
const issues = validate({ expect: { durationMs: -1 }, id: "ping", ping: { host: "127.0.0.1" }, type: "ping" });
test("durationMs 数组简写非法", () => {
const issues = validate({ expect: { durationMs: [1, 2] }, id: "ping", ping: { host: "127.0.0.1" }, type: "ping" });
expect(issues.some((item) => item.path.endsWith("expect.durationMs"))).toBe(true);
});
test("avgLatencyMs 类型非法", () => {
test("avgLatencyMs 对象简写非法", () => {
const issues = validate({
expect: { avgLatencyMs: "slow" },
expect: { avgLatencyMs: { foo: "bar" } },
id: "ping",
ping: { host: "127.0.0.1" },
type: "ping",
});
expect(issues.some((item) => item.path.endsWith("expect.avgLatencyMs"))).toBe(true);
expect(issues.some((item) => item.path.endsWith("expect.avgLatencyMs.foo"))).toBe(true);
});
test("host 为空字符串", () => {

View File

@@ -0,0 +1,115 @@
import { describe, expect, test } from "bun:test";
import type { CheckerValidationInput } from "../../../../../src/server/checker/runner/types";
import { validateCommandConfig } from "../../../../../src/server/checker/runner/cmd/validate";
import { validateDbConfig } from "../../../../../src/server/checker/runner/db/validate";
import { validateHttpConfig } from "../../../../../src/server/checker/runner/http/validate";
import { validatePingConfig } from "../../../../../src/server/checker/runner/icmp/validate";
import { validateLlmConfig } from "../../../../../src/server/checker/runner/llm/validate";
import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/validate";
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
function input(target: Record<string, unknown>): CheckerValidationInput {
return { defaults: {}, targets: [target as CheckerValidationInput["targets"][number]] };
}
describe("ValueMatcher primitive shorthand in checker validators", () => {
test("normalizes shorthand for all checker ValueMatcher fields", () => {
const targets = [
{
expect: { durationMs: 100 },
http: { url: "https://example.com" },
id: "http",
type: "http",
validate: validateHttpConfig,
},
{
expect: { durationMs: 100 },
id: "tcp",
tcp: { host: "127.0.0.1", port: 80 },
type: "tcp",
validate: validateTcpConfig,
},
{
expect: { durationMs: 100, responseSize: 1, sourceHost: "127.0.0.1", sourcePort: 53 },
id: "udp",
type: "udp",
udp: { host: "127.0.0.1", port: 53 },
validate: validateUdpConfig,
},
{
expect: { avgLatencyMs: 1, durationMs: 100, maxLatencyMs: 2, packetLossPercent: 0 },
id: "ping",
ping: { host: "127.0.0.1" },
type: "ping",
validate: validatePingConfig,
},
{
cmd: { exec: "true" },
expect: { durationMs: 100 },
id: "cmd",
type: "cmd",
validate: validateCommandConfig,
},
{
db: { url: "sqlite://:memory:" },
expect: { durationMs: 100, rowCount: 1 },
id: "db",
type: "db",
validate: validateDbConfig,
},
{
expect: {
durationMs: 100,
finishReason: "stop",
rawFinishReason: null,
stream: { firstTokenMs: 10 },
usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
},
id: "llm",
llm: {
mode: "stream",
model: "test-model",
prompt: "hello",
provider: "openai",
url: "https://example.com/v1/chat/completions",
},
type: "llm",
validate: validateLlmConfig,
},
];
for (const target of targets) {
const { validate, ...config } = target;
expect(validate(input(config))).toHaveLength(0);
expect((config.expect as Record<string, unknown>)["durationMs"]).toEqual({ equals: 100 });
}
});
test("rejects array and object shorthand while accepting explicit equals", () => {
const arrayTarget = {
expect: { durationMs: [1, 2] },
http: { url: "https://example.com" },
id: "array",
type: "http",
};
const objectTarget = {
expect: { durationMs: { foo: "bar" } },
http: { url: "https://example.com" },
id: "object",
type: "http",
};
const equalsObjectTarget = {
expect: { durationMs: { equals: { foo: "bar" } } },
http: { url: "https://example.com" },
id: "equals-object",
type: "http",
};
expect(validateHttpConfig(input(arrayTarget)).some((issue) => issue.path.includes("durationMs"))).toBe(true);
expect(validateHttpConfig(input(objectTarget)).some((issue) => issue.code === "unknown-matcher")).toBe(true);
expect(validateHttpConfig(input(equalsObjectTarget))).toHaveLength(0);
});
});

View File

@@ -106,18 +106,18 @@ describe("validateTcpConfig", () => {
expect(issues.some((i) => i.path.includes("connected"))).toBe(true);
});
test("expect durationMs 非 matcher", () => {
test("expect durationMs 数组简写非法", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { durationMs: "slow" },
expect: { durationMs: [1, 2] },
id: "t1",
tcp: { host: "127.0.0.1", port: 80 },
type: "tcp",
},
]),
);
expect(issues.some((i) => i.path.includes("durationMs"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("durationMs"))).toBe(true);
});
test("expect 未知字段", () => {

View File

@@ -213,12 +213,12 @@ describe("validateUdpConfig", () => {
expect(issues.some((i) => i.path.includes("responded") && i.message.includes("响应来源"))).toBe(true);
});
it("reports invalid-type for non-matcher expect.durationMs", () => {
it("reports invalid-type for array shorthand expect.durationMs", () => {
const issues = validateUdpConfig(
makeInput({
targets: [
{
expect: { durationMs: -100 },
expect: { durationMs: [1, 2] },
id: "test",
type: "udp",
udp: { host: "127.0.0.1", port: 53 },