import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { ProbeStore } from "../../../src/server/checker/store"; import type { CheckFailure, ResolvedTarget } from "../../../src/server/checker/types"; import { mkdir, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { checkerRegistry } from "../../../src/server/checker/runner"; import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; import { CommandChecker } from "../../../src/server/checker/runner/command/runner"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { checkerRegistry.register(new HttpChecker()); checkerRegistry.register(new CommandChecker()); } } beforeAll(() => { ensureRegistered(); }); const httpTarget: ResolvedTarget = { type: "http", name: "test-http", group: "default", http: { url: "https://example.com/health", method: "GET", headers: { Accept: "application/json" }, maxBodyBytes: 104857600, }, intervalMs: 30000, timeoutMs: 10000, expect: { status: [200], maxDurationMs: 3000 }, }; const commandTarget: ResolvedTarget = { type: "command", name: "test-cmd", group: "default", command: { exec: "ping", args: ["-c", "1", "localhost"], cwd: "/tmp", env: {}, maxOutputBytes: 104857600, }, intervalMs: 60000, timeoutMs: 5000, }; 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 rm(tempDir, { recursive: true, force: true }); }); 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); expect(config.url).toBe("https://example.com/health"); expect(config.method).toBe("GET"); expect(config.headers).toEqual({ Accept: "application/json" }); expect(config.maxBodyBytes).toBe(104857600); expect(t.interval_ms).toBe(30000); expect(t.timeout_ms).toBe(10000); expect(JSON.parse(t.expect!)).toEqual({ status: [200], maxDurationMs: 3000 }); }); 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); 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: ResolvedTarget = { ...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({ targetId: t1Id, timestamp: "2025-01-01T00:00:00.000Z", matched: true, durationMs: 150.5, statusDetail: "200 OK", failure: null, }); store.insertCheckResult({ targetId: t1Id, timestamp: "2025-01-01T00:00:30.000Z", matched: true, durationMs: 300, statusDetail: "200 OK", failure: null, }); const failure: CheckFailure = { kind: "error", phase: "duration", path: "$.maxDurationMs", expected: 3000, actual: 5000, message: "请求耗时 5000ms 超过限制 3000ms", }; store.insertCheckResult({ targetId: t1Id, timestamp: "2025-01-01T00:01:00.000Z", matched: false, durationMs: null, statusDetail: null, failure, }); 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({ targetId: t1Id, timestamp: `2025-01-01T01:${String(i).padStart(2, "0")}:00.000Z`, matched: true, durationMs: 100 + i, statusDetail: "200 OK", failure: null, }); } 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: ResolvedTarget = { type: "http", name: "cascade-test", group: "default", http: { url: "http://cascade.test", method: "GET", headers: {}, maxBodyBytes: 104857600 }, intervalMs: 30000, timeoutMs: 10000, }; cascadeStore.syncTargets([cascadeTarget]); const t = cascadeStore.getTargets()[0]!; cascadeStore.insertCheckResult({ targetId: t.id, timestamp: "2025-01-01T00:00:00.000Z", matched: true, durationMs: 100, statusDetail: "200 OK", failure: null, }); cascadeStore.insertCheckResult({ targetId: t.id, timestamp: "2025-01-01T00:01:00.000Z", matched: false, durationMs: null, statusDetail: null, failure: { kind: "error", phase: "status", path: "$", message: "fail" }, }); 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([ { type: "http", name: "no-records", group: "default", http: { url: "http://no.records", method: "GET", headers: {}, maxBodyBytes: 104857600 }, intervalMs: 30000, timeoutMs: 10000, }, ]); 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([ { type: "http", name: "no-stats", group: "default", http: { url: "http://no.stats", method: "GET", headers: {}, maxBodyBytes: 104857600 }, intervalMs: 30000, timeoutMs: 10000, }, ]); const stats = freshStore.getAllTargetStats(); expect(stats.size).toBe(0); freshStore.close(); }); });