| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- /**
- * Vitest 测试前置脚本
- *
- * 在所有测试运行前执行的全局配置
- */
- import { config } from "dotenv";
- import { afterAll, beforeAll } from "vitest";
- // ==================== 加载环境变量 ====================
- // 优先加载 .env.test(如果存在)
- config({ path: ".env.test", quiet: true });
- // 降级加载 .env
- config({ path: ".env", quiet: true });
- // ==================== 全局前置钩子 ====================
- beforeAll(async () => {
- console.log("\nVitest 测试环境初始化...\n");
- // 安全检查:确保使用测试数据库
- const dsn = process.env.DSN || "";
- const dbName = dsn.split("/").pop() || "";
- if (process.env.NODE_ENV === "production") {
- throw new Error("禁止在生产环境运行测试");
- }
- // 强制要求:测试必须使用包含 'test' 的数据库(CI 和本地都检查)
- if (dbName && !dbName.includes("test")) {
- // 允许通过环境变量显式跳过检查(仅用于特殊情况)
- if (process.env.ALLOW_NON_TEST_DB !== "true") {
- throw new Error(
- `安全检查失败: 数据库名称必须包含 'test' 字样\n` +
- ` 当前数据库: ${dbName}\n` +
- ` 建议使用测试专用数据库(如 claude_code_hub_test)\n` +
- ` 如需跳过检查,请设置环境变量: ALLOW_NON_TEST_DB=true`
- );
- }
- // 即使跳过检查也要发出警告
- console.warn("警告: 当前数据库不包含 'test' 字样");
- console.warn(` 数据库: ${dbName}`);
- console.warn(" 建议使用独立的测试数据库避免数据污染\n");
- }
- // 显示测试配置
- console.log("测试配置:");
- console.log(` - 数据库: ${dbName || "未配置"}`);
- console.log(` - Redis: ${process.env.REDIS_URL?.split("//")[1]?.split("@")[1] || "未配置"}`);
- console.log(` - API Base: ${process.env.API_BASE_URL || "http://localhost:13500"}`);
- console.log("");
- // 初始化默认错误规则(如果数据库可用)
- if (dsn) {
- try {
- const { syncDefaultErrorRules } = await import("@/repository/error-rules");
- await syncDefaultErrorRules();
- console.log("默认错误规则已同步\n");
- } catch (error) {
- console.warn("无法同步默认错误规则:", error);
- }
- }
- // ==================== 并行 Worker 清理协调 ====================
- // setupFiles 会在每个 worker 中执行;如果每个 worker 都在 afterAll 清理数据库,会出现“互相清理”的竞态。
- // 这里用 Redis 计数器实现:只有最后一个结束的 worker 才执行 cleanup。
- try {
- const shouldCleanup = Boolean(dsn) && process.env.AUTO_CLEANUP_TEST_DATA !== "false";
- if (!shouldCleanup) return;
- const dbNameForKey = dbName || "unknown";
- const counterKey = `cch:vitest:cleanup_workers:${dbNameForKey}`;
- const { getRedisClient } = await import("@/lib/redis");
- const redis = getRedisClient();
- if (!redis) return;
- // 等待连接就绪(enableOfflineQueue=false,未 ready 时发命令会直接报错)
- if (redis.status !== "ready") {
- await new Promise<void>((resolve) => {
- const timeout = setTimeout(resolve, 2000);
- redis.once("ready", () => {
- clearTimeout(timeout);
- resolve();
- });
- });
- }
- if (redis.status !== "ready") {
- console.warn("Redis 未就绪,跳过并行清理协调(不影响测试结果)");
- return;
- }
- const current = await redis.incr(counterKey);
- if (current === 1) {
- // 防止异常退出导致计数器常驻
- await redis.expire(counterKey, 60 * 15);
- }
- process.env.__VITEST_CLEANUP_COUNTER_KEY__ = counterKey;
- } catch (error) {
- console.warn("并行清理协调初始化失败(不影响测试结果):", error);
- }
- });
- // ==================== 全局清理钩子 ====================
- afterAll(async () => {
- console.log("\nVitest 测试环境清理...\n");
- // 清理测试期间创建的用户(仅清理最近 10 分钟内的)
- const dsn = process.env.DSN || "";
- if (dsn && process.env.AUTO_CLEANUP_TEST_DATA !== "false") {
- try {
- // 仅最后一个 worker 执行清理,避免并发互相删除
- const counterKey = process.env.__VITEST_CLEANUP_COUNTER_KEY__;
- const { getRedisClient } = await import("@/lib/redis");
- const redis = counterKey ? getRedisClient() : null;
- if (counterKey && redis) {
- if (redis.status !== "ready") {
- await new Promise<void>((resolve) => {
- const timeout = setTimeout(resolve, 2000);
- redis.once("ready", () => {
- clearTimeout(timeout);
- resolve();
- });
- });
- }
- if (redis.status === "ready") {
- const remaining = await redis.decr(counterKey);
- if (remaining <= 0) {
- const { cleanupRecentTestData } = await import("./cleanup-utils");
- const result = await cleanupRecentTestData();
- if (result.deletedUsers > 0) {
- console.log(`自动清理:删除 ${result.deletedUsers} 个测试用户\n`);
- }
- await redis.del(counterKey);
- } else {
- // 非最后一个 worker:跳过清理
- }
- } else {
- console.warn("Redis 未就绪,跳过自动清理(不影响测试结果)");
- }
- } else {
- // 无 Redis 协调:为了避免竞态,默认跳过清理
- console.warn("未启用清理协调,跳过自动清理(不影响测试结果)");
- }
- } catch (error) {
- console.warn(
- "自动清理失败(不影响测试结果):",
- error instanceof Error ? error.message : error
- );
- }
- }
- console.log("Vitest 测试环境清理完成\n");
- });
- // ==================== 全局 Mock 配置(可选)====================
- // 如果需要 mock 某些全局对象,可以在这里配置
- // 例如:mock console.error 以避免测试输出过多错误日志
- // 保存原始 console.error
- const originalConsoleError = console.error;
- // 在测试中静默某些预期的错误(可选)
- global.console.error = (...args: unknown[]) => {
- // 过滤掉某些已知的、预期的错误日志
- const message = args[0]?.toString() || "";
- // 跳过这些预期的错误日志
- const ignoredPatterns = [
- // 可以在这里添加需要忽略的错误模式
- // "某个预期的错误消息",
- ];
- const shouldIgnore = ignoredPatterns.some((pattern) => message.includes(pattern));
- if (!shouldIgnore) {
- originalConsoleError(...args);
- }
- };
- // ==================== 环境变量默认值 ====================
- // 设置测试环境默认值(如果未配置)
- process.env.NODE_ENV = process.env.NODE_ENV || "test";
- process.env.API_BASE_URL = process.env.API_BASE_URL || "http://localhost:13500/api/actions";
- // 便于 API 测试复用 ADMIN_TOKEN(validateKey 支持该 token 直通管理员会话)
- process.env.TEST_ADMIN_TOKEN = process.env.TEST_ADMIN_TOKEN || process.env.ADMIN_TOKEN;
- // ==================== React act 环境标记 ====================
- // React 18+ 在测试环境中会检查该标记,避免出现 “not configured to support act(...)” 的噪声警告。
- (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
- // ==================== 全局超时配置 ====================
- // 设置全局默认超时(可以被单个测试覆盖)
- const DEFAULT_TIMEOUT = 10000; // 10 秒
- // 导出配置供测试使用
- export const TEST_CONFIG = {
- timeout: DEFAULT_TIMEOUT,
- apiBaseUrl: process.env.API_BASE_URL,
- skipAuthTests: !process.env.TEST_AUTH_TOKEN,
- };
|