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');
+ });
});