This is an automated email from the ASF dual-hosted git repository.

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new af7378fe31a Strengthen MCP workflow E2E coverage (#39047)
af7378fe31a is described below

commit af7378fe31a9f7e0f2dc421a8762d6d2c2104092
Author: Liang Zhang <[email protected]>
AuthorDate: Wed Jul 8 12:38:09 2026 +0800

    Strengthen MCP workflow E2E coverage (#39047)
    
    Expand the Proxy workflow fixture with read and shadow storage units, and 
add real apply/validate E2E coverage for readwrite-splitting, shadow, and 
sharding workflows. Share model-facing payload assertions and item lookup 
helpers across Proxy workflow tests, and rename the generic workflow runtime 
support to remove the encrypt-specific name.
    
    Add focused MCP protocol contract coverage for parameterized Accept headers 
and cursor-bearing list requests, and extend the MCP architecture boundary test 
so feature modules do not depend on bootstrap packages.
---
 .../mcp/bootstrap/MCPArchitectureBoundaryTest.java |  31 ++++--
 .../AbstractProductionProxyWorkflowE2ETest.java    |  25 +++--
 .../HttpProductionProxyEncryptWorkflowE2ETest.java |   7 +-
 ...ductionProxyFeatureWorkflowContractE2ETest.java | 106 ++++++++++++++++-----
 .../HttpProductionProxyMaskWorkflowE2ETest.java    |   4 -
 ...ductionProxySecretReferenceWorkflowE2ETest.java |   3 +
 .../HttpTransportProtocolContractE2ETest.java      |  22 +++++
 ...t.java => ProxyWorkflowRuntimeTestSupport.java} |   8 +-
 .../proxy/workflow/database-logic-db.yaml          |  22 +++++
 9 files changed, 181 insertions(+), 47 deletions(-)

diff --git 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/MCPArchitectureBoundaryTest.java
 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/MCPArchitectureBoundaryTest.java
index 3cbe2ce4524..7bb4e23863a 100644
--- 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/MCPArchitectureBoundaryTest.java
+++ 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/MCPArchitectureBoundaryTest.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Collection;
 import java.util.List;
 import java.util.stream.Stream;
 
@@ -36,6 +37,14 @@ class MCPArchitectureBoundaryTest {
             "mcp/core/src/main/java",
             "mcp/bootstrap/src/main/java");
     
+    private static final List<String> FEATURE_MODULE_SOURCE_DIRECTORIES = 
List.of(
+            "mcp/features/broadcast/src/main/java",
+            "mcp/features/encrypt/src/main/java",
+            "mcp/features/mask/src/main/java",
+            "mcp/features/readwrite-splitting/src/main/java",
+            "mcp/features/shadow/src/main/java",
+            "mcp/features/sharding/src/main/java");
+    
     private static final List<String> FEATURE_PACKAGE_IMPORTS = List.of(
             "org.apache.shardingsphere.mcp.feature.broadcast",
             "org.apache.shardingsphere.mcp.feature.encrypt",
@@ -44,26 +53,36 @@ class MCPArchitectureBoundaryTest {
             "org.apache.shardingsphere.mcp.feature.shadow",
             "org.apache.shardingsphere.mcp.feature.sharding");
     
+    private static final List<String> BOOTSTRAP_PACKAGE_IMPORTS = 
List.of("org.apache.shardingsphere.mcp.bootstrap");
+    
     @Test
     void assertGenericModulesDoNotImportFeatures() throws IOException {
         Path projectRoot = findProjectRoot();
         for (String each : GENERIC_MODULE_SOURCE_DIRECTORIES) {
-            assertNoFeatureImport(projectRoot.resolve(each));
+            assertNoPackageImport(projectRoot.resolve(each), 
FEATURE_PACKAGE_IMPORTS, "Generic MCP modules must not import feature 
packages");
+        }
+    }
+    
+    @Test
+    void assertFeatureModulesDoNotImportBootstrap() throws IOException {
+        Path projectRoot = findProjectRoot();
+        for (String each : FEATURE_MODULE_SOURCE_DIRECTORIES) {
+            assertNoPackageImport(projectRoot.resolve(each), 
BOOTSTRAP_PACKAGE_IMPORTS, "MCP feature modules must not import bootstrap 
packages");
         }
     }
     
-    private void assertNoFeatureImport(final Path sourceDirectory) throws 
IOException {
+    private void assertNoPackageImport(final Path sourceDirectory, final 
Collection<String> forbiddenImports, final String message) throws IOException {
         try (Stream<Path> paths = Files.walk(sourceDirectory)) {
             List<Path> actualViolations = 
paths.filter(Files::isRegularFile).filter(each -> 
each.toString().endsWith(".java"))
-                    .filter(this::containsFeaturePackageImport).toList();
-            assertTrue(actualViolations.isEmpty(), () -> "Generic MCP modules 
must not import feature packages: " + actualViolations);
+                    .filter(each -> containsPackageImport(each, 
forbiddenImports)).toList();
+            assertTrue(actualViolations.isEmpty(), () -> message + ": " + 
actualViolations);
         }
     }
     
-    private boolean containsFeaturePackageImport(final Path path) {
+    private boolean containsPackageImport(final Path path, final 
Collection<String> forbiddenImports) {
         try {
             String source = Files.readString(path);
-            return FEATURE_PACKAGE_IMPORTS.stream().anyMatch(each -> 
source.contains("import " + each));
+            return forbiddenImports.stream().anyMatch(each -> 
source.contains("import " + each));
         } catch (final IOException ex) {
             throw new IllegalStateException("Failed to read " + path, ex);
         }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionProxyWorkflowE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionProxyWorkflowE2ETest.java
index 94b2a92ede9..64b89c87a13 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionProxyWorkflowE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionProxyWorkflowE2ETest.java
@@ -19,9 +19,10 @@ package 
org.apache.shardingsphere.test.e2e.mcp.runtime.production;
 
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
 import 
org.apache.shardingsphere.mcp.support.workflow.descriptor.WorkflowToolDescriptors;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.assertion.MCPModelContractAssertions;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.runtime.MySQLRuntimeTestSupport;
-import 
org.apache.shardingsphere.test.e2e.mcp.support.runtime.ProxyEncryptWorkflowRuntimeTestSupport;
-import 
org.apache.shardingsphere.test.e2e.mcp.support.runtime.ProxyEncryptWorkflowRuntimeTestSupport.ProxyEncryptWorkflowRuntimeFixture;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.runtime.ProxyWorkflowRuntimeTestSupport;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.runtime.ProxyWorkflowRuntimeTestSupport.ProxyWorkflowRuntimeFixture;
 import org.apache.shardingsphere.test.e2e.mcp.support.runtime.RuntimeTransport;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPInteractionClient;
 import org.junit.jupiter.api.AfterAll;
@@ -40,9 +41,9 @@ import static org.hamcrest.Matchers.is;
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 abstract class AbstractProductionProxyWorkflowE2ETest extends 
AbstractProductionRuntimeE2ETest {
     
-    private ProxyEncryptWorkflowRuntimeFixture runtimeFixture;
+    private ProxyWorkflowRuntimeFixture runtimeFixture;
     
-    private ProxyEncryptWorkflowRuntimeFixture sharedRuntimeFixture;
+    private ProxyWorkflowRuntimeFixture sharedRuntimeFixture;
     
     private boolean sharedRuntimeFixtureSelected;
     
@@ -81,7 +82,7 @@ abstract class AbstractProductionProxyWorkflowE2ETest extends 
AbstractProduction
                 prepareSharedRuntimeFixture();
                 return;
             }
-            runtimeFixture = 
ProxyEncryptWorkflowRuntimeTestSupport.createFixture();
+            runtimeFixture = ProxyWorkflowRuntimeTestSupport.createFixture();
         } catch (final SQLException ex) {
             throw new IOException(ex);
         }
@@ -89,7 +90,7 @@ abstract class AbstractProductionProxyWorkflowE2ETest extends 
AbstractProduction
     
     private void prepareSharedRuntimeFixture() throws SQLException {
         if (null == sharedRuntimeFixture) {
-            sharedRuntimeFixture = 
ProxyEncryptWorkflowRuntimeTestSupport.createFixture();
+            sharedRuntimeFixture = 
ProxyWorkflowRuntimeTestSupport.createFixture();
         }
         runtimeFixture = sharedRuntimeFixture;
     }
@@ -112,10 +113,12 @@ abstract class AbstractProductionProxyWorkflowE2ETest 
extends AbstractProduction
         assertThat(actualValidationResponse.toString(), 
String.valueOf(actualValidationResponse.get("overall_status")), is("passed"));
         assertThat(actualValidationResponse.toString(), 
getMapList(actualValidationResponse.get("issues")).size(), is(0));
         assertThat(actualValidationResponse.toString(), 
getMapList(actualValidationResponse.get("mismatches")).size(), is(0));
+        assertModelFacingPayloadContract(actualValidationResponse);
     }
     
     protected final void assertApplyCompleted(final Map<String, Object> 
actualApplyResponse) {
         assertThat(actualApplyResponse.toString(), 
String.valueOf(actualApplyResponse.get("status")), is("completed"));
+        assertModelFacingPayloadContract(actualApplyResponse);
     }
     
     protected final Map<String, Object> applyReviewedWorkflow(final 
MCPInteractionClient interactionClient, final String planId) throws 
IOException, InterruptedException {
@@ -125,6 +128,7 @@ abstract class AbstractProductionProxyWorkflowE2ETest 
extends AbstractProduction
     protected final Map<String, Object> previewWorkflow(final 
MCPInteractionClient interactionClient, final String planId) throws 
IOException, InterruptedException {
         Map<String, Object> result = 
interactionClient.call(WorkflowToolDescriptors.APPLY_TOOL_NAME, 
Map.of("plan_id", planId, "execution_mode", "preview"));
         assertThat(String.valueOf(result.get("status")), is("preview"));
+        assertModelFacingPayloadContract(result);
         return result;
     }
     
@@ -144,6 +148,15 @@ abstract class AbstractProductionProxyWorkflowE2ETest 
extends AbstractProduction
         return 
getMapList(payload.get("clarification_questions")).stream().map(each -> 
String.valueOf(each.get("display_message"))).toList();
     }
     
+    protected final void assertModelFacingPayloadContract(final Map<String, 
Object> payload) {
+        MCPModelContractAssertions.assertCanonicalNextActionLists(payload);
+    }
+    
+    protected final Map<String, Object> findItemByField(final List<Map<String, 
Object>> items, final String fieldName, final String expectedValue) {
+        return items.stream().filter(each -> 
expectedValue.equalsIgnoreCase(String.valueOf(each.get(fieldName)))).findFirst()
+                .orElseThrow(() -> new AssertionError(String.format("Failed to 
find item by %s=%s in %s", fieldName, expectedValue, items)));
+    }
+    
     protected final List<String> getStringList(final Object value) {
         return null == value ? List.of() : ((List<?>) 
value).stream().map(String::valueOf).toList();
     }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyEncryptWorkflowE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyEncryptWorkflowE2ETest.java
index 2e2e0199dec..a0a8d5d00cf 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyEncryptWorkflowE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyEncryptWorkflowE2ETest.java
@@ -76,12 +76,14 @@ class HttpProductionProxyEncryptWorkflowE2ETest extends 
AbstractProductionProxyW
             Map<String, Object> clarifyingResponse = 
interactionClient.call(PLAN_TOOL_NAME,
                     Map.of("table", "orders", "column", "status", 
"natural_language_intent", "encrypt status with reversible encryption, no 
equality, no like"));
             assertThat(String.valueOf(clarifyingResponse.get("status")), 
is("clarifying"));
+            assertModelFacingPayloadContract(clarifyingResponse);
             assertThat(getClarificationMessages(clarifyingResponse), 
is(List.of("Please provide logical database first.")));
             String planId = String.valueOf(clarifyingResponse.get("plan_id"));
             Map<String, Object> plannedResponse = 
interactionClient.call(PLAN_TOOL_NAME,
                     Map.of("plan_id", planId, "database", 
getLogicalDatabaseName(), "algorithm_type", "AES",
                             "cipher_column_name", "status_cipher", 
"primary_algorithm_properties", Map.of("aes-key-value", 
TEMPLATE_SECRET_VALUE)));
             assertThat(String.valueOf(plannedResponse.get("status")), 
is("planned"));
+            assertModelFacingPayloadContract(plannedResponse);
             assertSecretRedacted(plannedResponse, TEMPLATE_SECRET_VALUE);
             assertThat(String.valueOf(plannedResponse.get("current_step")), 
is("review"));
             
assertThat(getStringList(plannedResponse.get("global_steps")).size(), is(8));
@@ -351,11 +353,6 @@ class HttpProductionProxyEncryptWorkflowE2ETest extends 
AbstractProductionProxyW
         return result;
     }
     
-    private Map<String, Object> findItemByField(final List<Map<String, 
Object>> items, final String fieldName, final String expectedValue) {
-        return items.stream().filter(each -> 
expectedValue.equalsIgnoreCase(String.valueOf(each.get(fieldName)))).findFirst()
-                .orElseThrow(() -> new AssertionError(String.format("Failed to 
find item by %s=%s in %s", fieldName, expectedValue, items)));
-    }
-    
     private void assertSecretRedacted(final Map<String, Object> actual, final 
String secretValue) {
         if (!secretValue.isEmpty()) {
             assertFalse(String.valueOf(actual).contains(secretValue));
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyFeatureWorkflowContractE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyFeatureWorkflowContractE2ETest.java
index 2fce6de2238..8e3cd1e1efd 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyFeatureWorkflowContractE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyFeatureWorkflowContractE2ETest.java
@@ -19,7 +19,6 @@ package 
org.apache.shardingsphere.test.e2e.mcp.runtime.production;
 
 import 
org.apache.shardingsphere.mcp.support.workflow.descriptor.WorkflowToolDescriptors;
 import org.apache.shardingsphere.test.e2e.mcp.env.MCPE2ECondition;
-import 
org.apache.shardingsphere.test.e2e.mcp.support.assertion.MCPModelContractAssertions;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPInteractionClient;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.EnabledIf;
@@ -36,6 +35,7 @@ import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -49,8 +49,20 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest 
extends AbstractProducti
     
     private static final String BROADCAST_PLAN_TOOL_NAME = 
"database_gateway_plan_broadcast_rule";
     
+    private static final String READWRITE_SPLITTING_PLAN_TOOL_NAME = 
"database_gateway_plan_readwrite_splitting_rule";
+    
+    private static final String SHADOW_PLAN_TOOL_NAME = 
"database_gateway_plan_shadow_rule";
+    
+    private static final String SHARDING_PLAN_TOOL_NAME = 
"database_gateway_plan_sharding_table_rule";
+    
     private static final String BROADCAST_RULES_RESOURCE_URI = 
"shardingsphere://features/broadcast/databases/%s/rules";
     
+    private static final String READWRITE_SPLITTING_RULES_RESOURCE_URI = 
"shardingsphere://features/readwrite-splitting/databases/%s/rules";
+    
+    private static final String SHADOW_RULES_RESOURCE_URI = 
"shardingsphere://features/shadow/databases/%s/rules";
+    
+    private static final String SHARDING_TABLE_RULES_RESOURCE_URI = 
"shardingsphere://features/sharding/databases/%s/table-rules";
+    
     private static final List<String> FORBIDDEN_ARTIFACT_TOKENS = List.of(
             "create table", "alter table", "drop table", "create index", "drop 
index", "migrate", "migration", "backfill", "data probe", "physical metadata",
             "register storage unit", "alter storage unit", "unregister storage 
unit");
@@ -84,20 +96,6 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest 
extends AbstractProducti
         }
     }
     
-    @Test
-    void assertReadStorageUnitsThroughProxy() throws IOException, 
InterruptedException {
-        useSharedReadOnlyRuntimeFixture();
-        try (MCPInteractionClient interactionClient = 
createOpenedInteractionClient()) {
-            List<Map<String, Object>> actualStorageUnits = 
getPayloadItems(interactionClient.readResource(
-                    
String.format("shardingsphere://databases/%s/storage-units", 
getLogicalDatabaseName())));
-            assertThat(actualStorageUnits.stream().map(each -> 
String.valueOf(each.get("name"))).toList(), hasItem("ds_0"));
-            List<Map<String, Object>> actualStorageUnitDetail = 
getPayloadItems(interactionClient.readResource(
-                    
String.format("shardingsphere://databases/%s/storage-units/ds_0", 
getLogicalDatabaseName())));
-            assertThat(actualStorageUnitDetail.size(), is(1));
-            
assertThat(String.valueOf(actualStorageUnitDetail.getFirst().get("name")), 
is("ds_0"));
-        }
-    }
-    
     @Test
     void assertBroadcastWorkflowCanBeAppliedAndValidatedThroughProxy() throws 
IOException, InterruptedException {
         try (MCPInteractionClient interactionClient = 
createOpenedInteractionClient()) {
@@ -115,20 +113,65 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest 
extends AbstractProducti
         }
     }
     
+    @Test
+    void 
assertReadwriteSplittingWorkflowCanBeAppliedAndValidatedThroughProxy() throws 
IOException, InterruptedException {
+        try (MCPInteractionClient interactionClient = 
createOpenedInteractionClient()) {
+            assertStorageUnitsExposeWorkflowTopology(interactionClient);
+            planApplyAndValidateWorkflow(interactionClient, 
READWRITE_SPLITTING_PLAN_TOOL_NAME,
+                    Map.of("database", getLogicalDatabaseName(), 
"operation_type", "create", "rule", "readwrite_ds", "write_storage_unit", 
"ds_0",
+                            "read_storage_units", "ds_1", 
"transactional_read_query_strategy", "DYNAMIC", "load_balancer_type", 
"ROUND_ROBIN"));
+            List<Map<String, Object>> actualRules = 
getPayloadItems(interactionClient.readResource(
+                    String.format(READWRITE_SPLITTING_RULES_RESOURCE_URI, 
getLogicalDatabaseName())));
+            Map<String, Object> actualRule = findItemByField(actualRules, 
"name", "readwrite_ds");
+            
assertThat(String.valueOf(actualRule.get("write_storage_unit_name")), 
is("ds_0"));
+            
assertTrue(String.valueOf(actualRule.get("read_storage_unit_names")).contains("ds_1"));
+            
assertThat(String.valueOf(actualRule.get("transactional_read_query_strategy")).toUpperCase(Locale.ENGLISH),
 is("DYNAMIC"));
+        }
+    }
+    
+    @Test
+    void assertShadowWorkflowCanBeAppliedAndValidatedThroughProxy() throws 
IOException, InterruptedException {
+        try (MCPInteractionClient interactionClient = 
createOpenedInteractionClient()) {
+            planApplyAndValidateWorkflow(interactionClient, 
SHADOW_PLAN_TOOL_NAME,
+                    Map.of("database", getLogicalDatabaseName(), 
"operation_type", "create", "rule", "shadow_rule", "source_storage_unit", 
"ds_0",
+                            "shadow_storage_unit", "ds_shadow", "table", 
"orders", "algorithm_type", "VALUE_MATCH",
+                            "algorithm_properties", Map.of("operation", 
"insert", "column", "order_id", "value", "1")));
+            List<Map<String, Object>> actualRules = 
getPayloadItems(interactionClient.readResource(String.format(SHADOW_RULES_RESOURCE_URI,
 getLogicalDatabaseName())));
+            Map<String, Object> actualRule = findItemByField(actualRules, 
"rule_name", "shadow_rule");
+            assertThat(String.valueOf(actualRule.get("source_name")), 
is("ds_0"));
+            assertThat(String.valueOf(actualRule.get("shadow_name")), 
is("ds_shadow"));
+            assertThat(String.valueOf(actualRule.get("shadow_table")), 
is("orders"));
+            
assertThat(String.valueOf(actualRule.get("algorithm_type")).toUpperCase(Locale.ENGLISH),
 is("VALUE_MATCH"));
+        }
+    }
+    
+    @Test
+    void assertShardingWorkflowCanBeAppliedAndValidatedThroughProxy() throws 
IOException, InterruptedException {
+        try (MCPInteractionClient interactionClient = 
createOpenedInteractionClient()) {
+            planApplyAndValidateWorkflow(interactionClient, 
SHARDING_PLAN_TOOL_NAME,
+                    Map.of("database", getLogicalDatabaseName(), 
"operation_type", "create", "table", "orders", "column", "order_id",
+                            "data_nodes", "ds_0.orders", "strategy_type", 
"standard", "algorithm_type", "INLINE",
+                            "algorithm_properties", 
Map.of("algorithm-expression", "orders")));
+            List<Map<String, Object>> actualRules = 
getPayloadItems(interactionClient.readResource(
+                    String.format(SHARDING_TABLE_RULES_RESOURCE_URI, 
getLogicalDatabaseName())));
+            assertThat(actualRules.stream().map(each -> 
String.valueOf(each.get("table"))).toList(), hasItem("orders"));
+        }
+    }
+    
     private static Stream<Arguments> featureWorkflowScenarios() {
         return Stream.of(
                 Arguments.of("broadcast", new 
FeatureWorkflowScenario(BROADCAST_PLAN_TOOL_NAME,
                         Map.of("operation_type", "create", "tables", 
"orders"), "CREATE BROADCAST TABLE RULE")),
-                Arguments.of("readwrite-splitting", new 
FeatureWorkflowScenario("database_gateway_plan_readwrite_splitting_rule",
+                Arguments.of("readwrite-splitting", new 
FeatureWorkflowScenario(READWRITE_SPLITTING_PLAN_TOOL_NAME,
                         Map.of("operation_type", "create", "rule", 
"readwrite_ds", "write_storage_unit", "ds_0",
-                                "read_storage_units", "ds_0", 
"transactional_read_query_strategy", "DYNAMIC"),
+                                "read_storage_units", "ds_1", 
"transactional_read_query_strategy", "DYNAMIC"),
                         "CREATE READWRITE_SPLITTING RULE")),
-                Arguments.of("shadow", new 
FeatureWorkflowScenario("database_gateway_plan_shadow_rule",
+                Arguments.of("shadow", new 
FeatureWorkflowScenario(SHADOW_PLAN_TOOL_NAME,
                         Map.of("operation_type", "create", "rule", 
"shadow_rule", "source_storage_unit", "ds_0",
-                                "shadow_storage_unit", "ds_0_shadow", "table", 
"orders", "algorithm_type", "VALUE_MATCH",
+                                "shadow_storage_unit", "ds_shadow", "table", 
"orders", "algorithm_type", "VALUE_MATCH",
                                 "algorithm_properties", Map.of("operation", 
"insert", "column", "order_id", "value", "1")),
                         "CREATE SHADOW RULE")),
-                Arguments.of("sharding", new 
FeatureWorkflowScenario("database_gateway_plan_sharding_table_rule",
+                Arguments.of("sharding", new 
FeatureWorkflowScenario(SHARDING_PLAN_TOOL_NAME,
                         Map.of("operation_type", "create", "table", "orders", 
"column", "order_id",
                                 "data_nodes", "ds_0.orders", "strategy_type", 
"standard", "algorithm_type", "INLINE",
                                 "algorithm_properties", 
Map.of("algorithm-expression", "orders")),
@@ -146,8 +189,27 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest 
extends AbstractProducti
         FORBIDDEN_ARTIFACT_TOKENS.forEach(each -> 
assertFalse(actualPayload.contains(each)));
     }
     
-    private void assertModelFacingPayloadContract(final Map<String, Object> 
payload) {
-        MCPModelContractAssertions.assertCanonicalNextActionLists(payload);
+    private void assertStorageUnitsExposeWorkflowTopology(final 
MCPInteractionClient interactionClient) throws IOException, 
InterruptedException {
+        List<Map<String, Object>> actualStorageUnits = 
getPayloadItems(interactionClient.readResource(
+                String.format("shardingsphere://databases/%s/storage-units", 
getLogicalDatabaseName())));
+        assertThat(actualStorageUnits.stream().map(each -> 
String.valueOf(each.get("name"))).toList(), hasItems("ds_0", "ds_1", 
"ds_shadow"));
+        List<Map<String, Object>> actualStorageUnitDetail = 
getPayloadItems(interactionClient.readResource(
+                
String.format("shardingsphere://databases/%s/storage-units/ds_0", 
getLogicalDatabaseName())));
+        assertThat(actualStorageUnitDetail.size(), is(1));
+        
assertThat(String.valueOf(actualStorageUnitDetail.getFirst().get("name")), 
is("ds_0"));
+    }
+    
+    private void planApplyAndValidateWorkflow(final MCPInteractionClient 
interactionClient, final String toolName,
+                                              final Map<String, Object> 
arguments) throws IOException, InterruptedException {
+        Map<String, Object> actualPlanResponse = 
interactionClient.call(toolName, arguments);
+        assertThat(String.valueOf(actualPlanResponse.get("status")), 
is("planned"));
+        assertNoForbiddenArtifacts(actualPlanResponse);
+        assertModelFacingPayloadContract(actualPlanResponse);
+        String planId = String.valueOf(actualPlanResponse.get("plan_id"));
+        Map<String, Object> actualApplyResponse = 
applyReviewedWorkflow(interactionClient, planId);
+        assertApplyCompleted(actualApplyResponse);
+        
assertThat(getStringList(actualApplyResponse.get("executed_distsql")).size(), 
is(1));
+        assertValidationPassed(interactionClient.call(VALIDATE_TOOL_NAME, 
Map.of("plan_id", planId)));
     }
     
     private record FeatureWorkflowScenario(String toolName, Map<String, 
Object> planArguments, String expectedDistSQLToken) {
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyMaskWorkflowE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyMaskWorkflowE2ETest.java
index a754ae1e99c..4e4af10393d 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyMaskWorkflowE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxyMaskWorkflowE2ETest.java
@@ -175,8 +175,4 @@ class HttpProductionProxyMaskWorkflowE2ETest extends 
AbstractProductionProxyWork
         assertValidationPassed(interactionClient.call(VALIDATE_TOOL_NAME, 
Map.of("plan_id", planId)));
     }
     
-    private Map<String, Object> findItemByField(final List<Map<String, 
Object>> items, final String fieldName, final String expectedValue) {
-        return items.stream().filter(each -> 
expectedValue.equalsIgnoreCase(String.valueOf(each.get(fieldName)))).findFirst()
-                .orElseThrow(() -> new AssertionError(String.format("Failed to 
find item by %s=%s in %s", fieldName, expectedValue, items)));
-    }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxySecretReferenceWorkflowE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxySecretReferenceWorkflowE2ETest.java
index d49a0f9405c..69c4f5b8000 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxySecretReferenceWorkflowE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/HttpProductionProxySecretReferenceWorkflowE2ETest.java
@@ -60,6 +60,7 @@ class HttpProductionProxySecretReferenceWorkflowE2ETest 
extends AbstractProducti
             assertThat(String.valueOf(applyResponse.get("response_mode")), 
is("recovery"));
             assertThat(String.valueOf(applyResponse.get("status")), 
is("failed"));
             assertThat(String.valueOf(applyResponse.get("category")), 
is(MCPDiagnosticCategory.SECRET_REFERENCE_MANUAL_EXECUTION_REQUIRED));
+            assertModelFacingPayloadContract(applyResponse);
             assertThat(getMapList(applyResponse.get("step_results")).size(), 
is(0));
             
assertThat(getStringList(applyResponse.get("executed_distsql")).size(), is(0));
             
assertThat(getStringList(applyResponse.get("applied_artifacts")).size(), is(0));
@@ -78,6 +79,7 @@ class HttpProductionProxySecretReferenceWorkflowE2ETest 
extends AbstractProducti
     
     private void assertPlannedSecretReferencePayload(final Map<String, Object> 
planResponse) {
         assertThat(String.valueOf(planResponse.get("status")), is("planned"));
+        assertModelFacingPayloadContract(planResponse);
         Map<String, Object> secretReferenceSummary = 
getMap(planResponse.get("secret_reference_summary"));
         assertTrue((Boolean) secretReferenceSummary.get("required"));
         assertThat(secretReferenceSummary.get("reference_count"), is(1));
@@ -91,6 +93,7 @@ class HttpProductionProxySecretReferenceWorkflowE2ETest 
extends AbstractProducti
     
     private void assertSecretReferencedPreview(final Map<String, Object> 
previewResponse) {
         assertThat(String.valueOf(previewResponse.get("status")), 
is("preview"));
+        assertModelFacingPayloadContract(previewResponse);
         List<Map<String, Object>> previewArtifacts = 
getMapList(previewResponse.get("preview_artifacts"));
         assertThat(previewArtifacts.size(), is(1));
         assertThat(String.valueOf(previewArtifacts.getFirst().get("sql")), 
containsString("'aes-key-value'='<SECRET_VALUE_PRIMARY_AES_KEY_VALUE>'"));
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
index 8159ab6d1dc..b32d03e5310 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
@@ -84,6 +84,17 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         assertFalse(actual.headers().firstValue("MCP-Session-Id").isPresent());
     }
     
+    @Test
+    void assertAcceptInitializeWithParameterizedAcceptHeader() throws 
IOException, InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        HttpResponse<String> actual = sendInitializeRequest(httpClient,
+                Map.of("Accept", "application/json; charset=utf-8, 
text/event-stream; charset=utf-8"),
+                
MCPHttpTransportTestSupport.createInitializeRequestParams("mcp-e2e-programmatic"));
+        assertThat(actual.statusCode(), is(200));
+        
assertThat(actual.headers().firstValue("MCP-Protocol-Version").orElse(""), 
is(getProtocolVersion()));
+    }
+    
     @Test
     void assertRejectEventStreamWithoutAcceptHeader() throws IOException, 
InterruptedException {
         launchHttpTransport();
@@ -173,6 +184,17 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         assertFalse(actualResult.containsKey("nextCursor"));
     }
     
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("listMethodCases")
+    void assertListMethodAcceptsPaginationCursorParameter(final String method, 
final String resultKey) throws IOException, InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        Map<String, Object> actualResult = sendInitializedRequest(httpClient, 
sessionId, method + "-cursor-1", method, Map.of("cursor", "opaque-cursor"));
+        
assertFalse(MCPInteractionPayloads.castToList(actualResult.get(resultKey)).isEmpty());
+        assertFalse(actualResult.containsKey("nextCursor"));
+    }
+    
     @Test
     void assertEnforceToolCallLimitPerSession() throws IOException, 
InterruptedException {
         String propertyName = 
MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY;
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/ProxyEncryptWorkflowRuntimeTestSupport.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/ProxyWorkflowRuntimeTestSupport.java
similarity index 92%
rename from 
test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/ProxyEncryptWorkflowRuntimeTestSupport.java
rename to 
test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/ProxyWorkflowRuntimeTestSupport.java
index 86717dfc87c..f77892a34d6 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/ProxyEncryptWorkflowRuntimeTestSupport.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/ProxyWorkflowRuntimeTestSupport.java
@@ -37,7 +37,7 @@ import java.util.Map;
  * Proxy-backed runtime fixture support for workflow E2E tests.
  */
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
-public final class ProxyEncryptWorkflowRuntimeTestSupport {
+public final class ProxyWorkflowRuntimeTestSupport {
     
     private static final String LOGICAL_DATABASE_NAME = "logic_db";
     
@@ -53,7 +53,7 @@ public final class ProxyEncryptWorkflowRuntimeTestSupport {
      * @return runtime fixture
      * @throws SQLException SQL exception
      */
-    public static ProxyEncryptWorkflowRuntimeFixture createFixture() throws 
SQLException {
+    public static ProxyWorkflowRuntimeFixture createFixture() throws 
SQLException {
         GenericContainer<?> storageContainer = 
MySQLRuntimeTestSupport.createContainer().withNetworkAliases(STORAGE_NETWORK_ALIAS);
         ShardingSphereProxyEmbeddedContainer proxyContainer = 
createProxyContainer();
         boolean success = false;
@@ -63,7 +63,7 @@ public final class ProxyEncryptWorkflowRuntimeTestSupport {
             proxyContainer.dependsOn(storageContainer);
             proxyContainer.start();
             success = true;
-            return new ProxyEncryptWorkflowRuntimeFixture(storageContainer, 
proxyContainer, createRuntimeDatabases(proxyContainer.getProxyPort()));
+            return new ProxyWorkflowRuntimeFixture(storageContainer, 
proxyContainer, createRuntimeDatabases(proxyContainer.getProxyPort()));
         } finally {
             if (!success) {
                 proxyContainer.stop();
@@ -91,7 +91,7 @@ public final class ProxyEncryptWorkflowRuntimeTestSupport {
      */
     @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
     @Getter
-    public static final class ProxyEncryptWorkflowRuntimeFixture implements 
AutoCloseable {
+    public static final class ProxyWorkflowRuntimeFixture implements 
AutoCloseable {
         
         private final GenericContainer<?> storageContainer;
         
diff --git 
a/test/e2e/mcp/src/test/resources/proxy/workflow/database-logic-db.yaml 
b/test/e2e/mcp/src/test/resources/proxy/workflow/database-logic-db.yaml
index f68732a31a8..baf82a52336 100644
--- a/test/e2e/mcp/src/test/resources/proxy/workflow/database-logic-db.yaml
+++ b/test/e2e/mcp/src/test/resources/proxy/workflow/database-logic-db.yaml
@@ -29,6 +29,28 @@ dataSources:
     minPoolSize: 1
     customPoolProps:
       driverClassName: com.mysql.cj.jdbc.Driver
+  ds_1:
+    url: 
jdbc:mysql://mysql.workflow.host:3306/orders?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8
+    username: mcp
+    password: mcp
+    connectionTimeoutMilliseconds: 30000
+    idleTimeoutMilliseconds: 60000
+    maxLifetimeMilliseconds: 1800000
+    maxPoolSize: 2
+    minPoolSize: 1
+    customPoolProps:
+      driverClassName: com.mysql.cj.jdbc.Driver
+  ds_shadow:
+    url: 
jdbc:mysql://mysql.workflow.host:3306/orders?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8
+    username: mcp
+    password: mcp
+    connectionTimeoutMilliseconds: 30000
+    idleTimeoutMilliseconds: 60000
+    maxLifetimeMilliseconds: 1800000
+    maxPoolSize: 2
+    minPoolSize: 1
+    customPoolProps:
+      driverClassName: com.mysql.cj.jdbc.Driver
 
 rules:
 - !SINGLE


Reply via email to