swift-sdk/Tests/MCPTests/RootsTests.swift
Maksym Mova 6112a3995a
V0.11.x (#199)
* Support 2025-06-18 Spec (#167)

* Support OpenAI _meta for Tool

* Update init

* Add title and outputSchema

* Concurreny issue

* General fields

* Add Generate Fields to Server elements

* 4 tab spacing

* Update Version

* Elicitation

* Format

* More tests and inits

* Elicitation in README

* Fix orphaned test

* Add title to more structs

* Public init

* Fix meta validation and make tests more generic

* Format and make Result properties public again

* Revert more visibility changes

* feat: 2025-11-25 phase 1, includes Version, Icon, Progress (#181)

* chore: fixed unit tests

* feat: added icon and progess base structs

* feat: added progress notification and meta to callTool

* feat: added tests

* feat: simplified RequestMeta encoding/decoding

* tests: added unit test to track progress

* chore: rebased v0.11.x

* feat: implemented cancellation

* feat: added overloaded callTool method

* fix: encoding Response _meta fix

* chore: added old version to the versions list

* feat: 2025-11-25 phase 2; added Logging and Completions (#196)

* feat: added Logging and Completions

* feat: updated EmbeddedResource for the latest spec

* chore: changed encoding of meta in Prompts to encodeIfPresent

* feat: compliance tests + http server transport (#197)

* feat: http server transport

* feat: added http server transport

* feat: removed unused files

* feat: added conformance tests

* feat: improved conformace testing

* chore: removed redundant files

* feat/2025-11-25: sampling, elicitation, roots (#198)

* feat: Roots implementation

* feat: added unit testing

* feat: improved unit tests and fixed NetworkTransport crash

* feat: implemented sampling and elicitation

* feat: improved sampling tests

* feat: improved elicitation tests

* feat: improved confirmance tests

* feat: improved Prompts unit tests

* feat: removed test code

* feat: updated conformance tests

* chore: removed redundant commas

* chore: removed 6.0.3 swift version from ci

* chore: improved unit tests a little bit

* chore: update README.md

* chore: add server progress tracking example to README.md

* chore: updated README.md

* chore: updated transports table formatting

* chore: updated MockNetworkConnection

---------

Co-authored-by: Austin Evans <austinjevans@me.com>
Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
2026-02-19 13:09:31 +02:00

494 lines
16 KiB
Swift

import Foundation
import Testing
@testable import MCP
@Suite("Roots Tests")
struct RootsTests {
@Test("Root initialization with file:// URI")
func testRootInitialization() throws {
let root = Root(uri: "file:///workspace", name: "Workspace")
#expect(root.uri == "file:///workspace")
#expect(root.name == "Workspace")
}
@Test("Root initialization without name")
func testRootInitializationWithoutName() throws {
let root = Root(uri: "file:///home/user/docs")
#expect(root.uri == "file:///home/user/docs")
#expect(root.name == nil)
}
@Test("Root encoding and decoding")
func testRootEncodingDecoding() throws {
let root = Root(uri: "file:///workspace", name: "Workspace")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(root)
let decoded = try decoder.decode(Root.self, from: data)
#expect(decoded.uri == root.uri)
#expect(decoded.name == root.name)
}
@Test("Root encoding and decoding without name")
func testRootEncodingDecodingWithoutName() throws {
let root = Root(uri: "file:///home/user/docs")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(root)
let decoded = try decoder.decode(Root.self, from: data)
#expect(decoded.uri == root.uri)
#expect(decoded.name == nil)
}
@Test("Root JSON encoding format")
func testRootJSONFormat() throws {
let root = Root(uri: "file:///workspace", name: "Workspace")
let encoder = JSONEncoder()
let data = try encoder.encode(root)
let jsonString = String(data: data, encoding: .utf8)
#expect(jsonString?.contains("\"uri\"") == true)
#expect(jsonString?.contains("\"name\"") == true)
// The URI might be escaped differently, so just check it's present
#expect(jsonString?.contains("workspace") == true)
#expect(jsonString?.contains("Workspace") == true)
}
@Test("ListRoots method name")
func testListRootsMethodName() throws {
#expect(ListRoots.name == "roots/list")
}
@Test("ListRoots request creation")
func testListRootsRequestCreation() throws {
let request = ListRoots.request()
#expect(request.method == "roots/list")
#expect(request.params == Empty())
}
@Test("ListRoots request decoding with omitted params")
func testListRootsRequestDecodingWithOmittedParams() throws {
// Test decoding when params field is omitted
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"roots/list"}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<ListRoots>.self, from: data)
#expect(decoded.id == "test-id")
#expect(decoded.method == ListRoots.name)
}
@Test("ListRoots request decoding with null params")
func testListRootsRequestDecodingWithNullParams() throws {
// Test decoding when params field is null
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"roots/list","params":null}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<ListRoots>.self, from: data)
#expect(decoded.id == "test-id")
#expect(decoded.method == ListRoots.name)
}
@Test("ListRoots result validation")
func testListRootsResult() throws {
let roots = [
Root(uri: "file:///workspace", name: "Workspace"),
Root(uri: "file:///home/user/docs", name: "Documents"),
]
let result = ListRoots.Result(roots: roots)
#expect(result.roots.count == 2)
#expect(result.roots[0].uri == "file:///workspace")
#expect(result.roots[0].name == "Workspace")
#expect(result.roots[1].uri == "file:///home/user/docs")
#expect(result.roots[1].name == "Documents")
}
@Test("ListRoots result encoding and decoding")
func testListRootsResultEncodingDecoding() throws {
let roots = [
Root(uri: "file:///workspace", name: "Workspace"),
Root(uri: "file:///home/user/docs"),
]
let result = ListRoots.Result(roots: roots)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(result)
let decoded = try decoder.decode(ListRoots.Result.self, from: data)
#expect(decoded.roots.count == result.roots.count)
#expect(decoded.roots[0].uri == result.roots[0].uri)
#expect(decoded.roots[0].name == result.roots[0].name)
#expect(decoded.roots[1].uri == result.roots[1].uri)
#expect(decoded.roots[1].name == result.roots[1].name)
}
@Test("ListRoots response format")
func testListRootsResponseFormat() throws {
let response = ListRoots.response(
id: "test-id",
result: ListRoots.Result(
roots: [
Root(uri: "file:///workspace", name: "Workspace")
]
)
)
let encoder = JSONEncoder()
let data = try encoder.encode(response)
let jsonString = String(data: data, encoding: .utf8)
#expect(jsonString?.contains("\"jsonrpc\"") == true)
#expect(jsonString?.contains("\"2.0\"") == true)
#expect(jsonString?.contains("\"id\"") == true)
#expect(jsonString?.contains("\"result\"") == true)
#expect(jsonString?.contains("\"roots\"") == true)
}
@Test("RootsListChangedNotification name")
func testRootsListChangedNotificationName() throws {
#expect(RootsListChangedNotification.name == "notifications/roots/list_changed")
}
@Test("RootsListChangedNotification message creation")
func testRootsListChangedNotificationMessage() throws {
let notification = RootsListChangedNotification.message()
#expect(notification.method == "notifications/roots/list_changed")
#expect(notification.params == Empty())
}
@Test("RootsListChangedNotification encoding")
func testRootsListChangedNotificationEncoding() throws {
let notification = RootsListChangedNotification.message()
let encoder = JSONEncoder()
let data = try encoder.encode(notification)
let jsonString = String(data: data, encoding: .utf8)
#expect(jsonString?.contains("\"jsonrpc\"") == true)
#expect(jsonString?.contains("\"2.0\"") == true)
#expect(jsonString?.contains("\"method\"") == true)
// The method name might be escaped differently, so check key parts
#expect(jsonString?.contains("roots") == true)
#expect(jsonString?.contains("list_changed") == true)
}
@Test("Client.Capabilities.Roots initialization")
func testClientCapabilitiesRoots() throws {
let roots = Client.Capabilities.Roots(listChanged: true)
#expect(roots.listChanged == true)
}
@Test("Client.Capabilities.Roots optional listChanged")
func testClientCapabilitiesRootsOptional() throws {
let roots = Client.Capabilities.Roots()
#expect(roots.listChanged == nil)
}
@Test("Client.Capabilities with roots")
func testClientCapabilitiesWithRoots() throws {
let capabilities = Client.Capabilities(
roots: Client.Capabilities.Roots(listChanged: true)
)
#expect(capabilities.roots?.listChanged == true)
}
@Test("Root hashable conformance")
func testRootHashable() throws {
let root1 = Root(uri: "file:///workspace", name: "Workspace")
let root2 = Root(uri: "file:///workspace", name: "Workspace")
let root3 = Root(uri: "file:///other", name: "Other")
#expect(root1 == root2)
#expect(root1 != root3)
// Test in a Set
let set: Set<Root> = [root1, root2, root3]
#expect(set.count == 2) // root1 and root2 should be the same
}
@Test("Client Capabilities struct with roots encoding")
func testClientCapabilitiesWithRootsEncoding() throws {
let capabilities = Client.Capabilities(
roots: Client.Capabilities.Roots(listChanged: true)
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Client.Capabilities.self, from: data)
#expect(decoded.roots?.listChanged == true)
}
// MARK: - Integration Tests
@Test("Server listRoots with client that has roots capability")
func testServerListRootsWithCapableClient() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "test-server",
version: "1.0.0",
configuration: .strict
)
let testRoots = [
Root(uri: "file:///workspace", name: "Workspace"),
Root(uri: "file:///home/user/docs", name: "Documents"),
]
let client = Client(
name: "test-client",
version: "1.0.0",
capabilities: Client.Capabilities(
roots: Client.Capabilities.Roots(listChanged: true)
)
)
await client.withRootsHandler {
return testRoots
}
// Start server and client
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Wait for initialization to complete
try await Task.sleep(for: .milliseconds(100))
// Server requests roots from client - should succeed
let roots = try await server.listRoots()
#expect(roots == testRoots)
// Cleanup
await server.stop()
await client.disconnect()
}
@Test("Server listRoots fails when client lacks roots capability (strict mode)")
func testServerListRootsFailsWithoutCapability() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "test-server",
version: "1.0.0",
configuration: .strict
)
// Client has NO roots capability (default empty capabilities)
let client = Client(
name: "test-client",
version: "1.0.0"
)
// Start server and client
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Wait for initialization to complete
try await Task.sleep(for: .milliseconds(100))
// Server tries to request roots - should fail
await #expect(throws: MCPError.self) {
try await server.listRoots()
}
// Cleanup
await server.stop()
await client.disconnect()
}
@Test("Server listRoots succeeds when client lacks capability (non-strict mode)")
func testServerListRootsNonStrictMode() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "test-server",
version: "1.0.0",
configuration: .default // Non-strict mode
)
let testRoots = [Root(uri: "file:///workspace")]
// Client has NO roots capability but registers handler anyway
let client = Client(
name: "test-client",
version: "1.0.0"
)
await client.withRootsHandler {
return testRoots
}
// Start server and client
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Wait for initialization to complete
try await Task.sleep(for: .milliseconds(100))
// Server requests roots - should succeed in non-strict mode
let roots = try await server.listRoots()
#expect(roots == testRoots)
// Cleanup
await server.stop()
await client.disconnect()
}
@Test("Client sends roots list changed notification")
func testClientSendsRootsListChangedNotification() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "test-server",
version: "1.0.0"
)
let client = Client(
name: "test-client",
version: "1.0.0",
capabilities: Client.Capabilities(
roots: Client.Capabilities.Roots(listChanged: true)
)
)
// Register notification handler on server
nonisolated(unsafe) var didReceive = false
await server.onNotification(RootsListChangedNotification.self) { _ in
didReceive = true
}
// Start server and client
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Wait for initialization
try await Task.sleep(for: .milliseconds(100))
// Client sends notification
try await client.notifyRootsChanged()
// Wait for notification to be processed
try await Task.sleep(for: .milliseconds(100))
// Verify notification was received
#expect(didReceive)
// Cleanup
await server.stop()
await client.disconnect()
}
@Test("Server listRoots fails when client has no handler registered")
func testServerListRootsFailsWithoutHandler() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "test-server",
version: "1.0.0",
configuration: .default
)
let client = Client(
name: "test-client",
version: "1.0.0",
capabilities: Client.Capabilities(
roots: Client.Capabilities.Roots(listChanged: true)
)
)
// NO handler registered on client
// Start server and client
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Wait for initialization
try await Task.sleep(for: .milliseconds(100))
// Server requests roots - should fail with method not found
await #expect(throws: MCPError.self) {
try await server.listRoots()
}
// Cleanup
await server.stop()
await client.disconnect()
}
@Test("Multiple roots requests work correctly")
func testMultipleRootsRequests() async throws {
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
let server = Server(
name: "test-server",
version: "1.0.0"
)
nonisolated(unsafe) var count = 0
let client = Client(
name: "test-client",
version: "1.0.0",
capabilities: Client.Capabilities(
roots: Client.Capabilities.Roots(listChanged: true)
)
)
await client.withRootsHandler {
count += 1
return [Root(uri: "file:///workspace\(count)")]
}
// Start server and client
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
try await Task.sleep(for: .milliseconds(100))
// Make multiple requests
let roots1 = try await server.listRoots()
#expect(roots1.count == 1)
#expect(roots1[0].uri == "file:///workspace1")
let roots2 = try await server.listRoots()
#expect(roots2.count == 1)
#expect(roots2[0].uri == "file:///workspace2")
let roots3 = try await server.listRoots()
#expect(roots3.count == 1)
#expect(roots3[0].uri == "file:///workspace3")
// Cleanup
await server.stop()
await client.disconnect()
}
}