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 f38f156a fix(provider): scope deletes to the selected cluster and add 
regression coverage (#2034)
f38f156a is described below

commit f38f156a02b79e001e49edb9ecf7d99add965ed0
Author: xdz997 <[email protected]>
AuthorDate: Fri Aug 14 10:35:52 2026 +0800

    fix(provider): scope deletes to the selected cluster and add regression 
coverage (#2034)
    
    Consolidates #2030, #2032, #2034, #2037, #2039. Implementation changes:
    topic/group deletes and DLQ resend metadata handling are scoped to the
    selected cluster, DLQ resend skips system properties, trace time windows
    derive from message store time, and the no-redirect HTTP request factory
    is shared from common/util instead of duplicated per package. Regression
    tests added for system group filtering, OFFSET_ILLEGAL retries, alert and
    message query validation, reset-offset guards and no-redirect factories.
---
 .../AbstractPrometheusCompatibleMetricsSource.java |  14 +--
 .../studio/cluster/proxy/ProxyAddressService.java  |  14 +--
 .../util/NoRedirectClientHttpRequestFactory.java   |  31 ++++++
 .../provider/apache/RocketMQAdminClientImpl.java   |  12 ++-
 .../provider/apache/RocketMQDLQProvider.java       |   5 +-
 .../provider/apache/RocketMQMessageProvider.java   |  19 +++-
 .../NoRedirectClientHttpRequestFactoryTest.java    |  51 +++++++++
 .../studio/common/util/SystemGroupFilterTest.java  |  42 ++++++++
 .../studio/common/util/SystemTopicFilterTest.java  |  48 +++++++++
 .../studio/ops/alert/AlertServiceTest.java         |  45 ++++++++
 .../apache/RocketMQAdminClientImplTest.java        |  78 ++++++++++++++
 .../provider/apache/RocketMQDLQProviderTest.java   |  98 +++++++++++++++++
 .../apache/RocketMQMessageProviderTest.java        | 117 +++++++++++++++++++++
 .../apache/RocketMQMetadataProviderTest.java       |  26 +++++
 .../studio/settings/SettingsServiceTest.java       |  13 +++
 15 files changed, 585 insertions(+), 28 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
index 4bf11e7d..b7d044b9 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
@@ -14,8 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.rocketmq.studio.cluster.metrics;
 
+import 
org.apache.rocketmq.studio.common.util.NoRedirectClientHttpRequestFactory;
+
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -23,7 +27,6 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatusCode;
 import org.springframework.http.MediaType;
-import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
@@ -68,14 +71,7 @@ public abstract class 
AbstractPrometheusCompatibleMetricsSource implements Metri
     protected AbstractPrometheusCompatibleMetricsSource(RestClient.Builder 
restClientBuilder,
                                                          ObjectMapper 
objectMapper,
                                                          MetricsSourceSettings 
settings) {
-        // Disable redirect following to prevent SSRF bypass via HTTP redirect
-        SimpleClientHttpRequestFactory requestFactory = new 
SimpleClientHttpRequestFactory() {
-            @Override
-            protected void prepareConnection(java.net.HttpURLConnection 
connection, String httpMethod) throws IOException {
-                super.prepareConnection(connection, httpMethod);
-                connection.setInstanceFollowRedirects(false);
-            }
-        };
+        NoRedirectClientHttpRequestFactory requestFactory = new 
NoRedirectClientHttpRequestFactory();
         requestFactory.setConnectTimeout(settings.getConnectTimeout());
         requestFactory.setReadTimeout(settings.getReadTimeout());
         this.restClient = 
restClientBuilder.requestFactory(requestFactory).build();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
index 4804d267..71279b73 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
@@ -17,17 +17,19 @@
 
 package org.apache.rocketmq.studio.cluster.proxy;
 
+import 
org.apache.rocketmq.studio.common.util.NoRedirectClientHttpRequestFactory;
+
+
+
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatusCode;
 import org.springframework.http.ResponseEntity;
-import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.HttpStatusCodeException;
 import org.springframework.web.client.ResourceAccessException;
 import org.springframework.web.client.RestTemplate;
 
-import java.io.IOException;
 import java.time.Duration;
 
 import java.util.ArrayList;
@@ -53,13 +55,7 @@ public class ProxyAddressService {
     private final RestTemplate restTemplate;
 
     public ProxyAddressService() {
-        SimpleClientHttpRequestFactory factory = new 
SimpleClientHttpRequestFactory() {
-            @Override
-            protected void prepareConnection(java.net.HttpURLConnection 
connection, String httpMethod) throws IOException {
-                super.prepareConnection(connection, httpMethod);
-                connection.setInstanceFollowRedirects(false);
-            }
-        };
+        NoRedirectClientHttpRequestFactory factory = new 
NoRedirectClientHttpRequestFactory();
         factory.setConnectTimeout(Duration.ofSeconds(3));
         factory.setReadTimeout(Duration.ofSeconds(3));
         this.restTemplate = new RestTemplate(factory);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/NoRedirectClientHttpRequestFactory.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/NoRedirectClientHttpRequestFactory.java
new file mode 100644
index 00000000..bf89fc9e
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/NoRedirectClientHttpRequestFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+public class NoRedirectClientHttpRequestFactory extends 
SimpleClientHttpRequestFactory {
+
+    @Override
+    protected void prepareConnection(HttpURLConnection connection, String 
httpMethod) throws IOException {
+        super.prepareConnection(connection, httpMethod);
+        connection.setInstanceFollowRedirects(false);
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
index d22accd8..31e58954 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
@@ -283,7 +283,8 @@ public class RocketMQAdminClientImpl implements AdminClient 
{
         String namesrvAddr = namesrvAddr(instanceId);
         executeForInstance(instanceId, admin -> {
             try {
-                Set<String> brokerAddrs = getAllMasterBrokerAddrs(admin);
+                String clusterName = getClusterName(admin);
+                Set<String> brokerAddrs = 
getMasterBrokerAddrsForCluster(admin, clusterName);
 
                 // Delete from brokers
                 if (!brokerAddrs.isEmpty()) {
@@ -298,11 +299,11 @@ public class RocketMQAdminClientImpl implements 
AdminClient {
                         nsAddrs.add(trimmed);
                     }
                 }
-                admin.deleteTopicInNameServer(nsAddrs, getClusterName(admin), 
name);
+                admin.deleteTopicInNameServer(nsAddrs, clusterName, name);
 
                 // Topic names may be shared by several clusters managed by 
this Studio instance.
                 topicMapper.delete(new LambdaQueryWrapper<RmqTopic>()
-                        .eq(RmqTopic::getClusterId, getClusterName(admin))
+                        .eq(RmqTopic::getClusterId, clusterName)
                         .eq(RmqTopic::getName, name));
 
                 recordAudit("DELETE_TOPIC", name, "", "SUCCESS");
@@ -467,7 +468,8 @@ public class RocketMQAdminClientImpl implements AdminClient 
{
 
     private void doDeleteConsumerGroup(MQAdminExt admin, String name) {
         try {
-            Set<String> brokerAddrs = getAllMasterBrokerAddrs(admin);
+            String clusterName = getClusterName(admin);
+            Set<String> brokerAddrs = getMasterBrokerAddrsForCluster(admin, 
clusterName);
 
             for (String addr : brokerAddrs) {
                 admin.deleteSubscriptionGroup(addr, name, true);
@@ -475,7 +477,7 @@ public class RocketMQAdminClientImpl implements AdminClient 
{
 
             // Consumer group names may be shared by several clusters managed 
by this Studio instance.
             groupMapper.delete(new LambdaQueryWrapper<RmqGroup>()
-                    .eq(RmqGroup::getClusterId, getClusterName(admin))
+                    .eq(RmqGroup::getClusterId, clusterName)
                     .eq(RmqGroup::getName, name));
 
             recordAudit("DELETE_GROUP", name, "", "SUCCESS");
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
index 87ad7901..d8d8660e 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
@@ -300,7 +300,10 @@ public class RocketMQDLQProvider implements DLQProvider {
             if (userProperties != null) {
                 for (Map.Entry<String, String> entry : 
userProperties.entrySet()) {
                     String key = entry.getKey();
-                    if (!MessageConst.STRING_HASH_SET.contains(key)) {
+                    if (!MessageConst.STRING_HASH_SET.contains(key)
+                            && !MessageConst.PROPERTY_TAGS.equals(key)
+                            && !MessageConst.PROPERTY_KEYS.equals(key)
+                            && 
!MessageConst.PROPERTY_WAIT_STORE_MSG_OK.equals(key)) {
                         message.putUserProperty(key, entry.getValue());
                     }
                 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
index 86702559..8d315b20 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
@@ -267,15 +267,15 @@ public class RocketMQMessageProvider implements 
MessageProvider {
     @Override
     public TraceRecordVO getMessageTrace(String instanceId, String msgId, 
String topic) {
         return runtimeAdminClientResolver.execute(instanceId,
-                adminExt -> getMessageTrace(instanceId, (DefaultMQAdminExt) 
adminExt, msgId));
+                adminExt -> getMessageTrace(instanceId, (DefaultMQAdminExt) 
adminExt, msgId, topic));
     }
 
-    private TraceRecordVO getMessageTrace(String instanceId, DefaultMQAdminExt 
adminExt, String msgId) {
+    private TraceRecordVO getMessageTrace(String instanceId, DefaultMQAdminExt 
adminExt, String msgId, String topic) {
 
         long now = System.currentTimeMillis();
         long begin;
         long end;
-        long messageStoreTimestamp = resolveMessageStoreTimestamp(adminExt, 
msgId);
+        long messageStoreTimestamp = resolveMessageStoreTimestamp(adminExt, 
msgId, topic);
         if (messageStoreTimestamp > 0) {
             // Derive the trace query window from the message's own store 
timestamp
             // instead of a hardcoded 1-hour lookback. This ensures traces for 
messages
@@ -320,7 +320,18 @@ public class RocketMQMessageProvider implements 
MessageProvider {
      * query window can be derived from the message's own timeline rather than 
the
      * current time. Returns 0 if the message cannot be located.
      */
-    private long resolveMessageStoreTimestamp(DefaultMQAdminExt adminExt, 
String msgId) {
+    private long resolveMessageStoreTimestamp(DefaultMQAdminExt adminExt, 
String msgId, String topic) {
+        if (StringUtils.hasText(topic)) {
+            try {
+                MessageExt messageExt = adminExt.viewMessage(topic, msgId);
+                if (messageExt != null) {
+                    return messageExt.getStoreTimestamp();
+                }
+            } catch (Exception e) {
+                log.debug("Could not view message {} in topic {} for trace 
timestamp: {}",
+                        msgId, topic, e.getMessage());
+            }
+        }
         try {
             // No topic hint is available in the trace flow, so locate the 
message purely
             // by its offset msgId.
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/NoRedirectClientHttpRequestFactoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/NoRedirectClientHttpRequestFactoryTest.java
new file mode 100644
index 00000000..722ed53f
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/NoRedirectClientHttpRequestFactoryTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpMethod;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class NoRedirectClientHttpRequestFactoryTest {
+
+    @Test
+    void prepareConnectionShouldDisableRedirectsTest() throws Exception {
+        HttpURLConnection connection = (HttpURLConnection) 
URI.create("http://example.com";)
+                .toURL().openConnection();
+        TestableNoRedirectClientHttpRequestFactory requestFactory =
+                new TestableNoRedirectClientHttpRequestFactory();
+
+        assertThat(connection.getInstanceFollowRedirects()).isTrue();
+
+        requestFactory.prepare(connection, HttpMethod.GET.name());
+
+        assertThat(connection.getInstanceFollowRedirects()).isFalse();
+        connection.disconnect();
+    }
+
+    private static class TestableNoRedirectClientHttpRequestFactory
+            extends NoRedirectClientHttpRequestFactory {
+
+        void prepare(HttpURLConnection connection, String httpMethod) throws 
Exception {
+            prepareConnection(connection, httpMethod);
+        }
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
new file mode 100644
index 00000000..7068d46e
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SystemGroupFilterTest {
+
+    @Test
+    void shouldRecognizeKnownSystemConsumerGroupPrefixesTest() {
+        assertThat(SystemGroupFilter.isSystem(null)).isTrue();
+        assertThat(SystemGroupFilter.isSystem("")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("CID_RMQ_SYS_TRANS")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("CID_ONSAPI_OWNER")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("CID_SYS_RMQ_TRANS")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("CID_HOUSEKEEPING")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("rmq_sys_TRACE_DATA")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("%RETRY%consumer-a")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("%DLQ%consumer-a")).isTrue();
+        
assertThat(SystemGroupFilter.isSystem("TOOLS_CONSUMER_monitor")).isTrue();
+        
assertThat(SystemGroupFilter.isSystem("FILTERSRV_CONSUMER_filter")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("SELF_TEST_GROUP")).isTrue();
+
+        
assertThat(SystemGroupFilter.isSystem("order-service-consumer")).isFalse();
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemTopicFilterTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemTopicFilterTest.java
new file mode 100644
index 00000000..a3e0f5fa
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemTopicFilterTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SystemTopicFilterTest {
+
+    @Test
+    void shouldRecognizeSharedSystemTopicPrefixesAndBrokerNamesTest() {
+        Set<String> brokerNames = Set.of("broker-prod-a", "broker-prod-b");
+
+        assertThat(SystemTopicFilter.isSystem(null, brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("", brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("RMQ_SYS_TRANS_HALF_TOPIC", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("rmq_sys_TRACE_DATA", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("SCHEDULE_TOPIC_XXXX", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("%RETRY%consumer-a", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("%DLQ%consumer-a", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("CID_RMQ_SYS_TRANS", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("broker_config", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("BenchmarkTestTopic", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("TBW102", brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("SELF_TEST_TOPIC", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("OFFSET_MOVED_EVENT", 
brokerNames)).isTrue();
+        assertThat(SystemTopicFilter.isSystem("broker-prod-a", 
brokerNames)).isTrue();
+
+        assertThat(SystemTopicFilter.isSystem("orders", 
brokerNames)).isFalse();
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
index 3c17f0c3..f4bc37b6 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
@@ -350,6 +350,51 @@ class AlertServiceTest {
         
assertThat(exportedRule.path("alert").asText()).isEqualTo("RocketMQAlert");
     }
 
+    @Test
+    void exportPrometheusRulesYamlShouldPrefixAlertNamesStartingWithDigit() 
throws Exception {
+        AlertRuleVO rule = AlertRuleVO.builder()
+                .name("5xx spike")
+                .metric("rocketmq_consumer_lag_messages")
+                .operator(">")
+                .threshold(1)
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(rule));
+
+        String result = alertService.exportPrometheusRulesYaml();
+
+        JsonNode exportedRule = new ObjectMapper(new 
YAMLFactory()).readTree(result)
+                .path("groups").get(0).path("rules").get(0);
+        
assertThat(exportedRule.path("alert").asText()).isEqualTo("A_5xxspike");
+        assertThat(result).contains("# Rule 1: A_5xxspike");
+    }
+
+    @Test
+    void exportPrometheusRulesYamlShouldReplaceNonFiniteThresholds() throws 
Exception {
+        AlertRuleVO nanRule = AlertRuleVO.builder()
+                .name("Bad Threshold A")
+                .metric("rocketmq_consumer_lag_messages")
+                .operator(">")
+                .threshold(Double.NaN)
+                .enabled(true)
+                .build();
+        AlertRuleVO infinityRule = AlertRuleVO.builder()
+                .name("Bad Threshold B")
+                .metric("rocketmq_consumer_lag_messages")
+                .operator("<")
+                .threshold(Double.POSITIVE_INFINITY)
+                .enabled(true)
+                .build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(nanRule, 
infinityRule));
+
+        String result = alertService.exportPrometheusRulesYaml();
+
+        JsonNode rules = new ObjectMapper(new 
YAMLFactory()).readTree(result).path("groups").get(0).path("rules");
+        
assertThat(rules.get(0).path("expr").asText()).isEqualTo("rocketmq_consumer_lag_messages
 > 0");
+        
assertThat(rules.get(1).path("expr").asText()).isEqualTo("rocketmq_consumer_lag_messages
 < 0");
+        assertThat(result).doesNotContain("NaN", "Infinity");
+    }
+
     @Test
     void exportPrometheusRulesYamlShouldExcludeDisabledRules() {
         AlertRuleVO enabled = AlertRuleVO.builder()
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
index b935b742..16834041 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
@@ -134,6 +134,18 @@ class RocketMQAdminClientImplTest {
                 "instanceId=instance-a, topic=orders, 
timestamp=1784246400000", "SUCCESS");
     }
 
+    @Test
+    void resetOffsetShouldRejectBlankTopicBeforeResolvingAdmin() {
+        assertThatThrownBy(() -> adminClient.resetOffset("instance-a", 
"cg-orders", 1784246400000L, " "))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("topic is required for offset reset")
+                .satisfies(exception -> assertThat(((BusinessException) 
exception).getCode()).isEqualTo(400));
+
+        verifyNoInteractions(runtimeAdminClientResolver);
+        verify(adminFactory, never()).execute(anyString(), any(), any());
+        verifyNoInteractions(auditService);
+    }
+
     @Test
     void getConsumerGroupSurfacesAdminTimeout() throws Exception {
         when(adminExt.examineConsumerConnectionInfo("orders"))
@@ -183,6 +195,25 @@ class RocketMQAdminClientImplTest {
         }
     }
 
+    @Test
+    void 
createTopicShouldOnlyWriteTargetClusterBrokersInMultiClusterTopology() throws 
Exception {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
+        ClusterInfo clusterInfo = clusterInfoWithTwoClusters();
+        when(adminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
+        when(topicMapper.selectOne(any())).thenReturn(null);
+        doNothing().when(adminExt).createAndUpdateTopicConfig(anyString(), 
any(TopicConfig.class));
+
+        TopicVO topic = new TopicVO();
+        topic.setName("orders");
+
+        adminClient.createTopic(topic);
+
+        verify(adminExt).createAndUpdateTopicConfig(
+                org.mockito.ArgumentMatchers.eq("10.0.0.1:10911"), 
any(TopicConfig.class));
+        verify(adminExt, never()).createAndUpdateTopicConfig(
+                org.mockito.ArgumentMatchers.eq("10.0.1.1:10911"), 
any(TopicConfig.class));
+    }
+
     @Test
     void topicWritesUseSelectedInstanceAdmin() throws Exception {
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
@@ -249,6 +280,20 @@ class RocketMQAdminClientImplTest {
         verify(selectedAdmin).deleteTopicInNameServer(Set.of("10.0.0.2:9876"), 
"cluster-1", "orders");
     }
 
+    @Test
+    void deleteTopicScopesBrokerDeletionToSelectedClusterOnly() throws 
Exception {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
+        
when(adminExt.examineBrokerClusterInfo()).thenReturn(clusterInfoWithTwoClusters());
+        doNothing().when(adminExt).deleteTopicInBroker(any(), anyString());
+        doNothing().when(adminExt).deleteTopicInNameServer(any(), anyString(), 
anyString());
+
+        adminClient.deleteTopic(null, "orders");
+
+        verify(adminExt).deleteTopicInBroker(Set.of("10.0.0.1:10911"), 
"orders");
+        verify(adminExt).deleteTopicInNameServer(Set.of("10.0.0.1:9876"), 
"cluster-1", "orders");
+        verify(topicMapper).delete(any());
+    }
+
     @Test
     void createConsumerGroupUsesSelectedInstanceAdmin() throws Exception {
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqGroup.class);
@@ -307,6 +352,20 @@ class RocketMQAdminClientImplTest {
         assertThat(captor.getValue().getSqlSegment()).contains("cluster_id", 
"name");
     }
 
+    @Test
+    void deleteConsumerGroupScopesBrokerDeletionToSelectedClusterOnly() throws 
Exception {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqGroup.class);
+        
when(adminExt.examineBrokerClusterInfo()).thenReturn(clusterInfoWithTwoClusters());
+        doNothing().when(adminExt).deleteSubscriptionGroup(anyString(), 
anyString(),
+                org.mockito.ArgumentMatchers.anyBoolean());
+
+        adminClient.deleteConsumerGroup(null, "cg-orders");
+
+        verify(adminExt).deleteSubscriptionGroup("10.0.0.1:10911", 
"cg-orders", true);
+        verify(adminExt, never()).deleteSubscriptionGroup("10.0.1.1:10911", 
"cg-orders", true);
+        verify(groupMapper).delete(any());
+    }
+
     @Test
     void createTopicSucceedsWhenAuditRecordingFails() throws Exception {
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
@@ -353,6 +412,25 @@ class RocketMQAdminClientImplTest {
         return clusterInfo;
     }
 
+    private ClusterInfo clusterInfoWithTwoClusters() {
+        ClusterInfo clusterInfo = new ClusterInfo();
+        Map<String, Set<String>> clusterAddrTable = new HashMap<>();
+        clusterAddrTable.put("cluster-1", new HashSet<>(List.of("broker-1")));
+        clusterAddrTable.put("cluster-2", new HashSet<>(List.of("broker-2")));
+        clusterInfo.setClusterAddrTable(clusterAddrTable);
+        Map<String, BrokerData> brokerAddrTable = new HashMap<>();
+        BrokerData firstBroker = new BrokerData();
+        firstBroker.setBrokerName("broker-1");
+        firstBroker.setBrokerAddrs(new HashMap<>(Map.of(0L, 
"10.0.0.1:10911")));
+        BrokerData secondBroker = new BrokerData();
+        secondBroker.setBrokerName("broker-2");
+        secondBroker.setBrokerAddrs(new HashMap<>(Map.of(0L, 
"10.0.1.1:10911")));
+        brokerAddrTable.put("broker-1", firstBroker);
+        brokerAddrTable.put("broker-2", secondBroker);
+        clusterInfo.setBrokerAddrTable(brokerAddrTable);
+        return clusterInfo;
+    }
+
     @Test
     void sendMessageShouldNotFailWhenAuditRecordingFails() throws Exception {
         when(properties.getNamesrvAddr()).thenReturn("10.0.0.1:9876");
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index a566b048..d43e5da2 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -24,6 +24,7 @@ import org.apache.rocketmq.client.producer.SendResult;
 import org.apache.rocketmq.client.producer.SendStatus;
 import org.apache.rocketmq.common.MixAll;
 import org.apache.rocketmq.common.message.Message;
+import org.apache.rocketmq.common.message.MessageConst;
 import org.apache.rocketmq.common.message.MessageExt;
 import org.apache.rocketmq.common.message.MessageQueue;
 import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable;
@@ -38,6 +39,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockedConstruction;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -226,6 +228,55 @@ class RocketMQDLQProviderTest {
         }
     }
 
+    @Test
+    void resendMessagesRetriesFromCorrectedOffsetAfterOffsetIllegal() throws 
Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        MessageExt deadLetter = new MessageExt();
+        deadLetter.setMsgId("dlq-after-correction");
+        deadLetter.setTopic(dlqTopic);
+        deadLetter.setBody(new byte[] {1, 2, 3});
+        deadLetter.setStoreTimestamp(150L);
+        PullResult illegalOffset = new PullResult(PullStatus.OFFSET_ILLEGAL, 
20L, 0L, 30L, null);
+        PullResult foundAfterCorrection = new PullResult(PullStatus.FOUND, 
40L, 20L, 30L, List.of(deadLetter));
+        PullResult endOfQueue = new PullResult(PullStatus.NO_NEW_MSG, 50L, 
40L, 40L, List.of());
+        SendResult sendResult = new SendResult();
+        sendResult.setSendStatus(SendStatus.SEND_OK);
+
+        try (MockedConstruction<DefaultMQPullConsumer> mockedConsumers =
+                     mockConstruction(DefaultMQPullConsumer.class, (consumer, 
context) -> {
+                         doNothing().when(consumer).start();
+                         
when(consumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+                         when(consumer.searchOffset(queue, 
100L)).thenReturn(10L);
+                         when(consumer.searchOffset(queue, 
200L)).thenReturn(50L);
+                         when(consumer.pull(queue, "*", 10L, 
32)).thenReturn(illegalOffset);
+                         when(consumer.pull(queue, "*", 20L, 
32)).thenReturn(foundAfterCorrection);
+                         when(consumer.pull(queue, "*", 40L, 
32)).thenReturn(endOfQueue);
+                         doNothing().when(consumer).shutdown();
+                     });
+             MockedConstruction<DefaultMQProducer> mockedProducers =
+                     mockConstruction(DefaultMQProducer.class, (producer, 
context) -> {
+                         doNothing().when(producer).start();
+                         
when(producer.send(any(Message.class))).thenReturn(sendResult);
+                         doNothing().when(producer).shutdown();
+                     })) {
+            assertThat(provider.resendMessages("instance-a", "group-a", 100L, 
200L, "orders"))
+                    .extracting("matched", "resent", "failed", "outcome")
+                    .containsExactly(1, 1, 0, "SUCCESS");
+
+            DefaultMQPullConsumer consumer = 
mockedConsumers.constructed().get(0);
+            verify(consumer).pull(queue, "*", 10L, 32);
+            verify(consumer).pull(queue, "*", 20L, 32);
+            verify(consumer).pull(queue, "*", 40L, 32);
+            
verify(mockedProducers.constructed().get(0)).send(any(Message.class));
+        }
+        verify(auditService).record(
+                eq("RESEND_DLQ"),
+                eq("group-a"),
+                contains("matched=1, resent=1, failed=0"),
+                eq("SUCCESS"));
+    }
+
     @Test
     void resendMessagesCountsNonSendOkResultsAsFailures() throws Exception {
         String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
@@ -269,6 +320,53 @@ class RocketMQDLQProviderTest {
                 eq("FAILED"));
     }
 
+    @Test
+    void resendMessagesShouldCopyUserPropertiesAndSkipSystemProperties() 
throws Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        MessageExt deadLetter = new MessageExt();
+        deadLetter.setMsgId("msg-with-properties");
+        deadLetter.setTopic(dlqTopic);
+        deadLetter.setBody(new byte[] {1});
+        deadLetter.setStoreTimestamp(150L);
+        deadLetter.putUserProperty("traceId", "trace-123");
+        deadLetter.putUserProperty("tenantId", "tenant-a");
+        deadLetter.getProperties().put(MessageConst.PROPERTY_REAL_TOPIC, 
"system-topic-should-not-copy");
+        PullResult pullResult = new PullResult(PullStatus.FOUND, 1L, 0L, 0L, 
List.of(deadLetter));
+        SendResult sendResult = new SendResult();
+        sendResult.setSendStatus(SendStatus.SEND_OK);
+
+        try (MockedConstruction<DefaultMQPullConsumer> ignoredConsumers =
+                     mockConstruction(DefaultMQPullConsumer.class, (consumer, 
context) -> {
+                         doNothing().when(consumer).start();
+                         
when(consumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+                         when(consumer.searchOffset(queue, 
100L)).thenReturn(0L);
+                         when(consumer.searchOffset(queue, 
200L)).thenReturn(0L);
+                         when(consumer.pull(queue, "*", 0L, 
32)).thenReturn(pullResult);
+                         doNothing().when(consumer).shutdown();
+                     });
+             MockedConstruction<DefaultMQProducer> mockedProducers =
+                     mockConstruction(DefaultMQProducer.class, (producer, 
context) -> {
+                         doNothing().when(producer).start();
+                         
when(producer.send(any(Message.class))).thenReturn(sendResult);
+                         doNothing().when(producer).shutdown();
+                     })) {
+            assertThat(provider.resendMessages("instance-a", "group-a", 100L, 
200L, "target-topic"))
+                    .extracting("matched", "resent", "failed", "outcome")
+                    .containsExactly(1, 1, 0, "SUCCESS");
+
+            ArgumentCaptor<Message> messageCaptor = 
ArgumentCaptor.forClass(Message.class);
+            
verify(mockedProducers.constructed().get(0)).send(messageCaptor.capture());
+            Message resent = messageCaptor.getValue();
+            assertThat(resent.getProperties()).containsEntry("traceId", 
"trace-123")
+                    .containsEntry("tenantId", "tenant-a")
+                    .containsEntry("studio_dlq_origin_message_id", 
"msg-with-properties")
+                    .containsEntry("studio_dlq_origin_topic", dlqTopic);
+            assertThat(resent.getProperties())
+                    .doesNotContainEntry(MessageConst.PROPERTY_REAL_TOPIC, 
"system-topic-should-not-copy");
+        }
+    }
+
     @Test
     void createsUniqueProducerGroupsForConcurrentDlqResends() {
         String first = RocketMQDLQProvider.nextResendProducerGroup();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
index 6ff3e36e..87bbed05 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
@@ -21,7 +21,9 @@ import 
org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
 import org.apache.rocketmq.client.consumer.PullResult;
 import org.apache.rocketmq.client.consumer.PullStatus;
 import org.apache.rocketmq.client.trace.TraceConstants;
+import org.apache.rocketmq.common.message.MessageDecoder;
 import org.apache.rocketmq.common.message.MessageExt;
+import org.apache.rocketmq.common.message.MessageId;
 import org.apache.rocketmq.common.message.MessageQueue;
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
 import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
@@ -36,10 +38,12 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockedConstruction;
 import org.mockito.junit.jupiter.MockitoExtension;
 
+import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Set;
@@ -110,6 +114,36 @@ class RocketMQMessageProviderTest {
                 100L, 200L, 0);
     }
 
+    @Test
+    void queryMessagesShouldRejectInvertedTimeRangeBeforeAdminLookup() throws 
Exception {
+        assertThatThrownBy(() -> provider.queryMessages(
+                "instance-a", "TopicA", null, null, null, 200L, 100L))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Message query start time must be before end time")
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(400));
+
+        verify(runtimeAdminClientResolver).resolveEndpoint("instance-a");
+        verify(runtimeAdminClientResolver).execute(eq("instance-a"), any());
+        verify(adminExt, never()).queryMessage(anyString(), anyString(), 
anyInt(), anyLong(), anyLong());
+        verify(queryHistoryService, never()).recordMessageQuery(anyString(), 
anyString(), anyString(),
+                anyString(), anyString(), anyString(), any(), any(), anyInt());
+    }
+
+    @Test
+    void queryMessagesShouldRejectEqualTimeRangeBeforeAdminLookup() throws 
Exception {
+        assertThatThrownBy(() -> provider.queryMessages(
+                "instance-a", "TopicA", null, null, null, 100L, 100L))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Message query start time must be before end time")
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(400));
+
+        verify(runtimeAdminClientResolver).resolveEndpoint("instance-a");
+        verify(runtimeAdminClientResolver).execute(eq("instance-a"), any());
+        verify(adminExt, never()).queryMessage(anyString(), anyString(), 
anyInt(), anyLong(), anyLong());
+        verify(queryHistoryService, never()).recordMessageQuery(anyString(), 
anyString(), anyString(),
+                anyString(), anyString(), anyString(), any(), any(), anyInt());
+    }
+
     @Test
     void queryByKeySurfacesAdminFailure() throws Exception {
         when(adminExt.queryMessage("TopicA", "order-1", 64, 100L, 200L))
@@ -187,6 +221,41 @@ class RocketMQMessageProviderTest {
         }
     }
 
+    @Test
+    void queryByTopicRetriesFromCorrectedOffsetAfterOffsetIllegal() throws 
Exception {
+        MessageQueue queue = new MessageQueue("TopicA", "broker-a", 0);
+        MessageExt message = new MessageExt();
+        message.setMsgId("msg-after-correction");
+        message.setTopic("TopicA");
+        message.setBody("payload".getBytes(StandardCharsets.UTF_8));
+        message.setStoreTimestamp(150L);
+        PullResult illegalOffset = new PullResult(PullStatus.OFFSET_ILLEGAL, 
20L, 0L, 30L, null);
+        PullResult foundAfterCorrection = new PullResult(PullStatus.FOUND, 
40L, 20L, 30L, List.of(message));
+        PullResult endOfQueue = new PullResult(PullStatus.NO_NEW_MSG, 50L, 
40L, 40L, List.of());
+        try (MockedConstruction<DefaultMQPullConsumer> mockedConsumers =
+                     mockConstruction(DefaultMQPullConsumer.class, (consumer, 
context) -> {
+                         doNothing().when(consumer).start();
+                         
when(consumer.fetchSubscribeMessageQueues("TopicA")).thenReturn(Set.of(queue));
+                         when(consumer.searchOffset(queue, 
100L)).thenReturn(10L);
+                         when(consumer.searchOffset(queue, 
200L)).thenReturn(50L);
+                         when(consumer.pull(eq(queue), eq("*"), eq(10L), 
eq(32))).thenReturn(illegalOffset);
+                         when(consumer.pull(eq(queue), eq("*"), eq(20L), 
eq(32))).thenReturn(foundAfterCorrection);
+                         when(consumer.pull(eq(queue), eq("*"), eq(40L), 
eq(32))).thenReturn(endOfQueue);
+                         doNothing().when(consumer).shutdown();
+                     })) {
+            List<MessageRecordVO> messages = provider.queryMessages(
+                    "instance-a", "TopicA", null, null, null, 100L, 200L);
+
+            
assertThat(messages).extracting(MessageRecordVO::getMsgId).containsExactly("msg-after-correction");
+            DefaultMQPullConsumer consumer = 
mockedConsumers.constructed().get(0);
+            verify(consumer).pull(queue, "*", 10L, 32);
+            verify(consumer).pull(queue, "*", 20L, 32);
+            verify(consumer).pull(queue, "*", 40L, 32);
+        }
+        verify(queryHistoryService).recordMessageQuery("instance-a", "TOPIC", 
"TopicA", null, null, null,
+                100L, 200L, 1);
+    }
+
     @Test
     void toRecordVOBoundsMessageBodyAndProperties() {
         MessageExt message = new MessageExt();
@@ -298,6 +367,54 @@ class RocketMQMessageProviderTest {
         assertThat(record.getConsumerStatus()).isEmpty();
     }
 
+    @Test
+    void getMessageTraceShouldDeriveWindowFromTopicMessageStoreTimestamp() 
throws Exception {
+        MessageExt original = new MessageExt();
+        original.setMsgId("msg-with-topic");
+        original.setStoreTimestamp(10_000_000L);
+        when(adminExt.viewMessage("orders", 
"msg-with-topic")).thenReturn(original);
+        when(adminExt.queryMessage(anyString(), anyString(), anyInt(), 
anyLong(), anyLong()))
+                .thenReturn(new QueryResult(0L, List.of()));
+
+        provider.getMessageTrace("instance-a", "msg-with-topic", "orders");
+
+        ArgumentCaptor<Long> beginCaptor = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<Long> endCaptor = ArgumentCaptor.forClass(Long.class);
+        verify(adminExt).queryMessage(eq("RMQ_SYS_TRACE_TOPIC"), 
eq("msg-with-topic"), eq(64),
+                beginCaptor.capture(), endCaptor.capture());
+        assertThat(beginCaptor.getValue()).isEqualTo(10_000_000L - 5 * 
60_000L);
+        assertThat(endCaptor.getValue()).isGreaterThanOrEqualTo(10_000_000L + 
24 * 3600_000L);
+        verify(queryHistoryService).recordTraceQuery(eq("instance-a"), 
eq("msg-with-topic"), eq(null), eq(0), eq(0));
+    }
+
+    @Test
+    void 
getMessageTraceShouldUseFallbackOneHourWindowWhenMessageTimestampCannotBeResolved()
 throws Exception {
+        when(adminExt.queryMessage(anyString(), anyString(), anyInt(), 
anyLong(), anyLong()))
+                .thenReturn(new QueryResult(0L, List.of()));
+
+        provider.getMessageTrace("instance-a", "invalid-offset-id", "orders");
+
+        ArgumentCaptor<Long> beginCaptor = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<Long> endCaptor = ArgumentCaptor.forClass(Long.class);
+        verify(adminExt).queryMessage(eq("RMQ_SYS_TRACE_TOPIC"), 
eq("invalid-offset-id"), eq(64),
+                beginCaptor.capture(), endCaptor.capture());
+        assertThat(endCaptor.getValue() - 
beginCaptor.getValue()).isBetween(3_660_000L, 3_670_000L);
+        verify(queryHistoryService).recordTraceQuery(eq("instance-a"), 
eq("invalid-offset-id"), eq(null), eq(0), eq(0));
+    }
+
+    @Test
+    void 
offsetMessageIdFixtureShouldDecodeToBrokerAddressForTraceWindowTests() throws 
Exception {
+        String offsetMsgId = MessageDecoder.createMessageId(
+                new InetSocketAddress("127.0.0.1", 10911), 12345L);
+
+        MessageId decoded = MessageDecoder.decodeMessageId(offsetMsgId);
+
+        assertThat(decoded.getAddress()).isInstanceOf(InetSocketAddress.class);
+        InetSocketAddress address = (InetSocketAddress) decoded.getAddress();
+        assertThat(address.getHostString()).isEqualTo("127.0.0.1");
+        assertThat(address.getPort()).isEqualTo(10911);
+    }
+
     @Test
     void getMessageTraceSurfacesAdminFailure() throws Exception {
         when(adminExt.queryMessage(anyString(), anyString(), anyInt(), 
anyLong(), anyLong()))
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
index 4c5d07ba..f233111c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
@@ -54,6 +54,7 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -111,6 +112,31 @@ class RocketMQMetadataProviderTest {
         
assertThat(groups.get(0).getConsumeType()).isEqualTo(ConsumeType.CLUSTERING);
     }
 
+    @Test
+    void listConsumerGroupsShouldNotFetchLiveAdminInfoForEachGroup() {
+        RmqGroup first = new RmqGroup();
+        first.setName("group-a");
+        first.setClusterId("cluster-1");
+        first.setConsumeType("CLUSTERING");
+        first.setMaxRetry(16);
+        RmqGroup second = new RmqGroup();
+        second.setName("group-b");
+        second.setClusterId("cluster-1");
+        second.setConsumeType("BROADCASTING");
+        second.setMaxRetry(3);
+        when(groupMapper.selectList(any())).thenReturn(List.of(first, second));
+
+        RocketMQMetadataProvider provider = newProvider();
+
+        List<ConsumerGroupVO> groups = 
provider.listConsumerGroups("cluster-1", null);
+
+        
assertThat(groups).extracting(ConsumerGroupVO::getName).containsExactly("group-a",
 "group-b");
+        
assertThat(groups).extracting(ConsumerGroupVO::getOnlineInstances).containsExactly(0,
 0);
+        
assertThat(groups).extracting(ConsumerGroupVO::getTotalLag).containsExactly(0L, 
0L);
+        verify(groupMapper).selectList(any());
+        verifyNoInteractions(runtimeAdminClientResolver);
+    }
+
     @Test
     void getTopicRoutesShouldUseSelectedInstanceRuntimeClient() {
         List<BrokerRouteVO> routes = 
List.of(BrokerRouteVO.builder().brokerName("broker-a").build());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
index 76dc7abc..d14736d6 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
@@ -514,6 +514,19 @@ class SettingsServiceTest {
         assertThat(result.getMessage()).contains("local or private address");
     }
 
+    @Test
+    void testConnectionShouldRejectLoopbackIpv4Address() {
+        DataSourceTestDTO request = DataSourceTestDTO.builder()
+                .url("http://127.0.0.1:9090";)
+                .type("Prometheus")
+                .build();
+
+        DataSourceTestResultVO result = 
settingsService.testDataSource(request);
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getMessage()).contains("local or private address");
+    }
+
     @Test
     void testConnectionShouldRejectLinkLocalMetadataAddress() {
         DataSourceTestDTO request = DataSourceTestDTO.builder()

Reply via email to