| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- import { beforeEach, describe, expect, test, vi } from "vitest";
- import type { Provider } from "@/types/provider";
- const circuitBreakerMocks = vi.hoisted(() => ({
- isCircuitOpen: vi.fn(async () => false),
- getCircuitState: vi.fn(() => "closed"),
- }));
- vi.mock("@/lib/circuit-breaker", () => circuitBreakerMocks);
- const vendorTypeCircuitMocks = vi.hoisted(() => ({
- isVendorTypeCircuitOpen: vi.fn(async () => false),
- }));
- vi.mock("@/lib/vendor-type-circuit-breaker", () => vendorTypeCircuitMocks);
- const sessionManagerMocks = vi.hoisted(() => ({
- SessionManager: {
- getSessionProvider: vi.fn(async () => null as number | null),
- clearSessionProvider: vi.fn(async () => undefined),
- },
- }));
- vi.mock("@/lib/session-manager", () => sessionManagerMocks);
- const providerRepositoryMocks = vi.hoisted(() => ({
- findProviderById: vi.fn(async () => null as Provider | null),
- findAllProviders: vi.fn(async () => [] as Provider[]),
- }));
- vi.mock("@/repository/provider", () => providerRepositoryMocks);
- const rateLimitMocks = vi.hoisted(() => ({
- RateLimitService: {
- checkCostLimitsWithLease: vi.fn(async () => ({ allowed: true })),
- checkTotalCostLimit: vi.fn(async () => ({ allowed: true, current: 0 })),
- },
- }));
- vi.mock("@/lib/rate-limit", () => rateLimitMocks);
- beforeEach(() => {
- vi.resetAllMocks();
- });
- function createHaikuOnlyProvider(): Provider {
- return {
- id: 78,
- name: "zhipu_Haiku",
- isEnabled: true,
- providerType: "claude",
- groupTag: null,
- weight: 1,
- priority: 1,
- costMultiplier: 1,
- allowedModels: ["claude-haiku-4-5-20251001", "claude-haiku-4-5"],
- providerVendorId: null,
- limit5hUsd: null,
- limitDailyUsd: null,
- dailyResetMode: "fixed",
- dailyResetTime: "00:00",
- limitWeeklyUsd: null,
- limitMonthlyUsd: null,
- limitTotalUsd: null,
- totalCostResetAt: null,
- limitConcurrentSessions: 0,
- } as unknown as Provider;
- }
- function createOpusProvider(): Provider {
- return {
- id: 94,
- name: "yescode_team",
- isEnabled: true,
- providerType: "claude",
- groupTag: null,
- weight: 1,
- priority: 0,
- costMultiplier: 1,
- allowedModels: null, // supports all claude models
- providerVendorId: null,
- limit5hUsd: null,
- limitDailyUsd: null,
- dailyResetMode: "fixed",
- dailyResetTime: "00:00",
- limitWeeklyUsd: null,
- limitMonthlyUsd: null,
- limitTotalUsd: null,
- totalCostResetAt: null,
- limitConcurrentSessions: 0,
- } as unknown as Provider;
- }
- describe("findReusable - model mismatch clears stale binding", () => {
- test("should clear stale binding when bound provider does not support requested model", async () => {
- const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
- // Session bound to haiku-only provider
- sessionManagerMocks.SessionManager.getSessionProvider.mockResolvedValueOnce(78);
- providerRepositoryMocks.findProviderById.mockResolvedValueOnce(createHaikuOnlyProvider());
- const session = {
- sessionId: "4c25cf92",
- shouldReuseProvider: () => true,
- getOriginalModel: () => "claude-opus-4-6",
- authState: null,
- getCurrentModel: () => null,
- } as any;
- const result = await (ProxyProviderResolver as any).findReusable(session);
- expect(result).toBeNull();
- // Key assertion: clearSessionProvider should have been called
- expect(sessionManagerMocks.SessionManager.clearSessionProvider).toHaveBeenCalledWith(
- "4c25cf92"
- );
- });
- test("should NOT clear binding when bound provider supports requested model", async () => {
- const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
- // Session bound to provider that supports all claude models
- sessionManagerMocks.SessionManager.getSessionProvider.mockResolvedValueOnce(94);
- providerRepositoryMocks.findProviderById.mockResolvedValueOnce(createOpusProvider());
- rateLimitMocks.RateLimitService.checkCostLimitsWithLease.mockResolvedValueOnce({
- allowed: true,
- });
- rateLimitMocks.RateLimitService.checkTotalCostLimit.mockResolvedValueOnce({
- allowed: true,
- current: 0,
- });
- const session = {
- sessionId: "sess_ok",
- shouldReuseProvider: () => true,
- getOriginalModel: () => "claude-opus-4-6",
- authState: null,
- getCurrentModel: () => null,
- } as any;
- const result = await (ProxyProviderResolver as any).findReusable(session);
- // Should return the provider (model matches)
- expect(result).not.toBeNull();
- expect(result?.id).toBe(94);
- // clearSessionProvider should NOT have been called
- expect(sessionManagerMocks.SessionManager.clearSessionProvider).not.toHaveBeenCalled();
- });
- test("should NOT clear binding when shouldReuseProvider returns false", async () => {
- const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
- const session = {
- sessionId: "sess_short",
- shouldReuseProvider: () => false,
- getOriginalModel: () => "claude-opus-4-6",
- authState: null,
- } as any;
- const result = await (ProxyProviderResolver as any).findReusable(session);
- expect(result).toBeNull();
- // Should not even reach the model check, so no clear
- expect(sessionManagerMocks.SessionManager.clearSessionProvider).not.toHaveBeenCalled();
- expect(sessionManagerMocks.SessionManager.getSessionProvider).not.toHaveBeenCalled();
- });
- test("should clear binding for haiku-only provider when requesting haiku-4-5 variant not in allowlist", async () => {
- const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
- sessionManagerMocks.SessionManager.getSessionProvider.mockResolvedValueOnce(78);
- const provider = createHaikuOnlyProvider();
- // Restrictive allowlist - only allows specific variant
- provider.allowedModels = ["claude-haiku-4-5-20251001"];
- providerRepositoryMocks.findProviderById.mockResolvedValueOnce(provider);
- const session = {
- sessionId: "sess_variant",
- shouldReuseProvider: () => true,
- getOriginalModel: () => "claude-sonnet-4-5-20250929",
- authState: null,
- getCurrentModel: () => null,
- } as any;
- const result = await (ProxyProviderResolver as any).findReusable(session);
- expect(result).toBeNull();
- expect(sessionManagerMocks.SessionManager.clearSessionProvider).toHaveBeenCalledWith(
- "sess_variant"
- );
- });
- });
|