- 引入共享 ValueMatcher(equals/contains/regex/exists/empty/gt/gte/lt/lte) - 引入共享 ContentRules 数组(direct/json/css/xpath 提取器) - 引入共享 KeyValueExpect(动态键值断言,字面量等价 equals) - maxDurationMs → durationMs: ValueMatcher(所有 checker) - match → regex(固定无 flags) - Ping max* → packetLossPercent/avgLatencyMs/maxLatencyMs(ValueMatcher) - LLM finishReason/rawFinishReason → ValueMatcher - DB 新增 result: ContentRules - TCP banner → ContentRules 数组 - 删除旧模块:operator.ts、validate-operator.ts、duration.ts、body.ts、text.ts、output.ts - 更新全部 checker schema/validate/expect/execute - 更新 probe-config.schema.json、probes.example.yaml - 更新 README.md、DEVELOPMENT.md(含 expect 字段选择规范) - 同步 10 个 delta specs 到主 specs,归档 change
161 lines
5.5 KiB
TypeScript
161 lines
5.5 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
|
||
import type { ResolvedDbTarget } from "../../../../../src/server/checker/runner/db/types";
|
||
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";
|
||
|
||
import { DbChecker } from "../../../../../src/server/checker/runner/db/execute";
|
||
|
||
const checker = new DbChecker();
|
||
|
||
function makeCtx(timeoutMs = 5000): CheckerContext {
|
||
const controller = new AbortController();
|
||
setTimeout(() => controller.abort(), timeoutMs);
|
||
return { signal: controller.signal };
|
||
}
|
||
|
||
function makeTarget(db: Partial<ResolvedDbTarget["db"]>, overrides?: Partial<ResolvedDbTarget>): ResolvedDbTarget {
|
||
return {
|
||
db: {
|
||
url: "sqlite://:memory:",
|
||
...db,
|
||
},
|
||
description: null,
|
||
group: "default",
|
||
id: "test-db",
|
||
intervalMs: 60000,
|
||
name: "test-db",
|
||
timeoutMs: 5000,
|
||
type: "db",
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
describe("DbChecker", () => {
|
||
test("无 query 时仅测试连接成功", async () => {
|
||
const result = await checker.execute(makeTarget({}), makeCtx());
|
||
expect(result.matched).toBe(true);
|
||
expect(result.statusDetail).toBe("connected");
|
||
expect(result.failure).toBeNull();
|
||
});
|
||
|
||
test("执行查询成功", async () => {
|
||
const result = await checker.execute(makeTarget({ query: "SELECT 1 as num, 'hello' as str" }), makeCtx());
|
||
expect(result.matched).toBe(true);
|
||
expect(result.statusDetail).toBe("1 rows");
|
||
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
||
});
|
||
|
||
test("查询返回多行", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 1 as n UNION ALL SELECT 2 UNION ALL SELECT 3" }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(true);
|
||
expect(result.statusDetail).toBe("3 rows");
|
||
});
|
||
|
||
test("查询返回空结果", async () => {
|
||
const result = await checker.execute(makeTarget({ query: "SELECT 1 as n WHERE 1=0" }), makeCtx());
|
||
expect(result.matched).toBe(true);
|
||
expect(result.statusDetail).toBe("0 rows");
|
||
});
|
||
|
||
test("连接失败返回 connect phase 错误", async () => {
|
||
const result = await checker.execute(makeTarget({ url: "sqlite:///nonexistent/path/db.db" }), makeCtx());
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure!.phase).toBe("connect");
|
||
expect(result.failure!.message).toBeTruthy();
|
||
});
|
||
|
||
test("SQL 语法错误返回 query phase 错误", async () => {
|
||
const result = await checker.execute(makeTarget({ query: "SELECT INVALID SQL" }), makeCtx());
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure!.phase).toBe("query");
|
||
expect(result.failure!.message).toBeTruthy();
|
||
});
|
||
|
||
test("durationMs 超时返回失败", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 1" }, { expect: { durationMs: { lt: 0 } } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure!.phase).toBe("duration");
|
||
});
|
||
|
||
test("rowCount 断言通过", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 1 UNION ALL SELECT 2" }, { expect: { rowCount: { gte: 2 } } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(true);
|
||
});
|
||
|
||
test("rowCount 断言失败", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 1 UNION ALL SELECT 2" }, { expect: { rowCount: { gte: 5 } } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure!.phase).toBe("rowCount");
|
||
});
|
||
|
||
test("rows 断言通过(字面量形式)", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 100 as cnt" }, { expect: { rows: [{ cnt: 100 }] } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(true);
|
||
});
|
||
|
||
test("rows 断言通过(operator 形式)", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 150 as cnt" }, { expect: { rows: [{ cnt: { gte: 100 } }] } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(true);
|
||
});
|
||
|
||
test("rows 断言失败", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 50 as cnt" }, { expect: { rows: [{ cnt: { gte: 100 } }] } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure!.phase).toBe("row");
|
||
expect(result.failure!.path).toBe("rows[0].cnt");
|
||
});
|
||
|
||
test("rows 结果行数不足", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 1 as n" }, { expect: { rows: [{ n: 1 }, { n: 2 }] } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure!.phase).toBe("row");
|
||
expect(result.failure!.message).toContain("行数不足");
|
||
});
|
||
|
||
test("rows 只检查声明的列", async () => {
|
||
const result = await checker.execute(
|
||
makeTarget({ query: "SELECT 1 as cnt, 'ignored' as other" }, { expect: { rows: [{ cnt: { gte: 1 } }] } }),
|
||
makeCtx(),
|
||
);
|
||
expect(result.matched).toBe(true);
|
||
});
|
||
|
||
test("serialize 屏蔽凭据", () => {
|
||
const target = makeTarget({ url: "postgres://user:pass@host:5432/db" });
|
||
const s = checker.serialize(target);
|
||
expect(s.target).toBe("postgres://***:***@host:5432/db");
|
||
const config = JSON.parse(s.config) as { url: string };
|
||
expect(config.url).toBe("postgres://***:***@host:5432/db");
|
||
});
|
||
|
||
test("serialize 无凭据的 url 保持原样", () => {
|
||
const target = makeTarget({ url: "sqlite:///data/app.db" });
|
||
const s = checker.serialize(target);
|
||
expect(s.target).toBe("sqlite:///data/app.db");
|
||
});
|
||
});
|