- 新增顶层 variables 段支持 string/number/boolean 字面量
- target 字符串字段支持 、、{...} 转义语法
- 变量解析优先级: variables -> process.env -> 默认值 -> 报错
- 完整引用保留原始类型,部分引用拼接为字符串
- 变量替换在 YAML 解析后、AJV 校验前执行
- 替换仅作用于 targets,跳过 id/type 字段
- target 新增必填 id 字段作为唯一标识,name 改为可选展示名称
- 数据库存储/API/前端全面迁移到 id 标识
- 统一 checker 运行时类型检查为 es-toolkit predicates
- 同步 delta specs 到主 specs,归档 config-variables 变更
666 lines
21 KiB
TypeScript
666 lines
21 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/cmd/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/cmd/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();
|
|
});
|
|
|
|
function targetId(store: ProbeStore, name: string): string {
|
|
return store.getTargets().find((target) => target.name === name)!.id;
|
|
}
|
|
|
|
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",
|
|
},
|
|
id: "test-http",
|
|
intervalMs: 30000,
|
|
name: "test-http",
|
|
timeoutMs: 10000,
|
|
type: "http",
|
|
};
|
|
|
|
const commandTarget: ResolvedCommandTarget = {
|
|
cmd: {
|
|
args: ["-c", "1", "localhost"],
|
|
cwd: "/tmp",
|
|
env: {},
|
|
exec: "ping",
|
|
maxOutputBytes: 104857600,
|
|
},
|
|
group: "default",
|
|
id: "test-cmd",
|
|
intervalMs: 60000,
|
|
name: "test-cmd",
|
|
timeoutMs: 5000,
|
|
type: "cmd",
|
|
};
|
|
|
|
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 和 cmd targets", () => {
|
|
store.syncTargets([httpTarget, commandTarget]);
|
|
const targets = store.getTargets();
|
|
expect(targets).toHaveLength(2);
|
|
expect(targets.map((target) => target.name).sort()).toEqual(["test-cmd", "test-http"]);
|
|
});
|
|
|
|
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("cmd target 字段正确", () => {
|
|
const t = store.getTargets().find((t) => t.name === "test-cmd")!;
|
|
expect(t.type).toBe("cmd");
|
|
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 found = store.getTargetById(targetId(store, "test-http"));
|
|
expect(found).toBeDefined();
|
|
expect(found!.name).toBe("test-http");
|
|
});
|
|
|
|
test("getTargetById 不存在", () => {
|
|
expect(store.getTargetById("missing-target")).toBeNull();
|
|
});
|
|
|
|
test("写入 check result 并查询", () => {
|
|
store.syncTargets([httpTarget, commandTarget]);
|
|
const t1Id = targetId(store, "test-http");
|
|
|
|
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 t1Id = targetId(store, "test-http");
|
|
|
|
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("getTargetWindowStats 按时间窗口计算基础计数", () => {
|
|
const t1Id = targetId(store, "test-http");
|
|
|
|
const stats = store.getTargetWindowStats(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
|
expect(stats.totalChecks).toBeGreaterThan(0);
|
|
expect(stats.upChecks + stats.downChecks).toBe(stats.totalChecks);
|
|
expect(stats.availability).toBeGreaterThanOrEqual(0);
|
|
expect(stats.availability).toBeLessThanOrEqual(100);
|
|
});
|
|
|
|
test("无记录目标的窗口 stats", () => {
|
|
const t2Id = targetId(store, "test-cmd");
|
|
|
|
const stats = store.getTargetWindowStats(t2Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
|
expect(stats.totalChecks).toBe(0);
|
|
expect(stats.upChecks).toBe(0);
|
|
expect(stats.downChecks).toBe(0);
|
|
expect(stats.availability).toBe(0);
|
|
});
|
|
|
|
test("getLatestChecksMap 支持 Dashboard 组装当前状态", () => {
|
|
const latestChecksMap = store.getLatestChecksMap();
|
|
const latest = latestChecksMap.get(targetId(store, "test-http"));
|
|
|
|
expect(latest).toBeDefined();
|
|
expect(latest!.timestamp).toBe("2025-01-01T01:24:00.000Z");
|
|
});
|
|
|
|
test("getTargetCheckpoints 返回窗口内升序检查点", () => {
|
|
const t1Id = targetId(store, "test-http");
|
|
|
|
const checkpoints = store.getTargetCheckpoints(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
|
expect(checkpoints).toEqual([
|
|
{ duration_ms: 150.5, matched: 1, timestamp: "2025-01-01T00:00:00.000Z" },
|
|
{ duration_ms: 300, matched: 1, timestamp: "2025-01-01T00:00:30.000Z" },
|
|
{ duration_ms: null, matched: 0, timestamp: "2025-01-01T00:01:00.000Z" },
|
|
]);
|
|
});
|
|
|
|
test("getTargetDurations 返回成功检查耗时升序数组", () => {
|
|
const t1Id = targetId(store, "test-http");
|
|
|
|
const durations = store.getTargetDurations(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
|
expect(durations).toEqual([150.5, 300]);
|
|
});
|
|
|
|
test("getRecentSamples 返回最近采样数据", () => {
|
|
const t1Id = targetId(store, "test-http");
|
|
|
|
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("getAllRecentSamples 返回每个 target 的最近采样数据", () => {
|
|
const sampleStore = new ProbeStore(join(tempDir, "all-samples.db"));
|
|
const httpA: ResolvedHttpTarget = { ...httpTarget, id: "sample-http-a", name: "sample-http-a" };
|
|
const httpB: ResolvedHttpTarget = {
|
|
...httpTarget,
|
|
http: { ...httpTarget.http, url: "https://example.com/other" },
|
|
id: "sample-http-b",
|
|
name: "sample-http-b",
|
|
};
|
|
const httpEmpty: ResolvedHttpTarget = {
|
|
...httpTarget,
|
|
http: { ...httpTarget.http, url: "https://example.com/empty" },
|
|
id: "sample-http-empty",
|
|
name: "sample-http-empty",
|
|
};
|
|
sampleStore.syncTargets([httpA, httpB, httpEmpty]);
|
|
const targets = sampleStore.getTargets();
|
|
const targetAId = targets.find((t) => t.name === "sample-http-a")!.id;
|
|
const targetBId = targets.find((t) => t.name === "sample-http-b")!.id;
|
|
const emptyTargetId = targets.find((t) => t.name === "sample-http-empty")!.id;
|
|
|
|
for (const [index, timestamp] of [
|
|
"2025-01-01T00:00:00.000Z",
|
|
"2025-01-01T00:01:00.000Z",
|
|
"2025-01-01T00:02:00.000Z",
|
|
].entries()) {
|
|
sampleStore.insertCheckResult({
|
|
durationMs: 100 + index,
|
|
failure: null,
|
|
matched: index !== 1,
|
|
statusDetail: "200 OK",
|
|
targetId: targetAId,
|
|
timestamp,
|
|
});
|
|
}
|
|
sampleStore.insertCheckResult({
|
|
durationMs: 200,
|
|
failure: null,
|
|
matched: true,
|
|
statusDetail: "200 OK",
|
|
targetId: targetBId,
|
|
timestamp: "2025-01-01T00:03:00.000Z",
|
|
});
|
|
sampleStore.insertCheckResult({
|
|
durationMs: null,
|
|
failure: { kind: "error", message: "fail", path: "request", phase: "request" },
|
|
matched: false,
|
|
statusDetail: null,
|
|
targetId: targetBId,
|
|
timestamp: "2025-01-01T00:04:00.000Z",
|
|
});
|
|
|
|
const samples = sampleStore.getAllRecentSamples(2);
|
|
|
|
expect(samples.get(targetAId)).toEqual([
|
|
{ duration_ms: 102, matched: 1, timestamp: "2025-01-01T00:02:00.000Z" },
|
|
{ duration_ms: 101, matched: 0, timestamp: "2025-01-01T00:01:00.000Z" },
|
|
]);
|
|
expect(samples.get(targetBId)).toEqual([
|
|
{ duration_ms: null, matched: 0, timestamp: "2025-01-01T00:04:00.000Z" },
|
|
{ duration_ms: 200, matched: 1, timestamp: "2025-01-01T00:03:00.000Z" },
|
|
]);
|
|
expect(samples.has(emptyTargetId)).toBe(false);
|
|
|
|
sampleStore.close();
|
|
});
|
|
|
|
test("关闭后操作不报错", () => {
|
|
const closedStore = new ProbeStore(join(tempDir, "closed.db"));
|
|
closedStore.close();
|
|
expect(closedStore.getTargets()).toHaveLength(0);
|
|
expect(closedStore.getTargetById("closed-target")).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",
|
|
},
|
|
id: "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",
|
|
},
|
|
id: "no-records",
|
|
intervalMs: 30000,
|
|
name: "no-records",
|
|
timeoutMs: 10000,
|
|
type: "http",
|
|
},
|
|
]);
|
|
|
|
const map = freshStore.getLatestChecksMap();
|
|
expect(map.size).toBe(0);
|
|
|
|
freshStore.close();
|
|
});
|
|
|
|
test("getAllTargetWindowStats 返回所有 target 的窗口聚合统计", () => {
|
|
const t1Id = targetId(store, "test-http");
|
|
const t2Id = targetId(store, "test-cmd");
|
|
|
|
const stats = store.getAllTargetWindowStats("2024-01-01T00:00:00.000Z", "2026-12-31T23:59:59.999Z");
|
|
expect(stats).toBeInstanceOf(Map);
|
|
|
|
const stats1 = stats.get(t1Id);
|
|
expect(stats1).toBeDefined();
|
|
expect(stats1!.totalChecks).toBeGreaterThan(0);
|
|
expect(stats1!.upChecks + stats1!.downChecks).toBe(stats1!.totalChecks);
|
|
expect(stats1!.availability).toBeGreaterThanOrEqual(0);
|
|
|
|
const stats2 = stats.get(t2Id);
|
|
if (stats2) {
|
|
expect(stats2.totalChecks).toBe(0);
|
|
expect(stats2.availability).toBe(0);
|
|
}
|
|
});
|
|
|
|
test("getAllTargetWindowStats 对无记录的 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",
|
|
},
|
|
id: "no-stats",
|
|
intervalMs: 30000,
|
|
name: "no-stats",
|
|
timeoutMs: 10000,
|
|
type: "http",
|
|
},
|
|
]);
|
|
|
|
const stats = freshStore.getAllTargetWindowStats("2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z");
|
|
expect(stats.size).toBe(0);
|
|
|
|
freshStore.close();
|
|
});
|
|
|
|
test("getAllTargetWindowStats 与 getTargetWindowStats 的 availability 精度一致", () => {
|
|
const statsStore = new ProbeStore(join(tempDir, "stats-precision.db"));
|
|
const target: ResolvedHttpTarget = { ...httpTarget, id: "stats-precision", name: "stats-precision" };
|
|
statsStore.syncTargets([target]);
|
|
const targetId = statsStore.getTargets()[0]!.id;
|
|
|
|
for (const [index, matched] of [true, true, false].entries()) {
|
|
statsStore.insertCheckResult({
|
|
durationMs: 100,
|
|
failure: null,
|
|
matched,
|
|
statusDetail: matched ? "200 OK" : "500 ERROR",
|
|
targetId,
|
|
timestamp: `2025-01-01T00:0${index}:00.000Z`,
|
|
});
|
|
}
|
|
|
|
const targetStats = statsStore.getTargetWindowStats(
|
|
targetId,
|
|
"2025-01-01T00:00:00.000Z",
|
|
"2025-01-01T00:02:00.000Z",
|
|
);
|
|
const allStats = statsStore
|
|
.getAllTargetWindowStats("2025-01-01T00:00:00.000Z", "2025-01-01T00:02:00.000Z")
|
|
.get(targetId)!;
|
|
|
|
expect(targetStats.availability).toBe(66.67);
|
|
expect(targetStats.upChecks).toBe(2);
|
|
expect(targetStats.downChecks).toBe(1);
|
|
expect(allStats.availability).toBe(66.67);
|
|
expect(allStats.availability).toBe(targetStats.availability);
|
|
|
|
statsStore.close();
|
|
});
|
|
|
|
test("getDashboardIncidentStates 返回按 target 和 timestamp 升序排列的状态序列", () => {
|
|
const incidentStore = new ProbeStore(join(tempDir, "dashboard-incidents.db"));
|
|
const httpA: ResolvedHttpTarget = { ...httpTarget, id: "incident-http-a", name: "incident-http-a" };
|
|
const httpB: ResolvedHttpTarget = {
|
|
...httpTarget,
|
|
http: { ...httpTarget.http, url: "https://example.com/incident-b" },
|
|
id: "incident-http-b",
|
|
name: "incident-http-b",
|
|
};
|
|
incidentStore.syncTargets([httpA, httpB]);
|
|
const targets = incidentStore.getTargets();
|
|
const targetAId = targets.find((target) => target.name === "incident-http-a")!.id;
|
|
const targetBId = targets.find((target) => target.name === "incident-http-b")!.id;
|
|
|
|
incidentStore.insertCheckResult({
|
|
durationMs: 100,
|
|
failure: null,
|
|
matched: false,
|
|
statusDetail: null,
|
|
targetId: targetBId,
|
|
timestamp: "2025-01-01T00:03:00.000Z",
|
|
});
|
|
incidentStore.insertCheckResult({
|
|
durationMs: 100,
|
|
failure: null,
|
|
matched: true,
|
|
statusDetail: "200 OK",
|
|
targetId: targetAId,
|
|
timestamp: "2025-01-01T00:02:00.000Z",
|
|
});
|
|
incidentStore.insertCheckResult({
|
|
durationMs: 100,
|
|
failure: null,
|
|
matched: false,
|
|
statusDetail: null,
|
|
targetId: targetAId,
|
|
timestamp: "2025-01-01T00:01:00.000Z",
|
|
});
|
|
|
|
expect(incidentStore.getDashboardIncidentStates("2025-01-01T00:00:00.000Z", "2025-01-01T00:03:00.000Z")).toEqual([
|
|
{ matched: 0, target_id: targetAId, timestamp: "2025-01-01T00:01:00.000Z" },
|
|
{ matched: 1, target_id: targetAId, timestamp: "2025-01-01T00:02:00.000Z" },
|
|
{ matched: 0, target_id: targetBId, timestamp: "2025-01-01T00:03:00.000Z" },
|
|
]);
|
|
|
|
incidentStore.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();
|
|
});
|
|
});
|