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 203eb7a7 [ISSUE #1621] Ignore stale AI tool catalog responses (#1631)
203eb7a7 is described below
commit 203eb7a77ada529b636cd3d29b93494a2280a545
Author: 0 <[email protected]>
AuthorDate: Tue Aug 11 20:33:23 2026 +0800
[ISSUE #1621] Ignore stale AI tool catalog responses (#1631)
---
web/src/pages/ai/__tests__/AiPage.test.tsx | 29 ++++++++++++++++++++++++++++-
web/src/pages/ai/index.tsx | 17 +++++++++++++----
2 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/web/src/pages/ai/__tests__/AiPage.test.tsx
b/web/src/pages/ai/__tests__/AiPage.test.tsx
index 8f516f75..1f92bb3b 100644
--- a/web/src/pages/ai/__tests__/AiPage.test.tsx
+++ b/web/src/pages/ai/__tests__/AiPage.test.tsx
@@ -16,7 +16,7 @@
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
+import { act, fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from 'antd';
import { MemoryRouter } from 'react-router-dom';
@@ -136,6 +136,33 @@ describe('AiPage tool runner', () => {
expect(within(dialog).getByTestId('tool-result')).toHaveTextContent('"GRPC"');
});
+ it('ignores an older tool catalog after the cluster changes', async () => {
+ const oldTools = [{ name: 'rmq.old', description: 'old', parameters: {} }];
+ const latestTools = [{ name: 'rmq.latest', description: 'latest',
parameters: {} }];
+ let resolveOld!: (value: typeof oldTools) => void;
+ let resolveLatest!: (value: typeof latestTools) => void;
+ vi.mocked(listTools)
+ .mockReturnValueOnce(new Promise((resolve) => { resolveOld = resolve; }))
+ .mockReturnValueOnce(new Promise((resolve) => { resolveLatest = resolve;
}));
+ const user = userEvent.setup();
+ renderPage();
+
+ await user.click(screen.getByRole('button', { name: '工具' }));
+ const dialog = await screen.findByRole('dialog', { name: 'AI 工具' });
+ await waitFor(() => expect(listTools).toHaveBeenCalledWith('cluster-a'));
+ await user.click(within(dialog).getByRole('combobox', { name: '选择集群' }));
+ await user.click(
+ await screen.findByText('Cluster B', { selector:
'.ant-select-item-option-content' }),
+ );
+ await waitFor(() => expect(listTools).toHaveBeenCalledWith('cluster-b'));
+
+ await act(async () => resolveLatest(latestTools));
+ expect(await within(dialog).findByText('rmq.latest')).toBeInTheDocument();
+ await act(async () => resolveOld(oldTools));
+ expect(within(dialog).getByText('rmq.latest')).toBeInTheDocument();
+ expect(within(dialog).queryByText('rmq.old')).not.toBeInTheDocument();
+ });
+
it('reloads the available tools and template when the cluster changes',
async () => {
const user = userEvent.setup();
vi.mocked(listTools).mockImplementation(async (cluster) => [
diff --git a/web/src/pages/ai/index.tsx b/web/src/pages/ai/index.tsx
index e0023c9d..2926670b 100644
--- a/web/src/pages/ai/index.tsx
+++ b/web/src/pages/ai/index.tsx
@@ -402,6 +402,7 @@ const AiPage = () => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const conversationIdRef = useRef<string | null>(null);
+ const toolLoadRequestRef = useRef(0);
const consumedDraftRef = useRef(false);
const pendingAutoSendRef = useRef<{ prompt: string; model?: string;
enhance?: boolean } | null>(
null,
@@ -619,19 +620,23 @@ const AiPage = () => {
const loadTools = useCallback(
async (clusterId: string) => {
+ const requestId = ++toolLoadRequestRef.current;
setSelectedToolName('');
setToolResult(undefined);
setToolsLoading(true);
try {
const availableTools = await listTools(clusterId || undefined);
+ if (requestId !== toolLoadRequestRef.current) return;
setTools(availableTools);
const firstTool = availableTools.find((tool) => !tool.deprecated);
if (firstTool) selectTool(firstTool.name, availableTools, clusterId);
} catch {
- setTools([]);
- message.error('AI 工具目录加载失败');
+ if (requestId === toolLoadRequestRef.current) {
+ setTools([]);
+ message.error('AI 工具目录加载失败');
+ }
} finally {
- setToolsLoading(false);
+ if (requestId === toolLoadRequestRef.current) setToolsLoading(false);
}
},
[selectTool],
@@ -853,7 +858,11 @@ const AiPage = () => {
<Modal
title="AI 工具"
open={toolModalOpen}
- onCancel={() => setToolModalOpen(false)}
+ onCancel={() => {
+ toolLoadRequestRef.current += 1;
+ setToolsLoading(false);
+ setToolModalOpen(false);
+ }}
onOk={() => void handleExecuteTool()}
okText="执行"
cancelText="关闭"