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 e07fcd40e fix(alerts): clear the previous rule's test samples when 
editing (#4584)
e07fcd40e is described below

commit e07fcd40eafc0583b43ae0ebf491f0cfb9feb537
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Sep 24 17:45:31 2026 +0800

    fix(alerts): clear the previous rule's test samples when editing (#4584)
    
    fix(alerts): clear the previous rule's test samples when editing
    
    The rule editor keeps its dry-run result in page state. Every entry point
    reset it except openEditModal, so after saving rule A and opening rule B the
    editor rendered A's samples under B's title, as if B had just been tested.
    
    Reset the test result when the edit dialog opens, matching the create and
    duplicate entry points.
---
 .../ops/__tests__/AlertsTestResultResidue.test.tsx | 175 +++++++++++++++++++++
 web/src/pages/ops/alerts.tsx                       |   1 +
 2 files changed, 176 insertions(+)

diff --git a/web/src/pages/ops/__tests__/AlertsTestResultResidue.test.tsx 
b/web/src/pages/ops/__tests__/AlertsTestResultResidue.test.tsx
new file mode 100644
index 000000000..67bb06ae0
--- /dev/null
+++ b/web/src/pages/ops/__tests__/AlertsTestResultResidue.test.tsx
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import type { AlertRule, PageResult } from '../../../api/ops';
+import { LangProvider } from '../../../i18n/LangContext';
+import { listInstances } from '../../../services/instanceService';
+import {
+  listAlertRulesPage,
+  listAlertRuleRuntime,
+  listNativeAlertMetrics,
+  testAlertRule,
+  updateAlertRule,
+} from '../../../services/opsService';
+import AlertsPage from '../alerts';
+
+vi.mock('../../../services/instanceService', () => ({
+  listInstances: vi.fn(),
+}));
+
+vi.mock('../../../services/opsService', () => ({
+  createAlertRule: vi.fn(),
+  deleteAlertRule: vi.fn(),
+  listAlertRulesPage: vi.fn(),
+  listAlertRuleRuntime: vi.fn(),
+  listNativeAlertMetrics: vi.fn(),
+  toggleAlertRule: vi.fn(),
+  bulkToggleAlertRules: vi.fn(),
+  bulkDeleteAlertRules: vi.fn(),
+  exportAlertRulesTransfer: vi.fn(),
+  importAlertRulesTransfer: vi.fn(),
+  updateAlertRule: vi.fn(),
+  testAlertRule: vi.fn(),
+}));
+
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: vi.fn().mockImplementation((query: string) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: vi.fn(),
+      removeListener: vi.fn(),
+      addEventListener: vi.fn(),
+      removeEventListener: vi.fn(),
+      dispatchEvent: vi.fn(),
+    })),
+  });
+});
+
+const buildRule = (id: number, name: string): AlertRule => ({
+  id,
+  name,
+  instanceId: 'local',
+  metric: 'broker.disk.usage_ratio',
+  operator: '>',
+  threshold: 85,
+  thresholdUnit: '%',
+  duration: '1m',
+  channels: ['dingtalk'],
+  enabled: true,
+  lastTriggered: null,
+  description: '',
+});
+
+const pageResult = (rules: AlertRule[]): PageResult<AlertRule> => ({
+  items: rules.map((rule) => ({ ...rule, channels: [...rule.channels] })),
+  total: rules.length,
+  page: 1,
+  size: 20,
+});
+
+const renderPage = () =>
+  render(
+    <App>
+      <LangProvider>
+        <AlertsPage />
+      </LangProvider>
+    </App>,
+  );
+
+function getRuleRow(ruleName: string) {
+  const row = screen.getByText(ruleName).closest('tr');
+  if (!row) throw new Error(`Row not found: ${ruleName}`);
+  return row;
+}
+
+describe('AlertsPage rule test results', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(listInstances).mockResolvedValue([
+      {
+        id: 1,
+        name: 'local',
+        remark: '',
+        type: 'DIRECT',
+        endpoint: '127.0.0.1:9876',
+        topicCount: 0,
+        consumerGroupCount: 0,
+        gmtCreate: '',
+        gmtModified: '',
+      },
+    ]);
+    vi.mocked(listAlertRulesPage).mockResolvedValue(
+      pageResult([buildRule(1, 'Rule A'), buildRule(2, 'Rule B')]),
+    );
+    vi.mocked(listAlertRuleRuntime).mockResolvedValue([]);
+    vi.mocked(listNativeAlertMetrics).mockResolvedValue([
+      {
+        key: 'broker.disk.usage_ratio',
+        label: 'Broker disk usage ratio',
+        thresholdUnit: 'ratio',
+        supportsConsumerGroup: false,
+      },
+    ]);
+    vi.mocked(updateAlertRule).mockImplementation(async (rule: 
Partial<AlertRule>) =>
+      buildRule(Number(rule.id), String(rule.name)),
+    );
+    vi.mocked(testAlertRule).mockResolvedValue({
+      samples: [
+        {
+          labels: { broker: 'stale-broker-sample' },
+          availability: 'AVAILABLE',
+          currentValue: 987654,
+          conditionMet: true,
+        },
+      ],
+    });
+  });
+
+  it('shows no test samples for a rule the user never tested', async () => {
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('Rule A');
+    await user.click(within(getRuleRow('Rule A')).getByRole('button', { name: 
'编辑' }));
+    await screen.findByRole('dialog');
+
+    // Run the test on Rule A and confirm the sample table is shown for it.
+    await user.click(screen.getByRole('button', { name: '试运行' }));
+    await screen.findByText('broker=stale-broker-sample');
+
+    // Save Rule A. The saved dialog keeps no test result, so the next editor 
must not
+    // present Rule A's samples as its own.
+    await user.click(within(screen.getByRole('dialog')).getByRole('button', { 
name: /编\s*辑/ }));
+    await waitFor(() => expect(updateAlertRule).toHaveBeenCalledTimes(1));
+
+    // antd keeps the closed dialog mounted, so drive the second open without 
waiting for
+    // its leave transition, then assert on the editor the user actually sees.
+    fireEvent.click(within(getRuleRow('Rule B')).getByRole('button', { name: 
'编辑' }));
+    await screen.findByDisplayValue('Rule B');
+
+    const reopened = screen.getByRole('dialog');
+    expect(within(reopened).queryByText('规则试运行结果')).toBeNull();
+    
expect(within(reopened).queryByText('broker=stale-broker-sample')).toBeNull();
+  }, 30_000);
+});
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index e94b2ead3..d9a301831 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -496,6 +496,7 @@ const AlertsPage = ({ domain = 'CLUSTER' }: 
AlertsPageProps) => {
 
   const openEditModal = (rule: AlertRule) => {
     setEditingRule(rule);
+    setTestResult(null);
     form.setFieldsValue({
       ...rule,
       metric: normalizeMetric(rule.metric),

Reply via email to