test: add DSL builder tests for Request, Capabilities, and Content types (#529)
# Add DSL builder tests for Request, Capabilities, and Content types - Introduced unit tests for `RequestDslTest`, `CapabilitiesDslTest`, and `ContentDslTest` to validate DSL builder functionality. - Covered edge cases, nested properties, custom fields, and overwrite behavior for requests, capabilities, and content creation. - Update AGENTS.md ## Motivation and Context for confidence ## Breaking Changes No ## Types of changes - [x] Tests ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed
This commit is contained in:
parent
efed85ae06
commit
b2878ed406
3 changed files with 806 additions and 13 deletions
|
|
@ -0,0 +1,135 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.types.dsl
|
||||
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest
|
||||
import kotlinx.serialization.json.double
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* Tests for ClientCapabilities DSL builder.
|
||||
*
|
||||
* Verifies that capabilities can be constructed via DSL within InitializeRequest,
|
||||
* covering minimal (empty), full (all fields), and variant patterns.
|
||||
*/
|
||||
@OptIn(ExperimentalMcpApi::class)
|
||||
class CapabilitiesDslTest {
|
||||
@Test
|
||||
fun `capabilities should build minimal empty capabilities`() {
|
||||
val request = buildInitializeRequest {
|
||||
protocolVersion = "2024-11-05"
|
||||
capabilities { }
|
||||
info("Test", "1.0")
|
||||
}
|
||||
|
||||
request.params.capabilities.shouldNotBeNull {
|
||||
sampling.shouldBeNull()
|
||||
roots.shouldBeNull()
|
||||
elicitation.shouldBeNull()
|
||||
experimental.shouldBeNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities should build full with all fields and nested properties`() {
|
||||
val request = buildInitializeRequest {
|
||||
protocolVersion = "2024-11-05"
|
||||
capabilities {
|
||||
sampling {
|
||||
put("temperature", 0.7)
|
||||
put("maxTokens", 1000)
|
||||
put("topP", 0.95)
|
||||
}
|
||||
roots(listChanged = true)
|
||||
elicitation {
|
||||
put("mode", "interactive")
|
||||
put("timeout", 30)
|
||||
put("retries", 3)
|
||||
}
|
||||
experimental {
|
||||
put("customFeature", true)
|
||||
put("version", "1.0")
|
||||
put("beta", false)
|
||||
put("maxConcurrency", 10)
|
||||
}
|
||||
}
|
||||
info("Test", "1.0")
|
||||
}
|
||||
|
||||
request.params.capabilities.shouldNotBeNull {
|
||||
sampling shouldNotBeNull {
|
||||
get("temperature")?.jsonPrimitive?.double shouldBe 0.7
|
||||
get("maxTokens")?.jsonPrimitive?.content shouldBe "1000"
|
||||
get("topP")?.jsonPrimitive?.double shouldBe 0.95
|
||||
}
|
||||
roots shouldNotBeNull {
|
||||
listChanged shouldBe true
|
||||
}
|
||||
elicitation shouldNotBeNull {
|
||||
get("mode")?.jsonPrimitive?.content shouldBe "interactive"
|
||||
get("timeout")?.jsonPrimitive?.content shouldBe "30"
|
||||
get("retries")?.jsonPrimitive?.content shouldBe "3"
|
||||
}
|
||||
experimental shouldNotBeNull {
|
||||
get("customFeature")?.jsonPrimitive?.content shouldBe "true"
|
||||
get("version")?.jsonPrimitive?.content shouldBe "1.0"
|
||||
get("beta")?.jsonPrimitive?.content shouldBe "false"
|
||||
get("maxConcurrency")?.jsonPrimitive?.content shouldBe "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities should support roots variants`() {
|
||||
// Test listChanged = true
|
||||
val requestTrue = buildInitializeRequest {
|
||||
protocolVersion = "2024-11-05"
|
||||
capabilities { roots(listChanged = true) }
|
||||
info("Test", "1.0")
|
||||
}
|
||||
requestTrue.params.capabilities.roots?.listChanged shouldBe true
|
||||
|
||||
// Test listChanged = false
|
||||
val requestFalse = buildInitializeRequest {
|
||||
protocolVersion = "2024-11-05"
|
||||
capabilities { roots(listChanged = false) }
|
||||
info("Test", "1.0")
|
||||
}
|
||||
requestFalse.params.capabilities.roots?.listChanged shouldBe false
|
||||
|
||||
// Test listChanged = null (not provided)
|
||||
val requestNull = buildInitializeRequest {
|
||||
protocolVersion = "2024-11-05"
|
||||
capabilities { roots() }
|
||||
info("Test", "1.0")
|
||||
}
|
||||
requestNull.params.capabilities.roots?.listChanged.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities should overwrite when same field set multiple times`() {
|
||||
val request = buildInitializeRequest {
|
||||
protocolVersion = "2024-11-05"
|
||||
capabilities {
|
||||
sampling { put("temperature", 0.5) }
|
||||
sampling { put("temperature", 0.9) } // Should overwrite
|
||||
experimental { put("feature", "v1") }
|
||||
experimental { put("feature", "v2") } // Should overwrite
|
||||
}
|
||||
info("Test", "1.0")
|
||||
}
|
||||
|
||||
request.params.capabilities.shouldNotBeNull {
|
||||
sampling shouldNotBeNull {
|
||||
get("temperature")?.jsonPrimitive?.double shouldBe 0.9
|
||||
}
|
||||
experimental shouldNotBeNull {
|
||||
get("feature")?.jsonPrimitive?.content shouldBe "v2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.types.dsl
|
||||
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Annotations
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.AudioContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ImageContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Role
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.assistantAudio
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.assistantImage
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.buildCreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.userText
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* Tests for MediaContent DSL builders (TextContent, ImageContent, AudioContent).
|
||||
*
|
||||
* Verifies content can be constructed via DSL within sampling messages,
|
||||
* covering minimal (required fields), full (all fields), validation, and edge cases.
|
||||
*/
|
||||
@OptIn(ExperimentalMcpApi::class)
|
||||
class ContentDslTest {
|
||||
|
||||
// ========================================================================
|
||||
// TextContent Tests
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `userText should build minimal with text only`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as TextContent).shouldNotBeNull {
|
||||
text shouldBe "Hello, world!"
|
||||
annotations.shouldBeNull()
|
||||
meta.shouldBeNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userText should build full with all fields and nested meta`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = "Analyze the quarterly sales report for Q4 2025"
|
||||
annotations(
|
||||
audience = listOf(Role.User, Role.Assistant),
|
||||
priority = 0.85,
|
||||
lastModified = "2025-02-17T10:30:00Z",
|
||||
)
|
||||
meta {
|
||||
put("source", "dashboard")
|
||||
put("userId", "user-123")
|
||||
put("sessionId", 42)
|
||||
put("analyzed", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as TextContent).shouldNotBeNull {
|
||||
text shouldBe "Analyze the quarterly sales report for Q4 2025"
|
||||
annotations shouldNotBeNull {
|
||||
audience shouldBe listOf(Role.User, Role.Assistant)
|
||||
priority shouldBe 0.85
|
||||
lastModified shouldBe "2025-02-17T10:30:00Z"
|
||||
}
|
||||
meta shouldNotBeNull {
|
||||
get("source")?.jsonPrimitive?.content shouldBe "dashboard"
|
||||
get("userId")?.jsonPrimitive?.content shouldBe "user-123"
|
||||
get("sessionId")?.jsonPrimitive?.content shouldBe "42"
|
||||
get("analyzed")?.jsonPrimitive?.content shouldBe "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userText should handle unicode and special characters`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = "Hello 🌍! Ça va? Москва 北京 مرحبا"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as TextContent).text shouldBe "Hello 🌍! Ça va? Москва 北京 مرحبا"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userText should handle empty string`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as TextContent).text shouldBe ""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userText should throw if text is missing`() {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ImageContent Tests
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `assistantImage should build minimal with data and mimeType`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantImage {
|
||||
data =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
mimeType = "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as ImageContent).shouldNotBeNull {
|
||||
data shouldBe
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
mimeType shouldBe "image/png"
|
||||
annotations.shouldBeNull()
|
||||
meta.shouldBeNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantImage should build full with all fields and image metadata`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantImage {
|
||||
data = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVR42mNk+M9QzzCCgAEYjRZnRwEA"
|
||||
mimeType = "image/jpeg"
|
||||
annotations(
|
||||
audience = listOf(Role.Assistant),
|
||||
priority = 0.92,
|
||||
lastModified = "2025-02-17T11:45:30Z",
|
||||
)
|
||||
meta {
|
||||
put("width", 1920)
|
||||
put("height", 1080)
|
||||
put("format", "jpeg")
|
||||
put("compression", "lossy")
|
||||
put("quality", 85)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as ImageContent).shouldNotBeNull {
|
||||
mimeType shouldBe "image/jpeg"
|
||||
annotations shouldNotBeNull {
|
||||
audience shouldBe listOf(Role.Assistant)
|
||||
priority shouldBe 0.92
|
||||
lastModified shouldBe "2025-02-17T11:45:30Z"
|
||||
}
|
||||
meta shouldNotBeNull {
|
||||
get("width")?.jsonPrimitive?.content shouldBe "1920"
|
||||
get("height")?.jsonPrimitive?.content shouldBe "1080"
|
||||
get("format")?.jsonPrimitive?.content shouldBe "jpeg"
|
||||
get("compression")?.jsonPrimitive?.content shouldBe "lossy"
|
||||
get("quality")?.jsonPrimitive?.content shouldBe "85"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantImage should accept various MIME types`() {
|
||||
val mimeTypes = listOf("image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml")
|
||||
|
||||
mimeTypes.forEach { mime ->
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantImage {
|
||||
data = "base64data"
|
||||
mimeType = mime
|
||||
}
|
||||
}
|
||||
}
|
||||
(request.params.messages[0].content as ImageContent).mimeType shouldBe mime
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantImage should throw if data is missing`() {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantImage {
|
||||
mimeType = "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantImage should throw if mimeType is missing`() {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantImage {
|
||||
data = "base64data"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AudioContent Tests
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `assistantAudio should build minimal with data and mimeType`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantAudio {
|
||||
data = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA=="
|
||||
mimeType = "audio/wav"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as AudioContent).shouldNotBeNull {
|
||||
data shouldBe "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA=="
|
||||
mimeType shouldBe "audio/wav"
|
||||
annotations.shouldBeNull()
|
||||
meta.shouldBeNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantAudio should build full with all fields and audio metadata`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantAudio {
|
||||
data = "//uQxAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADIADMzMzMzMzMzMzMzMzMzMzMzMzMzMzM"
|
||||
mimeType = "audio/mpeg"
|
||||
annotations(
|
||||
audience = listOf(Role.User),
|
||||
priority = 0.75,
|
||||
lastModified = "2025-02-17T12:00:00Z",
|
||||
)
|
||||
meta {
|
||||
put("duration", 180)
|
||||
put("bitrate", 128)
|
||||
put("codec", "mp3")
|
||||
put("sampleRate", 44100)
|
||||
put("channels", 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as AudioContent).shouldNotBeNull {
|
||||
mimeType shouldBe "audio/mpeg"
|
||||
annotations shouldNotBeNull {
|
||||
audience shouldBe listOf(Role.User)
|
||||
priority shouldBe 0.75
|
||||
lastModified shouldBe "2025-02-17T12:00:00Z"
|
||||
}
|
||||
meta shouldNotBeNull {
|
||||
get("duration")?.jsonPrimitive?.content shouldBe "180"
|
||||
get("bitrate")?.jsonPrimitive?.content shouldBe "128"
|
||||
get("codec")?.jsonPrimitive?.content shouldBe "mp3"
|
||||
get("sampleRate")?.jsonPrimitive?.content shouldBe "44100"
|
||||
get("channels")?.jsonPrimitive?.content shouldBe "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantAudio should throw if data is missing`() {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantAudio {
|
||||
mimeType = "audio/wav"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assistantAudio should throw if mimeType is missing`() {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
assistantAudio {
|
||||
data = "base64audio"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Annotations Edge Cases
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `annotations should support boundary priority values`() {
|
||||
// Priority = 0.0 (minimum)
|
||||
val requestMin = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = "Min priority"
|
||||
annotations(priority = 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
(requestMin.params.messages[0].content as TextContent).annotations?.priority shouldBe 0.0
|
||||
|
||||
// Priority = 1.0 (maximum)
|
||||
val requestMax = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = "Max priority"
|
||||
annotations(priority = 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
(requestMax.params.messages[0].content as TextContent).annotations?.priority shouldBe 1.0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `annotations should support direct object construction`() {
|
||||
val request = buildCreateMessageRequest {
|
||||
maxTokens = 100
|
||||
messages {
|
||||
userText {
|
||||
text = "With annotations object"
|
||||
annotations(Annotations(priority = 0.5, audience = listOf(Role.User)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(request.params.messages[0].content as TextContent).annotations shouldNotBeNull {
|
||||
priority shouldBe 0.5
|
||||
audience shouldBe listOf(Role.User)
|
||||
lastModified.shouldBeNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.types.dsl
|
||||
|
||||
import io.kotest.matchers.collections.shouldContainAll
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.RequestId
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.buildListToolsRequest
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* Tests for RequestMeta and PaginatedRequest DSL builders.
|
||||
*
|
||||
* Verifies meta and cursor can be constructed via DSL within paginated requests,
|
||||
* covering minimal (no params), full (all field types), variants, and edge cases.
|
||||
*/
|
||||
@OptIn(ExperimentalMcpApi::class)
|
||||
class RequestDslTest {
|
||||
@Test
|
||||
fun `request should build minimal without any params`() {
|
||||
val request = buildListToolsRequest { }
|
||||
|
||||
request.params.shouldBeNull()
|
||||
request.method shouldBe Method.Defined.ToolsList
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request should build full with cursor and meta containing all field types`() {
|
||||
val request = buildListToolsRequest {
|
||||
cursor = "next-page-eyJvZmZzZXQiOjEwMH0"
|
||||
meta {
|
||||
// ProgressToken
|
||||
progressToken("progress-abc123")
|
||||
|
||||
// String fields
|
||||
put("requestId", "req-456")
|
||||
put("source", "api-gateway")
|
||||
|
||||
// Number fields
|
||||
put("priority", 5)
|
||||
put("retryCount", 3L)
|
||||
put("timeout", 30000)
|
||||
|
||||
// Boolean fields
|
||||
put("urgent", true)
|
||||
put("cached", false)
|
||||
|
||||
// Null field
|
||||
put("optional", null)
|
||||
|
||||
// Nested JsonObject
|
||||
putJsonObject("context") {
|
||||
put("userId", "user-789")
|
||||
put("sessionId", "sess-abc")
|
||||
put("authenticated", true)
|
||||
put("permissions", 7)
|
||||
}
|
||||
|
||||
// JsonArray
|
||||
putJsonArray("tags") {
|
||||
add("production")
|
||||
add("critical")
|
||||
add("monitored")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.params shouldNotBeNull {
|
||||
cursor shouldBe "next-page-eyJvZmZzZXQiOjEwMH0"
|
||||
meta shouldNotBeNull {
|
||||
// ProgressToken
|
||||
progressToken shouldNotBeNull {
|
||||
shouldBeInstanceOf<RequestId.StringId>()
|
||||
value shouldBe "progress-abc123"
|
||||
}
|
||||
|
||||
// String fields
|
||||
get("requestId")?.jsonPrimitive?.content shouldBe "req-456"
|
||||
get("source")?.jsonPrimitive?.content shouldBe "api-gateway"
|
||||
|
||||
// Number fields
|
||||
get("priority")?.jsonPrimitive?.int shouldBe 5
|
||||
get("retryCount")?.jsonPrimitive?.long shouldBe 3L
|
||||
get("timeout")?.jsonPrimitive?.int shouldBe 30000
|
||||
|
||||
// Boolean fields
|
||||
get("urgent")?.jsonPrimitive?.boolean shouldBe true
|
||||
get("cached")?.jsonPrimitive?.boolean shouldBe false
|
||||
|
||||
// Null field
|
||||
get("optional") shouldBe JsonNull
|
||||
|
||||
// Nested JsonObject
|
||||
get("context") shouldNotBeNull {
|
||||
jsonObject["userId"]?.jsonPrimitive?.content shouldBe "user-789"
|
||||
jsonObject["sessionId"]?.jsonPrimitive?.content shouldBe "sess-abc"
|
||||
jsonObject["authenticated"]?.jsonPrimitive?.boolean shouldBe true
|
||||
jsonObject["permissions"]?.jsonPrimitive?.int shouldBe 7
|
||||
}
|
||||
|
||||
// JsonArray
|
||||
get("tags") shouldNotBeNull {
|
||||
jsonArray.map { it.jsonPrimitive.content } shouldContainAll listOf(
|
||||
"production",
|
||||
"critical",
|
||||
"monitored",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta progressToken should support String Int and Long types`() {
|
||||
// String progressToken
|
||||
val stringRequest = buildListToolsRequest {
|
||||
meta { progressToken("token-abc") }
|
||||
}
|
||||
stringRequest.params?.meta?.progressToken shouldNotBeNull {
|
||||
shouldBeInstanceOf<RequestId.StringId>()
|
||||
value shouldBe "token-abc"
|
||||
}
|
||||
|
||||
// Int progressToken
|
||||
val intRequest = buildListToolsRequest {
|
||||
meta { progressToken(42) }
|
||||
}
|
||||
intRequest.params?.meta?.progressToken shouldNotBeNull {
|
||||
shouldBeInstanceOf<RequestId.NumberId>()
|
||||
value shouldBe 42L
|
||||
}
|
||||
|
||||
// Long progressToken
|
||||
val longRequest = buildListToolsRequest {
|
||||
meta { progressToken(999L) }
|
||||
}
|
||||
longRequest.params?.meta?.progressToken shouldNotBeNull {
|
||||
shouldBeInstanceOf<RequestId.NumberId>()
|
||||
value shouldBe 999L
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta should support custom fields without progressToken`() {
|
||||
val request = buildListToolsRequest {
|
||||
meta {
|
||||
put("requestId", "req-123")
|
||||
put("source", "cli")
|
||||
// No progressToken
|
||||
}
|
||||
}
|
||||
|
||||
request.params shouldNotBeNull {
|
||||
meta shouldNotBeNull {
|
||||
progressToken.shouldBeNull()
|
||||
get("requestId")?.jsonPrimitive?.content shouldBe "req-123"
|
||||
get("source")?.jsonPrimitive?.content shouldBe "cli"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cursor should work without meta`() {
|
||||
val request = buildListToolsRequest {
|
||||
cursor = "page-2-cursor"
|
||||
}
|
||||
|
||||
request.params shouldNotBeNull {
|
||||
cursor shouldBe "page-2-cursor"
|
||||
meta.shouldBeNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cursor should handle empty string`() {
|
||||
val request = buildListToolsRequest {
|
||||
cursor = ""
|
||||
}
|
||||
|
||||
request.params shouldNotBeNull {
|
||||
cursor shouldBe ""
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta should handle special characters in keys`() {
|
||||
val request = buildListToolsRequest {
|
||||
meta {
|
||||
put("key-with-dashes", "value1")
|
||||
put("key.with.dots", "value2")
|
||||
put("key_with_underscores", "value3")
|
||||
put("keyWithCamelCase", "value4")
|
||||
}
|
||||
}
|
||||
|
||||
request.params?.meta shouldNotBeNull {
|
||||
get("key-with-dashes")?.jsonPrimitive?.content shouldBe "value1"
|
||||
get("key.with.dots")?.jsonPrimitive?.content shouldBe "value2"
|
||||
get("key_with_underscores")?.jsonPrimitive?.content shouldBe "value3"
|
||||
get("keyWithCamelCase")?.jsonPrimitive?.content shouldBe "value4"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta should overwrite when progressToken set multiple times`() {
|
||||
val request = buildListToolsRequest {
|
||||
meta {
|
||||
progressToken("first")
|
||||
progressToken(123) // Different type
|
||||
progressToken("second") // Back to string, should be final
|
||||
}
|
||||
}
|
||||
|
||||
request.params?.meta?.progressToken shouldNotBeNull {
|
||||
shouldBeInstanceOf<RequestId.StringId>()
|
||||
value shouldBe "second"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta should overwrite when custom field set multiple times`() {
|
||||
val request = buildListToolsRequest {
|
||||
meta {
|
||||
put("key", "first")
|
||||
put("key", 123) // Different type
|
||||
put("key", "third") // Back to string, should be final
|
||||
}
|
||||
}
|
||||
|
||||
request.params?.meta shouldNotBeNull {
|
||||
get("key")?.jsonPrimitive?.content shouldBe "third"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `meta should handle very long cursor strings`() {
|
||||
val longCursor = "x".repeat(1000)
|
||||
val request = buildListToolsRequest {
|
||||
cursor = longCursor
|
||||
}
|
||||
|
||||
request.params?.cursor shouldBe longCursor
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue