Преглед изворни кода

perf(providers): async architecture refactor for provider save
performance

- Remove all revalidatePath blocking calls from CRUD operations
- Add getProviderStatisticsAsync() for independent statistics loading
- Frontend: separate useQuery for statistics with 30s staleTime, 60s
refetchInterval
- Add Skeleton loading states for statistics columns
- Add ProviderStatistics and ProviderStatisticsMap types
- Fix Chinese console.error messages to English for consistency
- Add 9 unit tests for async optimization

Expected performance improvement:
- Create provider: 30s -> <500ms
- Update provider: 10-20s -> <500ms
- Delete provider: 5-10s -> <200ms

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

ding113 пре 1 месец
родитељ
комит
54bada89

+ 49 - 22
src/actions/providers.ts

@@ -1,6 +1,5 @@
 "use server";
 
-import { revalidatePath } from "next/cache";
 import { GeminiAuth } from "@/app/v1/_lib/gemini/auth";
 import { isClientAbortError } from "@/app/v1/_lib/proxy/errors";
 import { getSession } from "@/lib/auth";
@@ -50,6 +49,7 @@ import type {
   CodexReasoningSummaryPreference,
   CodexTextVerbosityPreference,
   ProviderDisplay,
+  ProviderStatisticsMap,
   ProviderType,
 } from "@/types/provider";
 import type { ActionResult } from "./types";
@@ -144,19 +144,10 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
       return [];
     }
 
-    // 并行获取供应商列表和统计数据
-    const [providers, statistics] = await Promise.all([
-      findAllProvidersFresh(),
-      getProviderStatistics().catch((error) => {
-        logger.trace("getProviders:statistics_error", {
-          message: error.message,
-          stack: error.stack,
-          name: error.name,
-        });
-        logger.error("获取供应商统计数据失败:", error);
-        return []; // 统计查询失败时返回空数组,不影响供应商列表显示
-      }),
-    ]);
+    // 仅获取供应商列表,统计数据由前端异步获取
+    const providers = await findAllProvidersFresh();
+    // 空统计数组,保持后续合并逻辑兼容
+    const statistics: Awaited<ReturnType<typeof getProviderStatistics>> = [];
 
     logger.trace("getProviders:raw_data", {
       providerCount: providers.length,
@@ -281,6 +272,50 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
   }
 }
 
+/**
+ * Async get provider statistics data (today cost, call count, last call info)
+ * Called independently by frontend, does not block main list loading
+ */
+export async function getProviderStatisticsAsync(): Promise<ProviderStatisticsMap> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") return {};
+
+    const statistics = await getProviderStatistics();
+
+    // Transform to Record<providerId, stats> format
+    const result: ProviderStatisticsMap = {};
+
+    for (const s of statistics) {
+      let lastCallTimeStr: string | null = null;
+      if (s.last_call_time) {
+        if (s.last_call_time instanceof Date) {
+          lastCallTimeStr = s.last_call_time.toISOString();
+        } else if (typeof s.last_call_time === "string") {
+          lastCallTimeStr = s.last_call_time;
+        } else {
+          const date = new Date(s.last_call_time as string | number);
+          if (!Number.isNaN(date.getTime())) {
+            lastCallTimeStr = date.toISOString();
+          }
+        }
+      }
+
+      result[s.id] = {
+        todayCost: s.today_cost,
+        todayCalls: s.today_calls,
+        lastCallTime: lastCallTimeStr,
+        lastCallModel: s.last_call_model,
+      };
+    }
+
+    return result;
+  } catch (error) {
+    logger.error("Failed to get provider statistics async:", error);
+    return {};
+  }
+}
+
 /**
  * 获取所有可用的供应商分组标签(用于用户表单中的下拉建议)
  */
@@ -523,9 +558,6 @@ export async function addProvider(data: {
     // 广播缓存更新(跨实例即时生效)
     await broadcastProviderCacheInvalidation({ operation: "add", providerId: provider.id });
 
-    revalidatePath("/settings/providers");
-    logger.trace("addProvider:revalidated", { path: "/settings/providers" });
-
     return { ok: true };
   } catch (error) {
     logger.trace("addProvider:error", {
@@ -664,7 +696,6 @@ export async function editProvider(
     // 广播缓存更新(跨实例即时生效)
     await broadcastProviderCacheInvalidation({ operation: "edit", providerId });
 
-    revalidatePath("/settings/providers");
     return { ok: true };
   } catch (error) {
     logger.error("更新服务商失败:", error);
@@ -701,7 +732,6 @@ export async function removeProvider(providerId: number): Promise<ActionResult>
     // 广播缓存更新(跨实例即时生效)
     await broadcastProviderCacheInvalidation({ operation: "remove", providerId });
 
-    revalidatePath("/settings/providers");
     return { ok: true };
   } catch (error) {
     logger.error("删除服务商失败:", error);
@@ -770,7 +800,6 @@ export async function resetProviderCircuit(providerId: number): Promise<ActionRe
     }
 
     resetCircuit(providerId);
-    revalidatePath("/settings/providers");
 
     return { ok: true };
   } catch (error) {
@@ -798,8 +827,6 @@ export async function resetProviderTotalUsage(providerId: number): Promise<Actio
       return { ok: false, error: "供应商不存在" };
     }
 
-    revalidatePath("/settings/providers");
-    revalidatePath("/dashboard/quotas/providers");
     return { ok: true };
   } catch (error) {
     logger.error("重置供应商总用量失败:", error);

+ 2 - 2
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-stats.tsx

@@ -1,6 +1,5 @@
 "use client";
 
-import { useTranslations } from "next-intl";
 import {
   Calendar,
   Clock,
@@ -12,11 +11,12 @@ import {
   Server,
   Zap,
 } from "lucide-react";
+import { useTranslations } from "next-intl";
 import { Badge } from "@/components/ui/badge";
 import { Separator } from "@/components/ui/separator";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { cn } from "@/lib/utils";
 import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 
 interface SessionStatsProps {
   stats: {

+ 7 - 1
src/app/[locale]/settings/providers/_components/provider-list.tsx

@@ -2,7 +2,7 @@
 import { Globe } from "lucide-react";
 import { useTranslations } from "next-intl";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import type { ProviderDisplay } from "@/types/provider";
+import type { ProviderDisplay, ProviderStatisticsMap } from "@/types/provider";
 import type { User } from "@/types/user";
 import { ProviderRichListItem } from "./provider-rich-list-item";
 
@@ -19,6 +19,8 @@ interface ProviderListProps {
       recoveryMinutes: number | null;
     }
   >;
+  statistics?: ProviderStatisticsMap;
+  statisticsLoading?: boolean;
   currencyCode?: CurrencyCode;
   enableMultiProviderTypes: boolean;
 }
@@ -27,6 +29,8 @@ export function ProviderList({
   providers,
   currentUser,
   healthStatus,
+  statistics = {},
+  statisticsLoading = false,
   currencyCode = "USD",
   enableMultiProviderTypes,
 }: ProviderListProps) {
@@ -52,6 +56,8 @@ export function ProviderList({
           provider={provider}
           currentUser={currentUser}
           healthStatus={healthStatus[provider.id]}
+          statistics={statistics[provider.id]}
+          statisticsLoading={statisticsLoading}
           currencyCode={currencyCode}
           enableMultiProviderTypes={enableMultiProviderTypes}
         />

+ 17 - 2
src/app/[locale]/settings/providers/_components/provider-manager-loader.tsx

@@ -1,9 +1,13 @@
 "use client";
 
 import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
-import { getProviders, getProvidersHealthStatus } from "@/actions/providers";
+import {
+  getProviderStatisticsAsync,
+  getProviders,
+  getProvidersHealthStatus,
+} from "@/actions/providers";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import type { ProviderDisplay } from "@/types/provider";
+import type { ProviderDisplay, ProviderStatisticsMap } from "@/types/provider";
 import type { User } from "@/types/user";
 import { AddProviderDialog } from "./add-provider-dialog";
 import { ProviderManager } from "./provider-manager";
@@ -63,6 +67,15 @@ function ProviderManagerLoaderContent({
     queryFn: getProvidersHealthStatus,
   });
 
+  // Statistics loaded independently with longer cache
+  const { data: statistics = {} as ProviderStatisticsMap, isLoading: isStatisticsLoading } =
+    useQuery<ProviderStatisticsMap>({
+      queryKey: ["providers-statistics"],
+      queryFn: getProviderStatisticsAsync,
+      staleTime: 30_000,
+      refetchInterval: 60_000,
+    });
+
   const {
     data: systemSettings,
     isLoading: isSettingsLoading,
@@ -81,6 +94,8 @@ function ProviderManagerLoaderContent({
       providers={providers}
       currentUser={currentUser}
       healthStatus={healthStatus}
+      statistics={statistics}
+      statisticsLoading={isStatisticsLoading}
       currencyCode={currencyCode}
       enableMultiProviderTypes={enableMultiProviderTypes}
       loading={loading}

+ 7 - 1
src/app/[locale]/settings/providers/_components/provider-manager.tsx

@@ -16,7 +16,7 @@ import { Skeleton } from "@/components/ui/skeleton";
 import { Switch } from "@/components/ui/switch";
 import { useDebounce } from "@/lib/hooks/use-debounce";
 import type { CurrencyCode } from "@/lib/utils/currency";
-import type { ProviderDisplay, ProviderType } from "@/types/provider";
+import type { ProviderDisplay, ProviderStatisticsMap, ProviderType } from "@/types/provider";
 import type { User } from "@/types/user";
 import { ProviderList } from "./provider-list";
 import { ProviderSortDropdown, type SortKey } from "./provider-sort-dropdown";
@@ -35,6 +35,8 @@ interface ProviderManagerProps {
       recoveryMinutes: number | null;
     }
   >;
+  statistics?: ProviderStatisticsMap;
+  statisticsLoading?: boolean;
   currencyCode?: CurrencyCode;
   enableMultiProviderTypes: boolean;
   loading?: boolean;
@@ -46,6 +48,8 @@ export function ProviderManager({
   providers,
   currentUser,
   healthStatus,
+  statistics = {},
+  statisticsLoading = false,
   currencyCode = "USD",
   enableMultiProviderTypes,
   loading = false,
@@ -317,6 +321,8 @@ export function ProviderManager({
             providers={filteredProviders}
             currentUser={currentUser}
             healthStatus={healthStatus}
+            statistics={statistics}
+            statisticsLoading={statisticsLoading}
             currencyCode={currencyCode}
             enableMultiProviderTypes={enableMultiProviderTypes}
           />

+ 31 - 12
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx

@@ -42,6 +42,7 @@ import {
   DialogHeader,
   DialogTitle,
 } from "@/components/ui/dialog";
+import { Skeleton } from "@/components/ui/skeleton";
 import { Switch } from "@/components/ui/switch";
 import { PROVIDER_GROUP, PROVIDER_LIMITS } from "@/lib/constants/provider.constants";
 import { getProviderTypeConfig, getProviderTypeTranslationKey } from "@/lib/provider-type-utils";
@@ -49,7 +50,7 @@ import { copyToClipboard, isClipboardSupported } from "@/lib/utils/clipboard";
 import { getContrastTextColor, getGroupColor } from "@/lib/utils/color";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { formatCurrency } from "@/lib/utils/currency";
-import type { ProviderDisplay } from "@/types/provider";
+import type { ProviderDisplay, ProviderStatistics } from "@/types/provider";
 import type { User } from "@/types/user";
 import { ProviderForm } from "./forms/provider-form";
 import { InlineEditPopover } from "./inline-edit-popover";
@@ -64,6 +65,8 @@ interface ProviderRichListItemProps {
     circuitOpenUntil: number | null;
     recoveryMinutes: number | null;
   };
+  statistics?: ProviderStatistics;
+  statisticsLoading?: boolean;
   currencyCode?: CurrencyCode;
   enableMultiProviderTypes: boolean;
   onEdit?: () => void;
@@ -75,6 +78,8 @@ export function ProviderRichListItem({
   provider,
   currentUser,
   healthStatus,
+  statistics,
+  statisticsLoading = false,
   currencyCode = "USD",
   enableMultiProviderTypes,
   onEdit: onEditProp,
@@ -178,7 +183,7 @@ export function ProviderRichListItem({
             });
           }
         } catch (error) {
-          console.error("删除供应商失败:", error);
+          console.error("Failed to delete provider:", error);
           toast.error(tList("deleteFailed"), {
             description: tList("deleteError"),
           });
@@ -249,7 +254,7 @@ export function ProviderRichListItem({
           });
         }
       } catch (error) {
-        console.error("重置熔断器失败:", error);
+        console.error("Failed to reset circuit breaker:", error);
         toast.error(tList("resetCircuitFailed"), {
           description: tList("deleteError"),
         });
@@ -275,7 +280,7 @@ export function ProviderRichListItem({
           });
         }
       } catch (error) {
-        console.error("重置总用量失败:", error);
+        console.error("Failed to reset total usage:", error);
         toast.error(tList("resetUsageFailed"), {
           description: tList("deleteError"),
         });
@@ -304,7 +309,7 @@ export function ProviderRichListItem({
           });
         }
       } catch (error) {
-        console.error("状态切换失败:", error);
+        console.error("Failed to toggle provider status:", error);
         toast.error(tList("toggleFailed"), {
           description: tList("deleteError"),
         });
@@ -327,7 +332,7 @@ export function ProviderRichListItem({
         toast.error(tInline("saveFailed"), { description: res.error || tList("unknownError") });
         return false;
       } catch (error) {
-        console.error(`更新 ${fieldName} 失败:`, error);
+        console.error(`Failed to update ${fieldName}:`, error);
         toast.error(tInline("saveFailed"), { description: tList("unknownError") });
         return false;
       }
@@ -529,12 +534,26 @@ export function ProviderRichListItem({
         {/* 今日用量(仅大屏) */}
         <div className="hidden lg:block text-center flex-shrink-0 min-w-[100px]">
           <div className="text-xs text-muted-foreground">{tList("todayUsageLabel")}</div>
-          <div className="font-medium">
-            {tList("todayUsageCount", { count: provider.todayCallCount || 0 })}
-          </div>
-          <div className="text-xs font-mono text-muted-foreground mt-0.5">
-            {formatCurrency(parseFloat(provider.todayTotalCostUsd || "0"), currencyCode)}
-          </div>
+          {statisticsLoading ? (
+            <>
+              <Skeleton className="h-5 w-16 mx-auto my-0.5" />
+              <Skeleton className="h-4 w-12 mx-auto mt-0.5" />
+            </>
+          ) : (
+            <>
+              <div className="font-medium">
+                {tList("todayUsageCount", {
+                  count: statistics?.todayCalls ?? provider.todayCallCount ?? 0,
+                })}
+              </div>
+              <div className="text-xs font-mono text-muted-foreground mt-0.5">
+                {formatCurrency(
+                  parseFloat(statistics?.todayCost ?? provider.todayTotalCostUsd ?? "0"),
+                  currencyCode
+                )}
+              </div>
+            </>
+          )}
         </div>
 
         {/* 操作按钮 */}

+ 16 - 0
src/types/provider.ts

@@ -212,6 +212,22 @@ export interface ProviderDisplay {
   lastCallModel?: string | null;
 }
 
+/**
+ * Provider statistics loaded asynchronously
+ * Used by getProviderStatisticsAsync() return type
+ */
+export interface ProviderStatistics {
+  todayCost: string;
+  todayCalls: number;
+  lastCallTime: string | null;
+  lastCallModel: string | null;
+}
+
+/**
+ * Map of provider ID to statistics
+ */
+export type ProviderStatisticsMap = Record<number, ProviderStatistics>;
+
 export interface CreateProviderData {
   name: string;
   url: string;

+ 307 - 0
tests/unit/actions/providers.test.ts

@@ -0,0 +1,307 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const getSessionMock = vi.fn();
+
+const findAllProvidersFreshMock = vi.fn();
+const getProviderStatisticsMock = vi.fn();
+const createProviderMock = vi.fn();
+const updateProviderMock = vi.fn();
+const deleteProviderMock = vi.fn();
+
+const publishProviderCacheInvalidationMock = vi.fn();
+const saveProviderCircuitConfigMock = vi.fn();
+const deleteProviderCircuitConfigMock = vi.fn();
+const clearConfigCacheMock = vi.fn();
+const clearProviderStateMock = vi.fn();
+
+const revalidatePathMock = vi.fn();
+
+vi.mock("@/lib/auth", () => ({
+  getSession: getSessionMock,
+}));
+
+vi.mock("@/repository/provider", () => ({
+  createProvider: createProviderMock,
+  deleteProvider: deleteProviderMock,
+  findAllProviders: vi.fn(async () => []),
+  findAllProvidersFresh: findAllProvidersFreshMock,
+  findProviderById: vi.fn(async () => null),
+  getProviderStatistics: getProviderStatisticsMock,
+  resetProviderTotalCostResetAt: vi.fn(async () => {}),
+  updateProvider: updateProviderMock,
+}));
+
+vi.mock("@/lib/cache/provider-cache", () => ({
+  publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
+}));
+
+vi.mock("@/lib/redis/circuit-breaker-config", () => ({
+  deleteProviderCircuitConfig: deleteProviderCircuitConfigMock,
+  saveProviderCircuitConfig: saveProviderCircuitConfigMock,
+}));
+
+vi.mock("@/lib/circuit-breaker", () => ({
+  clearConfigCache: clearConfigCacheMock,
+  clearProviderState: clearProviderStateMock,
+  getAllHealthStatusAsync: vi.fn(async () => ({})),
+  resetCircuit: vi.fn(),
+}));
+
+vi.mock("@/lib/logger", () => ({
+  logger: {
+    trace: vi.fn(),
+    debug: vi.fn(),
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+vi.mock("next/cache", () => ({
+  revalidatePath: revalidatePathMock,
+}));
+
+function nowMs(): number {
+  if (typeof performance !== "undefined" && typeof performance.now === "function") {
+    return performance.now();
+  }
+  return Date.now();
+}
+
+async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
+  let timeoutId: ReturnType<typeof setTimeout> | undefined;
+  const timeout = new Promise<never>((_, reject) => {
+    timeoutId = setTimeout(() => reject(new Error(`超时:${ms}ms`)), ms);
+  });
+
+  try {
+    return await Promise.race([promise, timeout]);
+  } finally {
+    if (timeoutId) clearTimeout(timeoutId);
+  }
+}
+
+describe("Provider Actions - Async Optimization", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+
+    getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
+
+    findAllProvidersFreshMock.mockResolvedValue([
+      {
+        id: 1,
+        name: "p1",
+        url: "https://api.example.com",
+        key: "sk-test-1234567890",
+        isEnabled: true,
+        weight: 1,
+        priority: 0,
+        costMultiplier: 1,
+        groupTag: "default",
+        providerType: "claude",
+        preserveClientIp: false,
+        modelRedirects: null,
+        allowedModels: null,
+        joinClaudePool: false,
+        codexInstructionsStrategy: "inherit",
+        mcpPassthroughType: "none",
+        mcpPassthroughUrl: null,
+        limit5hUsd: null,
+        limitDailyUsd: null,
+        dailyResetMode: "fixed",
+        dailyResetTime: "00:00",
+        limitWeeklyUsd: null,
+        limitMonthlyUsd: null,
+        limitTotalUsd: null,
+        limitConcurrentSessions: 0,
+        maxRetryAttempts: null,
+        circuitBreakerFailureThreshold: 5,
+        circuitBreakerOpenDuration: 1800000,
+        circuitBreakerHalfOpenSuccessThreshold: 2,
+        proxyUrl: null,
+        proxyFallbackToDirect: false,
+        firstByteTimeoutStreamingMs: null,
+        streamingIdleTimeoutMs: null,
+        requestTimeoutNonStreamingMs: null,
+        websiteUrl: null,
+        faviconUrl: null,
+        cacheTtlPreference: "inherit",
+        context1mPreference: "inherit",
+        codexReasoningEffortPreference: "inherit",
+        codexReasoningSummaryPreference: "inherit",
+        codexTextVerbosityPreference: "inherit",
+        codexParallelToolCallsPreference: "inherit",
+        tpm: null,
+        rpm: null,
+        rpd: null,
+        cc: null,
+        createdAt: new Date("2026-01-01T00:00:00.000Z"),
+        updatedAt: new Date("2026-01-01T00:00:00.000Z"),
+      },
+    ]);
+
+    getProviderStatisticsMock.mockResolvedValue([]);
+
+    createProviderMock.mockResolvedValue({
+      id: 123,
+      circuitBreakerFailureThreshold: 5,
+      circuitBreakerOpenDuration: 1800000,
+      circuitBreakerHalfOpenSuccessThreshold: 2,
+    });
+
+    updateProviderMock.mockResolvedValue({
+      id: 1,
+      circuitBreakerFailureThreshold: 5,
+      circuitBreakerOpenDuration: 1800000,
+      circuitBreakerHalfOpenSuccessThreshold: 2,
+    });
+
+    deleteProviderMock.mockResolvedValue(undefined);
+    publishProviderCacheInvalidationMock.mockResolvedValue(undefined);
+    saveProviderCircuitConfigMock.mockResolvedValue(undefined);
+    deleteProviderCircuitConfigMock.mockResolvedValue(undefined);
+    clearProviderStateMock.mockResolvedValue(undefined);
+  });
+
+  describe("getProviders", () => {
+    it("should return providers without blocking on statistics", async () => {
+      getProviderStatisticsMock.mockImplementation(() => new Promise(() => {}));
+
+      const { getProviders } = await import("@/actions/providers");
+      const result = await withTimeout(getProviders(), 200);
+
+      expect(result).toHaveLength(1);
+      expect(result[0]?.id).toBe(1);
+      expect(getProviderStatisticsMock).not.toHaveBeenCalled();
+    });
+
+    it("should complete within 500ms", async () => {
+      getProviderStatisticsMock.mockImplementation(() => new Promise(() => {}));
+
+      const { getProviders } = await import("@/actions/providers");
+      const start = nowMs();
+      const result = await withTimeout(getProviders(), 500);
+      const elapsed = nowMs() - start;
+
+      expect(result).toHaveLength(1);
+      expect(elapsed).toBeLessThan(500);
+    });
+  });
+
+  describe("getProviderStatisticsAsync", () => {
+    it("should return statistics map by provider id", async () => {
+      getProviderStatisticsMock.mockResolvedValue([
+        {
+          id: 1,
+          today_cost: "1.23",
+          today_calls: 10,
+          last_call_time: new Date("2026-01-01T00:00:00.000Z"),
+          last_call_model: "model-a",
+        },
+        {
+          id: 2,
+          today_cost: "0",
+          today_calls: 0,
+          last_call_time: "2026-01-02T00:00:00.000Z",
+          last_call_model: null,
+        },
+      ]);
+
+      const { getProviderStatisticsAsync } = await import("@/actions/providers");
+      const result = await getProviderStatisticsAsync();
+
+      expect(result[1]).toEqual({
+        todayCost: "1.23",
+        todayCalls: 10,
+        lastCallTime: "2026-01-01T00:00:00.000Z",
+        lastCallModel: "model-a",
+      });
+      expect(result[2]).toEqual({
+        todayCost: "0",
+        todayCalls: 0,
+        lastCallTime: "2026-01-02T00:00:00.000Z",
+        lastCallModel: null,
+      });
+    });
+
+    it("should return empty object for non-admin", async () => {
+      getSessionMock.mockResolvedValueOnce({ user: { id: 2, role: "user" } });
+
+      const { getProviderStatisticsAsync } = await import("@/actions/providers");
+      const result = await getProviderStatisticsAsync();
+
+      expect(result).toEqual({});
+      expect(getProviderStatisticsMock).not.toHaveBeenCalled();
+    });
+
+    it("should handle errors gracefully and return empty object", async () => {
+      getProviderStatisticsMock.mockRejectedValueOnce(new Error("boom"));
+
+      const { getProviderStatisticsAsync } = await import("@/actions/providers");
+      const result = await getProviderStatisticsAsync();
+
+      expect(result).toEqual({});
+    });
+  });
+
+  describe("addProvider", () => {
+    it("should not call revalidatePath", async () => {
+      const { addProvider } = await import("@/actions/providers");
+      const result = await addProvider({
+        name: "p2",
+        url: "https://api.example.com",
+        key: "sk-test-2",
+        tpm: null,
+        rpm: null,
+        rpd: null,
+        cc: null,
+      });
+
+      expect(result.ok).toBe(true);
+      expect(revalidatePathMock).not.toHaveBeenCalled();
+    });
+
+    it("should complete quickly without blocking", async () => {
+      const { addProvider } = await import("@/actions/providers");
+      const start = nowMs();
+      await withTimeout(
+        addProvider({
+          name: "p2",
+          url: "https://api.example.com",
+          key: "sk-test-2",
+          tpm: null,
+          rpm: null,
+          rpd: null,
+          cc: null,
+        }),
+        500
+      );
+      const elapsed = nowMs() - start;
+
+      expect(elapsed).toBeLessThan(500);
+      expect(revalidatePathMock).not.toHaveBeenCalled();
+    });
+  });
+
+  // 说明:当前代码实现的函数名为 editProvider/removeProvider。
+  // 这里按需求用例命名 describe,但实际调用对应实现以确保测试可编译、可运行。
+  describe("updateProvider", () => {
+    it("should not call revalidatePath", async () => {
+      const { editProvider } = await import("@/actions/providers");
+      const result = await editProvider(1, { name: "p1-updated" });
+
+      expect(result.ok).toBe(true);
+      expect(revalidatePathMock).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("deleteProvider", () => {
+    it("should not call revalidatePath", async () => {
+      const { removeProvider } = await import("@/actions/providers");
+      const result = await removeProvider(1);
+
+      expect(result.ok).toBe(true);
+      expect(revalidatePathMock).not.toHaveBeenCalled();
+    });
+  });
+});