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();
     }

Reply via email to