1
0
Files
DiAL/tests/server/checker/engine.test.ts

538 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types";
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
import type { ProbeStore } from "../../../src/server/checker/store";
import type { ResolvedTargetBase } from "../../../src/server/checker/types";
import { ProbeEngine } from "../../../src/server/checker/engine";
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 { createMemoryLogger } from "../../../src/server/logger";
const processEnv = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
);
function createMockStore(targetNames: string[]) {
const targets = targetNames.map((name) => ({ id: name, name }));
const results: Array<Record<string, unknown>> = [];
return {
_results: results,
getLatestChecksMap() {
return new Map();
},
getTargets() {
return targets.map(({ id, name }) => ({
config: "",
expect: null,
grp: "default",
id,
interval_ms: 60000,
name,
target: "",
timeout_ms: 5000,
type: "cmd" as const,
}));
},
insertCheckResult(result: Record<string, unknown>) {
results.push(result);
},
};
}
function ensureRegistered() {
if (!checkerRegistry.supportedTypes.includes("http")) {
checkerRegistry.register(new HttpChecker());
checkerRegistry.register(new CommandChecker());
}
}
function makeCommandTarget(name: string, overrides?: Partial<ResolvedCommandTarget>): ResolvedCommandTarget {
return {
cmd: {
args: ["-e", "console.log('hello')"],
cwd: process.cwd(),
env: processEnv,
exec: "bun",
maxOutputBytes: 1024 * 1024,
},
description: null,
group: "default",
id: name,
intervalMs: 60000,
name,
timeoutMs: 5000,
type: "cmd",
...overrides,
};
}
describe("ProbeEngine", () => {
test("start/stop 不抛错", () => {
ensureRegistered();
const mockStore = createMockStore(["test"]) as unknown as ProbeStore;
const targets: ResolvedTargetBase[] = [makeCommandTarget("test")];
const engine = new ProbeEngine(mockStore, targets);
engine.start();
engine.stop();
expect(true).toBe(true);
});
test("单次 probeGroup 执行 cmd 检查", 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: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([target]);
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
expect(results.length).toBe(1);
expect(results[0]!["matched"]).toBe(true);
expect((results[0]!["observation"] as Record<string, unknown>)["exitCode"]).toBe(0);
});
test("多个目标并发执行", async () => {
const targetA = makeCommandTarget("echo-a", {
cmd: {
args: ["-e", "console.log('a')"],
cwd: process.cwd(),
env: processEnv,
exec: "bun",
maxOutputBytes: 1024 * 1024,
},
});
const targetB = makeCommandTarget("echo-b", {
cmd: {
args: ["-e", "console.log('b')"],
cwd: process.cwd(),
env: processEnv,
exec: "bun",
maxOutputBytes: 1024 * 1024,
},
});
const mockStore = createMockStore(["echo-a", "echo-b"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [targetA, targetB]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([targetA, targetB]);
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
expect(results.length).toBe(2);
});
test("失败目标不阻塞其他目标", async () => {
const badTarget = makeCommandTarget("bad-cmd", {
cmd: {
args: ["-e", "process.exit(1)"],
cwd: process.cwd(),
env: processEnv,
exec: "bun",
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: ResolvedTargetBase[]) => 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["matched"] === false);
const goodResult = results.find((r) => r["matched"] === true);
expect(badResult).toBeDefined();
expect(goodResult).toBeDefined();
});
test("checker rejected 时写入 internal error 结果", async () => {
ensureRegistered();
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target, ctx) => {
if (target.name === "reject-cmd") {
throw new Error("boom");
}
return originalExecute(target, ctx);
};
const rejectTarget = makeCommandTarget("reject-cmd");
const goodTarget = makeCommandTarget("good-cmd");
const mockStore = createMockStore(["reject-cmd", "good-cmd"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [rejectTarget, goodTarget]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([rejectTarget, goodTarget]);
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
expect(results.length).toBe(2);
expect(results[0]!["targetId"]).toBe("reject-cmd");
expect(results[0]!["matched"]).toBe(false);
expect(results[0]!["durationMs"]).toBeNull();
expect(results[0]!["observation"]).toBeNull();
expect(results[0]!["failure"]).toEqual({
kind: "error",
message: "boom",
path: "engine",
phase: "internal",
});
expect(typeof results[0]!["timestamp"]).toBe("string");
expect(results[1]!["targetId"]).toBe("good-cmd");
expect(results[1]!["matched"]).toBe(true);
checker.execute = originalExecute;
});
test("并发限制 maxConcurrentChecks", async () => {
const targets = Array.from({ length: 5 }, (_, i) =>
makeCommandTarget(`cmd-${i}`, {
cmd: {
args: ["-e", `console.log('${i}')`],
cwd: process.cwd(),
env: processEnv,
exec: "bun",
maxOutputBytes: 1024 * 1024,
},
}),
);
const mockStore = createMockStore(targets.map((t) => t.name ?? t.id)) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, targets, 2);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => 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["matched"]).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("未注册的 target id 不写入结果", 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: ResolvedTargetBase[]) => 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({
fetch() {
return new Response("ok");
},
port: 0,
});
try {
const httpTarget: ResolvedHttpTarget = {
description: null,
group: "default",
http: {
headers: {},
ignoreSSL: false,
maxBodyBytes: 1024 * 1024,
maxRedirects: 0,
method: "GET",
url: `http://localhost:${httpServer.port}/`,
},
id: "http-test",
intervalMs: 60000,
name: "http-test",
timeoutMs: 5000,
type: "http",
};
const mockStore = createMockStore(["http-test"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [httpTarget]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => 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]!["matched"]).toBe(true);
expect((results[0]!["observation"] as Record<string, unknown>)["statusCode"]).toBe(200);
} finally {
void httpServer.stop();
}
});
test("retentionMs > 0 时 start 调用 prune", () => {
let pruneCalled = false;
const mockStore = {
...createMockStore(["test"]),
prune() {
pruneCalled = true;
return 0;
},
} as unknown as ProbeStore;
const targets: ResolvedTargetBase[] = [makeCommandTarget("test")];
const engine = new ProbeEngine(mockStore, targets, 20, 86400000);
engine.start();
expect(pruneCalled).toBe(true);
engine.stop();
});
test("retentionMs = 0 时不调用 prune", () => {
let pruneCalled = false;
const mockStore = {
...createMockStore(["test"]),
prune() {
pruneCalled = true;
return 0;
},
} as unknown as ProbeStore;
const targets: ResolvedTargetBase[] = [makeCommandTarget("test")];
const engine = new ProbeEngine(mockStore, targets, 20, 0);
engine.start();
expect(pruneCalled).toBe(false);
engine.stop();
});
test("retentionMs 未传时不调用 prune", () => {
let pruneCalled = false;
const mockStore = {
...createMockStore(["test"]),
prune() {
pruneCalled = true;
return 0;
},
} as unknown as ProbeStore;
const targets: ResolvedTargetBase[] = [makeCommandTarget("test")];
const engine = new ProbeEngine(mockStore, targets);
engine.start();
expect(pruneCalled).toBe(false);
engine.stop();
});
describe("日志与状态变化", () => {
test("checker rejected 时 logger 记录 error", async () => {
ensureRegistered();
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target, ctx) => {
if (target.id === "fail-target") throw new Error("explode");
return originalExecute(target, ctx);
};
const logger = createMemoryLogger();
const mockStore = createMockStore(["fail-target", "ok-target"]) as unknown as ProbeStore;
const engine = new ProbeEngine(
mockStore,
[makeCommandTarget("fail-target"), makeCommandTarget("ok-target")],
20,
0,
logger,
);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget("fail-target"), makeCommandTarget("ok-target")]);
const errorLogs = logger.entries.filter((e) => e.level === "error");
expect(errorLogs.length).toBeGreaterThanOrEqual(1);
expect(errorLogs[0]!.msg).toContain("探针执行失败");
checker.execute = originalExecute;
});
test("状态变化 UP→DOWN 记录 warn 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "state-test";
const mockStore = {
...createMockStore([targetId]),
getLatestChecksMap() {
return new Map([[targetId, { matched: 1 }]]);
},
} as unknown as ProbeStore;
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target) => {
if (target.id === targetId) {
return {
detail: null,
durationMs: 10,
failure: { kind: "error", message: "fail", path: "exitCode", phase: "body" },
matched: false,
observation: null,
targetId,
timestamp: new Date().toISOString(),
};
}
return originalExecute(target, { signal: new AbortController().signal });
};
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const stateLogs = logger.entries.filter((e) => e.level === "warn" && e.msg.includes("UP → DOWN"));
expect(stateLogs.length).toBe(1);
expect(stateLogs[0]!.msg).toContain(targetId);
checker.execute = originalExecute;
});
test("状态变化 DOWN→UP 记录 info 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "recover-test";
const mockStore = {
...createMockStore([targetId]),
getLatestChecksMap() {
return new Map([[targetId, { matched: 0 }]]);
},
} as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const recoverLogs = logger.entries.filter((e) => e.level === "info" && e.msg.includes("DOWN → UP"));
expect(recoverLogs.length).toBe(1);
expect(recoverLogs[0]!.msg).toContain(targetId);
});
test("稳态 UP→UP 不产生状态变化日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "steady-up";
const mockStore = {
...createMockStore([targetId]),
getLatestChecksMap() {
return new Map([[targetId, { matched: 1 }]]);
},
} as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const stateChangeLogs = logger.entries.filter(
(e) => e.msg.includes("UP → DOWN") || e.msg.includes("DOWN → UP") || e.msg.includes("首次 DOWN"),
);
expect(stateChangeLogs.length).toBe(0);
});
test("首次检查 DOWN 记录 warn 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "first-down";
const mockStore = createMockStore([targetId]) as unknown as ProbeStore;
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target) => {
if (target.id === targetId) {
return {
detail: null,
durationMs: 10,
failure: { kind: "error", message: "fail", path: "exitCode", phase: "body" },
matched: false,
observation: null,
targetId,
timestamp: new Date().toISOString(),
};
}
return originalExecute(target, { signal: new AbortController().signal });
};
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const firstDownLogs = logger.entries.filter((e) => e.level === "warn" && e.msg.includes("首次 DOWN"));
expect(firstDownLogs.length).toBe(1);
checker.execute = originalExecute;
});
test("每次检查产出 debug 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "debug-target";
const mockStore = createMockStore([targetId]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const debugLogs = logger.entries.filter((e) => e.level === "debug");
expect(debugLogs.length).toBeGreaterThanOrEqual(1);
const first = debugLogs[0]!;
expect(first.obj).toBeDefined();
expect(first.obj!["targetId"]).toBe(targetId);
});
test("无 logger 时不抛错noop logger 兜底)", async () => {
ensureRegistered();
const mockStore = createMockStore(["no-log"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget("no-log")]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget("no-log")]);
});
});
});