Files
Alfred/tests/server/bootstrap.test.ts
lanyuanxiaoyao eb93de52d8 fix: 修正 markdown-to-jsx 导入方式 + 新增 formatDateLabel 日期工具函数
- TextPart: default import → named import
- MaterialCard: 使用 formatDateLabel 显示今天/昨天/日期
- 清理旧测试文件,新增 ResourceTable 测试
2026-06-03 21:08:00 +08:00

292 lines
8.3 KiB
TypeScript

/* 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<void>): Promise<string[]> {
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> = {}): 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);
});
test("启动时将数据库传递给 startServer", async () => {
let started = false;
let receivedDb: unknown = undefined;
const cfg = makeTempConfig();
const deps: BootstrapDependencies = {
createLogger: async () => createMemoryLogger(),
loadConfig: async () => cfg,
onSignal: (_signal, _handler) => {},
startServer: (options: { db: unknown }) => {
receivedDb = options.db;
started = true;
return {};
},
};
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
expect(started).toBe(true);
expect(receivedDb).not.toBeUndefined();
expect(typeof (receivedDb as { close?: unknown }).close).toBe("function");
});
});