1
0

feat: 将 demo 项目转化为 HTTP 拨测监控工具

新增 YAML 配置解析(Bun 内置 YAML)、SQLite 数据存储(bun:sqlite)、按 interval 分组并发拨测引擎、REST API(/api/summary、/api/targets、/api/targets/:id/history、/api/targets/:id/trend)、React 前端 Dashboard(统计卡片、目标表格、可展开详情面板、recharts 趋势图)。CLI 简化为仅接受配置文件路径。移除 /api/demo 路由和相关 demo 代码。保留 /health、静态资源服务和 SPA fallback。
This commit is contained in:
2026-05-09 17:04:25 +08:00
parent 9267f6585c
commit 57d3a5cfb4
43 changed files with 2910 additions and 525 deletions

View File

@@ -0,0 +1,92 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { ProbeStore } from "../../../src/server/checker/store";
import { ProbeEngine } from "../../../src/server/checker/engine";
import type { ResolvedTarget } from "../../../src/server/checker/types";
import { mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
describe("ProbeEngine", () => {
let tempDir: string;
let store: ProbeStore;
const target: ResolvedTarget = {
name: "httpbin",
url: "https://httpbin.org/get",
method: "GET",
headers: {},
intervalMs: 60000,
timeoutMs: 10000,
};
beforeAll(async () => {
tempDir = join(tmpdir(), `gc-engine-test-${Date.now()}`);
await mkdir(tempDir, { recursive: true });
store = new ProbeStore(join(tempDir, "test.db"));
store.syncTargets([target]);
});
afterAll(async () => {
store.close();
await rm(tempDir, { recursive: true, force: true });
});
test("groupByInterval 分组逻辑", () => {
const targets: ResolvedTarget[] = [
{ name: "a", url: "http://a.com", method: "GET", headers: {}, intervalMs: 30000, timeoutMs: 10000 },
{ name: "b", url: "http://b.com", method: "GET", headers: {}, intervalMs: 30000, timeoutMs: 10000 },
{ name: "c", url: "http://c.com", method: "GET", headers: {}, intervalMs: 60000, timeoutMs: 10000 },
];
const engine = new ProbeEngine(store, targets);
engine.start();
engine.stop();
// 只要能启动和停止不出错就行
expect(true).toBe(true);
});
test("engine start/stop 不抛错", () => {
const engine = new ProbeEngine(store, [target]);
engine.start();
engine.stop();
expect(true).toBe(true);
});
test("单次拨测写入数据库", async () => {
const engine = new ProbeEngine(store, [target]);
// 手动调用 probeGroup 不启动 timer
const probeGroup = (engine as unknown as { probeGroup: (t: ResolvedTarget[]) => Promise<void> }).probeGroup.bind(engine);
await probeGroup([target]);
const dbTargets = store.getTargets();
const latest = store.getLatestCheck(dbTargets[0]!.id);
expect(latest).not.toBeNull();
expect(latest!.success === 1 || latest!.success === 0).toBe(true);
});
test("单目标失败隔离", async () => {
const badTarget: ResolvedTarget = {
name: "bad-target",
url: "http://127.0.0.1:1/impossible",
method: "GET",
headers: {},
intervalMs: 60000,
timeoutMs: 2000,
};
store.syncTargets([target, badTarget]);
const engine = new ProbeEngine(store, [target, badTarget]);
const probeGroup = (engine as unknown as { probeGroup: (t: ResolvedTarget[]) => Promise<void> }).probeGroup.bind(engine);
await probeGroup([target, badTarget]);
const dbTargets = store.getTargets();
const goodResult = store.getLatestCheck(dbTargets.find((t) => t.name === "httpbin")!.id);
const badResult = store.getLatestCheck(dbTargets.find((t) => t.name === "bad-target")!.id);
expect(goodResult).not.toBeNull();
expect(badResult).not.toBeNull();
expect(badResult!.success).toBe(0);
});
});