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