1
0
Files
DiAL/tests/server/checker/store.test.ts
lanyuanxiaoyao bcfb907bd3 feat: 基础设施加固 — 修复构建、数据保留、错误边界、bundle 拆分
- 修复 build script 引用已删除的 registerCheckers,恢复生产构建
- 生产入口添加 SIGINT/SIGTERM 优雅关闭(与 dev.ts 一致)
- 新增 runtime.retention 配置(默认 7d),ProbeStore.prune() 定时清理过期数据
- parseDuration 扩展支持 h/d 单位
- 新增前端 ErrorBoundary 组件,防止渲染错误白屏
- Vite codeSplitting.groups 拆分 vendor chunks(业务代码 1180KB → 47KB)
- 同步 delta specs 到主规范
2026-05-13 16:48:56 +08:00

506 lines
15 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/command/types";
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
import type { CheckFailure } from "../../../src/server/checker/types";
import { checkerRegistry } from "../../../src/server/checker/runner";
import { CommandChecker } from "../../../src/server/checker/runner/command/execute";
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
import { ProbeStore } from "../../../src/server/checker/store";
import { rmRetry } from "../../helpers";
function ensureRegistered() {
if (!checkerRegistry.supportedTypes.includes("http")) {
checkerRegistry.register(new HttpChecker());
checkerRegistry.register(new CommandChecker());
}
}
beforeAll(() => {
ensureRegistered();
});
const httpTarget: ResolvedHttpTarget = {
expect: { maxDurationMs: 3000, status: [200] },
group: "default",
http: {
headers: { Accept: "application/json" },
ignoreSSL: false,
maxBodyBytes: 104857600,
maxRedirects: 0,
method: "GET",
url: "https://example.com/health",
},
intervalMs: 30000,
name: "test-http",
timeoutMs: 10000,
type: "http",
};
const commandTarget: ResolvedCommandTarget = {
command: {
args: ["-c", "1", "localhost"],
cwd: "/tmp",
env: {},
exec: "ping",
maxOutputBytes: 104857600,
},
group: "default",
intervalMs: 60000,
name: "test-cmd",
timeoutMs: 5000,
type: "command",
};
describe("ProbeStore", () => {
let tempDir: string;
let store: ProbeStore;
beforeAll(async () => {
tempDir = join(tmpdir(), `gc-store-test-${Date.now()}`);
await mkdir(tempDir, { recursive: true });
store = new ProbeStore(join(tempDir, "test.db"));
});
afterAll(async () => {
store.close();
await rmRetry(tempDir);
});
test("初始化后无 targets", () => {
expect(store.getTargets()).toHaveLength(0);
});
test("同步 http 和 command targets", () => {
store.syncTargets([httpTarget, commandTarget]);
const targets = store.getTargets();
expect(targets).toHaveLength(2);
expect(targets[0]!.name).toBe("test-http");
expect(targets[1]!.name).toBe("test-cmd");
});
test("http target 字段正确", () => {
const t = store.getTargets().find((t) => t.name === "test-http")!;
expect(t.type).toBe("http");
expect(t.target).toBe("https://example.com/health");
const config = JSON.parse(t.config) as {
headers: Record<string, string>;
ignoreSSL: boolean;
maxBodyBytes: number;
maxRedirects: number;
method: string;
url: string;
};
expect(config.url).toBe("https://example.com/health");
expect(config.method).toBe("GET");
expect(config.headers).toEqual({ Accept: "application/json" });
expect(config.ignoreSSL).toBe(false);
expect(config.maxBodyBytes).toBe(104857600);
expect(config.maxRedirects).toBe(0);
expect(t.interval_ms).toBe(30000);
expect(t.timeout_ms).toBe(10000);
expect(JSON.parse(t.expect!)).toEqual({ maxDurationMs: 3000, status: [200] });
});
test("command target 字段正确", () => {
const t = store.getTargets().find((t) => t.name === "test-cmd")!;
expect(t.type).toBe("command");
expect(t.target).toBe("exec ping -c 1 localhost");
const config = JSON.parse(t.config) as { args: string[]; cwd: string; exec: string; maxOutputBytes: number };
expect(config.exec).toBe("ping");
expect(config.args).toEqual(["-c", "1", "localhost"]);
expect(config.cwd).toBe("/tmp");
expect(config.maxOutputBytes).toBe(104857600);
expect(t.interval_ms).toBe(60000);
expect(t.timeout_ms).toBe(5000);
expect(t.expect).toBeNull();
});
test("同步更新已有 target", () => {
const updated: ResolvedHttpTarget = {
...httpTarget,
http: { ...httpTarget.http, url: "https://example.com/v2" },
};
store.syncTargets([updated, commandTarget]);
const t = store.getTargets().find((t) => t.name === "test-http")!;
expect(t.target).toBe("https://example.com/v2");
expect(store.getTargets()).toHaveLength(2);
});
test("同步删除 target", () => {
store.syncTargets([httpTarget]);
const targets = store.getTargets();
expect(targets).toHaveLength(1);
expect(targets[0]!.name).toBe("test-http");
});
test("重新同步回来", () => {
store.syncTargets([httpTarget, commandTarget]);
expect(store.getTargets()).toHaveLength(2);
});
test("getTargetById", () => {
const targets = store.getTargets();
const found = store.getTargetById(targets[0]!.id);
expect(found).toBeDefined();
expect(found!.name).toBe("test-http");
});
test("getTargetById 不存在", () => {
expect(store.getTargetById(99999)).toBeNull();
});
test("写入 check result 并查询", () => {
store.syncTargets([httpTarget, commandTarget]);
const targets = store.getTargets();
const t1Id = targets[0]!.id;
store.insertCheckResult({
durationMs: 150.5,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t1Id,
timestamp: "2025-01-01T00:00:00.000Z",
});
store.insertCheckResult({
durationMs: 300,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t1Id,
timestamp: "2025-01-01T00:00:30.000Z",
});
const failure: CheckFailure = {
actual: 5000,
expected: 3000,
kind: "error",
message: "请求耗时 5000ms 超过限制 3000ms",
path: "$.maxDurationMs",
phase: "duration",
};
store.insertCheckResult({
durationMs: null,
failure,
matched: false,
statusDetail: null,
targetId: t1Id,
timestamp: "2025-01-01T00:01:00.000Z",
});
const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z", 1, 10);
expect(history.items).toHaveLength(3);
expect(history.items[0]!.timestamp).toBe("2025-01-01T00:01:00.000Z");
const latest = store.getLatestCheck(t1Id)!;
expect(latest.matched).toBe(0);
expect(latest.failure).not.toBeNull();
const parsedFailure = JSON.parse(latest.failure!) as CheckFailure;
expect(parsedFailure.kind).toBe("error");
expect(parsedFailure.phase).toBe("duration");
expect(parsedFailure.message).toBe("请求耗时 5000ms 超过限制 3000ms");
});
test("getHistory 默认 limit=20", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
for (let i = 0; i < 25; i++) {
store.insertCheckResult({
durationMs: 100 + i,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t1Id,
timestamp: `2025-01-01T01:${String(i).padStart(2, "0")}:00.000Z`,
});
}
const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z");
expect(history.items).toHaveLength(20);
});
test("getTargetStats 计算可用率和 duration", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const stats = store.getTargetStats(t1Id);
expect(stats.totalChecks).toBeGreaterThan(0);
expect(stats.availability).toBeGreaterThanOrEqual(0);
expect(stats.availability).toBeLessThanOrEqual(100);
});
test("无记录目标的 stats", () => {
const targets = store.getTargets();
const t2Id = targets.find((t) => t.name === "test-cmd")!.id;
const stats = store.getTargetStats(t2Id);
expect(stats.totalChecks).toBe(0);
expect(stats.availability).toBe(0);
});
test("getSummary 返回总览统计", () => {
const summary = store.getSummary();
expect(summary.total).toBe(2);
expect(summary.up + summary.down).toBe(2);
expect(summary.lastCheckTime).not.toBeNull();
});
test("getTrend 返回趋势数据", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const trend = store.getTrend(t1Id, "2024-01-01T00:00:00.000Z", "2026-12-31T23:59:59.999Z");
expect(Array.isArray(trend)).toBe(true);
if (trend.length > 0) {
expect(trend[0]!.hour).toBeDefined();
expect(trend[0]!.avgDurationMs).toBeDefined();
expect(trend[0]!.availability).toBeGreaterThanOrEqual(0);
expect(trend[0]!.totalChecks).toBeGreaterThan(0);
}
});
test("getRecentSamples 返回最近采样数据", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const samples = store.getRecentSamples(t1Id, 10);
expect(Array.isArray(samples)).toBe(true);
expect(samples.length).toBeGreaterThan(0);
for (const sample of samples) {
expect(typeof sample.timestamp).toBe("string");
expect(typeof sample.matched).toBe("number");
}
});
test("关闭后操作不报错", () => {
const closedStore = new ProbeStore(join(tempDir, "closed.db"));
closedStore.close();
expect(closedStore.getTargets()).toHaveLength(0);
expect(closedStore.getTargetById(1)).toBeNull();
});
test("删除 target 级联删除 check_results", () => {
const cascadeStore = new ProbeStore(join(tempDir, "cascade.db"));
const cascadeTarget: ResolvedHttpTarget = {
group: "default",
http: {
headers: {},
ignoreSSL: false,
maxBodyBytes: 104857600,
maxRedirects: 0,
method: "GET",
url: "http://cascade.test",
},
intervalMs: 30000,
name: "cascade-test",
timeoutMs: 10000,
type: "http",
};
cascadeStore.syncTargets([cascadeTarget]);
const t = cascadeStore.getTargets()[0]!;
cascadeStore.insertCheckResult({
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t.id,
timestamp: "2025-01-01T00:00:00.000Z",
});
cascadeStore.insertCheckResult({
durationMs: null,
failure: { kind: "error", message: "fail", path: "$", phase: "status" },
matched: false,
statusDetail: null,
targetId: t.id,
timestamp: "2025-01-01T00:01:00.000Z",
});
expect(cascadeStore.getLatestCheck(t.id)).not.toBeNull();
cascadeStore.syncTargets([]);
expect(cascadeStore.getTargets()).toHaveLength(0);
expect(cascadeStore.getLatestCheck(t.id)).toBeNull();
cascadeStore.close();
});
test("getLatestChecksMap 返回所有 target 的最新 check", () => {
const targets = store.getTargets();
const map = store.getLatestChecksMap();
expect(map).toBeInstanceOf(Map);
for (const target of targets) {
const latest = map.get(target.id);
if (latest) {
expect(latest.target_id).toBe(target.id);
}
}
});
test("getLatestChecksMap 对无记录的 target 不包含 key", () => {
const freshStore = new ProbeStore(join(tempDir, "fresh-map.db"));
freshStore.syncTargets([
{
group: "default",
http: {
headers: {},
ignoreSSL: false,
maxBodyBytes: 104857600,
maxRedirects: 0,
method: "GET",
url: "http://no.records",
},
intervalMs: 30000,
name: "no-records",
timeoutMs: 10000,
type: "http",
},
]);
const map = freshStore.getLatestChecksMap();
expect(map.size).toBe(0);
freshStore.close();
});
test("getAllTargetStats 返回所有 target 的聚合统计", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const t2Id = targets[1]!.id;
const stats = store.getAllTargetStats();
expect(stats).toBeInstanceOf(Map);
const stats1 = stats.get(t1Id);
expect(stats1).toBeDefined();
expect(stats1!.totalChecks).toBeGreaterThan(0);
expect(stats1!.availability).toBeGreaterThanOrEqual(0);
const stats2 = stats.get(t2Id);
if (stats2) {
expect(stats2.totalChecks).toBe(0);
expect(stats2.availability).toBe(0);
}
});
test("getAllTargetStats 对无记录的 target 不包含 key", () => {
const freshStore = new ProbeStore(join(tempDir, "fresh-stats.db"));
freshStore.syncTargets([
{
group: "default",
http: {
headers: {},
ignoreSSL: false,
maxBodyBytes: 104857600,
maxRedirects: 0,
method: "GET",
url: "http://no.stats",
},
intervalMs: 30000,
name: "no-stats",
timeoutMs: 10000,
type: "http",
},
]);
const stats = freshStore.getAllTargetStats();
expect(stats.size).toBe(0);
freshStore.close();
});
test("prune 删除过期数据", () => {
const pruneStore = new ProbeStore(join(tempDir, "prune.db"));
pruneStore.syncTargets([httpTarget]);
const t = pruneStore.getTargets()[0]!;
pruneStore.insertCheckResult({
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t.id,
timestamp: "2020-01-01T00:00:00.000Z",
});
pruneStore.insertCheckResult({
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t.id,
timestamp: new Date().toISOString(),
});
const deleted = pruneStore.prune(86400000);
expect(deleted).toBe(1);
const history = pruneStore.getHistory(t.id, "2000-01-01T00:00:00.000Z", "2099-12-31T23:59:59.999Z");
expect(history.total).toBe(1);
pruneStore.close();
});
test("prune 无过期数据返回 0", () => {
const pruneStore = new ProbeStore(join(tempDir, "prune-none.db"));
pruneStore.syncTargets([httpTarget]);
const t = pruneStore.getTargets()[0]!;
pruneStore.insertCheckResult({
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t.id,
timestamp: new Date().toISOString(),
});
const deleted = pruneStore.prune(86400000);
expect(deleted).toBe(0);
pruneStore.close();
});
test("prune 不影响保留期内数据", () => {
const pruneStore = new ProbeStore(join(tempDir, "prune-keep.db"));
pruneStore.syncTargets([httpTarget]);
const t = pruneStore.getTargets()[0]!;
const now = Date.now();
pruneStore.insertCheckResult({
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t.id,
timestamp: new Date(now - 3600000).toISOString(),
});
pruneStore.insertCheckResult({
durationMs: 200,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: t.id,
timestamp: new Date(now).toISOString(),
});
const deleted = pruneStore.prune(7200000);
expect(deleted).toBe(0);
const history = pruneStore.getHistory(t.id, "2000-01-01T00:00:00.000Z", "2099-12-31T23:59:59.999Z");
expect(history.total).toBe(2);
pruneStore.close();
});
});