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 7f7a9f4f15b Strengthen MCP workflow E2E contracts (#39048)
7f7a9f4f15b is described below
commit 7f7a9f4f15b60b2ef89bb740b3c3f66f99d4b00c
Author: Liang Zhang <[email protected]>
AuthorDate: Wed Jul 8 14:40:49 2026 +0800
Strengthen MCP workflow E2E contracts (#39048)
Add feature workflow schema and unknown-argument recovery checks through
the HTTP protocol surface, including tool-result isError semantics.
Verify Proxy-backed feature workflows reject duplicate create requests
after successful apply and validation, and assert secret-reference
workflow responses redact raw reference values from HTTP response bodies.
---
...ductionProxyFeatureWorkflowContractE2ETest.java | 14 ++++++-
.../FeatureWorkflowContractE2ETest.java | 43 ++++++++++++++++++++++
.../programmatic/HttpTransportRecoveryE2ETest.java | 36 ++++++++++++++++++
3 files changed, 91 insertions(+), 2 deletions(-)
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 8e3cd1e1efd..9b68ef971fc 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
@@ -99,8 +99,8 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest
extends AbstractProducti
@Test
void assertBroadcastWorkflowCanBeAppliedAndValidatedThroughProxy() throws
IOException, InterruptedException {
try (MCPInteractionClient interactionClient =
createOpenedInteractionClient()) {
- Map<String, Object> actualPlanResponse =
interactionClient.call(BROADCAST_PLAN_TOOL_NAME,
- Map.of("database", getLogicalDatabaseName(),
"operation_type", "create", "tables", "orders"));
+ Map<String, Object> arguments = Map.of("database",
getLogicalDatabaseName(), "operation_type", "create", "tables", "orders");
+ Map<String, Object> actualPlanResponse =
interactionClient.call(BROADCAST_PLAN_TOOL_NAME, arguments);
assertThat(String.valueOf(actualPlanResponse.get("status")),
is("planned"));
String planId = String.valueOf(actualPlanResponse.get("plan_id"));
Map<String, Object> actualApplyResponse =
interactionClient.call(APPLY_TOOL_NAME,
@@ -110,6 +110,7 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest
extends AbstractProducti
assertValidationPassed(interactionClient.call(VALIDATE_TOOL_NAME,
Map.of("plan_id", planId)));
List<Map<String, Object>> actualRules =
getPayloadItems(interactionClient.readResource(String.format(BROADCAST_RULES_RESOURCE_URI,
getLogicalDatabaseName())));
assertThat(actualRules.stream().map(each ->
String.valueOf(each.get("broadcast_table"))).toList(), hasItem("orders"));
+ assertDuplicateCreateFails(interactionClient,
BROADCAST_PLAN_TOOL_NAME, arguments);
}
}
@@ -210,6 +211,15 @@ class HttpProductionProxyFeatureWorkflowContractE2ETest
extends AbstractProducti
assertApplyCompleted(actualApplyResponse);
assertThat(getStringList(actualApplyResponse.get("executed_distsql")).size(),
is(1));
assertValidationPassed(interactionClient.call(VALIDATE_TOOL_NAME,
Map.of("plan_id", planId)));
+ assertDuplicateCreateFails(interactionClient, toolName, arguments);
+ }
+
+ private void assertDuplicateCreateFails(final MCPInteractionClient
interactionClient, final String toolName,
+ final Map<String, Object>
arguments) throws IOException, InterruptedException {
+ Map<String, Object> actual = interactionClient.call(toolName,
arguments);
+ assertThat(String.valueOf(actual.get("status")), is("failed"));
+ assertFalse(getMapList(actual.get("issues")).isEmpty());
+ assertModelFacingPayloadContract(actual);
}
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/programmatic/FeatureWorkflowContractE2ETest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/FeatureWorkflowContractE2ETest.java
index 7448233258b..d7ef9e288c8 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/FeatureWorkflowContractE2ETest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/FeatureWorkflowContractE2ETest.java
@@ -19,6 +19,7 @@ package
org.apache.shardingsphere.test.e2e.mcp.runtime.programmatic;
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.MCPHttpTransportTestSupport;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -27,6 +28,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -55,6 +57,8 @@ class FeatureWorkflowContractE2ETest extends
AbstractSharedHttpProgrammaticRunti
HttpClient httpClient = HttpClient.newHttpClient();
String sessionId = initializeSession(httpClient);
assertFeatureDiscovery(httpClient, sessionId, scenario);
+ assertFeatureToolSchemaMatchesPlanArguments(httpClient, sessionId,
scenario);
+ assertRejectUnsupportedPlanArgument(httpClient, sessionId, scenario);
assertClarifiesMissingDatabase(httpClient, sessionId, scenario);
assertRecoversWhenDistSQLIsUnsupportedByDirectDatabase(httpClient,
sessionId, scenario);
}
@@ -88,6 +92,45 @@ class FeatureWorkflowContractE2ETest extends
AbstractSharedHttpProgrammaticRunti
assertModelFacingPayloadContract(payload);
}
+ private void assertFeatureToolSchemaMatchesPlanArguments(final HttpClient
httpClient, final String sessionId,
+ final
FeatureWorkflowScenario scenario) throws IOException, InterruptedException {
+ HttpResponse<String> actual = sendRawPostRequest(httpClient,
createSessionHeaders(sessionId),
MCPHttpTransportTestSupport.createJsonRpcRequestBody(
+ scenario.toolName() + "-schema-1", "tools/list", Map.of()));
+ assertThat(actual.statusCode(), is(200));
+ Map<String, Object> payload =
castToMap(parseJsonBody(actual.body()).get("result"));
+ assertModelFacingPayloadContract(payload);
+ Map<String, Object> actualTool =
findByKey(castToMapList(payload.get("tools")), "name", scenario.toolName());
+ Map<String, Object> actualInputSchema =
castToMap(actualTool.get("inputSchema"));
+ assertFalse((Boolean) actualInputSchema.get("additionalProperties"));
+ Map<String, Object> actualProperties =
castToMap(actualInputSchema.get("properties"));
+ for (String each : scenario.planArguments().keySet()) {
+ assertTrue(actualProperties.containsKey(each), each);
+ }
+ }
+
+ private List<Map<String, Object>> castToMapList(final Object value) {
+ return ((List<?>) value).stream().map(this::castToMap).toList();
+ }
+
+ private Map<String, Object> findByKey(final List<Map<String, Object>>
values, final String key, final String expectedValue) {
+ return values.stream().filter(each ->
expectedValue.equals(each.get(key))).findFirst().orElseThrow();
+ }
+
+ private void assertRejectUnsupportedPlanArgument(final HttpClient
httpClient, final String sessionId,
+ final
FeatureWorkflowScenario scenario) throws IOException, InterruptedException {
+ Map<String, Object> arguments = new
LinkedHashMap<>(scenario.planArguments());
+ arguments.put("client_hint", "narrow");
+ HttpResponse<String> actual = sendToolCallRequest(httpClient,
sessionId, scenario.toolName(), arguments);
+ assertThat(actual.statusCode(), is(200));
+ Map<String, Object> result =
castToMap(parseJsonBody(actual.body()).get("result"));
+ assertTrue((Boolean) result.get("isError"));
+ Map<String, Object> payload =
castToMap(result.get("structuredContent"));
+ Map<String, Object> recovery = getRecoveryPayload(payload,
"validation");
+ assertThat(recovery.get("category"), is("unknown_argument"));
+ assertThat(recovery.get("argument_path"), is("client_hint"));
+ assertModelFacingPayloadContract(payload);
+ }
+
private void assertClarifiesMissingDatabase(final HttpClient httpClient,
final String sessionId, final FeatureWorkflowScenario scenario) throws
IOException, InterruptedException {
HttpResponse<String> actual = sendToolCallRequest(httpClient,
sessionId, scenario.toolName(), Map.of("natural_language_intent", "plan
DistSQL-only feature rule"));
assertThat(actual.statusCode(), is(200));
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportRecoveryE2ETest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportRecoveryE2ETest.java
index b82845a6642..f32d88aca9c 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportRecoveryE2ETest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportRecoveryE2ETest.java
@@ -20,6 +20,7 @@ package
org.apache.shardingsphere.test.e2e.mcp.runtime.programmatic;
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.fixture.MCPWorkflowSecretReferenceFixture;
import
org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPHttpTransportTestSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
@@ -126,6 +127,41 @@ class HttpTransportRecoveryE2ETest extends
AbstractSharedHttpProgrammaticRuntime
assertModelFacingPayloadContract(payload);
}
+ @Test
+ void assertWorkflowResponseRedactsSecretReference() throws IOException,
InterruptedException {
+ launchHttpTransport();
+ HttpClient httpClient = HttpClient.newHttpClient();
+ String sessionId = initializeSession(httpClient);
+ HttpResponse<String> planResponse = sendToolCallRequest(httpClient,
sessionId, "database_gateway_plan_encrypt_rule",
createSecretReferencedEncryptRulePlanArguments());
+ assertThat(planResponse.statusCode(), is(200));
+ assertSecretReferenceRedacted(planResponse.body());
+ Map<String, Object> planPayload =
getStructuredContent(planResponse.body());
+ assertModelFacingPayloadContract(planPayload);
+ String planId = String.valueOf(planPayload.get("plan_id"));
+ HttpResponse<String> previewResponse = sendToolCallRequest(httpClient,
sessionId, WorkflowToolDescriptors.APPLY_TOOL_NAME,
+ Map.of("plan_id", planId, "execution_mode", "preview"));
+ assertThat(previewResponse.statusCode(), is(200));
+ assertSecretReferenceRedacted(previewResponse.body());
+
assertModelFacingPayloadContract(getStructuredContent(previewResponse.body()));
+ }
+
+ private Map<String, Object>
createSecretReferencedEncryptRulePlanArguments() {
+ return Map.of(
+ "database", "logic_db",
+ "schema", "logic_db",
+ "table", "orders",
+ "column", "status",
+ "operation_type", "create",
+ "algorithm_type", "AES",
+ "primary_algorithm_properties", Map.of("aes-key-value",
MCPWorkflowSecretReferenceFixture.createSecretReferenceInput()),
+ "structured_intent_evidence",
Map.of("requires_equality_filter", false, "requires_like_query", false));
+ }
+
+ private void assertSecretReferenceRedacted(final String payload) {
+
assertFalse(payload.contains(MCPWorkflowSecretReferenceFixture.SECRET_REF),
payload);
+
assertFalse(payload.contains(MCPWorkflowSecretReferenceFixture.INPUT_LABEL),
payload);
+ }
+
private Map<String, Object> getFirstNextAction(final Map<String, Object>
recovery) {
return castToMapList(recovery.get("next_actions")).getFirst();
}