swift-sdk/Tests/MCPTests/SamplingTests.swift
yehorsobko 973b46b8f7
feat: 2025-11-25 review fixes (#201)
* fix: review fixes of the latest changes

* fix: updated unit tests

* chore: more conformance compliance improvements

* chore: reverted protocol changes

* chore: more compliance improvements

* chore: more conformance compliance improvements

* chore: improvements to the backcompat and updated to Tool unit tests

* chore: ci update
2026-03-03 17:33:28 +02:00

1093 lines
39 KiB
Swift

import Logging
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
@testable import MCP
@Suite("Sampling Tests")
struct SamplingTests {
@Test("Sampling.Message encoding and decoding")
func testSamplingMessageCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text content
let textMessage: Sampling.Message = .user("Hello, world!")
let textData = try encoder.encode(textMessage)
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
#expect(decodedTextMessage.role == .user)
if case .single(.text(let text)) = decodedTextMessage.content {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test image content
let imageMessage: Sampling.Message = .assistant(
.image(data: "base64imagedata", mimeType: "image/png"))
let imageData = try encoder.encode(imageMessage)
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
#expect(decodedImageMessage.role == .assistant)
if case .single(.image(let data, let mimeType)) = decodedImageMessage.content {
#expect(data == "base64imagedata")
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
}
@Test("ModelPreferences encoding and decoding")
func testModelPreferencesCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let preferences = Sampling.ModelPreferences(
hints: [
Sampling.ModelPreferences.Hint(name: "claude-4"),
Sampling.ModelPreferences.Hint(name: "gpt-4.1"),
],
costPriority: 0.8,
speedPriority: 0.3,
intelligencePriority: 0.9
)
let data = try encoder.encode(preferences)
let decoded = try decoder.decode(Sampling.ModelPreferences.self, from: data)
#expect(decoded.hints?.count == 2)
#expect(decoded.hints?[0].name == "claude-4")
#expect(decoded.hints?[1].name == "gpt-4.1")
#expect(decoded.costPriority?.doubleValue == 0.8)
#expect(decoded.speedPriority?.doubleValue == 0.3)
#expect(decoded.intelligencePriority?.doubleValue == 0.9)
}
@Test("ContextInclusion encoding and decoding")
func testContextInclusionCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let contexts: [Sampling.ContextInclusion] = [.none, .thisServer, .allServers]
for context in contexts {
let data = try encoder.encode(context)
let decoded = try decoder.decode(Sampling.ContextInclusion.self, from: data)
#expect(decoded == context)
}
}
@Test("StopReason encoding and decoding")
func testStopReasonCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let reasons: [Sampling.StopReason] = [.endTurn, .stopSequence, .maxTokens]
for reason in reasons {
let data = try encoder.encode(reason)
let decoded = try decoder.decode(Sampling.StopReason.self, from: data)
#expect(decoded == reason)
}
}
@Test("CreateMessage request parameters")
func testCreateMessageParameters() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let messages: [Sampling.Message] = [
.user("What is the weather like?"),
.assistant("I need to check the weather for you."),
]
let modelPreferences = Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "claude-4-sonnet")],
costPriority: 0.5,
speedPriority: 0.7,
intelligencePriority: 0.9
)
let parameters = CreateSamplingMessage.Parameters(
messages: messages,
modelPreferences: modelPreferences,
systemPrompt: "You are a helpful weather assistant.",
includeContext: .thisServer,
temperature: 0.7,
maxTokens: 150,
stopSequences: ["END", "STOP"],
_meta: Metadata(additionalFields: ["provider": "test"])
)
let data = try encoder.encode(parameters)
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
#expect(decoded.messages.count == 2)
#expect(decoded.messages[0].role == .user)
#expect(decoded.systemPrompt == "You are a helpful weather assistant.")
#expect(decoded.includeContext == .thisServer)
#expect(decoded.temperature == 0.7)
#expect(decoded.maxTokens == 150)
#expect(decoded.stopSequences?.count == 2)
#expect(decoded.stopSequences?[0] == "END")
#expect(decoded.stopSequences?[1] == "STOP")
#expect(decoded._meta?["provider"]?.stringValue == "test")
}
@Test("CreateMessage result")
func testCreateMessageResult() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let result = CreateSamplingMessage.Result(
model: "claude-4-sonnet",
stopReason: .endTurn,
role: .assistant,
content: .text("The weather is sunny and 75°F.")
)
let data = try encoder.encode(result)
let decoded = try decoder.decode(CreateSamplingMessage.Result.self, from: data)
#expect(decoded.model == "claude-4-sonnet")
#expect(decoded.stopReason == .endTurn)
#expect(decoded.role == .assistant)
if case .single(.text(let text)) = decoded.content {
#expect(text == "The weather is sunny and 75°F.")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("CreateMessage request creation")
func testCreateMessageRequest() throws {
let messages: [Sampling.Message] = [
.user("Hello")
]
let request = CreateSamplingMessage.request(
.init(
messages: messages,
maxTokens: 100
)
)
#expect(request.method == "sampling/createMessage")
#expect(request.params.messages.count == 1)
#expect(request.params.maxTokens == 100)
}
@Test("Client capabilities include sampling")
func testClientCapabilitiesIncludeSampling() throws {
let capabilities = Client.Capabilities(
sampling: .init()
)
#expect(capabilities.sampling != nil)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Client.Capabilities.self, from: data)
#expect(decoded.sampling != nil)
}
@Test("Client sampling handler registration")
func testClientSamplingHandlerRegistration() async throws {
let client = Client(name: "TestClient", version: "1.0")
// Test that sampling handler can be registered
let handlerClient = await client.withSamplingHandler { parameters in
// Mock handler that returns a simple response
return CreateSamplingMessage.Result(
model: "test-model",
stopReason: .endTurn,
role: .assistant,
content: .text("Test response")
)
}
// Should return self for method chaining
#expect(handlerClient === client)
}
@Test("Sampling message content JSON format")
func testSamplingMessageContentJSONFormat() throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
// Test text content JSON format
let textContent: Sampling.Message.Content = .text("Hello")
let textData = try encoder.encode(textContent)
let textJSON = String(data: textData, encoding: .utf8)!
#expect(textJSON.contains("\"type\":\"text\""))
#expect(textJSON.contains("\"text\":\"Hello\""))
// Test image content JSON format
let imageContent: Sampling.Message.Content = .image(
data: "base64data", mimeType: "image/png")
let imageData = try encoder.encode(imageContent)
let imageJSON = String(data: imageData, encoding: .utf8)!
#expect(imageJSON.contains("\"type\":\"image\""))
#expect(imageJSON.contains("\"data\":\"base64data\""))
#expect(imageJSON.contains("\"mimeType\":\"image\\/png\""))
}
@Test("UnitInterval in Sampling.ModelPreferences")
func testUnitIntervalInModelPreferences() throws {
// Test that UnitInterval validation works in Sampling.ModelPreferences
let validPreferences = Sampling.ModelPreferences(
costPriority: 0.5,
speedPriority: 1.0,
intelligencePriority: 0.0
)
#expect(validPreferences.costPriority?.doubleValue == 0.5)
#expect(validPreferences.speedPriority?.doubleValue == 1.0)
#expect(validPreferences.intelligencePriority?.doubleValue == 0.0)
// Test JSON encoding/decoding preserves UnitInterval constraints
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(validPreferences)
let decoded = try decoder.decode(Sampling.ModelPreferences.self, from: data)
#expect(decoded.costPriority?.doubleValue == 0.5)
#expect(decoded.speedPriority?.doubleValue == 1.0)
#expect(decoded.intelligencePriority?.doubleValue == 0.0)
}
@Test("Message factory methods")
func testMessageFactoryMethods() throws {
// Test user message factory method
let userMessage: Sampling.Message = .user("Hello, world!")
#expect(userMessage.role == .user)
if case .single(.text(let text)) = userMessage.content {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test assistant message factory method
let assistantMessage: Sampling.Message = .assistant("Hi there!")
#expect(assistantMessage.role == .assistant)
if case .single(.text(let text)) = assistantMessage.content {
#expect(text == "Hi there!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test with image content
let imageMessage: Sampling.Message = .user(
.image(data: "base64data", mimeType: "image/png"))
#expect(imageMessage.role == .user)
if case .single(.image(let data, let mimeType)) = imageMessage.content {
#expect(data == "base64data")
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
}
@Test("Content ExpressibleByStringLiteral")
func testContentExpressibleByStringLiteral() throws {
// Test string literal assignment
let content: Sampling.Message.Content = "Hello from string literal"
if case .single(.text(let text)) = content {
#expect(text == "Hello from string literal")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in message creation
let message: Sampling.Message = .user("Direct string literal")
if case .single(.text(let text)) = message.content {
#expect(text == "Direct string literal")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in array context
let messages: [Sampling.Message] = [
.user("First message"),
.assistant("Second message"),
.user("Third message"),
]
#expect(messages.count == 3)
#expect(messages[0].role == .user)
#expect(messages[1].role == .assistant)
#expect(messages[2].role == .user)
}
@Test("Content ExpressibleByStringInterpolation")
func testContentExpressibleByStringInterpolation() throws {
let userName = "Alice"
let temperature = 72
let location = "San Francisco"
// Test string interpolation
let content: Sampling.Message.Content =
"Hello \(userName), the temperature in \(location) is \(temperature)°F"
if case .single(.text(let text)) = content {
#expect(text == "Hello Alice, the temperature in San Francisco is 72°F")
} else {
#expect(Bool(false), "Expected text content")
}
// Test in message creation with interpolation
let message = Sampling.Message.user(
"Welcome \(userName)! Today's weather in \(location) is \(temperature)°F")
if case .single(.text(let text)) = message.content {
#expect(text == "Welcome Alice! Today's weather in San Francisco is 72°F")
} else {
#expect(Bool(false), "Expected text content")
}
// Test complex interpolation
let items = ["apples", "bananas", "oranges"]
let count = items.count
let listMessage: Sampling.Message = .assistant(
"You have \(count) items: \(items.joined(separator: ", "))")
if case .single(.text(let text)) = listMessage.content {
#expect(text == "You have 3 items: apples, bananas, oranges")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Message factory methods with string interpolation")
func testMessageFactoryMethodsWithStringInterpolation() throws {
let customerName = "Bob"
let orderNumber = "ORD-12345"
let issueType = "delivery delay"
// Test user message with interpolation
let userMessage: Sampling.Message = .user(
"Hi, I'm \(customerName) and I have an issue with order \(orderNumber)")
#expect(userMessage.role == .user)
if case .single(.text(let text)) = userMessage.content {
#expect(text == "Hi, I'm Bob and I have an issue with order ORD-12345")
} else {
#expect(Bool(false), "Expected text content")
}
// Test assistant message with interpolation
let assistantMessage: Sampling.Message = .assistant(
"Hello \(customerName), I can help you with your \(issueType) issue for order \(orderNumber)"
)
#expect(assistantMessage.role == .assistant)
if case .single(.text(let text)) = assistantMessage.content {
#expect(
text
== "Hello Bob, I can help you with your delivery delay issue for order ORD-12345"
)
} else {
#expect(Bool(false), "Expected text content")
}
// Test in conversation array
let conversation: [Sampling.Message] = [
.user("Hello, I'm \(customerName)"),
.assistant("Hi \(customerName), how can I help you today?"),
.user("I have an issue with order \(orderNumber) - it's a \(issueType)"),
.assistant(
"I understand you're experiencing a \(issueType) with order \(orderNumber). Let me look into that for you."
),
]
#expect(conversation.count == 4)
// Verify interpolated content
if case .single(.text(let text)) = conversation[2].content {
#expect(text == "I have an issue with order ORD-12345 - it's a delivery delay")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Ergonomic API usage patterns")
func testErgonomicAPIUsagePatterns() throws {
// Test various ergonomic usage patterns enabled by the new API
// Pattern 1: Simple conversation
let simpleConversation: [Sampling.Message] = [
.user("What's the weather like?"),
.assistant("I'd be happy to help you check the weather!"),
.user("Thanks!"),
]
#expect(simpleConversation.count == 3)
// Pattern 2: Dynamic content with interpolation
let productName = "Smart Thermostat"
let price = 199.99
let discount = 20
let salesConversation: [Sampling.Message] = [
.user("Tell me about the \(productName)"),
.assistant("The \(productName) is priced at $\(String(format: "%.2f", price))"),
.user("Do you have any discounts?"),
.assistant(
"Yes! We currently have a \(discount)% discount, bringing the price to $\(String(format: "%.2f", price * (1.0 - Double(discount)/100.0)))"
),
]
#expect(salesConversation.count == 4)
// Pattern 3: Mixed content types
let mixedContent: [Sampling.Message] = [
.user("Can you analyze this image?"),
.assistant(.image(data: "analysis_chart_data", mimeType: "image/png")),
.user("What does it show?"),
.assistant("The chart shows a clear upward trend in sales."),
]
#expect(mixedContent.count == 4)
// Verify content types
if case .single(.text) = mixedContent[0].content,
case .single(.image) = mixedContent[1].content,
case .single(.text) = mixedContent[2].content,
case .single(.text) = mixedContent[3].content
{
// All content types are correct
} else {
#expect(Bool(false), "Content types don't match expected pattern")
}
// Pattern 4: Encoding/decoding still works
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(simpleConversation)
let decoded = try decoder.decode([Sampling.Message].self, from: data)
#expect(decoded.count == 3)
#expect(decoded[0].role == .user)
#expect(decoded[1].role == .assistant)
#expect(decoded[2].role == .user)
}
}
@Suite("Sampling Integration Tests")
struct SamplingIntegrationTests {
@Test
func testSamplingHandlerRegistration() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "SamplingHandlerTestServer",
version: "1.0"
)
let client = Client(
name: "SamplingHandlerTestClient",
version: "1.0"
)
nonisolated(unsafe) var handlerCalled = false
// Register sampling handler
await client.withSamplingHandler { parameters in
handlerCalled = true
#expect(parameters.messages.count == 1)
// Mock LLM response
return CreateSamplingMessage.Result(
model: "test-model-v1",
stopReason: .endTurn,
role: .assistant,
content: .text("This is a test completion from the mock LLM.")
)
}
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Test that the handler actually gets called
let messages: [Sampling.Message] = [.user("Test")]
let result = try await server.requestSampling(messages: messages, maxTokens: 100)
#expect(handlerCalled, "Sampling handler should have been called")
#expect(result.model == "test-model-v1")
#expect(result.stopReason == .endTurn)
await server.stop()
await client.disconnect()
}
@Test
func testServerSamplingRequestAPI() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "SamplingRequestTestServer",
version: "1.0"
)
let client = Client(
name: "SamplingTestClient",
version: "1.0"
)
// Register sampling handler on client to respond to server's request
let responseContentString = "Based on the analysis, sales show strong growth through Q3 with Q4 stabilization."
await client.withSamplingHandler { parameters in
// Verify the request parameters were passed correctly
#expect(parameters.messages.count == 3)
#expect(parameters.systemPrompt == "You are a business analyst expert.")
#expect(parameters.includeContext == .thisServer)
#expect(parameters.temperature == 0.7)
#expect(parameters.maxTokens == 500)
#expect(parameters.stopSequences?.count == 2)
#expect(parameters._meta?["requestId"]?.stringValue == "test-123")
// Return mock LLM response
return CreateSamplingMessage.Result(
model: "claude-4-sonnet",
stopReason: .endTurn,
role: .assistant,
content: .text(responseContentString)
)
}
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Test sampling request with comprehensive parameters
let messages: [Sampling.Message] = [
.user("Analyze the following data and provide insights:"),
.user("Sales data: Q1: $100k, Q2: $150k, Q3: $200k, Q4: $180k"),
.user("Marketing data: Q1: $50k, Q2: $75k, Q3: $100k, Q4: $90k"),
]
let modelPreferences = Sampling.ModelPreferences(
hints: [
Sampling.ModelPreferences.Hint(name: "claude-4-sonnet"),
Sampling.ModelPreferences.Hint(name: "gpt-4.1"),
],
costPriority: 0.3,
speedPriority: 0.7,
intelligencePriority: 0.9
)
// Test that the API works end-to-end
let result = try await server.requestSampling(
messages: messages,
modelPreferences: modelPreferences,
systemPrompt: "You are a business analyst expert.",
includeContext: .thisServer,
temperature: 0.7,
maxTokens: 500,
stopSequences: ["END_ANALYSIS", "\n\n---"],
_meta: Metadata(additionalFields: [
"requestId": "test-123",
"priority": "high",
"department": "analytics",
])
)
// Verify the response
#expect(result.model == "claude-4-sonnet")
#expect(result.stopReason == .endTurn)
#expect(result.role == .assistant)
if case .single(.text(let text)) = result.content {
#expect(text == responseContentString)
} else {
Issue.record("Expected text content")
}
await server.stop()
await client.disconnect()
}
@Test
func testSamplingMessageTypes() async throws {
// Test comprehensive message content types
let textMessage: Sampling.Message = .user("What do you see in this data?")
let imageMessage: Sampling.Message = .user(
.image(
data:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png"
))
// Test encoding/decoding of different message types
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text message
let textData = try encoder.encode(textMessage)
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
#expect(decodedTextMessage == textMessage)
// Test image message
let imageData = try encoder.encode(imageMessage)
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
#expect(decodedImageMessage == imageMessage)
}
@Test
func testSamplingResultTypes() async throws {
// Test different result content types and stop reasons
let textResult = CreateSamplingMessage.Result(
model: "claude-4-sonnet",
stopReason: .endTurn,
role: .assistant,
content: .text(
"Based on the sales data analysis, I can see a strong upward trend through Q3, with a slight decline in Q4. This suggests seasonal factors or market saturation."
)
)
let imageResult = CreateSamplingMessage.Result(
model: "dall-e-3",
stopReason: .maxTokens,
role: .assistant,
content: .image(
data: "generated_chart_base64_data_here",
mimeType: "image/png"
)
)
let stopSequenceResult = CreateSamplingMessage.Result(
model: "gpt-4.1",
stopReason: .stopSequence,
role: .assistant,
content: .text("Analysis complete.\nEND_ANALYSIS")
)
// Test encoding/decoding of different result types
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text result
let textData = try encoder.encode(textResult)
let decodedTextResult = try decoder.decode(
CreateSamplingMessage.Result.self, from: textData
)
#expect(decodedTextResult == textResult)
// Test image result
let imageData = try encoder.encode(imageResult)
let decodedImageResult = try decoder.decode(
CreateSamplingMessage.Result.self, from: imageData
)
#expect(decodedImageResult == imageResult)
// Test stop sequence result
let stopData = try encoder.encode(stopSequenceResult)
let decodedStopResult = try decoder.decode(
CreateSamplingMessage.Result.self, from: stopData
)
#expect(decodedStopResult == stopSequenceResult)
}
@Test
func testSamplingErrorHandling() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "ErrorTestServer",
version: "1.0"
)
// Client WITHOUT sampling capability
let client = Client(
name: "ErrorTestClient",
version: "1.0"
)
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Test sampling request - should fail because client doesn't support sampling
let messages: [Sampling.Message] = [
.user("Test message")
]
await #expect(throws: MCPError.self) {
_ = try await server.requestSampling(
messages: messages,
maxTokens: 100
)
}
await server.stop()
await client.disconnect()
}
@Test("Strict mode succeeds when client declares sampling capability")
func testSamplingStrictCapabilitiesSuccess() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "StrictTestServer",
version: "1.0",
configuration: .strict
)
let client = Client(
name: "StrictTestClient",
version: "1.0",
capabilities: .init(sampling: .init()),
configuration: .strict
)
// Register sampling handler
await client.withSamplingHandler { _ in
CreateSamplingMessage.Result(
model: "test-model",
stopReason: .endTurn,
role: .assistant,
content: .text("Strict mode success")
)
}
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Should succeed because client declares sampling capability
let result = try await server.requestSampling(
messages: [.user("Test message")],
maxTokens: 100
)
#expect(result.model == "test-model")
#expect(result.role == .assistant)
if case .single(.text(let text)) = result.content {
#expect(text == "Strict mode success")
} else {
Issue.record("Expected text content")
}
await server.stop()
await client.disconnect()
}
@Test("Strict mode fails when client doesn't declare sampling capability")
func testSamplingStrictCapabilitiesError() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "StrictTestServer",
version: "1.0",
configuration: .strict
)
// Client WITHOUT sampling capability in strict mode
let client = Client(
name: "StrictTestClient",
version: "1.0",
capabilities: .init(),
configuration: .strict
)
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Should fail because client doesn't declare sampling capability in strict mode
await #expect(throws: MCPError.self) {
_ = try await server.requestSampling(
messages: [.user("Test message")],
maxTokens: 100
)
}
await server.stop()
await client.disconnect()
}
@Test("Non-strict mode succeeds even without client capability declaration")
func testSamplingNonStrictCapabilities() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "NonStrictTestServer",
version: "1.0",
configuration: .default // Non-strict mode
)
// Client WITHOUT sampling capability in non-strict mode
let client = Client(
name: "NonStrictTestClient",
version: "1.0",
capabilities: .init(),
configuration: .default
)
// Register sampling handler anyway
await client.withSamplingHandler { _ in
CreateSamplingMessage.Result(
model: "test-model",
stopReason: .endTurn,
role: .assistant,
content: .text("Non-strict mode success")
)
}
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Should succeed because server is in non-strict mode
let result = try await server.requestSampling(
messages: [.user("Test message")],
maxTokens: 100
)
#expect(result.model == "test-model")
if case .single(.text(let text)) = result.content {
#expect(text == "Non-strict mode success")
} else {
Issue.record("Expected text content")
}
await server.stop()
await client.disconnect()
}
@Test
func testSamplingParameterValidation() async throws {
// Test parameter validation and edge cases
let validMessages: [Sampling.Message] = [
.user("Valid message")
]
// Test with valid parameters
let validParams = CreateSamplingMessage.Parameters(
messages: validMessages,
maxTokens: 100
)
#expect(validParams.messages.count == 1)
#expect(validParams.maxTokens == 100)
// Test with comprehensive parameters
let comprehensiveParams = CreateSamplingMessage.Parameters(
messages: validMessages,
modelPreferences: Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "claude-4")],
costPriority: 0.5,
speedPriority: 0.8,
intelligencePriority: 0.9
),
systemPrompt: "You are a helpful assistant.",
includeContext: .allServers,
temperature: 0.7,
maxTokens: 500,
stopSequences: ["STOP", "END"],
_meta: Metadata(additionalFields: [
"sessionId": "test-session-123",
"userId": "user-456",
])
)
#expect(comprehensiveParams.messages.count == 1)
#expect(comprehensiveParams.modelPreferences?.hints?.count == 1)
#expect(comprehensiveParams.systemPrompt == "You are a helpful assistant.")
#expect(comprehensiveParams.includeContext == .allServers)
#expect(comprehensiveParams.temperature == 0.7)
#expect(comprehensiveParams.maxTokens == 500)
#expect(comprehensiveParams.stopSequences?.count == 2)
#expect(comprehensiveParams._meta?.fields.count == 2)
// Test encoding/decoding of comprehensive parameters
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(comprehensiveParams)
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
#expect(decoded == comprehensiveParams)
}
@Test
func testSamplingWorkflowScenarios() async throws {
// Test realistic sampling workflow scenarios
// Scenario 1: Data Analysis Request
let dataAnalysisMessages: [Sampling.Message] = [
.user("Please analyze the following customer feedback data:"),
.user(
"""
Feedback Summary:
- 85% positive sentiment
- Top complaints: shipping delays (12%), product quality (8%)
- Top praise: customer service (45%), product features (40%)
- NPS Score: 72
"""),
]
let dataAnalysisParams = CreateSamplingMessage.Parameters(
messages: dataAnalysisMessages,
modelPreferences: Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "claude-4-sonnet")],
speedPriority: 0.3,
intelligencePriority: 0.9
),
systemPrompt: "You are an expert business analyst. Provide actionable insights.",
includeContext: .thisServer,
temperature: 0.3, // Lower temperature for analytical tasks
maxTokens: 400,
stopSequences: ["---END---"],
_meta: Metadata(additionalFields: ["analysisType": "customer-feedback"])
)
// Scenario 2: Creative Content Generation
let creativeMessages: [Sampling.Message] = [
.user(
"Write a compelling product description for a new smart home device.")
]
let creativeParams = CreateSamplingMessage.Parameters(
messages: creativeMessages,
modelPreferences: Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "gpt-4.1")],
costPriority: 0.4,
speedPriority: 0.6,
intelligencePriority: 0.8
),
systemPrompt: "You are a creative marketing copywriter.",
temperature: 0.8, // Higher temperature for creativity
maxTokens: 200,
_meta: Metadata(additionalFields: ["contentType": "marketing-copy"])
)
// Test parameter encoding for both scenarios
let encoder = JSONEncoder()
let analysisData = try encoder.encode(dataAnalysisParams)
let creativeData = try encoder.encode(creativeParams)
// Test decoding
let decoder = JSONDecoder()
let decodedAnalysis = try decoder.decode(
CreateSamplingMessage.Parameters.self, from: analysisData)
let decodedCreative = try decoder.decode(
CreateSamplingMessage.Parameters.self, from: creativeData)
#expect(decodedAnalysis == dataAnalysisParams)
#expect(decodedCreative == creativeParams)
}
}
@Suite("Sampling 2025-11-25 Spec Tests")
struct Sampling2025_11_25Tests {
@Test("Audio content encoding and decoding")
func testAudioContent() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let audioMessage: Sampling.Message = .user(
.audio(data: "base64audiodata", mimeType: "audio/mp3"))
let data = try encoder.encode(audioMessage)
let decoded = try decoder.decode(Sampling.Message.self, from: data)
#expect(decoded.role == .user)
if case .single(.audio(let audioData, let mimeType)) = decoded.content {
#expect(audioData == "base64audiodata")
#expect(mimeType == "audio/mp3")
} else {
#expect(Bool(false), "Expected audio content")
}
}
@Test("StopReason.toolUse encoding and decoding")
func testToolUseStopReason() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let result = CreateSamplingMessage.Result(
model: "claude-4",
stopReason: .toolUse,
role: .assistant,
content: .single(.text("I need to use a tool"))
)
let data = try encoder.encode(result)
let decoded = try decoder.decode(CreateSamplingMessage.Result.self, from: data)
#expect(decoded == result)
}
@Test("Multiple content blocks")
func testMultipleContentBlocks() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let blocks: [Sampling.Message.Content.ContentBlock] = [
.text("Here's an image:"),
.image(data: "imagedata", mimeType: "image/png"),
.text("And some audio:"),
.audio(data: "audiodata", mimeType: "audio/mp3")
]
let content = Sampling.Message.Content.multiple(blocks)
let message = Sampling.Message.assistant(content)
let data = try encoder.encode(message)
let decoded = try decoder.decode(Sampling.Message.self, from: data)
#expect(decoded == message)
}
@Test("Tools parameter encoding and decoding")
func testToolsParameter() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let tool = Tool(
name: "get_weather",
description: "Get current weather",
inputSchema: [
"type": "object",
"properties": [
"location": ["type": "string"]
]
]
)
let params = CreateSamplingMessage.Parameters(
messages: [.user("What's the weather?")],
maxTokens: 100,
tools: [tool],
toolChoice: .init(mode: .auto)
)
let data = try encoder.encode(params)
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
#expect(decoded == params)
}
@Test("Client sampling capabilities with sub-capabilities")
func testSamplingSubCapabilities() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let capabilities = Client.Capabilities(
sampling: .init(tools: .init(), context: .init())
)
#expect(capabilities.sampling?.tools != nil)
#expect(capabilities.sampling?.context != nil)
let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Client.Capabilities.self, from: data)
#expect(decoded == capabilities)
}
}