测试基础设施 - 统一 SQLite 测试 DB/临时目录 helper(tests/helpers.ts),支持 Windows EBUSY 重试清理 - 测试库使用 PRAGMA journal_mode=DELETE 避免 WAL 句柄延迟 - 路由 handler 测试改用 createMigratedMemoryTestDatabase 避免 File DB 锁 - SQLite 聚焦 --rerun-each=20 全部通过(720 pass) 后端测试补强 - 新增 tests/server/app.test.ts 真实 startServer 集成测试 - 覆盖 /api/meta、项目 CRUD、错误路径、静态 fallback、安全 header - bootstrap/logger 测试捕获预期输出,消除测试噪音 前端测试补强 - 移除 .ant-* 内部类名依赖,改为角色/文本/导航/请求契约断言 - 项目页补充搜索、Tab 切换、表单、表格操作、错误反馈行为测试 - 新增 hooks(use-theme-preference、use-sidebar-collapsed、use-projects)纯逻辑测试 - 新增 ErrorBoundary 错误展示和刷新按钮测试 - 新增搜索清空行为测试 - 测试 setup 过滤 antd/rc-trigger NaN height warning 产品修复(测试暴露) - 修复 ProjectToolbar 搜索框无法输入(新增 draftKeyword 状态) - 加固 ProjectFormModal 表单字段同步(useEffect 替代不可靠的 afterOpenChange) - 清理 ProjectFormModal 冗余 afterOpenChange 同步逻辑 重构与合规 - ProjectContext 拆分为三文件满足 React Fast Refresh 规则 - use-projects.ts 导出内部 helper 函数供测试验证 - scripts/build.ts 提取纯生成函数供测试使用,修复构建步骤日志编号 - 修复 build 测试覆盖真实生成逻辑 文档同步 - 更新后端/前端/开发文档测试规范、质量门禁和 helper 使用说明
229 lines
6.7 KiB
TypeScript
229 lines
6.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/require-await */
|
|
import { 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", () => {
|
|
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) => {
|
|
signalRegistered = true;
|
|
};
|
|
const mockStartServer = (options: StartServerOptions) => {
|
|
loggerPassedToServer = options.logger;
|
|
started = true;
|
|
return {};
|
|
};
|
|
|
|
const deps: BootstrapDependencies = {
|
|
createLogger: async () => createMemoryLogger(),
|
|
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();
|
|
},
|
|
loadConfig: async () => cfg,
|
|
onSignal: () => {},
|
|
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,
|
|
loadConfig: async () => cfg,
|
|
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 shutdownHandler: (() => void) | undefined;
|
|
|
|
const mockLogger = createMemoryLogger();
|
|
mockLogger.flush = () => {
|
|
flushed = true;
|
|
};
|
|
|
|
const cfg = makeTempConfig();
|
|
const deps: BootstrapDependencies = {
|
|
createLogger: async () => mockLogger,
|
|
loadConfig: async () => cfg,
|
|
onSignal: (_signal, handler) => {
|
|
shutdownHandler = handler;
|
|
},
|
|
startServer: () => ({}),
|
|
};
|
|
|
|
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
|
|
|
expect(shutdownHandler).toBeDefined();
|
|
shutdownHandler!();
|
|
expect(flushed).toBe(true);
|
|
});
|
|
});
|