import { describe, expect, test } from "bun:test"; import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/command/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/command/execute"; import { HttpChecker } from "../../../src/server/checker/runner/http/execute"; function createMockStore(targetNames: string[]) { let nextId = 1; const targets = targetNames.map((name) => ({ id: nextId++, name })); const results: Array> = []; return { _results: results, getTargets() { return targets.map(({ id, name }) => ({ config: "", expect: null, grp: "default", id, interval_ms: 60000, name, target: "", timeout_ms: 5000, type: "command" 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 { command: { args: ["hello"], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024, }, group: "default", intervalMs: 60000, name, timeoutMs: 5000, type: "command", ...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 执行 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: 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]!["statusDetail"]).toBe("exitCode=0"); }); test("多个目标并发执行", async () => { const targetA = makeCommandTarget("echo-a", { command: { args: ["a"], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 }, }); const targetB = makeCommandTarget("echo-b", { command: { args: ["b"], cwd: "/tmp", env: {}, exec: "echo", 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", { command: { args: [], cwd: "/tmp", env: {}, exec: "false", 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("command"); const originalExecute = checker.execute.bind(checker); checker.execute = async (target, ctx) => { if (target.name === "reject-cmd") { throw new Error("boom"); } return originalExecute(target, ctx); }; try { 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(1); expect(results[0]!["matched"]).toBe(false); expect(results[0]!["durationMs"]).toBeNull(); expect(results[0]!["statusDetail"]).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(2); expect(results[1]!["matched"]).toBe(true); } finally { checker.execute = originalExecute; } }); test("并发限制 maxConcurrentChecks", async () => { const targets = Array.from({ length: 5 }, (_, i) => makeCommandTarget(`cmd-${i}`, { command: { args: [String(i)], cwd: "/tmp", env: {}, exec: "echo", 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: 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("未注册的 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: 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 = { group: "default", http: { headers: {}, ignoreSSL: false, maxBodyBytes: 1024 * 1024, maxRedirects: 0, method: "GET", url: `http://localhost:${httpServer.port}/`, }, 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]!["statusDetail"]).toBe("HTTP 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(); }); });