import { describe, expect, test } from "bun:test"; import { join } from "node:path"; import type { ResolvedConfig } from "../../src/server/checker/config-loader"; import type { ProbeEngine } from "../../src/server/checker/engine"; import type { ProbeStore } from "../../src/server/checker/store"; import type { ResolvedTargetBase } from "../../src/server/checker/types"; import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstrap"; type ShutdownSignal = "SIGINT" | "SIGTERM"; const target: ResolvedTargetBase = { group: "default", intervalMs: 30000, name: "test", timeoutMs: 5000, type: "command", }; function createHarness(overrides: BootstrapDependencies = {}) { const calls: string[] = []; const shutdownHandlers = new Map void>(); const config: ResolvedConfig = { configDir: "/tmp", dataDir: "/tmp/dial-data", host: "127.0.0.1", maxConcurrentChecks: 3, port: 3000, retentionMs: 1000, targets: [target], }; const store = { close() { calls.push("store.close"); }, syncTargets(targets: ResolvedTargetBase[]) { calls.push(`syncTargets:${targets.length}`); }, } as unknown as ProbeStore; const engine = { start() { calls.push("engine.start"); }, stop() { calls.push("engine.stop"); }, } as unknown as ProbeEngine; const dependencies: BootstrapDependencies = { createEngine(actualStore, targets, maxConcurrentChecks, retentionMs) { expect(actualStore).toBe(store); calls.push(`createEngine:${targets.length}:${maxConcurrentChecks}:${retentionMs}`); return engine; }, createStore(dbPath) { calls.push(`createStore:${dbPath}`); return store; }, exit(code) { calls.push(`exit:${code}`); throw new Error(`exit:${code}`); }, loadConfig(configPath) { calls.push(`loadConfig:${configPath}`); return Promise.resolve(config); }, logError(...data) { calls.push(`logError:${String(data[1])}`); }, onSignal(signal, handler) { calls.push(`onSignal:${signal}`); shutdownHandlers.set(signal, handler); }, startServer(options) { expect(options.config).toEqual({ host: config.host, port: config.port }); expect(options.store).toBe(store); calls.push(`startServer:${options.mode}`); }, ...overrides, }; return { calls, dependencies, shutdownHandlers }; } describe("bootstrap", () => { test("开发模式执行完整启动序列", async () => { const { calls, dependencies } = createHarness(); await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies); expect(calls).toEqual([ "loadConfig:/tmp/probes.yaml", `createStore:${join("/tmp/dial-data", "probe.db")}`, "syncTargets:1", "createEngine:1:3:1000", "engine.start", "onSignal:SIGINT", "onSignal:SIGTERM", "startServer:development", ]); }); test("收到退出信号时停止 engine 并关闭 store", async () => { const { calls, dependencies, shutdownHandlers } = createHarness(); await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies); expect(() => shutdownHandlers.get("SIGINT")!()).toThrow("exit:0"); expect(calls.slice(-3)).toEqual(["engine.stop", "store.close", "exit:0"]); }); test("启动失败时输出错误并以非零退出", async () => { const { calls, dependencies } = createHarness({ loadConfig() { return Promise.reject(new Error("bad config")); }, }); let error: unknown; try { await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies); } catch (caught) { error = caught; } expect(error).toBeInstanceOf(Error); expect((error as Error).message).toBe("exit:1"); expect(calls).toEqual(["logError:bad config", "exit:1"]); }); });