feat: 重构为多类型 checker 通用框架,支持 HTTP 与命令检查
- 引入 typed target 判别联合,支持 http 与 command 两种 checker - expect 重构为有序规则数组,按配置顺序快速失败并生成结构化 failure - 新增 command runner,支持 exec + args 本地命令执行 - 引入全局并发限制 maxConcurrentChecks 和 size 解析 (KB/MB/GB) - HTTP/command 各自独立 expect pipeline,应用领域默认成功语义 - SQLite schema、API、Dashboard 全链路调整为 checker 通用契约 - 补充完整测试覆盖(192 tests),更新 README 与示例配置
This commit is contained in:
@@ -1,96 +1,204 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ProbeStore } from "../../../src/server/checker/store";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
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";
|
||||
import type { ProbeStore } from "../../../src/server/checker/store";
|
||||
import type { ResolvedCommandTarget, ResolvedHttpTarget, ResolvedTarget } from "../../../src/server/checker/types";
|
||||
|
||||
function createMockStore(targetNames: string[]) {
|
||||
let nextId = 1;
|
||||
const targets = targetNames.map((name) => ({ id: nextId++, name }));
|
||||
const results: Array<Record<string, unknown>> = [];
|
||||
|
||||
return {
|
||||
getTargets() {
|
||||
return targets.map(({ id, name }) => ({
|
||||
id,
|
||||
name,
|
||||
type: "command" as const,
|
||||
target: "",
|
||||
config: "",
|
||||
interval_ms: 60000,
|
||||
timeout_ms: 5000,
|
||||
expect: null,
|
||||
}));
|
||||
},
|
||||
insertCheckResult(result: Record<string, unknown>) {
|
||||
results.push(result);
|
||||
},
|
||||
_results: results,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommandTarget(name: string, overrides?: Partial<ResolvedCommandTarget>): ResolvedCommandTarget {
|
||||
return {
|
||||
type: "command",
|
||||
name,
|
||||
command: {
|
||||
exec: "echo",
|
||||
args: ["hello"],
|
||||
cwd: "/tmp",
|
||||
env: {},
|
||||
maxOutputBytes: 1024 * 1024,
|
||||
},
|
||||
intervalMs: 60000,
|
||||
timeoutMs: 5000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
test("start/stop 不抛错", () => {
|
||||
const mockStore = createMockStore(["test"]) as unknown as ProbeStore;
|
||||
const targets: ResolvedTarget[] = [makeCommandTarget("test")];
|
||||
const engine = new ProbeEngine(mockStore, targets);
|
||||
engine.start();
|
||||
engine.stop();
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("单次拨测写入数据库", async () => {
|
||||
const engine = new ProbeEngine(store, [target]);
|
||||
// 手动调用 probeGroup 不启动 timer
|
||||
test("单次 probeGroup 执行 command 检查", async () => {
|
||||
const target = makeCommandTarget("cmd-echo");
|
||||
const mockStore = createMockStore(["cmd-echo"]) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, [target]);
|
||||
|
||||
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);
|
||||
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]!.success).toBe(true);
|
||||
expect(results[0]!.matched).toBe(true);
|
||||
expect(results[0]!.statusDetail).toBe("exitCode=0");
|
||||
});
|
||||
|
||||
test("单目标失败隔离", async () => {
|
||||
const badTarget: ResolvedTarget = {
|
||||
name: "bad-target",
|
||||
url: "http://127.0.0.1:1/impossible",
|
||||
method: "GET",
|
||||
headers: {},
|
||||
intervalMs: 60000,
|
||||
timeoutMs: 2000,
|
||||
};
|
||||
test("多个目标并发执行", async () => {
|
||||
const targetA = makeCommandTarget("echo-a", {
|
||||
command: { exec: "echo", args: ["a"], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 },
|
||||
});
|
||||
const targetB = makeCommandTarget("echo-b", {
|
||||
command: { exec: "echo", args: ["b"], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 },
|
||||
});
|
||||
|
||||
store.syncTargets([target, badTarget]);
|
||||
const mockStore = createMockStore(["echo-a", "echo-b"]) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, [targetA, targetB]);
|
||||
|
||||
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]);
|
||||
await probeGroup([targetA, targetB]);
|
||||
|
||||
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);
|
||||
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
|
||||
expect(results.length).toBe(2);
|
||||
});
|
||||
|
||||
expect(goodResult).not.toBeNull();
|
||||
expect(badResult).not.toBeNull();
|
||||
expect(badResult!.success).toBe(0);
|
||||
test("失败目标不阻塞其他目标", async () => {
|
||||
const badTarget = makeCommandTarget("bad-cmd", {
|
||||
command: { exec: "false", args: [], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 },
|
||||
});
|
||||
const goodTarget = makeCommandTarget("good-cmd");
|
||||
|
||||
const mockStore = createMockStore(["bad-cmd", "good-cmd"]) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, [badTarget, goodTarget]);
|
||||
|
||||
const probeGroup = (engine as unknown as { probeGroup: (t: ResolvedTarget[]) => Promise<void> }).probeGroup.bind(
|
||||
engine,
|
||||
);
|
||||
await probeGroup([badTarget, goodTarget]);
|
||||
|
||||
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
|
||||
expect(results.length).toBe(2);
|
||||
|
||||
const badResult = results.find((r) => r.success === false);
|
||||
const goodResult = results.find((r) => r.success === true);
|
||||
expect(badResult).toBeDefined();
|
||||
expect(goodResult).toBeDefined();
|
||||
});
|
||||
|
||||
test("并发限制 maxConcurrentChecks", async () => {
|
||||
const targets = Array.from({ length: 5 }, (_, i) =>
|
||||
makeCommandTarget(`cmd-${i}`, {
|
||||
command: { exec: "echo", args: [String(i)], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 },
|
||||
}),
|
||||
);
|
||||
|
||||
const mockStore = createMockStore(targets.map((t) => t.name)) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, targets, 2);
|
||||
|
||||
const probeGroup = (engine as unknown as { probeGroup: (t: ResolvedTarget[]) => Promise<void> }).probeGroup.bind(
|
||||
engine,
|
||||
);
|
||||
await probeGroup(targets);
|
||||
|
||||
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
|
||||
expect(results.length).toBe(5);
|
||||
for (const r of results) {
|
||||
expect(r.success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("groupByInterval 按间隔分组", () => {
|
||||
const targetA = makeCommandTarget("a", { intervalMs: 30000 });
|
||||
const targetB = makeCommandTarget("b", { intervalMs: 30000 });
|
||||
const targetC = makeCommandTarget("c", { intervalMs: 60000 });
|
||||
|
||||
const mockStore = createMockStore(["a", "b", "c"]) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, [targetA, targetB, targetC]);
|
||||
engine.start();
|
||||
engine.stop();
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("未注册的 targetName 不写入结果", async () => {
|
||||
const target = makeCommandTarget("unknown-target");
|
||||
const mockStore = createMockStore(["other-name"]) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, [target]);
|
||||
|
||||
const probeGroup = (engine as unknown as { probeGroup: (t: ResolvedTarget[]) => Promise<void> }).probeGroup.bind(
|
||||
engine,
|
||||
);
|
||||
await probeGroup([target]);
|
||||
|
||||
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
|
||||
test("HTTP 目标运行", async () => {
|
||||
const httpServer = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("ok");
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const httpTarget: ResolvedHttpTarget = {
|
||||
type: "http",
|
||||
name: "http-test",
|
||||
http: {
|
||||
url: `http://localhost:${httpServer.port}/`,
|
||||
method: "GET",
|
||||
headers: {},
|
||||
maxBodyBytes: 1024 * 1024,
|
||||
},
|
||||
intervalMs: 60000,
|
||||
timeoutMs: 5000,
|
||||
};
|
||||
|
||||
const mockStore = createMockStore(["http-test"]) as unknown as ProbeStore;
|
||||
const engine = new ProbeEngine(mockStore, [httpTarget]);
|
||||
|
||||
const probeGroup = (engine as unknown as { probeGroup: (t: ResolvedTarget[]) => Promise<void> }).probeGroup.bind(
|
||||
engine,
|
||||
);
|
||||
await probeGroup([httpTarget]);
|
||||
|
||||
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]!.success).toBe(true);
|
||||
expect(results[0]!.statusDetail).toBe("HTTP 200");
|
||||
} finally {
|
||||
httpServer.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user