leaderboard-route.test.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. const mocks = vi.hoisted(() => ({
  3. getSession: vi.fn(),
  4. getLeaderboardWithCache: vi.fn(),
  5. getSystemSettings: vi.fn(),
  6. formatCurrency: vi.fn(),
  7. }));
  8. vi.mock("@/lib/auth", () => ({
  9. getSession: mocks.getSession,
  10. }));
  11. vi.mock("@/repository/system-config", () => ({
  12. getSystemSettings: mocks.getSystemSettings,
  13. }));
  14. vi.mock("@/lib/utils", () => ({
  15. formatCurrency: mocks.formatCurrency,
  16. }));
  17. vi.mock("@/lib/redis", () => ({
  18. getLeaderboardWithCache: mocks.getLeaderboardWithCache,
  19. }));
  20. describe("GET /api/leaderboard", () => {
  21. beforeEach(() => {
  22. vi.clearAllMocks();
  23. mocks.formatCurrency.mockImplementation((val: number) => String(val));
  24. mocks.getSystemSettings.mockResolvedValue({
  25. currencyDisplay: "USD",
  26. allowGlobalUsageView: true,
  27. });
  28. mocks.getLeaderboardWithCache.mockResolvedValue([]);
  29. });
  30. it("returns 401 when session is missing", async () => {
  31. mocks.getSession.mockResolvedValue(null);
  32. const { GET } = await import("@/app/api/leaderboard/route");
  33. const url = "http://localhost/api/leaderboard";
  34. const response = await GET({ nextUrl: new URL(url) } as any);
  35. expect(response.status).toBe(401);
  36. });
  37. it("parses and trims userTags/userGroups and caps at 20 items", async () => {
  38. mocks.getSession.mockResolvedValue({ user: { id: 1, name: "u", role: "admin" } });
  39. const tags = Array.from({ length: 25 }, (_, i) => ` t${i} `).join(",");
  40. const groups = " a, ,b ,c, ";
  41. const { GET } = await import("@/app/api/leaderboard/route");
  42. const url = `http://localhost/api/leaderboard?scope=user&period=daily&userTags=${encodeURIComponent(
  43. tags
  44. )}&userGroups=${encodeURIComponent(groups)}`;
  45. const response = await GET({ nextUrl: new URL(url) } as any);
  46. expect(response.status).toBe(200);
  47. expect(mocks.getLeaderboardWithCache).toHaveBeenCalledTimes(1);
  48. const callArgs = mocks.getLeaderboardWithCache.mock.calls[0];
  49. const options = callArgs[4];
  50. expect(options.userTags).toHaveLength(20);
  51. expect(options.userTags?.[0]).toBe("t0");
  52. expect(options.userGroups).toEqual(["a", "b", "c"]);
  53. });
  54. it("does not apply userTags/userGroups when scope is not user", async () => {
  55. mocks.getSession.mockResolvedValue({ user: { id: 1, name: "u", role: "admin" } });
  56. const { GET } = await import("@/app/api/leaderboard/route");
  57. const url =
  58. "http://localhost/api/leaderboard?scope=provider&period=daily&userTags=a&userGroups=b";
  59. const response = await GET({ nextUrl: new URL(url) } as any);
  60. expect(response.status).toBe(200);
  61. expect(mocks.getLeaderboardWithCache).toHaveBeenCalledTimes(1);
  62. const callArgs = mocks.getLeaderboardWithCache.mock.calls[0];
  63. const options = callArgs[4];
  64. expect(options.userTags).toBeUndefined();
  65. expect(options.userGroups).toBeUndefined();
  66. });
  67. describe("additive provider fields", () => {
  68. it("includes avgCostPerRequest and avgCostPerMillionTokens in provider scope response", async () => {
  69. mocks.getSession.mockResolvedValue({ user: { id: 1, name: "u", role: "admin" } });
  70. mocks.getLeaderboardWithCache.mockResolvedValue([
  71. {
  72. providerId: 1,
  73. providerName: "test-provider",
  74. totalRequests: 100,
  75. totalCost: 5.0,
  76. totalTokens: 500000,
  77. successRate: 0.95,
  78. avgTtfbMs: 200,
  79. avgTokensPerSecond: 50,
  80. avgCostPerRequest: 0.05,
  81. avgCostPerMillionTokens: 10.0,
  82. },
  83. ]);
  84. const { GET } = await import("@/app/api/leaderboard/route");
  85. const url = "http://localhost/api/leaderboard?scope=provider&period=daily";
  86. const response = await GET({ nextUrl: new URL(url) } as any);
  87. const body = await response.json();
  88. expect(response.status).toBe(200);
  89. expect(body).toHaveLength(1);
  90. const entry = body[0];
  91. // Additive fields must be present
  92. expect(entry).toHaveProperty("avgCostPerRequest", 0.05);
  93. expect(entry).toHaveProperty("avgCostPerMillionTokens", 10.0);
  94. // Formatted variants should exist
  95. expect(entry).toHaveProperty("avgCostPerRequestFormatted");
  96. expect(entry).toHaveProperty("avgCostPerMillionTokensFormatted");
  97. // Existing fields must still be present
  98. expect(entry).toHaveProperty("totalCostFormatted");
  99. expect(entry).toHaveProperty("providerId", 1);
  100. expect(entry).toHaveProperty("providerName", "test-provider");
  101. });
  102. it("formats null avgCost fields without error", async () => {
  103. mocks.getSession.mockResolvedValue({ user: { id: 1, name: "u", role: "admin" } });
  104. mocks.getLeaderboardWithCache.mockResolvedValue([
  105. {
  106. providerId: 2,
  107. providerName: "zero-provider",
  108. totalRequests: 0,
  109. totalCost: 0,
  110. totalTokens: 0,
  111. successRate: 0,
  112. avgTtfbMs: 0,
  113. avgTokensPerSecond: 0,
  114. avgCostPerRequest: null,
  115. avgCostPerMillionTokens: null,
  116. },
  117. ]);
  118. const { GET } = await import("@/app/api/leaderboard/route");
  119. const url = "http://localhost/api/leaderboard?scope=provider&period=daily";
  120. const response = await GET({ nextUrl: new URL(url) } as any);
  121. const body = await response.json();
  122. expect(response.status).toBe(200);
  123. const entry = body[0];
  124. expect(entry.avgCostPerRequest).toBeNull();
  125. expect(entry.avgCostPerMillionTokens).toBeNull();
  126. });
  127. it("includes modelStats in providerCacheHitRate scope response", async () => {
  128. mocks.getSession.mockResolvedValue({ user: { id: 1, name: "u", role: "admin" } });
  129. mocks.getLeaderboardWithCache.mockResolvedValue([
  130. {
  131. providerId: 1,
  132. providerName: "cache-provider",
  133. totalRequests: 50,
  134. cacheReadTokens: 10000,
  135. totalCost: 2.5,
  136. cacheCreationCost: 1.0,
  137. totalInputTokens: 20000,
  138. totalTokens: 20000,
  139. cacheHitRate: 0.5,
  140. modelStats: [
  141. {
  142. model: "claude-3-opus",
  143. totalRequests: 30,
  144. cacheReadTokens: 8000,
  145. totalInputTokens: 15000,
  146. cacheHitRate: 0.53,
  147. },
  148. {
  149. model: "claude-3-sonnet",
  150. totalRequests: 20,
  151. cacheReadTokens: 2000,
  152. totalInputTokens: 5000,
  153. cacheHitRate: 0.4,
  154. },
  155. ],
  156. },
  157. ]);
  158. const { GET } = await import("@/app/api/leaderboard/route");
  159. const url = "http://localhost/api/leaderboard?scope=providerCacheHitRate&period=daily";
  160. const response = await GET({ nextUrl: new URL(url) } as any);
  161. const body = await response.json();
  162. expect(response.status).toBe(200);
  163. expect(body).toHaveLength(1);
  164. const entry = body[0];
  165. expect(entry).toHaveProperty("modelStats");
  166. expect(entry.modelStats).toHaveLength(2);
  167. expect(entry.modelStats[0]).toHaveProperty("model", "claude-3-opus");
  168. expect(entry.modelStats[0]).toHaveProperty("cacheHitRate", 0.53);
  169. });
  170. });
  171. });