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> = []; 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) { 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 { 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 } ).probeGroup.bind(engine); await probeGroup([target]); const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(1); expect(results[0]!["matched"]).toBe(true); expect((results[0]!["observation"] as Record)["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 } ).probeGroup.bind(engine); await probeGroup([targetA, targetB]); const results = (mockStore as unknown as { _results: Array> })._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 } ).probeGroup.bind(engine); await probeGroup([badTarget, goodTarget]); const results = (mockStore as unknown as { _results: Array> })._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 } ).probeGroup.bind(engine); await probeGroup([rejectTarget, goodTarget]); const results = (mockStore as unknown as { _results: Array> })._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 } ).probeGroup.bind(engine); await probeGroup(targets); const results = (mockStore as unknown as { _results: Array> })._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 } ).probeGroup.bind(engine); await probeGroup([target]); const results = (mockStore as unknown as { _results: Array> })._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 } ).probeGroup.bind(engine); await probeGroup([httpTarget]); const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(1); expect(results[0]!["matched"]).toBe(true); expect((results[0]!["observation"] as Record)["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 } ).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 } ).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 } ).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 } ).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 } ).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 } ).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 } ).probeGroup.bind(engine); await probeGroup([makeCommandTarget("no-log")]); }); }); });