session-cache.test.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
  2. const CLEANUP_INTERVAL_GLOBAL_KEY = "__CCH_CACHE_CLEANUP_INTERVAL_ID__";
  3. async function loadSessionCache() {
  4. const mod = await import("@/lib/cache/session-cache");
  5. return {
  6. getActiveSessionsCache: mod.getActiveSessionsCache,
  7. setActiveSessionsCache: mod.setActiveSessionsCache,
  8. getSessionDetailsCache: mod.getSessionDetailsCache,
  9. setSessionDetailsCache: mod.setSessionDetailsCache,
  10. clearActiveSessionsCache: mod.clearActiveSessionsCache,
  11. clearAllSessionsCache: mod.clearAllSessionsCache,
  12. clearSessionDetailsCache: mod.clearSessionDetailsCache,
  13. clearAllSessionCache: mod.clearAllSessionCache,
  14. startCacheCleanup: mod.startCacheCleanup,
  15. stopCacheCleanup: mod.stopCacheCleanup,
  16. getCacheStats: mod.getCacheStats,
  17. };
  18. }
  19. function getCleanupIntervalId() {
  20. return (globalThis as any)[CLEANUP_INTERVAL_GLOBAL_KEY] as ReturnType<typeof setInterval> | null;
  21. }
  22. function setCleanupIntervalId(value: ReturnType<typeof setInterval> | null) {
  23. (globalThis as any)[CLEANUP_INTERVAL_GLOBAL_KEY] = value;
  24. }
  25. beforeEach(() => {
  26. vi.clearAllMocks();
  27. vi.resetModules();
  28. vi.useFakeTimers();
  29. vi.setSystemTime(new Date("2026-01-03T00:00:00.000Z"));
  30. setCleanupIntervalId(null);
  31. });
  32. afterEach(() => {
  33. const intervalId = getCleanupIntervalId();
  34. if (intervalId) {
  35. clearInterval(intervalId);
  36. }
  37. setCleanupIntervalId(null);
  38. vi.useRealTimers();
  39. });
  40. describe("SessionCache(Session 数据缓存层)", () => {
  41. test("未写入时应返回 null;写入后 TTL 内应可读取", async () => {
  42. const { getActiveSessionsCache, setActiveSessionsCache } = await loadSessionCache();
  43. expect(getActiveSessionsCache()).toBeNull();
  44. setActiveSessionsCache([
  45. {
  46. sessionId: "s_1",
  47. requestCount: 1,
  48. totalCostUsd: "0",
  49. totalInputTokens: 0,
  50. totalOutputTokens: 0,
  51. totalCacheCreationTokens: 0,
  52. totalCacheReadTokens: 0,
  53. totalDurationMs: 0,
  54. firstRequestAt: null,
  55. lastRequestAt: null,
  56. providers: [],
  57. models: [],
  58. userName: "u",
  59. userId: 1,
  60. keyName: "k",
  61. keyId: 1,
  62. userAgent: null,
  63. apiType: null,
  64. cacheTtlApplied: null,
  65. },
  66. ]);
  67. expect(getActiveSessionsCache()).toEqual(
  68. expect.arrayContaining([expect.objectContaining({ sessionId: "s_1" })])
  69. );
  70. });
  71. test("TTL 过期后应返回 null(并清理过期条目)", async () => {
  72. const { getActiveSessionsCache, setActiveSessionsCache, getCacheStats } =
  73. await loadSessionCache();
  74. setActiveSessionsCache(
  75. [
  76. {
  77. sessionId: "s_expired",
  78. requestCount: 1,
  79. totalCostUsd: "0",
  80. totalInputTokens: 0,
  81. totalOutputTokens: 0,
  82. totalCacheCreationTokens: 0,
  83. totalCacheReadTokens: 0,
  84. totalDurationMs: 0,
  85. firstRequestAt: null,
  86. lastRequestAt: null,
  87. providers: [],
  88. models: [],
  89. userName: "u",
  90. userId: 1,
  91. keyName: "k",
  92. keyId: 1,
  93. userAgent: null,
  94. apiType: null,
  95. cacheTtlApplied: null,
  96. },
  97. ],
  98. "active_sessions"
  99. );
  100. expect(getCacheStats().activeSessions.size).toBe(1);
  101. // activeSessionsCache TTL = 2s,且实现为 age > ttl 才过期
  102. vi.advanceTimersByTime(2001);
  103. expect(getActiveSessionsCache()).toBeNull();
  104. expect(getCacheStats().activeSessions.size).toBe(0);
  105. });
  106. test("clear* 系列函数应删除对应缓存", async () => {
  107. const {
  108. getActiveSessionsCache,
  109. setActiveSessionsCache,
  110. getSessionDetailsCache,
  111. setSessionDetailsCache,
  112. clearActiveSessionsCache,
  113. clearAllSessionsCache,
  114. clearSessionDetailsCache,
  115. clearAllSessionCache,
  116. } = await loadSessionCache();
  117. setActiveSessionsCache(
  118. [
  119. {
  120. sessionId: "s_1",
  121. requestCount: 1,
  122. totalCostUsd: "0",
  123. totalInputTokens: 0,
  124. totalOutputTokens: 0,
  125. totalCacheCreationTokens: 0,
  126. totalCacheReadTokens: 0,
  127. totalDurationMs: 0,
  128. firstRequestAt: null,
  129. lastRequestAt: null,
  130. providers: [],
  131. models: [],
  132. userName: "u",
  133. userId: 1,
  134. keyName: "k",
  135. keyId: 1,
  136. userAgent: null,
  137. apiType: null,
  138. cacheTtlApplied: null,
  139. },
  140. ],
  141. "active_sessions"
  142. );
  143. setActiveSessionsCache([], "all_sessions");
  144. setSessionDetailsCache("s_1", {
  145. sessionId: "s_1",
  146. requestCount: 1,
  147. totalCostUsd: "0",
  148. totalInputTokens: 0,
  149. totalOutputTokens: 0,
  150. totalCacheCreationTokens: 0,
  151. totalCacheReadTokens: 0,
  152. totalDurationMs: 0,
  153. firstRequestAt: null,
  154. lastRequestAt: null,
  155. providers: [],
  156. models: [],
  157. userName: "u",
  158. userId: 1,
  159. keyName: "k",
  160. keyId: 1,
  161. userAgent: null,
  162. apiType: null,
  163. cacheTtlApplied: null,
  164. });
  165. clearActiveSessionsCache();
  166. expect(getActiveSessionsCache()).toBeNull();
  167. clearAllSessionsCache();
  168. // clearAllSessionsCache 删除 all_sessions,而非 active_sessions
  169. expect(getActiveSessionsCache("all_sessions")).toBeNull();
  170. clearSessionDetailsCache("s_1");
  171. expect(getSessionDetailsCache("s_1")).toBeNull();
  172. // 再次写入后,clearAllSessionCache 应清空两类缓存
  173. setActiveSessionsCache([], "active_sessions");
  174. setSessionDetailsCache("s_2", {
  175. sessionId: "s_2",
  176. requestCount: 1,
  177. totalCostUsd: "0",
  178. totalInputTokens: 0,
  179. totalOutputTokens: 0,
  180. totalCacheCreationTokens: 0,
  181. totalCacheReadTokens: 0,
  182. totalDurationMs: 0,
  183. firstRequestAt: null,
  184. lastRequestAt: null,
  185. providers: [],
  186. models: [],
  187. userName: "u",
  188. userId: 1,
  189. keyName: "k",
  190. keyId: 1,
  191. userAgent: null,
  192. apiType: null,
  193. cacheTtlApplied: null,
  194. });
  195. clearAllSessionCache();
  196. expect(getActiveSessionsCache()).toBeNull();
  197. expect(getSessionDetailsCache("s_2")).toBeNull();
  198. });
  199. test("startCacheCleanup/stopCacheCleanup:应幂等且能清理过期条目", async () => {
  200. const {
  201. setActiveSessionsCache,
  202. getActiveSessionsCache,
  203. getCacheStats,
  204. startCacheCleanup,
  205. stopCacheCleanup,
  206. } = await loadSessionCache();
  207. // 未启动时 stop 应无副作用
  208. expect(getCleanupIntervalId()).toBeNull();
  209. stopCacheCleanup();
  210. expect(getCleanupIntervalId()).toBeNull();
  211. const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
  212. startCacheCleanup(1);
  213. const firstId = getCleanupIntervalId();
  214. expect(firstId).not.toBeNull();
  215. // 重复启动应直接返回,不应创建新的 interval
  216. startCacheCleanup(1);
  217. expect(getCleanupIntervalId()).toBe(firstId);
  218. expect(setIntervalSpy).toHaveBeenCalledTimes(1);
  219. // 写入一个会过期的条目(activeSessions TTL=2s)
  220. setActiveSessionsCache(
  221. [
  222. {
  223. sessionId: "s_expired_by_cleanup",
  224. requestCount: 1,
  225. totalCostUsd: "0",
  226. totalInputTokens: 0,
  227. totalOutputTokens: 0,
  228. totalCacheCreationTokens: 0,
  229. totalCacheReadTokens: 0,
  230. totalDurationMs: 0,
  231. firstRequestAt: null,
  232. lastRequestAt: null,
  233. providers: [],
  234. models: [],
  235. userName: "u",
  236. userId: 1,
  237. keyName: "k",
  238. keyId: 1,
  239. userAgent: null,
  240. apiType: null,
  241. cacheTtlApplied: null,
  242. },
  243. ],
  244. "active_sessions"
  245. );
  246. expect(getCacheStats().activeSessions.size).toBe(1);
  247. // 推进到 >2s,等待 cleanup interval 执行(每 1s)
  248. vi.advanceTimersByTime(3000);
  249. expect(getCacheStats().activeSessions.size).toBe(0);
  250. expect(getActiveSessionsCache()).toBeNull();
  251. stopCacheCleanup();
  252. expect(getCleanupIntervalId()).toBeNull();
  253. });
  254. });