/* eslint-disable @typescript-eslint/require-await */ import { afterEach, describe, expect, test } from "bun:test"; import { mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { ResolvedConfig } from "../../src/server/config/types"; import type { Logger } from "../../src/server/logger"; import type { StartServerOptions } from "../../src/server/server"; import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstrap"; import { createMemoryLogger } from "../../src/server/logger"; async function captureConsoleError(callback: () => Promise): Promise { const originalError = console.error; const errors: string[] = []; console.error = (...args: unknown[]) => { errors.push(args.map(String).join(" ")); }; try { await callback(); } finally { console.error = originalError; } return errors; } function makeTempConfig(overrides: Partial = {}): ResolvedConfig { const base = join(tmpdir(), `bootstrap-test-${Date.now()}`); mkdirSync(base, { recursive: true }); return { configDir: base, dataDir: join(base, "data"), host: "127.0.0.1", logging: { consoleLevel: "info", fileLevel: "info", filePath: join(base, "data", "logs", "test.log"), rotationFrequency: "daily", rotationMaxFiles: 14, rotationSizeBytes: 52428800, rotationSizeRaw: "50MB", }, port: 0, ...overrides, }; } describe("bootstrap", () => { const shutdownHandlers: Array<() => void> = []; afterEach(() => { for (const fn of shutdownHandlers) { try { fn(); } catch { // exit mock throws, that's expected } } shutdownHandlers.length = 0; }); test("使用默认依赖启动", async () => { let started = false; let signalRegistered = false; let loggerPassedToServer: Logger | undefined; const cfg = makeTempConfig(); const mockLoadConfig = (async () => cfg) as unknown as BootstrapDependencies["loadConfig"]; const mockOnSignal = (_signal: string, handler: () => void) => { shutdownHandlers.push(handler); signalRegistered = true; }; const mockStartServer = (options: StartServerOptions) => { loggerPassedToServer = options.logger; started = true; return {}; }; const deps: BootstrapDependencies = { createLogger: async () => createMemoryLogger(), exit: (code: number) => { throw new Error(`exit(${code})`); }, loadConfig: mockLoadConfig, onSignal: mockOnSignal, startServer: mockStartServer, }; await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps); expect(started).toBe(true); expect(signalRegistered).toBe(true); expect(loggerPassedToServer).toBeDefined(); }); test("传递 version 给 startServer", async () => { let receivedVersion: string | undefined; let loggerCreated = false; const cfg = makeTempConfig(); const deps: BootstrapDependencies = { createLogger: async (_logConfig, _mode, version) => { loggerCreated = true; expect(version).toBe("1.2.3"); return createMemoryLogger(); }, exit: (code: number) => { throw new Error(`exit(${code})`); }, loadConfig: async () => cfg, onSignal: (_signal, handler) => { shutdownHandlers.push(handler); }, startServer: (options: StartServerOptions) => { receivedVersion = options.version; return {}; }, }; await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production", version: "1.2.3" }, deps); expect(receivedVersion).toBe("1.2.3"); expect(loggerCreated).toBe(true); }); test("logger 初始化失败时使用 fallback 并退出", async () => { let exitCode: number | undefined; const cfg = makeTempConfig(); const deps: BootstrapDependencies = { createLogger: async () => { throw new Error("pino import failed"); }, exit: (code: number) => { exitCode = code; throw new Error("exit called"); }, loadConfig: async () => cfg, startServer: () => { throw new Error("should not reach"); }, }; const errors = await captureConsoleError(async () => { try { await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps); } catch { // expected - exit threw } }); expect(exitCode).toBe(1); expect(errors).toContain("日志初始化失败: pino import failed"); expect(errors).toContain("启动失败: exit called"); }); test("启动失败时调用 logger.fatal 并 flush", async () => { let fatalCalled = false; let flushCalled = false; let exitCode: number | undefined; const mockLogger = createMemoryLogger(); const origFatal = mockLogger.fatal.bind(mockLogger); const origFlush = mockLogger.flush.bind(mockLogger); mockLogger.fatal = (objOrMsg, msg?) => { fatalCalled = true; origFatal(objOrMsg, msg); }; mockLogger.flush = () => { flushCalled = true; origFlush(); }; const cfg = makeTempConfig(); const deps: BootstrapDependencies = { createLogger: async () => mockLogger, exit: (code: number) => { exitCode = code; throw new Error("exit called"); }, loadConfig: async () => cfg, startServer: () => { throw new Error("server start failed"); }, }; try { await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps); } catch { // expected } expect(fatalCalled).toBe(true); expect(flushCalled).toBe(true); expect(exitCode).toBe(1); }); test("数据目录创建后记录日志", async () => { const cfg = makeTempConfig(); let infoDataDir: string | undefined; const mockLogger = createMemoryLogger(); const origInfo = mockLogger.info.bind(mockLogger); mockLogger.info = (objOrMsg, msg?) => { if (typeof objOrMsg === "object" && "dataDir" in objOrMsg) { infoDataDir = objOrMsg["dataDir"] as string; } origInfo(objOrMsg, msg); }; const deps: BootstrapDependencies = { createLogger: async () => mockLogger, exit: (code: number) => { throw new Error(`exit(${code})`); }, loadConfig: async () => cfg, onSignal: (_signal, handler) => { shutdownHandlers.push(handler); }, startServer: () => ({}), }; await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "development" }, deps); expect(infoDataDir).toBe(cfg.dataDir); }); test("shutdown 时 flush logger", async () => { let flushed = false; let exitCode: number | undefined; const mockLogger = createMemoryLogger(); mockLogger.flush = () => { flushed = true; }; const cfg = makeTempConfig(); const deps: BootstrapDependencies = { createLogger: async () => mockLogger, exit: (code: number) => { exitCode = code; throw new Error("exit called"); }, loadConfig: async () => cfg, onSignal: (_signal, handler) => { shutdownHandlers.push(handler); }, startServer: () => ({}), }; await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps); const handler = shutdownHandlers.pop(); expect(handler).toBeDefined(); try { handler!(); } catch { // expected - exit threw } expect(flushed).toBe(true); expect(exitCode).toBe(0); }); });