leaderboard-cache.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. import { getRedisClient } from "@/lib/redis/client";
  3. import { getLeaderboardWithCache } from "@/lib/redis/leaderboard-cache";
  4. import {
  5. findDailyUserCacheHitRateLeaderboard,
  6. type UserCacheHitRateLeaderboardEntry,
  7. } from "@/repository/leaderboard";
  8. vi.mock("@/lib/logger", () => ({
  9. logger: {
  10. debug: vi.fn(),
  11. info: vi.fn(),
  12. warn: vi.fn(),
  13. error: vi.fn(),
  14. },
  15. }));
  16. vi.mock("@/lib/redis/client", () => ({
  17. getRedisClient: vi.fn(),
  18. }));
  19. vi.mock("@/lib/utils/timezone", () => ({
  20. resolveSystemTimezone: vi.fn().mockResolvedValue("UTC"),
  21. }));
  22. vi.mock("@/repository/leaderboard", async () => {
  23. const actual = await vi.importActual<typeof import("@/repository/leaderboard")>(
  24. "@/repository/leaderboard"
  25. );
  26. return {
  27. ...actual,
  28. findDailyUserCacheHitRateLeaderboard: vi.fn(),
  29. };
  30. });
  31. type RedisMock = {
  32. get: ReturnType<typeof vi.fn>;
  33. set: ReturnType<typeof vi.fn>;
  34. setex: ReturnType<typeof vi.fn>;
  35. del: ReturnType<typeof vi.fn>;
  36. };
  37. function createRedisMock(): RedisMock {
  38. return {
  39. get: vi.fn(),
  40. set: vi.fn(),
  41. setex: vi.fn(),
  42. del: vi.fn(),
  43. };
  44. }
  45. function createUserCacheHitRateRows(): UserCacheHitRateLeaderboardEntry[] {
  46. return [
  47. {
  48. userId: 1,
  49. userName: "alice",
  50. totalRequests: 12,
  51. totalCost: 1.23,
  52. cacheReadTokens: 456,
  53. cacheCreationCost: 0.45,
  54. totalInputTokens: 789,
  55. totalTokens: 789,
  56. cacheHitRate: 0.577,
  57. },
  58. ];
  59. }
  60. describe("getLeaderboardWithCache", () => {
  61. beforeEach(() => {
  62. vi.clearAllMocks();
  63. vi.useRealTimers();
  64. });
  65. it("passes user filters to userCacheHitRate queries on Redis cache miss", async () => {
  66. vi.useFakeTimers();
  67. vi.setSystemTime(new Date("2026-04-13T00:00:00Z"));
  68. const redis = createRedisMock();
  69. const rows = createUserCacheHitRateRows();
  70. redis.get.mockResolvedValueOnce(null);
  71. redis.set.mockResolvedValueOnce("OK");
  72. redis.setex.mockResolvedValueOnce("OK");
  73. redis.del.mockResolvedValueOnce(1);
  74. vi.mocked(getRedisClient).mockReturnValue(
  75. redis as unknown as NonNullable<ReturnType<typeof getRedisClient>>
  76. );
  77. vi.mocked(findDailyUserCacheHitRateLeaderboard).mockResolvedValueOnce(rows);
  78. const result = await getLeaderboardWithCache("daily", "USD", "userCacheHitRate", undefined, {
  79. userTags: ["vip", "team-a"],
  80. userGroups: ["group-1"],
  81. includeModelStats: true,
  82. });
  83. expect(result).toEqual(rows);
  84. expect(findDailyUserCacheHitRateLeaderboard).toHaveBeenCalledWith(
  85. { userTags: ["vip", "team-a"], userGroups: ["group-1"] },
  86. true
  87. );
  88. expect(redis.setex).toHaveBeenCalledWith(
  89. "leaderboard:userCacheHitRate:daily:2026-04-13:USD:includeModelStats:tags:team-a,vip:groups:group-1",
  90. 60,
  91. JSON.stringify(rows)
  92. );
  93. });
  94. it("falls back to direct query when Redis is unavailable and still preserves userCacheHitRate filters", async () => {
  95. const rows = createUserCacheHitRateRows();
  96. vi.mocked(getRedisClient).mockReturnValue(null);
  97. vi.mocked(findDailyUserCacheHitRateLeaderboard).mockResolvedValueOnce(rows);
  98. const result = await getLeaderboardWithCache("daily", "USD", "userCacheHitRate", undefined, {
  99. userTags: ["vip"],
  100. userGroups: ["group-1"],
  101. });
  102. expect(result).toEqual(rows);
  103. expect(findDailyUserCacheHitRateLeaderboard).toHaveBeenCalledWith(
  104. { userTags: ["vip"], userGroups: ["group-1"] },
  105. undefined
  106. );
  107. });
  108. });