swift-sdk/Tests/MCPTests/ProgressTests.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

390 lines
13 KiB
Swift

import Foundation
import Testing
@testable import MCP
@Suite("Progress Tests")
struct ProgressTests {
// MARK: - ProgressToken Tests
@Test("ProgressToken string encoding and decoding")
func testProgressTokenStringEncodingDecoding() throws {
let token = ProgressToken.string("test-token-123")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(token)
let decoded = try decoder.decode(ProgressToken.self, from: data)
#expect(decoded == token)
if case .string(let value) = decoded {
#expect(value == "test-token-123")
} else {
#expect(Bool(false), "Expected string token")
}
}
@Test("ProgressToken integer encoding and decoding")
func testProgressTokenIntegerEncodingDecoding() throws {
let token = ProgressToken.integer(42)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(token)
let decoded = try decoder.decode(ProgressToken.self, from: data)
#expect(decoded == token)
if case .integer(let value) = decoded {
#expect(value == 42)
} else {
#expect(Bool(false), "Expected integer token")
}
}
@Test("ProgressToken unique generation")
func testProgressTokenUnique() {
let token1 = ProgressToken.unique()
let token2 = ProgressToken.unique()
#expect(token1 != token2)
if case .string(let value1) = token1, case .string(let value2) = token2 {
#expect(value1 != value2)
} else {
#expect(Bool(false), "Expected string tokens")
}
}
@Test("ProgressToken hashable")
func testProgressTokenHashable() {
let token1 = ProgressToken.string("test")
let token2 = ProgressToken.string("test")
let token3 = ProgressToken.integer(1)
let token4 = ProgressToken.integer(1)
#expect(token1 == token2)
#expect(token3 == token4)
#expect(token1 != token3)
let set: Set<ProgressToken> = [token1, token2, token3, token4]
#expect(set.count == 2)
}
// MARK: - RequestMeta Tests
@Test("RequestMeta empty initialization")
func testRequestMetaEmptyInit() throws {
let meta = Metadata()
#expect(meta.progressToken == nil)
#expect(meta.fields.isEmpty)
}
@Test("RequestMeta with progress token")
func testRequestMetaWithProgressToken() throws {
let token = ProgressToken.string("my-token")
let meta = Metadata(progressToken: token)
#expect(meta.progressToken == token)
#expect(meta.fields["progressToken"] == .string("my-token"))
}
@Test("RequestMeta with integer progress token")
func testRequestMetaWithIntegerProgressToken() throws {
let token = ProgressToken.integer(42)
let meta = Metadata(progressToken: token)
#expect(meta.progressToken == token)
#expect(meta.fields["progressToken"] == .int(42))
}
@Test("RequestMeta encoding with progress token")
func testRequestMetaEncodingWithProgressToken() throws {
let token = ProgressToken.string("my-token")
let meta = Metadata(progressToken: token)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(meta)
let decoded = try decoder.decode(Metadata.self, from: data)
#expect(decoded.progressToken == token)
}
@Test("RequestMeta encoding without progress token")
func testRequestMetaEncodingWithoutProgressToken() throws {
let meta = Metadata()
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(meta)
let decoded = try decoder.decode(Metadata.self, from: data)
#expect(decoded.progressToken == nil)
}
@Test("RequestMeta JSON representation with progress token")
func testRequestMetaJSONWithProgressToken() throws {
let token = ProgressToken.string("test-token")
let meta = Metadata(progressToken: token)
let encoder = JSONEncoder()
let data = try encoder.encode(meta)
let jsonString = String(data: data, encoding: .utf8)!
#expect(jsonString.contains("progressToken"))
#expect(jsonString.contains("test-token"))
}
@Test("RequestMeta with additional fields")
func testRequestMetaWithAdditionalFields() throws {
let meta = Metadata(
progressToken: .string("token"),
additionalFields: ["customField": .string("customValue")]
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(meta)
let decoded = try decoder.decode(Metadata.self, from: data)
#expect(decoded.progressToken == .string("token"))
#expect(decoded.fields["customField"] == .string("customValue"))
}
@Test("RequestMeta with only additional fields")
func testRequestMetaWithOnlyAdditionalFields() throws {
let meta = Metadata(additionalFields: [
"customKey": .int(123),
"anotherKey": .string("value")
])
#expect(meta.progressToken == nil)
#expect(meta.fields["customKey"] == .int(123))
#expect(meta.fields["anotherKey"] == .string("value"))
}
// MARK: - ProgressNotification Tests
@Test("ProgressNotification name")
func testProgressNotificationName() {
#expect(ProgressNotification.name == "notifications/progress")
}
@Test("ProgressNotification parameters encoding and decoding")
func testProgressNotificationParametersEncodingDecoding() throws {
let params = ProgressNotification.Parameters(
progressToken: .string("test-token"),
progress: 50,
total: 100,
message: "Processing..."
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(params)
let decoded = try decoder.decode(ProgressNotification.Parameters.self, from: data)
#expect(decoded.progressToken == .string("test-token"))
#expect(decoded.progress == 50)
#expect(decoded.total == 100)
#expect(decoded.message == "Processing...")
}
@Test("ProgressNotification parameters without optional fields")
func testProgressNotificationParametersWithoutOptionals() throws {
let params = ProgressNotification.Parameters(
progressToken: .integer(42),
progress: 75
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(params)
let decoded = try decoder.decode(ProgressNotification.Parameters.self, from: data)
#expect(decoded.progressToken == .integer(42))
#expect(decoded.progress == 75)
#expect(decoded.total == nil)
#expect(decoded.message == nil)
}
@Test("ProgressNotification message creation")
func testProgressNotificationMessage() throws {
let params = ProgressNotification.Parameters(
progressToken: .string("my-token"),
progress: 30,
total: 100,
message: "Step 3 of 10"
)
let message = ProgressNotification.message(params)
#expect(message.method == "notifications/progress")
#expect(message.params.progressToken == .string("my-token"))
#expect(message.params.progress == 30)
#expect(message.params.total == 100)
#expect(message.params.message == "Step 3 of 10")
}
// MARK: - CallTool with _meta Tests
@Test("CallTool parameters without meta")
func testCallToolParametersWithoutMeta() throws {
let params = CallTool.Parameters(name: "test_tool", arguments: ["key": "value"])
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(params)
let decoded = try decoder.decode(CallTool.Parameters.self, from: data)
#expect(decoded.name == "test_tool")
#expect(decoded.arguments?["key"] == .string("value"))
#expect(decoded._meta == nil)
// Verify _meta is not included in JSON when nil
let jsonString = String(data: data, encoding: .utf8)!
#expect(!jsonString.contains("_meta"))
}
@Test("CallTool parameters with progress token")
func testCallToolParametersWithProgressToken() throws {
let token = ProgressToken.string("call-tool-token")
let meta = Metadata(progressToken: token)
let params = CallTool.Parameters(name: "test_tool", arguments: ["key": "value"], meta: meta)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(params)
let decoded = try decoder.decode(CallTool.Parameters.self, from: data)
#expect(decoded.name == "test_tool")
#expect(decoded.arguments?["key"] == .string("value"))
#expect(decoded._meta?.progressToken == token)
// Verify _meta is included in JSON
let jsonString = String(data: data, encoding: .utf8)!
#expect(jsonString.contains("_meta"))
#expect(jsonString.contains("progressToken"))
#expect(jsonString.contains("call-tool-token"))
}
@Test("CallTool request encoding with progress token")
func testCallToolRequestEncodingWithProgressToken() throws {
let token = ProgressToken.string("request-token")
let meta = Metadata(progressToken: token)
let request = CallTool.request(.init(name: "my_tool", arguments: ["arg": 42], meta: meta))
let encoder = JSONEncoder()
let data = try encoder.encode(request)
let jsonString = String(data: data, encoding: .utf8)!
// Note: JSON encoding may escape forward slashes as \/
#expect(jsonString.contains("tools") && jsonString.contains("call"))
#expect(jsonString.contains("my_tool"))
#expect(jsonString.contains("_meta"))
#expect(jsonString.contains("progressToken"))
#expect(jsonString.contains("request-token"))
}
@Test("CallTool request decoding with _meta")
func testCallToolRequestDecodingWithMeta() throws {
let jsonString = """
{
"jsonrpc": "2.0",
"id": "test-id",
"method": "tools/call",
"params": {
"_meta": {
"progressToken": "decoded-token"
},
"name": "decoded_tool",
"arguments": {"x": 10}
}
}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let request = try decoder.decode(Request<CallTool>.self, from: data)
#expect(request.id == "test-id")
#expect(request.method == "tools/call")
#expect(request.params.name == "decoded_tool")
#expect(request.params.arguments?["x"] == .int(10))
#expect(request.params._meta?.progressToken == .string("decoded-token"))
}
@Test("CallTool request decoding without _meta")
func testCallToolRequestDecodingWithoutMeta() throws {
let jsonString = """
{
"jsonrpc": "2.0",
"id": "test-id",
"method": "tools/call",
"params": {
"name": "tool_without_meta",
"arguments": {"y": 20}
}
}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let request = try decoder.decode(Request<CallTool>.self, from: data)
#expect(request.params.name == "tool_without_meta")
#expect(request.params.arguments?["y"] == .int(20))
#expect(request.params._meta == nil)
}
@Test("Client notification updates should fire when server sends them")
func testClientNotificationUpdates() async throws {
let pair = await InMemoryTransport.createConnectedPair()
let client = Client(name: "testClient", version: "1")
let token = ProgressToken.unique()
var progresses: [Double] = []
await client.onNotification(ProgressNotification.self) { message in
let receivedToken = message.params.progressToken
#expect(receivedToken == token)
await MainActor.run {
progresses.append(message.params.progress)
}
}
let server = Server(name: "testServer", version: "1")
let expectedToolCallResult = CallTool.Result(content: [.text(text: "success", annotations: nil, _meta: nil)])
await server.withMethodHandler(CallTool.self) { params in
if let token = params._meta?.progressToken {
for i in 1...5 {
let notification = ProgressNotification.message(
.init(progressToken: token, progress: Double(i * 20))
)
try await server.notify(notification)
}
}
return .init(content: [.text(text: "success", annotations: nil, _meta: nil)])
}
try await server.start(transport: pair.server)
try await client.connect(transport: pair.client)
let context: RequestContext<CallTool.Result> = try await client.callTool(name: "random", meta: Metadata(progressToken: token))
let result = try await context.value
#expect(progresses == [20, 40, 60, 80, 100])
#expect(result.content == expectedToolCallResult.content)
#expect(result.isError == nil)
}
}