This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 2eb7e157 fix(ai): ignore stale LLM connection results (#1566)
2eb7e157 is described below

commit 2eb7e157babcac72e0141f9142163d566ee18a0f
Author: 0 <[email protected]>
AuthorDate: Tue Aug 11 20:23:55 2026 +0800

    fix(ai): ignore stale LLM connection results (#1566)
---
 web/src/pages/studio/LlmSettings.tsx               | 28 ++++++++++++++++++----
 .../studio/__tests__/LlmSettingsPage.test.tsx      | 25 ++++++++++++++++++-
 2 files changed, 47 insertions(+), 6 deletions(-)

diff --git a/web/src/pages/studio/LlmSettings.tsx 
b/web/src/pages/studio/LlmSettings.tsx
index f56babd3..84f0614e 100644
--- a/web/src/pages/studio/LlmSettings.tsx
+++ b/web/src/pages/studio/LlmSettings.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import {
   Alert,
   Button,
@@ -80,6 +80,7 @@ const LlmSettingsPage: React.FC = () => {
   const [apiKeyConfigured, setApiKeyConfigured] = useState(false);
   const [modelOptions, setModelOptions] = useState<{ value: string; label: 
string }[]>([]);
   const [testResult, setTestResult] = useState<TestState | null>(null);
+  const testRequestIdRef = useRef(0);
 
   const buildModelOptions = (
     nextProvider: string,
@@ -126,11 +127,19 @@ const LlmSettingsPage: React.FC = () => {
       });
     return () => {
       cancelled = true;
+      testRequestIdRef.current += 1;
     };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
+  const invalidateTestRequest = () => {
+    testRequestIdRef.current += 1;
+    setTesting(false);
+    setTestResult(null);
+  };
+
   const handleProviderChange = (nextProvider: string) => {
+    invalidateTestRequest();
     setModelOptions(fallbackModelOptions(nextProvider));
     const fallbackModel = fallbackModelOptions(nextProvider)[0]?.value;
     form.setFieldsValue({
@@ -138,7 +147,6 @@ const LlmSettingsPage: React.FC = () => {
       model: fallbackModel,
       apiBase: DEFAULT_BASE_URL[nextProvider] || form.getFieldValue('apiBase'),
     });
-    setTestResult(null);
   };
 
   const buildPayload = async (): Promise<LlmConfig | null> => {
@@ -177,14 +185,23 @@ const LlmSettingsPage: React.FC = () => {
   const handleTest = async () => {
     const payload = await buildPayload();
     if (!payload) return;
+    const requestId = testRequestIdRef.current + 1;
+    testRequestIdRef.current = requestId;
     setTesting(true);
     setTestResult(null);
     try {
-      applyTestResult(await testLlmConnection(payload));
+      const result = await testLlmConnection(payload);
+      if (testRequestIdRef.current === requestId) {
+        applyTestResult(result);
+      }
     } catch {
-      setTestResult({ success: false, msg: '连接测试请求失败,请稍后重试' });
+      if (testRequestIdRef.current === requestId) {
+        setTestResult({ success: false, msg: '连接测试请求失败,请稍后重试' });
+      }
     } finally {
-      setTesting(false);
+      if (testRequestIdRef.current === requestId) {
+        setTesting(false);
+      }
     }
   };
 
@@ -219,6 +236,7 @@ const LlmSettingsPage: React.FC = () => {
           form={form}
           layout="vertical"
           initialValues={{ provider: 'tongyi', engine: 'claude-code' }}
+          onValuesChange={invalidateTestRequest}
         >
           <Form.Item
             label="执行引擎"
diff --git a/web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx 
b/web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx
index 022cfc44..3b40c999 100644
--- a/web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx
+++ b/web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { App } from 'antd';
-import { render, screen, waitFor } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
 import { LangProvider } from '../../../i18n/LangContext';
@@ -94,4 +94,27 @@ describe('LlmSettingsPage', () => {
     expect(payload).toMatchObject({ provider: 'tongyi', model: 'qwen3.8-max' 
});
     expect(payload.apiKey).toBeUndefined();
   });
+
+  it('ignores a connection result after the tested configuration changes', 
async () => {
+    let resolveTest!: (result: { status: number; msg: string }) => void;
+    llmApiMocks.testLlmConnection.mockImplementationOnce(
+      () =>
+        new Promise((resolve) => {
+          resolveTest = resolve;
+        }),
+    );
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('密钥已配置');
+    await user.click(screen.getByRole('button', { name: /测试连接/ }));
+    await waitFor(() => 
expect(llmApiMocks.testLlmConnection).toHaveBeenCalledTimes(1));
+
+    await user.click(screen.getAllByRole('combobox')[1]);
+    await user.click(await screen.findByText('OpenAI', { selector: 
'.ant-select-item-option-content' }));
+    await act(async () => resolveTest({ status: 0, msg: 'old provider 
succeeded' }));
+
+    expect(screen.queryByText('old provider 
succeeded')).not.toBeInTheDocument();
+    expect(screen.getByRole('button', { name: /测试连接/ 
})).not.toHaveClass('ant-btn-loading');
+  });
 });

Reply via email to