1
0
Files
DiAL/tests/server/checker/runner/db/execute.test.ts
lanyuanxiaoyao 375dd3492b feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail
- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
2026-05-19 22:49:00 +08:00

161 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.observation).toMatchObject({ connected: true });
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.observation).toMatchObject({ connected: true, rowCount: 1 });
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.observation).toMatchObject({ connected: true, rowCount: 3 });
});
test("查询返回空结果", async () => {
const result = await checker.execute(makeTarget({ query: "SELECT 1 as n WHERE 1=0" }), makeCtx());
expect(result.matched).toBe(true);
expect(result.observation).toMatchObject({ connected: true, rowCount: 0 });
});
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");
});
});