system-settings-cache.test.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
  2. import type { SystemSettings } from "@/types/system-config";
  3. const getSystemSettingsMock = vi.fn();
  4. const loggerDebugMock = vi.fn();
  5. const loggerWarnMock = vi.fn();
  6. const loggerInfoMock = vi.fn();
  7. vi.mock("server-only", () => ({}));
  8. vi.mock("@/repository/system-config", () => ({
  9. getSystemSettings: () => getSystemSettingsMock(),
  10. }));
  11. vi.mock("@/lib/logger", () => ({
  12. logger: {
  13. debug: loggerDebugMock,
  14. warn: loggerWarnMock,
  15. info: loggerInfoMock,
  16. trace: vi.fn(),
  17. error: vi.fn(),
  18. },
  19. }));
  20. function createSettings(overrides: Partial<SystemSettings> = {}): SystemSettings {
  21. const base: SystemSettings = {
  22. id: 1,
  23. siteTitle: "Claude Code Hub",
  24. allowGlobalUsageView: false,
  25. currencyDisplay: "USD",
  26. billingModelSource: "original",
  27. codexPriorityBillingSource: "requested",
  28. timezone: null,
  29. enableAutoCleanup: false,
  30. cleanupRetentionDays: 30,
  31. cleanupSchedule: "0 2 * * *",
  32. cleanupBatchSize: 10000,
  33. enableClientVersionCheck: false,
  34. verboseProviderError: false,
  35. enableHttp2: false,
  36. enableHighConcurrencyMode: false,
  37. interceptAnthropicWarmupRequests: false,
  38. enableThinkingSignatureRectifier: true,
  39. enableThinkingBudgetRectifier: true,
  40. enableBillingHeaderRectifier: true,
  41. enableCodexSessionIdCompletion: true,
  42. enableClaudeMetadataUserIdInjection: true,
  43. enableResponseFixer: true,
  44. responseFixerConfig: {
  45. fixTruncatedJson: true,
  46. fixSseFormat: true,
  47. fixEncoding: true,
  48. maxJsonDepth: 200,
  49. maxFixSize: 1024 * 1024,
  50. },
  51. quotaDbRefreshIntervalSeconds: 10,
  52. quotaLeasePercent5h: 0.05,
  53. quotaLeasePercentDaily: 0.05,
  54. quotaLeasePercentWeekly: 0.05,
  55. quotaLeasePercentMonthly: 0.05,
  56. quotaLeaseCapUsd: null,
  57. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  58. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  59. };
  60. return { ...base, ...overrides };
  61. }
  62. async function loadCache() {
  63. const mod = await import("@/lib/config/system-settings-cache");
  64. return {
  65. getCachedSystemSettings: mod.getCachedSystemSettings,
  66. isHttp2Enabled: mod.isHttp2Enabled,
  67. invalidateSystemSettingsCache: mod.invalidateSystemSettingsCache,
  68. };
  69. }
  70. beforeEach(() => {
  71. vi.clearAllMocks();
  72. vi.resetModules();
  73. vi.useFakeTimers();
  74. vi.setSystemTime(new Date("2026-01-03T00:00:00.000Z"));
  75. });
  76. afterEach(() => {
  77. vi.useRealTimers();
  78. });
  79. describe("SystemSettingsCache", () => {
  80. test("首次调用应从数据库获取并缓存;TTL 内再次调用应直接返回缓存", async () => {
  81. getSystemSettingsMock.mockResolvedValueOnce(createSettings({ id: 101 }));
  82. const { getCachedSystemSettings } = await loadCache();
  83. const first = await getCachedSystemSettings();
  84. const second = await getCachedSystemSettings();
  85. expect(first).toEqual(expect.objectContaining({ id: 101 }));
  86. // 缓存返回应保持引用一致,避免不必要的对象创建
  87. expect(second).toBe(first);
  88. expect(getSystemSettingsMock).toHaveBeenCalledTimes(1);
  89. expect(loggerDebugMock).toHaveBeenCalledTimes(1);
  90. expect(loggerWarnMock).not.toHaveBeenCalled();
  91. });
  92. test("TTL 过期后应重新获取并更新缓存", async () => {
  93. const settingsA = createSettings({ id: 201, enableHttp2: false });
  94. const settingsB = createSettings({ id: 202, enableHttp2: true });
  95. getSystemSettingsMock.mockResolvedValueOnce(settingsA).mockResolvedValueOnce(settingsB);
  96. const { getCachedSystemSettings } = await loadCache();
  97. const first = await getCachedSystemSettings();
  98. expect(first).toBe(settingsA);
  99. expect(getSystemSettingsMock).toHaveBeenCalledTimes(1);
  100. vi.setSystemTime(new Date("2026-01-03T00:01:00.001Z"));
  101. const second = await getCachedSystemSettings();
  102. expect(second).toBe(settingsB);
  103. expect(getSystemSettingsMock).toHaveBeenCalledTimes(2);
  104. });
  105. test("当获取失败且已有缓存时,应 fail-open 返回上一份缓存", async () => {
  106. const cached = createSettings({ id: 301, interceptAnthropicWarmupRequests: true });
  107. getSystemSettingsMock.mockResolvedValueOnce(cached);
  108. const { getCachedSystemSettings } = await loadCache();
  109. const first = await getCachedSystemSettings();
  110. expect(first).toBe(cached);
  111. vi.setSystemTime(new Date("2026-01-03T00:01:00.001Z"));
  112. getSystemSettingsMock.mockRejectedValueOnce(new Error("db down"));
  113. const second = await getCachedSystemSettings();
  114. expect(second).toBe(cached);
  115. expect(loggerWarnMock).toHaveBeenCalledTimes(1);
  116. });
  117. test("当获取失败且无缓存时,应返回最小默认设置,并显式关闭 warmup 拦截", async () => {
  118. getSystemSettingsMock.mockRejectedValueOnce(new Error("db down"));
  119. const { getCachedSystemSettings } = await loadCache();
  120. const settings = await getCachedSystemSettings();
  121. expect(settings).toEqual(
  122. expect.objectContaining({
  123. siteTitle: "Claude Code Hub",
  124. enableHttp2: false,
  125. enableHighConcurrencyMode: false,
  126. interceptAnthropicWarmupRequests: false,
  127. codexPriorityBillingSource: "requested",
  128. })
  129. );
  130. expect(loggerWarnMock).toHaveBeenCalledTimes(1);
  131. });
  132. test("invalidateSystemSettingsCache 应清空缓存并触发下一次重新获取", async () => {
  133. const settingsA = createSettings({ id: 401 });
  134. const settingsB = createSettings({ id: 402 });
  135. getSystemSettingsMock.mockResolvedValueOnce(settingsA).mockResolvedValueOnce(settingsB);
  136. const { getCachedSystemSettings, invalidateSystemSettingsCache } = await loadCache();
  137. expect(await getCachedSystemSettings()).toBe(settingsA);
  138. invalidateSystemSettingsCache();
  139. expect(loggerInfoMock).toHaveBeenCalledTimes(1);
  140. expect(await getCachedSystemSettings()).toBe(settingsB);
  141. expect(getSystemSettingsMock).toHaveBeenCalledTimes(2);
  142. });
  143. test("isHttp2Enabled 应读取缓存并返回 enableHttp2", async () => {
  144. getSystemSettingsMock.mockResolvedValueOnce(createSettings({ id: 501, enableHttp2: true }));
  145. const { isHttp2Enabled } = await loadCache();
  146. expect(await isHttp2Enabled()).toBe(true);
  147. });
  148. });