/** * Unit tests for x-connection-id pinning in src/sse/handlers/chat.js * * Covers: * - the dashboard per-key test header is forwarded to getProviderCredentials * as preferredConnectionId (same contract as embeddings/images/video) * - a pinned request does NOT rotate to another account on failure * - an unpinned request keeps rotating (regression guard) */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; const authMocks = vi.hoisted(() => ({ getProviderCredentials: vi.fn(), markAccountUnavailable: vi.fn(async () => ({ shouldFallback: true })), clearAccountError: vi.fn(async () => {}), extractApiKey: vi.fn(() => null), isValidApiKey: vi.fn(async () => true), isModelAllowedForKey: vi.fn(() => true), })); const tokenMocks = vi.hoisted(() => ({ checkAndRefreshToken: vi.fn(async (_p, creds) => creds), updateProviderCredentials: vi.fn(async () => {}), })); vi.mock("@/sse/services/auth.js", () => authMocks); vi.mock("@/sse/services/tokenRefresh.js", () => tokenMocks); vi.mock("@/lib/localDb", () => ({ getSettings: vi.fn(async () => ({ requireApiKey: false })), getComboByName: vi.fn(async () => null), getModelAliases: vi.fn(async () => ({})), getCustomModels: vi.fn(async () => []), getProviderNodes: vi.fn(async () => []), getProviderConnections: vi.fn(async () => []), updateProviderCredentials: vi.fn(async () => {}), })); vi.mock("@/sse/utils/logger.js", () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), maskKey: (k) => k, })); // chatCore would perform the upstream call; stub success so the loop exits. vi.mock("open-sse/handlers/chatCore.js", () => ({ handleChatCore: vi.fn(async ({ credentials }) => ({ success: true, response: new Response(JSON.stringify({ servedBy: credentials.connectionId }), { status: 200, headers: { "Content-Type": "application/json" }, }), })), })); vi.mock("open-sse/services/combo.js", () => ({ getComboModels: vi.fn(async () => null), resetComboRotation: vi.fn(), detectRequiredCapabilities: vi.fn(() => new Set()), augmentModelsWithCapacityAdapter: vi.fn((m) => m), withCapacityAdapterStripping: vi.fn((fn) => fn), getActiveAdapterStrategy: vi.fn(() => "fallback"), })); import { handleChat } from "@/sse/handlers/chat.js"; const originalFetch = global.fetch; const makeRequest = (body, headers = {}) => new Request("http://localhost/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify(body), }); const account = (id) => ({ connectionId: id, connectionName: `acc-${id}`, accessToken: "tok", refreshToken: "ref", authType: "oauth", }); beforeEach(() => { global.fetch = vi.fn(); authMocks.getProviderCredentials.mockReset(); authMocks.getProviderCredentials.mockImplementation(async (_p, exclude) => exclude.size === 0 ? account("conn-A") : null, ); authMocks.markAccountUnavailable.mockClear(); authMocks.clearAccountError.mockClear(); tokenMocks.checkAndRefreshToken.mockClear(); }); afterEach(() => { global.fetch = originalFetch; }); describe("chat x-connection-id pinning", () => { it("forwards x-connection-id as preferredConnectionId", async () => { const res = await handleChat( makeRequest({ model: "prov/m1", messages: [{ role: "user", content: "hi" }] }, { "x-connection-id": "conn-B" }), ); expect(res.status).toBe(200); expect(authMocks.getProviderCredentials).toHaveBeenCalledWith( "prov", expect.anything(), "m1", expect.objectContaining({ preferredConnectionId: "conn-B" }), ); }); it("passes null when no pin header is present", async () => { await handleChat(makeRequest({ model: "prov/m1", messages: [{ role: "user", content: "hi" }] })); expect(authMocks.getProviderCredentials).toHaveBeenCalledWith( "prov", expect.anything(), "m1", expect.objectContaining({ preferredConnectionId: null }), ); }); });