provider-selector-total-limit.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import type { Provider } from "@/types/provider";
  3. const circuitBreakerMocks = vi.hoisted(() => ({
  4. isCircuitOpen: vi.fn(async () => false),
  5. getCircuitState: vi.fn(() => "closed"),
  6. }));
  7. vi.mock("@/lib/circuit-breaker", () => circuitBreakerMocks);
  8. const sessionManagerMocks = vi.hoisted(() => ({
  9. SessionManager: {
  10. getSessionProvider: vi.fn(async () => null as number | null),
  11. },
  12. }));
  13. vi.mock("@/lib/session-manager", () => sessionManagerMocks);
  14. const providerRepositoryMocks = vi.hoisted(() => ({
  15. findProviderById: vi.fn(async () => null as Provider | null),
  16. findAllProviders: vi.fn(async () => [] as Provider[]),
  17. }));
  18. vi.mock("@/repository/provider", () => providerRepositoryMocks);
  19. const rateLimitMocks = vi.hoisted(() => ({
  20. RateLimitService: {
  21. checkCostLimitsWithLease: vi.fn(async () => ({ allowed: true })),
  22. checkTotalCostLimit: vi.fn(async () => ({ allowed: true, current: 0 })),
  23. },
  24. }));
  25. vi.mock("@/lib/rate-limit", () => rateLimitMocks);
  26. beforeEach(() => {
  27. vi.resetAllMocks();
  28. });
  29. describe("ProxyProviderResolver.filterByLimits - provider total limit", () => {
  30. test("当供应商达到总消费上限时应被过滤掉", async () => {
  31. const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
  32. const resetAt = new Date("2026-01-04T00:00:00.000Z");
  33. const providers: Provider[] = [
  34. {
  35. id: 1,
  36. name: "p1",
  37. isEnabled: true,
  38. providerType: "claude",
  39. groupTag: null,
  40. weight: 1,
  41. priority: 0,
  42. costMultiplier: 1,
  43. // rate limit fields
  44. limit5hUsd: null,
  45. limitDailyUsd: null,
  46. dailyResetMode: "fixed",
  47. dailyResetTime: "00:00",
  48. limitWeeklyUsd: null,
  49. limitMonthlyUsd: null,
  50. limitTotalUsd: 10,
  51. totalCostResetAt: resetAt,
  52. limitConcurrentSessions: 0,
  53. } as unknown as Provider,
  54. {
  55. id: 2,
  56. name: "p2",
  57. isEnabled: true,
  58. providerType: "claude",
  59. groupTag: null,
  60. weight: 1,
  61. priority: 0,
  62. costMultiplier: 1,
  63. limit5hUsd: null,
  64. limitDailyUsd: null,
  65. dailyResetMode: "fixed",
  66. dailyResetTime: "00:00",
  67. limitWeeklyUsd: null,
  68. limitMonthlyUsd: null,
  69. limitTotalUsd: null,
  70. totalCostResetAt: null,
  71. limitConcurrentSessions: 0,
  72. } as unknown as Provider,
  73. ];
  74. rateLimitMocks.RateLimitService.checkTotalCostLimit.mockImplementation(async (id: number) => {
  75. if (id === 1) return { allowed: false, current: 10, reason: "limit reached" };
  76. return { allowed: true, current: 0 };
  77. });
  78. const filtered = await (ProxyProviderResolver as any).filterByLimits(providers);
  79. expect(filtered.map((p: Provider) => p.id)).toEqual([2]);
  80. expect(rateLimitMocks.RateLimitService.checkTotalCostLimit).toHaveBeenCalledWith(
  81. 1,
  82. "provider",
  83. 10,
  84. { resetAt }
  85. );
  86. });
  87. });
  88. describe("ProxyProviderResolver.findReusable - provider total limit", () => {
  89. test("当会话复用的供应商达到总限额时应拒绝复用", async () => {
  90. const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
  91. const resetAt = new Date("2026-01-04T00:00:00.000Z");
  92. sessionManagerMocks.SessionManager.getSessionProvider.mockResolvedValueOnce(1);
  93. providerRepositoryMocks.findProviderById.mockResolvedValueOnce({
  94. id: 1,
  95. name: "p1",
  96. isEnabled: true,
  97. providerType: "claude",
  98. groupTag: null,
  99. weight: 1,
  100. priority: 0,
  101. costMultiplier: 1,
  102. limit5hUsd: null,
  103. limitDailyUsd: null,
  104. dailyResetMode: "fixed",
  105. dailyResetTime: "00:00",
  106. limitWeeklyUsd: null,
  107. limitMonthlyUsd: null,
  108. limitTotalUsd: 10,
  109. totalCostResetAt: resetAt,
  110. limitConcurrentSessions: 0,
  111. } as unknown as Provider);
  112. rateLimitMocks.RateLimitService.checkTotalCostLimit.mockResolvedValueOnce({
  113. allowed: false,
  114. current: 10,
  115. reason: "limit reached",
  116. });
  117. const session = {
  118. sessionId: "s1",
  119. shouldReuseProvider: () => true,
  120. authState: null,
  121. getCurrentModel: () => null,
  122. getOriginalModel: () => null,
  123. } as any;
  124. const reused = await (ProxyProviderResolver as any).findReusable(session);
  125. expect(reused).toBeNull();
  126. expect(rateLimitMocks.RateLimitService.checkCostLimitsWithLease).toHaveBeenCalledWith(
  127. 1,
  128. "provider",
  129. {
  130. limit_5h_usd: null,
  131. limit_daily_usd: null,
  132. daily_reset_mode: "fixed",
  133. daily_reset_time: "00:00",
  134. limit_weekly_usd: null,
  135. limit_monthly_usd: null,
  136. }
  137. );
  138. expect(rateLimitMocks.RateLimitService.checkTotalCostLimit).toHaveBeenCalledWith(
  139. 1,
  140. "provider",
  141. 10,
  142. { resetAt }
  143. );
  144. });
  145. });