Просмотр исходного кода

fix(leaderboard): honor range in user insights overview

Ding 3 недель назад
Родитель
Сommit
d2db683e43

+ 2 - 2
messages/en/dashboard.json

@@ -493,8 +493,8 @@
       "overview": "Overview",
       "keyTrend": "Key Usage Trend",
       "modelBreakdown": "Model Breakdown",
-      "todayRequests": "Today Requests",
-      "todayCost": "Today Cost",
+      "requests": "Requests",
+      "cost": "Cost",
       "avgResponseTime": "Avg Response Time",
       "errorRate": "Error Rate",
       "timeRange": {

+ 2 - 2
messages/ja/dashboard.json

@@ -493,8 +493,8 @@
       "overview": "概要",
       "keyTrend": "Key 使用トレンド",
       "modelBreakdown": "モデル内訳",
-      "todayRequests": "本日リクエスト",
-      "todayCost": "本日コスト",
+      "requests": "リクエスト数",
+      "cost": "コスト",
       "avgResponseTime": "平均応答時間",
       "errorRate": "エラー率",
       "timeRange": {

+ 2 - 2
messages/ru/dashboard.json

@@ -493,8 +493,8 @@
       "overview": "Обзор",
       "keyTrend": "Тренд использования ключей",
       "modelBreakdown": "Разбивка по моделям",
-      "todayRequests": "Запросы за сегодня",
-      "todayCost": "Стоимость за сегодня",
+      "requests": "Запросы",
+      "cost": "Стоимость",
       "avgResponseTime": "Среднее время ответа",
       "errorRate": "Частота ошибок",
       "timeRange": {

+ 2 - 2
messages/zh-CN/dashboard.json

@@ -493,8 +493,8 @@
       "overview": "概览",
       "keyTrend": "Key 使用趋势",
       "modelBreakdown": "模型明细",
-      "todayRequests": "今日请求",
-      "todayCost": "今日费用",
+      "requests": "请求数",
+      "cost": "费用",
       "avgResponseTime": "平均响应时间",
       "errorRate": "错误率",
       "timeRange": {

+ 2 - 2
messages/zh-TW/dashboard.json

@@ -493,8 +493,8 @@
       "overview": "概覽",
       "keyTrend": "Key 使用趨勢",
       "modelBreakdown": "模型明細",
-      "todayRequests": "今日請求",
-      "todayCost": "今日費用",
+      "requests": "請求數",
+      "cost": "費用",
       "avgResponseTime": "平均回應時間",
       "errorRate": "錯誤率",
       "timeRange": {

+ 13 - 6
src/actions/admin-user-insights.ts

@@ -1,15 +1,15 @@
 "use server";
 
 import { getSession } from "@/lib/auth";
-import { getOverviewWithCache } from "@/lib/redis/overview-cache";
 import { getStatisticsWithCache } from "@/lib/redis/statistics-cache";
 import {
   type AdminUserModelBreakdownItem,
   type AdminUserProviderBreakdownItem,
   getUserModelBreakdown,
+  getUserOverviewMetrics,
   getUserProviderBreakdown,
+  type UserInsightsOverviewMetrics,
 } from "@/repository/admin-user-insights";
-import type { OverviewMetricsWithComparison } from "@/repository/overview";
 import { getSystemSettings } from "@/repository/system-config";
 import { findUserById } from "@/repository/user";
 import type { DatabaseKeyStatRow } from "@/types/statistics";
@@ -26,12 +26,16 @@ function isValidTimeRange(value: string): value is ValidTimeRange {
 }
 
 /**
- * Get overview metrics for a specific user (admin only).
+ * Get overview metrics for a specific user and date range (admin only).
  */
-export async function getUserInsightsOverview(targetUserId: number): Promise<
+export async function getUserInsightsOverview(
+  targetUserId: number,
+  startDate?: string,
+  endDate?: string
+): Promise<
   ActionResult<{
     user: User;
-    overview: OverviewMetricsWithComparison;
+    overview: UserInsightsOverviewMetrics;
     currencyCode: string;
   }>
 > {
@@ -40,13 +44,16 @@ export async function getUserInsightsOverview(targetUserId: number): Promise<
     return { ok: false, error: "Unauthorized" };
   }
 
+  const dateError = validateDateRange(startDate, endDate);
+  if (dateError) return dateError;
+
   const user = await findUserById(targetUserId);
   if (!user) {
     return { ok: false, error: "User not found" };
   }
 
   const [overview, settings] = await Promise.all([
-    getOverviewWithCache(targetUserId),
+    getUserOverviewMetrics(targetUserId, startDate, endDate),
     getSystemSettings(),
   ]);
 

+ 1 - 1
src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsx

@@ -42,7 +42,7 @@ export function UserInsightsView({ userId, userName }: UserInsightsViewProps) {
         </div>
       </div>
 
-      <UserOverviewCards userId={userId} />
+      <UserOverviewCards userId={userId} startDate={startDate} endDate={endDate} />
 
       <UserInsightsFilterBar userId={userId} filters={filters} onFiltersChange={setFilters} />
 

+ 12 - 10
src/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards.tsx

@@ -10,6 +10,8 @@ import { type CurrencyCode, formatCurrency } from "@/lib/utils";
 
 interface UserOverviewCardsProps {
   userId: number;
+  startDate?: string;
+  endDate?: string;
 }
 
 function formatResponseTime(ms: number): string {
@@ -17,13 +19,13 @@ function formatResponseTime(ms: number): string {
   return `${(ms / 1000).toFixed(1)}s`;
 }
 
-export function UserOverviewCards({ userId }: UserOverviewCardsProps) {
+export function UserOverviewCards({ userId, startDate, endDate }: UserOverviewCardsProps) {
   const t = useTranslations("dashboard.leaderboard.userInsights");
 
   const { data, isLoading, isError } = useQuery({
-    queryKey: ["user-insights-overview", userId],
+    queryKey: ["user-insights-overview", userId, startDate, endDate],
     queryFn: async () => {
-      const result = await getUserInsightsOverview(userId);
+      const result = await getUserInsightsOverview(userId, startDate, endDate);
       if (!result.ok) throw new Error(result.error);
       return result.data;
     },
@@ -64,15 +66,15 @@ export function UserOverviewCards({ userId }: UserOverviewCardsProps) {
 
   const metrics = [
     {
-      key: "todayRequests",
-      label: t("todayRequests"),
-      value: overview.todayRequests.toLocaleString(),
+      key: "requestCount",
+      label: t("requests"),
+      value: overview.requestCount.toLocaleString(),
       icon: TrendingUp,
     },
     {
-      key: "todayCost",
-      label: t("todayCost"),
-      value: formatCurrency(overview.todayCost, cc),
+      key: "cost",
+      label: t("cost"),
+      value: formatCurrency(overview.totalCost, cc),
       icon: DollarSign,
     },
     {
@@ -84,7 +86,7 @@ export function UserOverviewCards({ userId }: UserOverviewCardsProps) {
     {
       key: "errorRate",
       label: t("errorRate"),
-      value: `${overview.todayErrorRate.toFixed(1)}%`,
+      value: `${overview.errorRate.toFixed(1)}%`,
       icon: Activity,
     },
   ];

+ 53 - 1
src/repository/admin-user-insights.ts

@@ -1,11 +1,19 @@
 "use server";
 
-import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
+import { and, avg, count, desc, eq, gte, lt, sql, sum } from "drizzle-orm";
 import { db } from "@/drizzle/db";
 import { providers, usageLedger } from "@/drizzle/schema";
+import { Decimal, toCostDecimal } from "@/lib/utils/currency";
 import { LEDGER_BILLING_CONDITION } from "./_shared/ledger-conditions";
 import { getSystemSettings } from "./system-config";
 
+export interface UserInsightsOverviewMetrics {
+  requestCount: number;
+  totalCost: number;
+  avgResponseTime: number;
+  errorRate: number;
+}
+
 export interface AdminUserModelBreakdownItem {
   model: string | null;
   requests: number;
@@ -31,6 +39,50 @@ export interface AdminUserProviderBreakdownItem {
   cacheReadTokens: number;
 }
 
+/**
+ * Get overview metrics for a specific user within a date range.
+ */
+export async function getUserOverviewMetrics(
+  userId: number,
+  startDate?: string,
+  endDate?: string
+): Promise<UserInsightsOverviewMetrics> {
+  const conditions = [LEDGER_BILLING_CONDITION, eq(usageLedger.userId, userId)];
+
+  if (startDate) {
+    conditions.push(gte(usageLedger.createdAt, sql`${startDate}::date`));
+  }
+
+  if (endDate) {
+    conditions.push(lt(usageLedger.createdAt, sql`(${endDate}::date + INTERVAL '1 day')`));
+  }
+
+  const [result] = await db
+    .select({
+      requestCount: count(),
+      totalCost: sum(usageLedger.costUsd),
+      avgDuration: avg(usageLedger.durationMs),
+      errorCount: sql<number>`count(*) FILTER (WHERE NOT ${usageLedger.isSuccess})`,
+    })
+    .from(usageLedger)
+    .where(and(...conditions));
+
+  const costDecimal = toCostDecimal(result?.totalCost) ?? new Decimal(0);
+  const totalCost = costDecimal.toDecimalPlaces(6).toNumber();
+  const requestCount = Number(result?.requestCount || 0);
+  const errorCount = Number(result?.errorCount || 0);
+  const avgResponseTime = result?.avgDuration ? Math.round(Number(result.avgDuration)) : 0;
+  const errorRate =
+    requestCount > 0 ? parseFloat(((errorCount / requestCount) * 100).toFixed(2)) : 0;
+
+  return {
+    requestCount,
+    totalCost,
+    avgResponseTime,
+    errorRate,
+  };
+}
+
 /**
  * Get model-level usage breakdown for a specific user.
  * Groups by the billingModelSource-resolved model field and orders by cost DESC.

+ 47 - 15
tests/unit/actions/admin-user-insights.test.ts

@@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
 
 const mockGetSession = vi.hoisted(() => vi.fn());
 const mockFindUserById = vi.hoisted(() => vi.fn());
-const mockGetOverviewWithCache = vi.hoisted(() => vi.fn());
 const mockGetStatisticsWithCache = vi.hoisted(() => vi.fn());
+const mockGetUserOverviewMetrics = vi.hoisted(() => vi.fn());
 const mockGetUserModelBreakdown = vi.hoisted(() => vi.fn());
 const mockGetUserProviderBreakdown = vi.hoisted(() => vi.fn());
 const mockGetSystemSettings = vi.hoisted(() => vi.fn());
@@ -16,15 +16,12 @@ vi.mock("@/repository/user", () => ({
   findUserById: mockFindUserById,
 }));
 
-vi.mock("@/lib/redis/overview-cache", () => ({
-  getOverviewWithCache: mockGetOverviewWithCache,
-}));
-
 vi.mock("@/lib/redis/statistics-cache", () => ({
   getStatisticsWithCache: mockGetStatisticsWithCache,
 }));
 
 vi.mock("@/repository/admin-user-insights", () => ({
+  getUserOverviewMetrics: mockGetUserOverviewMetrics,
   getUserModelBreakdown: mockGetUserModelBreakdown,
   getUserProviderBreakdown: mockGetUserProviderBreakdown,
 }));
@@ -67,14 +64,10 @@ function createMockUser() {
 
 function createMockOverview() {
   return {
-    todayRequests: 50,
-    todayCost: 5.5,
+    requestCount: 50,
+    totalCost: 5.5,
     avgResponseTime: 200,
-    todayErrorRate: 2.0,
-    yesterdaySamePeriodRequests: 40,
-    yesterdaySamePeriodCost: 4.0,
-    yesterdaySamePeriodAvgResponseTime: 220,
-    recentMinuteRequests: 2,
+    errorRate: 2.0,
   };
 }
 
@@ -186,11 +179,11 @@ describe("getUserInsightsOverview", () => {
 
     mockGetSession.mockResolvedValueOnce(createAdminSession());
     mockFindUserById.mockResolvedValueOnce(user);
-    mockGetOverviewWithCache.mockResolvedValueOnce(overview);
+    mockGetUserOverviewMetrics.mockResolvedValueOnce(overview);
     mockGetSystemSettings.mockResolvedValueOnce(settings);
 
     const { getUserInsightsOverview } = await import("@/actions/admin-user-insights");
-    const result = await getUserInsightsOverview(10);
+    const result = await getUserInsightsOverview(10, "2026-03-01", "2026-03-09");
 
     expect(result.ok).toBe(true);
     if (result.ok) {
@@ -199,7 +192,46 @@ describe("getUserInsightsOverview", () => {
       expect(result.data.currencyCode).toBe("USD");
     }
     expect(mockFindUserById).toHaveBeenCalledWith(10);
-    expect(mockGetOverviewWithCache).toHaveBeenCalledWith(10);
+    expect(mockGetUserOverviewMetrics).toHaveBeenCalledWith(10, "2026-03-01", "2026-03-09");
+  });
+
+  it("rejects invalid startDate format", async () => {
+    mockGetSession.mockResolvedValueOnce(createAdminSession());
+
+    const { getUserInsightsOverview } = await import("@/actions/admin-user-insights");
+    const result = await getUserInsightsOverview(10, "not-a-date", "2026-03-09");
+
+    expect(result.ok).toBe(false);
+    if (!result.ok) {
+      expect(result.error).toContain("startDate");
+    }
+    expect(mockGetUserOverviewMetrics).not.toHaveBeenCalled();
+  });
+
+  it("rejects invalid endDate format", async () => {
+    mockGetSession.mockResolvedValueOnce(createAdminSession());
+
+    const { getUserInsightsOverview } = await import("@/actions/admin-user-insights");
+    const result = await getUserInsightsOverview(10, "2026-03-01", "03/09/2026");
+
+    expect(result.ok).toBe(false);
+    if (!result.ok) {
+      expect(result.error).toContain("endDate");
+    }
+    expect(mockGetUserOverviewMetrics).not.toHaveBeenCalled();
+  });
+
+  it("rejects startDate after endDate", async () => {
+    mockGetSession.mockResolvedValueOnce(createAdminSession());
+
+    const { getUserInsightsOverview } = await import("@/actions/admin-user-insights");
+    const result = await getUserInsightsOverview(10, "2026-03-09", "2026-03-01");
+
+    expect(result.ok).toBe(false);
+    if (!result.ok) {
+      expect(result.error).toContain("startDate must not be after endDate");
+    }
+    expect(mockGetUserOverviewMetrics).not.toHaveBeenCalled();
   });
 });
 

+ 70 - 20
tests/unit/dashboard/user-insights-page.test.tsx

@@ -11,17 +11,20 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import dashboardMessages from "@messages/en/dashboard.json";
 import myUsageMessages from "@messages/en/myUsage.json";
 import commonMessages from "@messages/en/common.json";
+import { resolveTimePresetDates } from "@/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types";
 
 // --- Hoisted mocks ---
 
 const mockGetUserInsightsOverview = vi.hoisted(() => vi.fn());
 const mockGetUserInsightsKeyTrend = vi.hoisted(() => vi.fn());
 const mockGetUserInsightsModelBreakdown = vi.hoisted(() => vi.fn());
+const mockGetUserInsightsProviderBreakdown = vi.hoisted(() => vi.fn());
 
 vi.mock("@/actions/admin-user-insights", () => ({
   getUserInsightsOverview: mockGetUserInsightsOverview,
   getUserInsightsKeyTrend: mockGetUserInsightsKeyTrend,
   getUserInsightsModelBreakdown: mockGetUserInsightsModelBreakdown,
+  getUserInsightsProviderBreakdown: mockGetUserInsightsProviderBreakdown,
 }));
 
 const routerPushMock = vi.fn();
@@ -123,14 +126,10 @@ describe("UserInsightsView", () => {
       data: {
         user: { id: 10, name: "TestUser" },
         overview: {
-          todayRequests: 42,
-          todayCost: 1.23,
+          requestCount: 42,
+          totalCost: 1.23,
           avgResponseTime: 850,
-          todayErrorRate: 2.5,
-          yesterdaySamePeriodRequests: 30,
-          yesterdaySamePeriodCost: 1.0,
-          yesterdaySamePeriodAvgResponseTime: 900,
-          recentMinuteRequests: 3,
+          errorRate: 2.5,
         },
         currencyCode: "USD",
       },
@@ -170,6 +169,25 @@ describe("UserInsightsView", () => {
         currencyCode: "USD",
       },
     });
+
+    mockGetUserInsightsProviderBreakdown.mockResolvedValue({
+      ok: true,
+      data: {
+        breakdown: [
+          {
+            providerId: 1,
+            providerName: "Provider A",
+            requests: 100,
+            cost: 1.5,
+            inputTokens: 5000,
+            outputTokens: 3000,
+            cacheCreationTokens: 1000,
+            cacheReadTokens: 500,
+          },
+        ],
+        currencyCode: "USD",
+      },
+    });
   });
 
   afterEach(() => {
@@ -194,6 +212,11 @@ describe("UserInsightsView", () => {
     expect(heading).not.toBeNull();
     expect(heading!.textContent).toContain("User Insights");
     expect(heading!.textContent).toContain("TestUser");
+    expect(mockGetUserInsightsOverview).toHaveBeenCalledWith(
+      10,
+      resolveTimePresetDates("7days").startDate,
+      resolveTimePresetDates("7days").endDate
+    );
 
     unmount();
   });
@@ -221,6 +244,32 @@ describe("UserInsightsView", () => {
 
     unmount();
   });
+
+  it("refetches overview with resolved 30-day range when timeRange changes", async () => {
+    const { UserInsightsView } = await import(
+      "@/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view"
+    );
+
+    const { container, unmount } = renderWithProviders(
+      <UserInsightsView userId={10} userName="TestUser" />
+    );
+
+    await flushMicrotasks();
+
+    const button = container.querySelector("[data-testid='user-insights-time-range-30days']");
+    expect(button).not.toBeNull();
+
+    act(() => {
+      (button as HTMLButtonElement).click();
+    });
+
+    await flushMicrotasks();
+
+    const { startDate, endDate } = resolveTimePresetDates("30days");
+    expect(mockGetUserInsightsOverview).toHaveBeenLastCalledWith(10, startDate, endDate);
+
+    unmount();
+  });
 });
 
 describe("UserOverviewCards", () => {
@@ -243,14 +292,10 @@ describe("UserOverviewCards", () => {
       data: {
         user: { id: 10, name: "TestUser" },
         overview: {
-          todayRequests: 42,
-          todayCost: 1.23,
+          requestCount: 42,
+          totalCost: 1.23,
           avgResponseTime: 850,
-          todayErrorRate: 2.5,
-          yesterdaySamePeriodRequests: 30,
-          yesterdaySamePeriodCost: 1.0,
-          yesterdaySamePeriodAvgResponseTime: 900,
-          recentMinuteRequests: 3,
+          errorRate: 2.5,
         },
         currencyCode: "USD",
       },
@@ -260,18 +305,20 @@ describe("UserOverviewCards", () => {
       "@/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards"
     );
 
-    const { container, unmount } = renderWithProviders(<UserOverviewCards userId={10} />);
+    const { container, unmount } = renderWithProviders(
+      <UserOverviewCards userId={10} startDate="2026-03-01" endDate="2026-03-09" />
+    );
 
     await flushMicrotasks();
 
     const cards = container.querySelectorAll("[data-testid^='user-insights-metric-']");
     expect(cards.length).toBe(4);
 
-    const todayRequests = container.querySelector(
-      "[data-testid='user-insights-metric-todayRequests']"
+    const requestCount = container.querySelector(
+      "[data-testid='user-insights-metric-requestCount']"
     );
-    expect(todayRequests).not.toBeNull();
-    expect(todayRequests!.textContent).toContain("42");
+    expect(requestCount).not.toBeNull();
+    expect(requestCount!.textContent).toContain("42");
 
     const avgResponseTime = container.querySelector(
       "[data-testid='user-insights-metric-avgResponseTime']"
@@ -282,6 +329,7 @@ describe("UserOverviewCards", () => {
     const errorRate = container.querySelector("[data-testid='user-insights-metric-errorRate']");
     expect(errorRate).not.toBeNull();
     expect(errorRate!.textContent).toContain("2.5%");
+    expect(mockGetUserInsightsOverview).toHaveBeenCalledWith(10, "2026-03-01", "2026-03-09");
 
     unmount();
   });
@@ -294,7 +342,9 @@ describe("UserOverviewCards", () => {
       "@/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-overview-cards"
     );
 
-    const { container, unmount } = renderWithProviders(<UserOverviewCards userId={10} />);
+    const { container, unmount } = renderWithProviders(
+      <UserOverviewCards userId={10} startDate="2026-03-01" endDate="2026-03-09" />
+    );
 
     await flushMicrotasks();
 

+ 147 - 0
tests/unit/repository/admin-user-insights-overview.test.ts

@@ -0,0 +1,147 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+function sqlToString(sqlObj: unknown): string {
+  const visited = new Set<unknown>();
+
+  const walk = (node: unknown): string => {
+    if (!node || visited.has(node)) return "";
+    visited.add(node);
+
+    if (typeof node === "string") return node;
+    if (typeof node === "number") return String(node);
+
+    if (typeof node === "object") {
+      const anyNode = node as Record<string, unknown>;
+      if (Array.isArray(anyNode)) {
+        return anyNode.map(walk).join("");
+      }
+
+      if (anyNode.value !== undefined) {
+        if (Array.isArray(anyNode.value)) {
+          return (anyNode.value as unknown[]).map(walk).join("");
+        }
+        return walk(anyNode.value);
+      }
+
+      if (anyNode.queryChunks) {
+        return walk(anyNode.queryChunks);
+      }
+    }
+
+    return "";
+  };
+
+  return walk(sqlObj);
+}
+
+function createThenableQuery<T>(result: T, whereArgs?: unknown[]) {
+  const query: any = Promise.resolve(result);
+  query.from = vi.fn(() => query);
+  query.innerJoin = vi.fn(() => query);
+  query.leftJoin = vi.fn(() => query);
+  query.groupBy = vi.fn(() => query);
+  query.orderBy = vi.fn(() => query);
+  query.limit = vi.fn(() => query);
+  query.offset = vi.fn(() => query);
+  query.where = vi.fn((arg: unknown) => {
+    whereArgs?.push(arg);
+    return query;
+  });
+  return query;
+}
+
+const selectResults: unknown[] = [];
+const allWhereArgs: unknown[][] = [];
+const capturedSelections: Array<Record<string, unknown>> = [];
+
+vi.mock("@/drizzle/db", () => ({
+  db: {
+    select: vi.fn((selection: unknown) => {
+      capturedSelections.push(selection as Record<string, unknown>);
+      const whereArgs: unknown[] = [];
+      allWhereArgs.push(whereArgs);
+      const result = selectResults.shift() ?? [];
+      return createThenableQuery(result, whereArgs);
+    }),
+  },
+}));
+
+vi.mock("@/drizzle/schema", () => ({
+  usageLedger: {
+    userId: "userId",
+    costUsd: "costUsd",
+    durationMs: "durationMs",
+    isSuccess: "isSuccess",
+    createdAt: "createdAt",
+    blockedBy: "blockedBy",
+    originalModel: "originalModel",
+    model: "model",
+    inputTokens: "inputTokens",
+    outputTokens: "outputTokens",
+    cacheCreationInputTokens: "cacheCreationInputTokens",
+    cacheReadInputTokens: "cacheReadInputTokens",
+    finalProviderId: "finalProviderId",
+    key: "key",
+  },
+  providers: {
+    id: "id",
+    name: "name",
+  },
+}));
+
+vi.mock("@/repository/system-config", () => ({
+  getSystemSettings: vi.fn(),
+}));
+
+describe("getUserOverviewMetrics", () => {
+  beforeEach(() => {
+    vi.resetModules();
+    selectResults.length = 0;
+    allWhereArgs.length = 0;
+    capturedSelections.length = 0;
+  });
+
+  it("applies the requested date range and computes aggregate metrics", async () => {
+    selectResults.push([
+      {
+        requestCount: 3,
+        totalCost: "1.5",
+        avgDuration: "200.6",
+        errorCount: 1,
+      },
+    ]);
+
+    const { getUserOverviewMetrics } = await import("@/repository/admin-user-insights");
+    const result = await getUserOverviewMetrics(10, "2026-03-01", "2026-03-09");
+
+    expect(result).toEqual({
+      requestCount: 3,
+      totalCost: 1.5,
+      avgResponseTime: 201,
+      errorRate: 33.33,
+    });
+
+    expect(allWhereArgs).toHaveLength(1);
+    const whereSql = sqlToString(allWhereArgs[0][0]);
+    expect(whereSql).toContain("2026-03-01");
+    expect(whereSql).toContain("2026-03-09");
+    expect(whereSql).toContain("INTERVAL '1 day'");
+
+    const errorCountSql = sqlToString(capturedSelections[0].errorCount).toLowerCase();
+    expect(errorCountSql).toContain("filter");
+  });
+
+  it("returns zeroed metrics when the aggregate query yields no rows", async () => {
+    selectResults.push([]);
+
+    const { getUserOverviewMetrics } = await import("@/repository/admin-user-insights");
+    const result = await getUserOverviewMetrics(10, "2026-03-01", "2026-03-09");
+
+    expect(result).toEqual({
+      requestCount: 0,
+      totalCost: 0,
+      avgResponseTime: 0,
+      errorRate: 0,
+    });
+  });
+});