import { beforeEach, describe, expect, it, vi } from "vitest"; import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes"; const getSessionMock = vi.fn(); const findAllProvidersFreshMock = vi.fn(); const updateProvidersBatchMock = vi.fn(); const publishCacheInvalidationMock = vi.fn(); const redisStore = new Map(); function readRedisValue(key: string): string | null { const entry = redisStore.get(key); if (!entry) { return null; } if (entry.expiresAt <= Date.now()) { redisStore.delete(key); return null; } return entry.value; } const redisSetexMock = vi.fn(async (key: string, ttlSeconds: number, value: string) => { redisStore.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000, }); return "OK"; }); const redisGetMock = vi.fn(async (key: string) => readRedisValue(key)); const redisDelMock = vi.fn(async (key: string) => { const existed = redisStore.delete(key); return existed ? 1 : 0; }); const redisEvalMock = vi.fn(async (_script: string, _numKeys: number, key: string) => { const value = readRedisValue(key); if (value === null) { return null; } redisStore.delete(key); return value; }); vi.mock("@/lib/auth", () => ({ getSession: getSessionMock, })); vi.mock("@/repository/provider", () => ({ findAllProvidersFresh: findAllProvidersFreshMock, updateProvidersBatch: updateProvidersBatchMock, deleteProvidersBatch: vi.fn(), })); vi.mock("@/lib/cache/provider-cache", () => ({ publishProviderCacheInvalidation: publishCacheInvalidationMock, })); vi.mock("@/lib/redis/client", () => ({ getRedisClient: () => ({ status: "ready", setex: redisSetexMock, get: redisGetMock, del: redisDelMock, eval: redisEvalMock, }), })); vi.mock("@/lib/circuit-breaker", () => ({ clearProviderState: vi.fn(), clearConfigCache: vi.fn(), resetCircuit: vi.fn(), getAllHealthStatusAsync: vi.fn(), })); vi.mock("@/lib/logger", () => ({ logger: { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })); function makeProvider(id: number, overrides: Record = {}) { return { id, name: `Provider-${id}`, url: "https://api.example.com/v1", key: "sk-test", providerVendorId: null, isEnabled: true, weight: 100, priority: 1, groupPriorities: null, costMultiplier: 1.0, groupTag: null, providerType: "claude", preserveClientIp: false, modelRedirects: null, allowedModels: null, mcpPassthroughType: "none", mcpPassthroughUrl: null, limit5hUsd: null, limitDailyUsd: null, dailyResetMode: "fixed", dailyResetTime: "00:00", limitWeeklyUsd: null, limitMonthlyUsd: null, limitTotalUsd: null, totalCostResetAt: null, limitConcurrentSessions: null, maxRetryAttempts: null, circuitBreakerFailureThreshold: 5, circuitBreakerOpenDuration: 1800000, circuitBreakerHalfOpenSuccessThreshold: 2, proxyUrl: null, proxyFallbackToDirect: false, firstByteTimeoutStreamingMs: 30000, streamingIdleTimeoutMs: 10000, requestTimeoutNonStreamingMs: 600000, websiteUrl: null, faviconUrl: null, cacheTtlPreference: null, swapCacheTtlBilling: false, context1mPreference: null, codexReasoningEffortPreference: null, codexReasoningSummaryPreference: null, codexTextVerbosityPreference: null, codexParallelToolCallsPreference: null, anthropicMaxTokensPreference: null, anthropicThinkingBudgetPreference: null, anthropicAdaptiveThinking: null, geminiGoogleSearchPreference: null, tpm: null, rpm: null, rpd: null, cc: null, createdAt: new Date("2025-01-01"), updatedAt: new Date("2025-01-01"), deletedAt: null, ...overrides, }; } describe("Apply Provider Batch Patch Engine", () => { beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); redisStore.clear(); redisSetexMock.mockClear(); redisGetMock.mockClear(); redisDelMock.mockClear(); redisEvalMock.mockClear(); getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } }); findAllProvidersFreshMock.mockResolvedValue([]); updateProvidersBatchMock.mockResolvedValue(0); publishCacheInvalidationMock.mockResolvedValue(undefined); }); /** Helper: create preview then apply with optional overrides */ async function setupPreviewAndApply( providerIds: number[], patch: Record, applyOverrides: Record = {} ) { const { previewProviderBatchPatch, applyProviderBatchPatch } = await import( "@/actions/providers" ); const preview = await previewProviderBatchPatch({ providerIds, patch }); if (!preview.ok) throw new Error(`Preview failed: ${preview.error}`); const applyInput = { previewToken: preview.data.previewToken, previewRevision: preview.data.previewRevision, providerIds, patch, ...applyOverrides, }; const apply = await applyProviderBatchPatch(applyInput); return { preview, apply, applyProviderBatchPatch }; } it("should call updateProvidersBatch with correct IDs and updates", async () => { const providers = [makeProvider(1, { groupTag: "old" }), makeProvider(2, { groupTag: "old" })]; findAllProvidersFreshMock.mockResolvedValue(providers); updateProvidersBatchMock.mockResolvedValue(2); const { apply } = await setupPreviewAndApply([1, 2], { group_tag: { set: "new-group" } }); expect(apply.ok).toBe(true); expect(updateProvidersBatchMock).toHaveBeenCalledOnce(); expect(updateProvidersBatchMock).toHaveBeenCalledWith( [1, 2], expect.objectContaining({ groupTag: "new-group" }) ); }); it("should publish cache invalidation after successful write", async () => { findAllProvidersFreshMock.mockResolvedValue([makeProvider(1)]); updateProvidersBatchMock.mockResolvedValue(1); const { apply } = await setupPreviewAndApply([1], { is_enabled: { set: false } }); expect(apply.ok).toBe(true); expect(publishCacheInvalidationMock).toHaveBeenCalledOnce(); }); it("should fetch providers for preimage during apply", async () => { const providers = [ makeProvider(1, { groupTag: "alpha", priority: 5 }), makeProvider(2, { groupTag: "beta", priority: 10 }), ]; findAllProvidersFreshMock.mockResolvedValue(providers); updateProvidersBatchMock.mockResolvedValue(2); const { apply } = await setupPreviewAndApply([1, 2], { group_tag: { set: "gamma" } }); expect(apply.ok).toBe(true); // preview calls findAllProvidersFresh once, apply calls it once more expect(findAllProvidersFreshMock).toHaveBeenCalledTimes(2); }); it("should only apply to non-excluded providers with excludeProviderIds", async () => { const providers = [ makeProvider(1, { groupTag: "a" }), makeProvider(2, { groupTag: "b" }), makeProvider(3, { groupTag: "c" }), ]; findAllProvidersFreshMock.mockResolvedValue(providers); updateProvidersBatchMock.mockResolvedValue(2); const { apply } = await setupPreviewAndApply( [1, 2, 3], { group_tag: { set: "unified" } }, { excludeProviderIds: [2] } ); expect(apply.ok).toBe(true); expect(updateProvidersBatchMock).toHaveBeenCalledWith( [1, 3], expect.objectContaining({ groupTag: "unified" }) ); }); it("should return NOTHING_TO_APPLY when all providers are excluded", async () => { findAllProvidersFreshMock.mockResolvedValue([makeProvider(1), makeProvider(2)]); const { apply } = await setupPreviewAndApply( [1, 2], { group_tag: { set: "x" } }, { excludeProviderIds: [1, 2] } ); expect(apply.ok).toBe(false); if (apply.ok) return; expect(apply.errorCode).toBe(PROVIDER_BATCH_PATCH_ERROR_CODES.NOTHING_TO_APPLY); expect(updateProvidersBatchMock).not.toHaveBeenCalled(); }); it("should set updatedCount from updateProvidersBatch return value", async () => { findAllProvidersFreshMock.mockResolvedValue([ makeProvider(1), makeProvider(2), makeProvider(3), ]); updateProvidersBatchMock.mockResolvedValue(3); const { apply } = await setupPreviewAndApply([1, 2, 3], { weight: { set: 50 } }); expect(apply.ok).toBe(true); if (!apply.ok) return; expect(apply.data.updatedCount).toBe(3); }); it("should reflect exclusions in updatedCount", async () => { findAllProvidersFreshMock.mockResolvedValue([ makeProvider(1), makeProvider(2), makeProvider(3), ]); updateProvidersBatchMock.mockResolvedValue(2); const { apply } = await setupPreviewAndApply( [1, 2, 3], { weight: { set: 50 } }, { excludeProviderIds: [3] } ); expect(apply.ok).toBe(true); if (!apply.ok) return; expect(apply.data.updatedCount).toBe(2); }); it("should return PREVIEW_EXPIRED for unknown preview token", async () => { const { applyProviderBatchPatch } = await import("@/actions/providers"); const result = await applyProviderBatchPatch({ previewToken: "provider_patch_preview_nonexistent", previewRevision: "rev", providerIds: [1], patch: { group_tag: { set: "x" } }, }); expect(result.ok).toBe(false); if (result.ok) return; expect(result.errorCode).toBe(PROVIDER_BATCH_PATCH_ERROR_CODES.PREVIEW_EXPIRED); }); it("should return PREVIEW_STALE for mismatched patch", async () => { findAllProvidersFreshMock.mockResolvedValue([makeProvider(1)]); const { previewProviderBatchPatch, applyProviderBatchPatch } = await import( "@/actions/providers" ); const preview = await previewProviderBatchPatch({ providerIds: [1], patch: { group_tag: { set: "original" } }, }); if (!preview.ok) throw new Error("Preview should succeed"); const result = await applyProviderBatchPatch({ previewToken: preview.data.previewToken, previewRevision: preview.data.previewRevision, providerIds: [1], patch: { group_tag: { set: "different" } }, }); expect(result.ok).toBe(false); if (result.ok) return; expect(result.errorCode).toBe(PROVIDER_BATCH_PATCH_ERROR_CODES.PREVIEW_STALE); }); it("should return cached result for same idempotencyKey without re-writing to DB", async () => { findAllProvidersFreshMock.mockResolvedValue([makeProvider(1), makeProvider(2)]); updateProvidersBatchMock.mockResolvedValue(2); const { previewProviderBatchPatch, applyProviderBatchPatch } = await import( "@/actions/providers" ); const preview = await previewProviderBatchPatch({ providerIds: [1, 2], patch: { group_tag: { set: "idem" } }, }); if (!preview.ok) throw new Error("Preview should succeed"); const applyInput = { previewToken: preview.data.previewToken, previewRevision: preview.data.previewRevision, providerIds: [1, 2], patch: { group_tag: { set: "idem" } }, idempotencyKey: "idem-key-1", }; const first = await applyProviderBatchPatch(applyInput); const second = await applyProviderBatchPatch(applyInput); expect(first.ok).toBe(true); expect(second.ok).toBe(true); if (!first.ok || !second.ok) return; expect(second.data.operationId).toBe(first.data.operationId); expect(updateProvidersBatchMock).toHaveBeenCalledOnce(); }); it("should prevent double-apply by marking snapshot as applied", async () => { findAllProvidersFreshMock.mockResolvedValue([makeProvider(1)]); updateProvidersBatchMock.mockResolvedValue(1); const { previewProviderBatchPatch, applyProviderBatchPatch } = await import( "@/actions/providers" ); const preview = await previewProviderBatchPatch({ providerIds: [1], patch: { group_tag: { set: "x" } }, }); if (!preview.ok) throw new Error("Preview should succeed"); const applyInput = { previewToken: preview.data.previewToken, previewRevision: preview.data.previewRevision, providerIds: [1], patch: { group_tag: { set: "x" } }, }; const first = await applyProviderBatchPatch(applyInput); const second = await applyProviderBatchPatch(applyInput); expect(first.ok).toBe(true); expect(second.ok).toBe(false); if (second.ok) return; expect(second.errorCode).toBe(PROVIDER_BATCH_PATCH_ERROR_CODES.PREVIEW_STALE); }); it("should map cost_multiplier to string for repository", async () => { findAllProvidersFreshMock.mockResolvedValue([makeProvider(1, { costMultiplier: 1.0 })]); updateProvidersBatchMock.mockResolvedValue(1); const { apply } = await setupPreviewAndApply([1], { cost_multiplier: { set: 2.5 } }); expect(apply.ok).toBe(true); expect(updateProvidersBatchMock).toHaveBeenCalledWith( [1], expect.objectContaining({ costMultiplier: "2.5" }) ); }); it("should map multiple fields correctly to repository format", async () => { findAllProvidersFreshMock.mockResolvedValue([ makeProvider(1, { groupTag: "old", weight: 100, priority: 1 }), ]); updateProvidersBatchMock.mockResolvedValue(1); const { apply } = await setupPreviewAndApply([1], { group_tag: { set: "new" }, weight: { set: 80 }, priority: { set: 5 }, }); expect(apply.ok).toBe(true); expect(updateProvidersBatchMock).toHaveBeenCalledWith( [1], expect.objectContaining({ groupTag: "new", weight: 80, priority: 5, }) ); }); it("should map clear mode to null for clearable fields", async () => { findAllProvidersFreshMock.mockResolvedValue([ makeProvider(1, { groupTag: "has-tag", modelRedirects: { a: "b" } }), ]); updateProvidersBatchMock.mockResolvedValue(1); const { apply } = await setupPreviewAndApply([1], { group_tag: { clear: true }, model_redirects: { clear: true }, }); expect(apply.ok).toBe(true); expect(updateProvidersBatchMock).toHaveBeenCalledWith( [1], expect.objectContaining({ groupTag: null, modelRedirects: null, }) ); }); it("should map anthropic_thinking_budget_preference clear to inherit", async () => { findAllProvidersFreshMock.mockResolvedValue([ makeProvider(1, { anthropicThinkingBudgetPreference: "8192" }), ]); updateProvidersBatchMock.mockResolvedValue(1); const { apply } = await setupPreviewAndApply([1], { anthropic_thinking_budget_preference: { clear: true }, }); expect(apply.ok).toBe(true); expect(updateProvidersBatchMock).toHaveBeenCalledWith( [1], expect.objectContaining({ anthropicThinkingBudgetPreference: "inherit", }) ); }); });