From c7ea53fa48af3c73249c1e13043dd7939daab949 Mon Sep 17 00:00:00 2001 From: LeenHawk Date: Tue, 12 May 2026 19:26:16 +0800 Subject: [PATCH] fix(console): persist rewrite rule deletion --- RELEASE_NOTE.md | 2 + .../admin/providers/RewriteRulesTab.test.tsx | 83 +++++++++++++++++++ .../admin/providers/RewriteRulesTab.tsx | 14 ++-- 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 frontend/console/src/modules/admin/providers/RewriteRulesTab.test.tsx diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 0cd42e92..eab41c86 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -21,6 +21,7 @@ - **Vertex CountToken/OpenAPI handling.** Vertex request body handling is stricter and OpenAPI chat-completions compatible requests route to the correct endpoint. - **Structured-output conversion cleanup.** OpenAI-to-Claude transforms drop deprecated `output_format`, avoid unsupported permissive JSON-object shims, and keep schema serialization strict. - **TOML export for rewrite rules.** Model alias/suffix rewrite rules no longer export empty filter dimensions as JSON null, avoiding `unsupported unit type` during config export (#94). +- **Console rewrite-rule deletion persists.** Deleting parameter rewrite rules from the console now saves the fresh `rewrite_rules` JSON immediately, so removed rules do not reappear after reload (#96). - **Responses/image stream schema tolerance.** Responses keepalive events and partial image-generation output items are accepted instead of turning valid upstream streams into local 500s. #### Changed @@ -45,6 +46,7 @@ - **Vertex CountToken / OpenAPI 处理.** Vertex 请求体处理更严格,OpenAPI chat-completions 兼容请求会路由到正确端点。 - **结构化输出转换清理.** OpenAI → Claude 转换删除废弃的 `output_format`,避免生成上游不支持的宽松 JSON-object shim,并保持 schema 序列化严格。 - **rewrite rules TOML 导出.** 模型别名 / 后缀变体自动生成的 rewrite rules 不再把空 filter 维度导出成 JSON null,避免配置导出时报 `unsupported unit type`(#94)。 +- **控制台删除 rewrite rule 会持久化.** 在控制台删除参数改写规则时,现在会立刻保存最新的 `rewrite_rules` JSON,删除后的规则不会刷新后又出现(#96)。 - **Responses / image stream schema 兼容.** Responses keepalive 事件和 image-generation 的 partial output item 现在会被接受,不再把有效上游流误转成本地 500。 #### 调整 diff --git a/frontend/console/src/modules/admin/providers/RewriteRulesTab.test.tsx b/frontend/console/src/modules/admin/providers/RewriteRulesTab.test.tsx new file mode 100644 index 00000000..5a9f8e8b --- /dev/null +++ b/frontend/console/src/modules/admin/providers/RewriteRulesTab.test.tsx @@ -0,0 +1,83 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { describe, expect, it, vi } from "vitest"; + +import { I18nProvider } from "../../../app/i18n"; +import { RewriteRulesTab } from "./RewriteRulesTab"; +import type { ProviderFormState } from "./index"; + +// @ts-expect-error React's test-only act flag is intentionally set on globalThis. +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +function getButtonByText(container: HTMLElement, text: string): HTMLButtonElement { + const button = Array.from(container.querySelectorAll("button")).find( + (item): item is HTMLButtonElement => item.textContent?.trim() === text, + ); + if (!button) { + throw new Error(`button not found: ${text}`); + } + return button; +} + +function getButtonContaining(container: HTMLElement, text: string): HTMLButtonElement { + const button = Array.from(container.querySelectorAll("button")).find((item): item is HTMLButtonElement => + item.textContent?.includes(text) ?? false, + ); + if (!button) { + throw new Error(`button containing text not found: ${text}`); + } + return button; +} + +const initialRules = [ + { path: "temperature", action: { type: "set" as const, value: 0.2 } }, + { path: "metadata.trace", action: { type: "remove" as const } }, +]; + +const baseForm: ProviderFormState = { + id: "1", + name: "anthropic", + label: "Anthropic", + channel: "anthropic", + settings: { rewrite_rules: JSON.stringify(initialRules) }, + routingRules: [], +}; + +describe("RewriteRulesTab", () => { + it("persists the fresh rewrite_rules JSON after deleting an existing rule", async () => { + const container = document.createElement("div"); + const root = createRoot(container); + const onSave = vi.fn(); + + try { + await act(async () => { + root.render( + + {}} + onSave={onSave} + notify={() => {}} + /> + , + ); + }); + + await act(async () => { + getButtonContaining(container, "temperature").click(); + }); + + await act(async () => { + getButtonByText(container, "Delete").click(); + }); + + expect(onSave).toHaveBeenCalledWith(JSON.stringify([initialRules[1]])); + } finally { + await act(async () => { + root.unmount(); + }); + } + }); +}); diff --git a/frontend/console/src/modules/admin/providers/RewriteRulesTab.tsx b/frontend/console/src/modules/admin/providers/RewriteRulesTab.tsx index eaf82d61..17e3097b 100644 --- a/frontend/console/src/modules/admin/providers/RewriteRulesTab.tsx +++ b/frontend/console/src/modules/admin/providers/RewriteRulesTab.tsx @@ -46,9 +46,11 @@ export function RewriteRulesTab({ ); const commit = (next: RewriteRule[]) => { + const nextJson = JSON.stringify(next); onChange({ - settings: { ...form.settings, rewrite_rules: JSON.stringify(next) }, + settings: { ...form.settings, rewrite_rules: nextJson }, }); + return nextJson; }; const batch = useBatchSelection({ @@ -57,9 +59,10 @@ export function RewriteRulesTab({ onBatchDelete: async (keys) => { const keySet = new Set(keys); const next = rules.filter((_, idx) => !keySet.has(String(idx))); - commit(next); + const nextJson = commit(next); setSelectedIdx(null); setDraft(null); + onSave(nextJson); }, onSuccess: (count) => { notify("success", t("batch.deleted", { count })); @@ -76,9 +79,11 @@ export function RewriteRulesTab({ }; const remove = (idx: number) => { - commit(rules.filter((_, i) => i !== idx)); + const next = rules.filter((_, i) => i !== idx); + const nextJson = commit(next); if (selectedIdx === idx) setSelectedIdx(null); else if (selectedIdx != null && selectedIdx > idx) setSelectedIdx(selectedIdx - 1); + onSave(nextJson); }; /// Current rule being edited (either draft or an existing one). @@ -104,8 +109,7 @@ export function RewriteRulesTab({ const save = () => { if (isDraft && draft) { const next = [...rules, draft]; - const nextJson = JSON.stringify(next); - commit(next); + const nextJson = commit(next); // After this render cycle, `rules` will include the new entry and we // want the list to highlight it. Use a timeout so the commit propagates // through the parent and our `rules` memo re-runs with the new array.