| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 |
- 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<string, { value: string; expiresAt: number }>();
- 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<string, unknown> = {}) {
- 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<string, unknown>,
- applyOverrides: Record<string, unknown> = {}
- ) {
- 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",
- })
- );
- });
- });
|