Przeglądaj źródła

fix(pr810): address coderabbit review comments

- fix semantic mismatch: use providersCount key for enabledProviders display
- fix probability formatting: use formatProbability() instead of Math.round
- fix i18n: translate selectionMethod enum via selectionMethods namespace
- add selectionMethods translations to all 5 language files
- add JSDoc to findSessionOriginChain repository function
- fix test: null providerChain mock now returns row with null providerChain
- fix test: add assertion before trigger click in error-details-dialog test
- add 2 missing test cases: non-admin unauthorized + exception path
ding113 2 dni temu
rodzic
commit
152b42856a

+ 6 - 0
messages/en/provider-chain.json

@@ -214,5 +214,11 @@
     "strictBlockSelectorError": "Strict mode: endpoint selector encountered an error, provider skipped without fallback",
     "vendorTypeAllTimeout": "Vendor-Type All Endpoints Timeout (524)",
     "vendorTypeAllTimeoutNote": "All endpoints for this vendor-type timed out. Vendor-type circuit breaker triggered."
+  },
+  "selectionMethods": {
+    "session_reuse": "Session Reuse",
+    "weighted_random": "Weighted Random",
+    "group_filtered": "Group Filtered",
+    "fail_open_fallback": "Fail-Open Fallback"
   }
 }

+ 6 - 0
messages/ja/provider-chain.json

@@ -214,5 +214,11 @@
     "strictBlockSelectorError": "厳格モード:エンドポイントセレクターでエラーが発生したため、フォールバックなしでプロバイダーをスキップ",
     "vendorTypeAllTimeout": "ベンダータイプ全エンドポイントタイムアウト(524)",
     "vendorTypeAllTimeoutNote": "このベンダータイプの全エンドポイントがタイムアウトしました。ベンダータイプサーキットブレーカーが発動しました。"
+  },
+  "selectionMethods": {
+    "session_reuse": "セッション再利用",
+    "weighted_random": "重み付きランダム",
+    "group_filtered": "グループフィルタ",
+    "fail_open_fallback": "フェイルオープンフォールバック"
   }
 }

+ 6 - 0
messages/ru/provider-chain.json

@@ -214,5 +214,11 @@
     "strictBlockSelectorError": "Строгий режим: ошибка селектора конечных точек, провайдер пропущен без отката",
     "vendorTypeAllTimeout": "Тайм-аут всех конечных точек типа поставщика (524)",
     "vendorTypeAllTimeoutNote": "Все конечные точки этого типа поставщика превысили тайм-аут. Активирован размыкатель типа поставщика."
+  },
+  "selectionMethods": {
+    "session_reuse": "Повторное использование сессии",
+    "weighted_random": "Взвешенный случайный",
+    "group_filtered": "Фильтрация по группе",
+    "fail_open_fallback": "Резервный вариант при сбое"
   }
 }

+ 6 - 0
messages/zh-CN/provider-chain.json

@@ -214,5 +214,11 @@
     "strictBlockSelectorError": "严格模式:端点选择器发生错误,跳过该供应商且不降级",
     "vendorTypeAllTimeout": "供应商类型全端点超时(524)",
     "vendorTypeAllTimeoutNote": "该供应商类型的所有端点均超时,已触发供应商类型临时熔断。"
+  },
+  "selectionMethods": {
+    "session_reuse": "会话复用",
+    "weighted_random": "加权随机",
+    "group_filtered": "分组过滤",
+    "fail_open_fallback": "故障开放回退"
   }
 }

+ 6 - 0
messages/zh-TW/provider-chain.json

@@ -214,5 +214,11 @@
     "strictBlockSelectorError": "嚴格模式:端點選擇器發生錯誤,跳過該供應商且不降級",
     "vendorTypeAllTimeout": "供應商類型全端點逾時(524)",
     "vendorTypeAllTimeoutNote": "該供應商類型的所有端點均逾時,已觸發供應商類型臨時熔斷。"
+  },
+  "selectionMethods": {
+    "session_reuse": "會話複用",
+    "weighted_random": "加權隨機",
+    "group_filtered": "分組過濾",
+    "fail_open_fallback": "故障開放回退"
   }
 }

+ 2 - 1
src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx

@@ -1161,7 +1161,8 @@ describe("error-details-dialog origin decision chain", () => {
       button.textContent?.includes("View original selection")
     );
 
-    click(trigger ?? null);
+    expect(trigger).toBeTruthy();
+    click(trigger!);
 
     await act(async () => {
       await Promise.resolve();

+ 2 - 4
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx

@@ -356,9 +356,7 @@ export function LogicTraceTab({
                             </div>
                             {ctx.enabledProviders !== undefined && (
                               <div>
-                                <span className="text-muted-foreground">
-                                  {t("logicTrace.healthyCount", { count: ctx.enabledProviders })}
-                                </span>
+                                <span className="text-muted-foreground">{t("logicTrace.providersCount", { count: ctx.enabledProviders })}</span>
                               </div>
                             )}
                             {ctx.afterHealthCheck !== undefined && (
@@ -389,7 +387,7 @@ export function LogicTraceTab({
                                     {c.probability !== undefined && (
                                       <span className="text-muted-foreground">
                                         {" "}
-                                        ({Math.round(c.probability * 100)}%)
+                                        {formatProbability(c.probability)}
                                       </span>
                                     )}
                                   </span>

+ 1 - 1
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx

@@ -260,7 +260,7 @@ export function ProviderChainPopover({
                     </div>
                     {sessionReuseItem?.selectionMethod && (
                       <div className="text-[10px] text-zinc-400 dark:text-zinc-500 pt-0.5">
-                        {tChain("summary.originHint", { method: sessionReuseItem.selectionMethod })}
+                        {tChain("summary.originHint", { method: tChain(`selectionMethods.${sessionReuseItem.selectionMethod}`) })}
                       </div>
                     )}
                   </div>

+ 4 - 0
src/repository/message.ts

@@ -277,6 +277,10 @@ export async function findMessageRequestBySessionId(
   return toMessageRequest(result);
 }
 
+/**
+ * 根据 sessionId 查询该 session 首条非 warmup 请求的 providerChain
+ * 用于展示会话来源链(原始选择决策)
+ */
 export async function findSessionOriginChain(
   sessionId: string
 ): Promise<ProviderChainItem[] | null> {

+ 22 - 0
tests/unit/actions/session-origin-chain.test.ts

@@ -94,6 +94,28 @@ describe("getSessionOriginChain", () => {
     expect(dbSelectMock).not.toHaveBeenCalled();
   });
 
+  test("non-admin without access: returns unauthorized error", async () => {
+    getSessionMock.mockResolvedValue({ user: { id: 3, role: "user" } });
+    findKeyListMock.mockResolvedValue([{ key: "user-key-3" }]);
+    dbLimitMock.mockResolvedValue([]);
+
+    const { getSessionOriginChain } = await import("@/actions/session-origin-chain");
+    const result = await getSessionOriginChain("sess-other-user");
+
+    expect(result).toEqual({ ok: false, error: "无权访问该 Session" });
+    expect(findSessionOriginChainMock).not.toHaveBeenCalled();
+  });
+
+  test("exception path: returns error on unexpected throw", async () => {
+    getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
+    findSessionOriginChainMock.mockRejectedValue(new Error("db error"));
+
+    const { getSessionOriginChain } = await import("@/actions/session-origin-chain");
+    const result = await getSessionOriginChain("sess-throws");
+
+    expect(result).toEqual({ ok: false, error: "获取会话来源链失败" });
+  });
+
   test("not found: returns ok with null data", async () => {
     getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
     findSessionOriginChainMock.mockResolvedValue(null);

+ 1 - 1
tests/unit/repository/message-origin-chain.test.ts

@@ -179,7 +179,7 @@ describe("repository/message findSessionOriginChain", () => {
   test("null providerChain: 首条非 warmup 记录 providerChain 为空时返回 null", async () => {
     vi.resetModules();
 
-    const selectMock = vi.fn(() => createThenableQuery([]));
+    const selectMock = vi.fn(() => createThenableQuery([{ providerChain: null }]));
 
     vi.doMock("@/drizzle/db", () => ({
       db: {