This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
commit f823a566f85a7508df39f80b5c9a290d54f60548 Author: James Bognar <[email protected]> AuthorDate: Mon Aug 3 20:06:54 2026 -0700 Add missed unit test coverage for MCP sampling + elicitation beans (TODO-328) Companion to 3c67b8a8e0: stages the McpV2Beans_Test.java updates (ElicitAction/ElicitRequest/ElicitResult/AudioContent/ModelHint/ ModelPreferences/CreateMessageRequest/CreateMessageResult/SamplingCapability round-trip coverage) that were left unstaged in the prior commit. Co-authored-by: Cursor <[email protected]> --- .../juneau/bean/mcp/v20260728/McpV2Beans_Test.java | 196 ++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java b/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java index 5a9a5b80f6..efd95a40d4 100644 --- a/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java +++ b/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java @@ -17,6 +17,7 @@ package org.apache.juneau.bean.mcp.v20260728; import static org.apache.juneau.BasicTestUtils.*; +import static org.apache.juneau.commons.utils.CollectionUtils.*; import static org.apache.juneau.test.bct.BctAssertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -697,11 +698,12 @@ class McpV2Beans_Test { @Test void h01_clientCapabilities_elicitationRoundTrip() { var caps = new ClientCapabilities() .setRoots(new RootsCapability().setListChanged(true)) - .setSampling(Map.of("x", 1)) + .setSampling(new SamplingCapability()) .setElicitation(new ElicitationCapability()) .setExperimental(Map.of("y", 2)); var copy = JsonParser.DEFAULT.read(JsonSerializer.DEFAULT.write(caps), ClientCapabilities.class); - assertBean(copy, "roots{listChanged},sampling,experimental", "{true},{x=1},{y=2}"); + assertBean(copy, "roots{listChanged},experimental", "{true},{y=2}"); + assertNotNull(copy.getSampling(), () -> "must serialize sampling: " + JsonSerializer.DEFAULT.write(copy)); assertNotNull(copy.getElicitation(), () -> "must serialize elicitation: " + JsonSerializer.DEFAULT.write(copy)); } @@ -710,4 +712,194 @@ class McpV2Beans_Test { var json = JsonSerializer.DEFAULT.write(new ClientCapabilities()); assertFalse(json.contains("elicitation"), () -> "elicitation must be omitted when unset: " + json); } + + @Test void i01_elicitAction_wireValuesRoundTrip() { + assertEquals("\"accept\"", JsonSerializer.DEFAULT.write(ElicitAction.ACCEPT)); + assertEquals("\"decline\"", JsonSerializer.DEFAULT.write(ElicitAction.DECLINE)); + assertEquals("\"cancel\"", JsonSerializer.DEFAULT.write(ElicitAction.CANCEL)); + assertEquals(ElicitAction.ACCEPT, JsonParser.DEFAULT.read("\"accept\"", ElicitAction.class)); + assertEquals(ElicitAction.DECLINE, JsonParser.DEFAULT.read("\"decline\"", ElicitAction.class)); + assertEquals(ElicitAction.CANCEL, JsonParser.DEFAULT.read("\"cancel\"", ElicitAction.class)); + assertEquals("accept", ElicitAction.ACCEPT.toWire()); + assertEquals("decline", ElicitAction.DECLINE.toWire()); + assertEquals("cancel", ElicitAction.CANCEL.toWire()); + } + + @Test void i02_elicitRequest_roundTrip() { + var request = new ElicitRequest().setMessage("Confirm?") + .setRequestedSchema(JsonMap.of("type", "object", "properties", + JsonMap.of("confirm", JsonMap.of("type", "boolean")))); + assertJsonRoundTrip(request, ElicitRequest.class); + assertBean(request, "message,requestedSchema{type,properties{confirm{type}}}", + "Confirm?,{object,{{boolean}}}"); + } + + @Test void i02b_elicitRequest_getRequestedSchema_matchesSiblingUnmodifiableViewContract() { + // getRequestedSchema()'s contract is pinned to match its siblings ElicitResult.getContent() and + // InputRequiredResult.getInputRequests(): a shallow Collections.unmodifiableMap(...) VIEW over the + // live backing map (blocks top-level mutation), not a defensive deep copy - so a nested structure + // (here "properties") remains mutable through the returned reference, same as those siblings. + var schema = JsonMap.of("type", "object", "properties", + JsonMap.of("confirm", JsonMap.of("type", "boolean"))); + var request = new ElicitRequest().setRequestedSchema(schema); + + var view = request.getRequestedSchema(); + assertThrows(UnsupportedOperationException.class, () -> view.put("type", "array")); + + @SuppressWarnings("unchecked") + var properties = (Map<String,Object>) view.get("properties"); + properties.put("extra", "leaked"); + @SuppressWarnings("unchecked") + var propertiesAgain = (Map<String,Object>) request.getRequestedSchema().get("properties"); + assertTrue(propertiesAgain.containsKey("extra"), + () -> "nested map must remain mutable through the view, matching sibling getters' shallow contract"); + } + + @Test void i03_elicitResult_acceptRoundTrip() { + var result = new ElicitResult().setAction(ElicitAction.ACCEPT) + .putContent("confirm", true).putContent("name", "al"); + var json = JsonSerializer.DEFAULT.write(result); + assertTrue(json.contains("\"action\":\"accept\"")); + assertTrue(json.contains("\"content\":{\"confirm\":true,\"name\":\"al\"}")); + var copy = JsonParser.DEFAULT.read(json, ElicitResult.class); + assertEquals(json, JsonSerializer.DEFAULT.write(copy)); + assertEquals(ElicitAction.ACCEPT, copy.getAction()); + assertEquals(Map.of("confirm", true, "name", "al"), copy.getContent()); + } + + @Test void i04_elicitResult_declineOmitsContent() { + var json = JsonSerializer.DEFAULT.write(new ElicitResult().setAction(ElicitAction.DECLINE)); + assertEquals("{\"action\":\"decline\"}", json); + assertFalse(json.contains("content"), () -> "content must be omitted when unset: " + json); + } + + @Test void i05_audioContent_roundTrip() { + var audio = new AudioContent().setData("QUJD").setMimeType("audio/wav"); + assertJsonRoundTrip(audio, AudioContent.class); + assertBean(audio, "data,mimeType", "QUJD,audio/wav"); + } + + @Test void i06_contentDictionary_typeAudioDeserializesToAudioContent() { + var json = "{\"type\":\"audio\",\"data\":\"QUJD\",\"mimeType\":\"audio/wav\"}"; + var copy = MCP_JSON_PARSER.read(json, Content.class); + assertTrue(copy instanceof AudioContent, () -> "must deserialize to AudioContent: " + copy); + assertBean((AudioContent)copy, "data,mimeType", "QUJD,audio/wav"); + } + + @Test void j01_modelHint_roundTrip() { + var hint = new ModelHint().setName("claude"); + assertJsonRoundTrip(hint, ModelHint.class); + assertBean(hint, "name", "claude"); + } + + @Test void j02_modelPreferences_roundTripAllFields() { + var prefs = new ModelPreferences() + .setHints(new ModelHint().setName("claude"), new ModelHint().setName("gpt")) + .setCostPriority(0.3).setSpeedPriority(0.5).setIntelligencePriority(0.9); + assertJsonRoundTrip(prefs, ModelPreferences.class); + assertBean(prefs, "hints{#{name}},costPriority,speedPriority,intelligencePriority", + "{[{claude},{gpt}]},0.3,0.5,0.9"); + } + + @Test void j03_modelPreferences_collectionSettersAndVarargAdders() { + var a = new ModelPreferences().setHints(list(new ModelHint().setName("claude"), new ModelHint().setName("gpt"))); + assertBean(a, "hints{#{name}}", "{[{claude},{gpt}]}"); + var b = new ModelPreferences().addHints(new ModelHint().setName("claude")).addHints(new ModelHint().setName("gpt")); + assertBean(b, "hints{#{name}}", "{[{claude},{gpt}]}"); + var c = new ModelPreferences().addHints(list(new ModelHint().setName("claude"))) + .addHints(list(new ModelHint().setName("gpt"))); + assertBean(c, "hints{#{name}}", "{[{claude},{gpt}]}"); + } + + @Test void k01_samplingMessage_textContentRoundTrip() { + var msg = new SamplingMessage().setRole(Role.USER).setContent(new TextContent().setText("hi")); + assertJsonRoundTrip(msg, SamplingMessage.class); + assertBean(msg, "role,content{text}", "USER,{hi}"); + } + + @Test void k02_samplingMessage_audioContentRoundTrip() { + var msg = new SamplingMessage().setRole(Role.ASSISTANT) + .setContent(new AudioContent().setData("QUJD").setMimeType("audio/wav")); + assertJsonRoundTrip(msg, SamplingMessage.class); + assertBean(msg, "role,content{data,mimeType}", "ASSISTANT,{QUJD,audio/wav}"); + assertTrue(MCP_JSON.write(msg).contains("\"type\":\"audio\"")); + } + + @Test void k03_createMessageRequest_allFieldsRoundTrip() { + var req = new CreateMessageRequest() + .setMessages( + new SamplingMessage().setRole(Role.USER).setContent(new TextContent().setText("hi")), + new SamplingMessage().setRole(Role.ASSISTANT).setContent(new TextContent().setText("hey"))) + .setModelPreferences(new ModelPreferences().addHints(new ModelHint().setName("claude"))) + .setSystemPrompt("be terse") + .setIncludeContext("thisServer") + .setTemperature(0.7) + .setMaxTokens(100) + .setStopSequences("STOP1", "STOP2") + .putMetadata("k", "v").putMetadata("k2", "v2"); + assertJsonRoundTrip(req, CreateMessageRequest.class); + assertBean(req, + "messages{#{role,content{text}}},modelPreferences{hints{#{name}}},systemPrompt,includeContext," + + "temperature,maxTokens,stopSequences,metadata", + "{[{USER,{hi}},{ASSISTANT,{hey}}]},{{[{claude}]}},be terse,thisServer,0.7,100,[STOP1,STOP2],{k=v,k2=v2}"); + } + + @Test void k04_createMessageRequest_collectionSettersAndVarargAdders() { + var a = new CreateMessageRequest().setMessages(list( + new SamplingMessage().setRole(Role.USER).setContent(new TextContent().setText("hi")), + new SamplingMessage().setRole(Role.ASSISTANT).setContent(new TextContent().setText("hey")))) + .setStopSequences(list("STOP1", "STOP2")); + assertBean(a, "messages{#{role}},stopSequences", "{[{USER},{ASSISTANT}]},[STOP1,STOP2]"); + var b = new CreateMessageRequest() + .addMessages(new SamplingMessage().setRole(Role.USER).setContent(new TextContent().setText("hi"))) + .addMessages(new SamplingMessage().setRole(Role.ASSISTANT).setContent(new TextContent().setText("hey"))) + .addStopSequences("STOP1").addStopSequences("STOP2"); + assertBean(b, "messages{#{role}},stopSequences", "{[{USER},{ASSISTANT}]},[STOP1,STOP2]"); + var c = new CreateMessageRequest() + .addMessages(list(new SamplingMessage().setRole(Role.USER).setContent(new TextContent().setText("hi")))) + .addMessages(list(new SamplingMessage().setRole(Role.ASSISTANT).setContent(new TextContent().setText("hey")))) + .addStopSequences(list("STOP1")).addStopSequences(list("STOP2")); + assertBean(c, "messages{#{role}},stopSequences", "{[{USER},{ASSISTANT}]},[STOP1,STOP2]"); + } + + @Test void k05_createMessageResult_audioContentRoundTrip() { + var result = new CreateMessageResult().setRole(Role.ASSISTANT) + .setContent(new AudioContent().setData("QUJD").setMimeType("audio/wav")) + .setModel("m").setStopReason("endTurn"); + assertJsonRoundTrip(result, CreateMessageResult.class); + assertBean(result, "role,content{data,mimeType},model,stopReason", + "ASSISTANT,{QUJD,audio/wav},m,endTurn"); + assertTrue(MCP_JSON.write(result).contains("\"type\":\"audio\"")); + } + + @Test void k06_samplingBeans_doNotExtendRequestParamsOrResult() { + assertEquals(Object.class, CreateMessageRequest.class.getSuperclass()); + assertEquals(Object.class, CreateMessageResult.class.getSuperclass()); + assertEquals(Object.class, SamplingMessage.class.getSuperclass()); + assertEquals(Object.class, ModelPreferences.class.getSuperclass()); + assertEquals(Object.class, ModelHint.class.getSuperclass()); + } + + @Test void l01_samplingCapability_roundTrip() { + assertEquals("{}", JsonSerializer.DEFAULT.write(new SamplingCapability())); + } + + @Test void l02_samplingCreateMessageMethodConstant() { + assertEquals("sampling/createMessage", McpMethods.SAMPLING_CREATE_MESSAGE); + } + + @Test void l03_clientCapabilities_samplingIsTypedCapability() { + var caps = new ClientCapabilities().setSampling(new SamplingCapability()); + var copy = JsonParser.DEFAULT.read(JsonSerializer.DEFAULT.write(caps), ClientCapabilities.class); + assertTrue(copy.getSampling() instanceof SamplingCapability); + } + + @Test void l09_roundTripsStillWorkForRootsAndLoggingCapabilities() { + var roots = JsonParser.DEFAULT.read(JsonSerializer.DEFAULT.write(new RootsCapability().setListChanged(true)), + RootsCapability.class); + assertBean(roots, "listChanged", "true"); + var logging = JsonParser.DEFAULT.read(JsonSerializer.DEFAULT.write(new LoggingCapability().setLevel("info")), + LoggingCapability.class); + assertBean(logging, "level", "info"); + } }
