2
0

setup.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * Vitest 测试前置脚本
  3. *
  4. * 在所有测试运行前执行的全局配置
  5. */
  6. import { config } from "dotenv";
  7. import { afterAll, beforeAll } from "vitest";
  8. // ==================== 加载环境变量 ====================
  9. // 优先加载 .env.test(如果存在)
  10. config({ path: ".env.test", quiet: true });
  11. // 降级加载 .env
  12. config({ path: ".env", quiet: true });
  13. // ==================== 全局前置钩子 ====================
  14. beforeAll(async () => {
  15. console.log("\nVitest 测试环境初始化...\n");
  16. // 安全检查:确保使用测试数据库
  17. const dsn = process.env.DSN || "";
  18. const dbName = dsn.split("/").pop() || "";
  19. if (process.env.NODE_ENV === "production") {
  20. throw new Error("禁止在生产环境运行测试");
  21. }
  22. // 强制要求:测试必须使用包含 'test' 的数据库(CI 和本地都检查)
  23. if (dbName && !dbName.includes("test")) {
  24. // 允许通过环境变量显式跳过检查(仅用于特殊情况)
  25. if (process.env.ALLOW_NON_TEST_DB !== "true") {
  26. throw new Error(
  27. `安全检查失败: 数据库名称必须包含 'test' 字样\n` +
  28. ` 当前数据库: ${dbName}\n` +
  29. ` 建议使用测试专用数据库(如 claude_code_hub_test)\n` +
  30. ` 如需跳过检查,请设置环境变量: ALLOW_NON_TEST_DB=true`
  31. );
  32. }
  33. // 即使跳过检查也要发出警告
  34. console.warn("警告: 当前数据库不包含 'test' 字样");
  35. console.warn(` 数据库: ${dbName}`);
  36. console.warn(" 建议使用独立的测试数据库避免数据污染\n");
  37. }
  38. // 显示测试配置
  39. console.log("测试配置:");
  40. console.log(` - 数据库: ${dbName || "未配置"}`);
  41. console.log(` - Redis: ${process.env.REDIS_URL?.split("//")[1]?.split("@")[1] || "未配置"}`);
  42. console.log(` - API Base: ${process.env.API_BASE_URL || "http://localhost:13500"}`);
  43. console.log("");
  44. // 初始化默认错误规则(如果数据库可用)
  45. if (dsn) {
  46. try {
  47. const { syncDefaultErrorRules } = await import("@/repository/error-rules");
  48. await syncDefaultErrorRules();
  49. console.log("默认错误规则已同步\n");
  50. } catch (error) {
  51. console.warn("无法同步默认错误规则:", error);
  52. }
  53. }
  54. // ==================== 并行 Worker 清理协调 ====================
  55. // setupFiles 会在每个 worker 中执行;如果每个 worker 都在 afterAll 清理数据库,会出现“互相清理”的竞态。
  56. // 这里用 Redis 计数器实现:只有最后一个结束的 worker 才执行 cleanup。
  57. try {
  58. const shouldCleanup = Boolean(dsn) && process.env.AUTO_CLEANUP_TEST_DATA !== "false";
  59. if (!shouldCleanup) return;
  60. const dbNameForKey = dbName || "unknown";
  61. const counterKey = `cch:vitest:cleanup_workers:${dbNameForKey}`;
  62. const { getRedisClient } = await import("@/lib/redis");
  63. const redis = getRedisClient();
  64. if (!redis) return;
  65. // 等待连接就绪(enableOfflineQueue=false,未 ready 时发命令会直接报错)
  66. if (redis.status !== "ready") {
  67. await new Promise<void>((resolve) => {
  68. const timeout = setTimeout(resolve, 2000);
  69. redis.once("ready", () => {
  70. clearTimeout(timeout);
  71. resolve();
  72. });
  73. });
  74. }
  75. if (redis.status !== "ready") {
  76. console.warn("Redis 未就绪,跳过并行清理协调(不影响测试结果)");
  77. return;
  78. }
  79. const current = await redis.incr(counterKey);
  80. if (current === 1) {
  81. // 防止异常退出导致计数器常驻
  82. await redis.expire(counterKey, 60 * 15);
  83. }
  84. process.env.__VITEST_CLEANUP_COUNTER_KEY__ = counterKey;
  85. } catch (error) {
  86. console.warn("并行清理协调初始化失败(不影响测试结果):", error);
  87. }
  88. });
  89. // ==================== 全局清理钩子 ====================
  90. afterAll(async () => {
  91. console.log("\nVitest 测试环境清理...\n");
  92. // 清理测试期间创建的用户(仅清理最近 10 分钟内的)
  93. const dsn = process.env.DSN || "";
  94. if (dsn && process.env.AUTO_CLEANUP_TEST_DATA !== "false") {
  95. try {
  96. // 仅最后一个 worker 执行清理,避免并发互相删除
  97. const counterKey = process.env.__VITEST_CLEANUP_COUNTER_KEY__;
  98. const { getRedisClient } = await import("@/lib/redis");
  99. const redis = counterKey ? getRedisClient() : null;
  100. if (counterKey && redis) {
  101. if (redis.status !== "ready") {
  102. await new Promise<void>((resolve) => {
  103. const timeout = setTimeout(resolve, 2000);
  104. redis.once("ready", () => {
  105. clearTimeout(timeout);
  106. resolve();
  107. });
  108. });
  109. }
  110. if (redis.status === "ready") {
  111. const remaining = await redis.decr(counterKey);
  112. if (remaining <= 0) {
  113. const { cleanupRecentTestData } = await import("./cleanup-utils");
  114. const result = await cleanupRecentTestData();
  115. if (result.deletedUsers > 0) {
  116. console.log(`自动清理:删除 ${result.deletedUsers} 个测试用户\n`);
  117. }
  118. await redis.del(counterKey);
  119. } else {
  120. // 非最后一个 worker:跳过清理
  121. }
  122. } else {
  123. console.warn("Redis 未就绪,跳过自动清理(不影响测试结果)");
  124. }
  125. } else {
  126. // 无 Redis 协调:为了避免竞态,默认跳过清理
  127. console.warn("未启用清理协调,跳过自动清理(不影响测试结果)");
  128. }
  129. } catch (error) {
  130. console.warn(
  131. "自动清理失败(不影响测试结果):",
  132. error instanceof Error ? error.message : error
  133. );
  134. }
  135. }
  136. console.log("Vitest 测试环境清理完成\n");
  137. });
  138. // ==================== 全局 Mock 配置(可选)====================
  139. // 如果需要 mock 某些全局对象,可以在这里配置
  140. // 例如:mock console.error 以避免测试输出过多错误日志
  141. // 保存原始 console.error
  142. const originalConsoleError = console.error;
  143. // 在测试中静默某些预期的错误(可选)
  144. global.console.error = (...args: unknown[]) => {
  145. // 过滤掉某些已知的、预期的错误日志
  146. const message = args[0]?.toString() || "";
  147. // 跳过这些预期的错误日志
  148. const ignoredPatterns = [
  149. // 可以在这里添加需要忽略的错误模式
  150. // "某个预期的错误消息",
  151. ];
  152. const shouldIgnore = ignoredPatterns.some((pattern) => message.includes(pattern));
  153. if (!shouldIgnore) {
  154. originalConsoleError(...args);
  155. }
  156. };
  157. // ==================== 环境变量默认值 ====================
  158. // 设置测试环境默认值(如果未配置)
  159. process.env.NODE_ENV = process.env.NODE_ENV || "test";
  160. process.env.API_BASE_URL = process.env.API_BASE_URL || "http://localhost:13500/api/actions";
  161. // 便于 API 测试复用 ADMIN_TOKEN(validateKey 支持该 token 直通管理员会话)
  162. process.env.TEST_ADMIN_TOKEN = process.env.TEST_ADMIN_TOKEN || process.env.ADMIN_TOKEN;
  163. // ==================== React act 环境标记 ====================
  164. // React 18+ 在测试环境中会检查该标记,避免出现 “not configured to support act(...)” 的噪声警告。
  165. (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
  166. // ==================== 全局超时配置 ====================
  167. // 设置全局默认超时(可以被单个测试覆盖)
  168. const DEFAULT_TIMEOUT = 10000; // 10 秒
  169. // 导出配置供测试使用
  170. export const TEST_CONFIG = {
  171. timeout: DEFAULT_TIMEOUT,
  172. apiBaseUrl: process.env.API_BASE_URL,
  173. skipAuthTests: !process.env.TEST_AUTH_TOKEN,
  174. };