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="关闭"

Reply via email to