diff --git a/lib/src/shared/protocol.dart b/lib/src/shared/protocol.dart index d2abe6dd..ae05f7f5 100644 --- a/lib/src/shared/protocol.dart +++ b/lib/src/shared/protocol.dart @@ -10,7 +10,9 @@ import 'transport.dart'; final _logger = Logger("mcp_dart.shared.protocol"); bool _isProgressToken(Object? token) => - token is String || (token is num && token.isFinite); + token is String || + token is int || + (token is double && token.isFinite && token == token.truncateToDouble()); const Set _statelessCacheableResultMethods = { Method.toolsList, @@ -204,7 +206,7 @@ class RequestHandlerExtra { if (!_isProgressToken(progressToken)) { _logger.warn( "Invalid progressToken type: ${progressToken.runtimeType}. " - "Expected string or finite number.", + "Expected string or integer.", ); return; } @@ -1428,7 +1430,7 @@ abstract class Protocol { _onerror( ArgumentError( "Received invalid progressToken: $progressToken. " - "Expected string or finite number.", + "Expected string or integer.", ), ); return; @@ -1650,7 +1652,7 @@ abstract class Protocol { if (!_isProgressToken(requestedProgressToken)) { return Future.error( ArgumentError( - 'progressToken must be a string or finite number when ' + 'progressToken must be a string or integer when ' 'onprogress is set.', ), ); diff --git a/lib/src/shared/transport.dart b/lib/src/shared/transport.dart index 1cfddae3..2011775e 100644 --- a/lib/src/shared/transport.dart +++ b/lib/src/shared/transport.dart @@ -45,14 +45,14 @@ abstract class Transport { } /// Optional capability for transports that can preserve JSON-RPC request IDs -/// with their full MCP shape (string or finite number) for request/stream +/// with their full MCP shape (string or integer) for request/stream /// correlation. /// /// Existing custom transports can keep implementing [Transport.send] with /// `int? relatedRequestId`. Transports that need to route messages by string /// request IDs should also implement this interface. abstract class RequestIdAwareTransport { - /// Sends a JSON-RPC message while preserving a string-or-number request ID. + /// Sends a JSON-RPC message while preserving a string-or-integer request ID. Future sendWithRequestId( JsonRpcMessage message, { RequestId? relatedRequestId, diff --git a/lib/src/types/json_rpc.dart b/lib/src/types/json_rpc.dart index 107b325e..c64fa063 100644 --- a/lib/src/types/json_rpc.dart +++ b/lib/src/types/json_rpc.dart @@ -158,7 +158,7 @@ typedef ProgressToken = dynamic; /// Parses a wire progress token. /// -/// MCP progress tokens are JSON strings or finite numbers. Reject malformed wire +/// MCP progress tokens are JSON strings or integers. Reject malformed wire /// shapes at decode boundaries instead of allowing dynamic values to leak into /// higher-level protocol code. ProgressToken parseProgressToken( @@ -168,11 +168,12 @@ ProgressToken parseProgressToken( if (value is String) { return value; } - if (value is num && value.isFinite) { - return value; + final integer = readOptionalInteger(value, fieldName); + if (integer != null) { + return integer; } throw FormatException( - 'Invalid $fieldName: expected string or finite number, ' + 'Invalid $fieldName: expected string or integer, ' 'got ${value.runtimeType}', ); } @@ -185,18 +186,19 @@ typedef RequestId = dynamic; /// Parses a JSON-RPC request identifier. /// -/// JSON-RPC/MCP request IDs are JSON strings or finite numbers for SDK request +/// JSON-RPC/MCP request IDs are JSON strings or integers for SDK request /// boundaries. Notifications omit the `id` member entirely, and responses may /// omit the `id` member for JSON-RPC error cases. RequestId parseRequestId(Object? value, {String fieldName = 'id'}) { if (value is String) { return value; } - if (value is num && value.isFinite) { - return value; + final integer = readOptionalInteger(value, fieldName); + if (integer != null) { + return integer; } throw FormatException( - 'Invalid $fieldName: expected string or finite number, ' + 'Invalid $fieldName: expected string or integer, ' 'got ${value.runtimeType}', ); } @@ -293,8 +295,8 @@ void validateMetaKeyName(String key, {String fieldName = '_meta'}) { /// Validates request metadata that can affect protocol behavior. /// -/// `_meta.progressToken` is an MCP wire token and must be a string or finite -/// number when present. [validateKeys] opts in to the MCP 2026 `_meta` +/// `_meta.progressToken` is an MCP wire token and must be a string or integer +/// when present. [validateKeys] opts in to the MCP 2026 `_meta` /// key-name grammar without changing stable/legacy request parsing. Map? validateRequestMeta( Map? meta, { @@ -508,7 +510,10 @@ class JsonRpcRequest extends JsonRpcMessage { if (params != null) ...readJsonObject(params, 'JsonRpcRequest.params'), if (meta != null) - '_meta': readJsonObject(meta, 'JsonRpcRequest._meta'), + '_meta': readJsonObject( + validateRequestMeta(meta), + 'JsonRpcRequest._meta', + ), }, }; } diff --git a/packages/mcp_dart_cli/README.md b/packages/mcp_dart_cli/README.md index 6e0c56ea..73534f3d 100644 --- a/packages/mcp_dart_cli/README.md +++ b/packages/mcp_dart_cli/README.md @@ -127,7 +127,7 @@ The CLI supports `sampling/createMessage` requests from the server (often used b ### Conformance -Run built-in fixture checks, MCP 2025-11-25 spec-critical checks, and deterministic fuzz checks for MCP protocol edge cases. The fixture suite covers JSON-RPC malformed-message handling, string and numeric request IDs, string and numeric progress tokens, and advertised protocol-version support. The spec suite covers raw-wire lifecycle, capability, elicitation, task-metadata, and progress-token dispatch and negative cases. +Run built-in fixture checks, MCP 2025-11-25 spec-critical checks, and deterministic fuzz checks for MCP protocol edge cases. The fixture suite covers JSON-RPC malformed-message handling, string and integer request IDs, string and integer progress tokens, fractional ID/token rejection, and advertised protocol-version support. The spec suite covers raw-wire lifecycle, capability, elicitation, task-metadata, and progress-token dispatch and negative cases. ```bash # Run all built-in fixture cases diff --git a/packages/mcp_dart_cli/lib/src/conformance_runner.dart b/packages/mcp_dart_cli/lib/src/conformance_runner.dart index 403f4f9b..ae720994 100644 --- a/packages/mcp_dart_cli/lib/src/conformance_runner.dart +++ b/packages/mcp_dart_cli/lib/src/conformance_runner.dart @@ -140,10 +140,10 @@ class ConformanceRunner { ), _ConformanceCase( suite: _fixtureSuite, - name: 'jsonrpc.preserves-numeric-response-id', + name: 'jsonrpc.preserves-integer-response-id', description: - 'Parses and serializes successful responses with numeric JSON-RPC IDs.', - check: _preservesNumericResponseId, + 'Parses and serializes successful responses with integer JSON-RPC IDs.', + check: _preservesIntegerResponseId, ), _ConformanceCase( suite: _fixtureSuite, @@ -154,10 +154,17 @@ class ConformanceRunner { ), _ConformanceCase( suite: _fixtureSuite, - name: 'jsonrpc.preserves-numeric-progress-token', + name: 'jsonrpc.preserves-integer-progress-token', description: - 'Parses and serializes progress notifications with numeric progress tokens.', - check: _preservesNumericProgressToken, + 'Parses and serializes progress notifications with integer progress tokens.', + check: _preservesIntegerProgressToken, + ), + _ConformanceCase( + suite: _fixtureSuite, + name: 'jsonrpc.rejects-fractional-ids-and-progress-tokens', + description: + 'Rejects fractional JSON-RPC request IDs, response IDs, and progress tokens.', + check: _rejectsFractionalIdsAndProgressTokens, ), _ConformanceCase( suite: _fixtureSuite, @@ -200,15 +207,15 @@ class ConformanceRunner { suite: _specSuite, name: 'progress.rejects-malformed-progress-token', description: - 'Rejects progress notifications whose progressToken is not a string or finite number.', + 'Rejects progress notifications whose progressToken is not a string or integer.', check: _rejectsMalformedProgressToken, ), _ConformanceCase( suite: _specSuite, - name: 'progress.dispatches-numeric-progress-token', + name: 'progress.dispatches-integer-progress-token', description: - 'Dispatches progress notifications for finite numeric progress tokens.', - check: _dispatchesNumericProgressToken, + 'Dispatches progress notifications for integer progress tokens.', + check: _dispatchesIntegerProgressToken, ), ]; @@ -338,12 +345,9 @@ class _GeneratedJsonRpcFixture { } _GeneratedJsonRpcFixture _generatedJsonRpcFixture(Random random, int index) { - final numericId = random.nextInt(1000000); - final numericIdValue = - random.nextBool() ? numericId : numericId + random.nextDouble(); + final integerId = random.nextInt(1000000); final stringId = 'req-${random.nextInt(1000000)}'; - final progressToken = - random.nextBool() ? numericIdValue : 'progress-$numericId'; + final progressToken = random.nextBool() ? integerId : 'progress-$integerId'; return switch (random.nextInt(6)) { 0 => _GeneratedJsonRpcFixture( @@ -352,7 +356,7 @@ _GeneratedJsonRpcFixture _generatedJsonRpcFixture(Random random, int index) { 'Generated request with an invalid JSON-RPC version is rejected.', message: { 'jsonrpc': '2.${random.nextInt(9) + 1}', - 'id': random.nextBool() ? numericId : stringId, + 'id': random.nextBool() ? integerId : stringId, 'method': Method.ping, }, expectation: _expectFormatExceptionForPayload, @@ -363,27 +367,27 @@ _GeneratedJsonRpcFixture _generatedJsonRpcFixture(Random random, int index) { 'Generated JSON-RPC envelope without request/response members is rejected.', message: { 'jsonrpc': jsonRpcVersion, - 'id': random.nextBool() ? numericId : stringId, + 'id': random.nextBool() ? integerId : stringId, 'params': {'noise': random.nextInt(100)}, }, expectation: _expectFormatExceptionForPayload, ), 2 => _GeneratedJsonRpcFixture( name: 'fuzz.jsonrpc.request-id.$index', - description: 'Generated requests preserve string-or-number IDs.', + description: 'Generated requests preserve string-or-integer IDs.', message: { 'jsonrpc': jsonRpcVersion, - 'id': random.nextBool() ? numericIdValue : stringId, + 'id': random.nextBool() ? integerId : stringId, 'method': Method.ping, }, expectation: _expectRequestIdRoundTrip, ), 3 => _GeneratedJsonRpcFixture( name: 'fuzz.jsonrpc.response-id.$index', - description: 'Generated responses preserve string-or-number IDs.', + description: 'Generated responses preserve string-or-integer IDs.', message: { 'jsonrpc': jsonRpcVersion, - 'id': random.nextBool() ? numericIdValue : stringId, + 'id': random.nextBool() ? integerId : stringId, 'result': {}, }, expectation: _expectResponseIdRoundTrip, @@ -391,7 +395,7 @@ _GeneratedJsonRpcFixture _generatedJsonRpcFixture(Random random, int index) { 4 => _GeneratedJsonRpcFixture( name: 'fuzz.jsonrpc.progress-token.$index', description: - 'Generated progress notifications preserve string-or-number progress tokens.', + 'Generated progress notifications preserve string-or-integer progress tokens.', message: { 'jsonrpc': jsonRpcVersion, 'method': Method.notificationsProgress, @@ -405,10 +409,11 @@ _GeneratedJsonRpcFixture _generatedJsonRpcFixture(Random random, int index) { ), _ => _GeneratedJsonRpcFixture( name: 'fuzz.jsonrpc.error-id.$index', - description: 'Generated error responses preserve string-or-number IDs.', + description: + 'Generated error responses preserve string-or-integer IDs.', message: { 'jsonrpc': jsonRpcVersion, - 'id': random.nextBool() ? numericIdValue : stringId, + 'id': random.nextBool() ? integerId : stringId, 'error': { 'code': ErrorCode.invalidRequest.value, 'message': 'generated invalid request', @@ -683,7 +688,7 @@ Future _rejectsMalformedProgressToken() async { ); } -Future _dispatchesNumericProgressToken() async { +Future _dispatchesIntegerProgressToken() async { final transport = _ConformanceTransport(); final server = McpServer( const Implementation(name: 'server', version: '1.0.0'), @@ -709,7 +714,7 @@ Future _dispatchesNumericProgressToken() async { 'name': 'progress_probe', 'arguments': {}, '_meta': { - 'progressToken': 1.5, + 'progressToken': 15, }, }, ), @@ -728,8 +733,8 @@ Future _dispatchesNumericProgressToken() async { final progress = ProgressNotification.fromJson( progressMessages.single.params ?? const {}, ); - if (progress.progressToken != 1.5) { - throw StateError('Expected numeric progress token to be preserved.'); + if (progress.progressToken != 15) { + throw StateError('Expected integer progress token to be preserved.'); } if (progress.progress != 1 || progress.total != 2) { throw StateError('Expected progress values to be preserved.'); @@ -828,6 +833,47 @@ Future _rejectsNullJsonRpcParamsMember() async { } } +Future _rejectsFractionalIdsAndProgressTokens() async { + for (final message in const [ + { + 'jsonrpc': jsonRpcVersion, + 'id': 1.5, + 'method': Method.ping, + }, + { + 'jsonrpc': jsonRpcVersion, + 'id': 1.5, + 'result': {}, + }, + { + 'jsonrpc': jsonRpcVersion, + 'id': 1.5, + 'error': { + 'code': -32600, + 'message': 'Invalid request', + }, + }, + { + 'jsonrpc': jsonRpcVersion, + 'id': 1, + 'method': Method.ping, + 'params': { + '_meta': {'progressToken': 1.5}, + }, + }, + { + 'jsonrpc': jsonRpcVersion, + 'method': Method.notificationsProgress, + 'params': { + 'progressToken': 1.5, + 'progress': 1, + }, + }, + ]) { + _expectThrowsFormatException(() => JsonRpcMessage.fromJson(message)); + } +} + Future _preservesStringResponseId() async { final message = JsonRpcMessage.fromJson(const { 'jsonrpc': jsonRpcVersion, @@ -846,21 +892,21 @@ Future _preservesStringResponseId() async { } } -Future _preservesNumericResponseId() async { +Future _preservesIntegerResponseId() async { final message = JsonRpcMessage.fromJson(const { 'jsonrpc': jsonRpcVersion, - 'id': 1.5, + 'id': 15, 'result': {}, }); if (message is! JsonRpcResponse) { throw StateError('Expected JsonRpcResponse, got ${message.runtimeType}.'); } - if (message.id != 1.5) { - throw StateError('Expected numeric response ID to be preserved.'); + if (message.id != 15) { + throw StateError('Expected integer response ID to be preserved.'); } - if (message.toJson()['id'] != 1.5) { - throw StateError('Expected serialized response ID to stay numeric.'); + if (message.toJson()['id'] != 15) { + throw StateError('Expected serialized response ID to stay an integer.'); } } @@ -933,12 +979,12 @@ Future _preservesStringProgressToken() async { } } -Future _preservesNumericProgressToken() async { +Future _preservesIntegerProgressToken() async { final message = JsonRpcMessage.fromJson(const { 'jsonrpc': jsonRpcVersion, 'method': Method.notificationsProgress, 'params': { - 'progressToken': 1.5, + 'progressToken': 15, 'progress': 1, 'total': 2, }, @@ -949,11 +995,11 @@ Future _preservesNumericProgressToken() async { 'Expected JsonRpcProgressNotification, got ${message.runtimeType}.', ); } - if (message.progressParams.progressToken != 1.5) { - throw StateError('Expected numeric progress token to be preserved.'); + if (message.progressParams.progressToken != 15) { + throw StateError('Expected integer progress token to be preserved.'); } - if (message.toJson()['params']['progressToken'] != 1.5) { - throw StateError('Expected serialized progress token to stay numeric.'); + if (message.toJson()['params']['progressToken'] != 15) { + throw StateError('Expected serialized progress token to stay an integer.'); } } diff --git a/packages/mcp_dart_cli/test/src/conformance_command_test.dart b/packages/mcp_dart_cli/test/src/conformance_command_test.dart index 401c8cc4..d3ac7ef3 100644 --- a/packages/mcp_dart_cli/test/src/conformance_command_test.dart +++ b/packages/mcp_dart_cli/test/src/conformance_command_test.dart @@ -26,9 +26,10 @@ void main() { 'jsonrpc.rejects-null-error-response-id', 'jsonrpc.rejects-null-params-member', 'jsonrpc.preserves-string-response-id', - 'jsonrpc.preserves-numeric-response-id', + 'jsonrpc.preserves-integer-response-id', 'jsonrpc.preserves-string-progress-token', - 'jsonrpc.preserves-numeric-progress-token', + 'jsonrpc.preserves-integer-progress-token', + 'jsonrpc.rejects-fractional-ids-and-progress-tokens', 'protocol-version.advertises-latest-2025-11-25', ]), ); @@ -47,7 +48,7 @@ void main() { 'elicitation.rejects-invalid-form-url-union', 'tasks.strips-unnegotiated-related-task-metadata', 'progress.rejects-malformed-progress-token', - 'progress.dispatches-numeric-progress-token', + 'progress.dispatches-integer-progress-token', ]), ); expect( diff --git a/test/mcp_2026_07_28_test.dart b/test/mcp_2026_07_28_test.dart index fdcfd354..b0f747c7 100644 --- a/test/mcp_2026_07_28_test.dart +++ b/test/mcp_2026_07_28_test.dart @@ -322,24 +322,24 @@ void main() { } }); - test('preserves finite numeric request ids and progress tokens', () { + test('preserves integer request ids and progress tokens', () { final message = JsonRpcMessage.fromJson( const { 'jsonrpc': jsonRpcVersion, - 'id': 1.5, + 'id': 1, 'method': Method.toolsList, 'params': { - '_meta': {'progressToken': 2.5}, + '_meta': {'progressToken': 2}, }, }, ); expect(message, isA()); final request = message as JsonRpcListToolsRequest; - expect(request.id, 1.5); - expect(request.progressToken, 2.5); - expect(request.toJson()['id'], 1.5); - expect(request.toJson()['params']['_meta']['progressToken'], 2.5); + expect(request.id, 1); + expect(request.progressToken, 2); + expect(request.toJson()['id'], 1); + expect(request.toJson()['params']['_meta']['progressToken'], 2); }); test('rejects URL elicitation relative URI values', () { diff --git a/test/server/stdio_test.dart b/test/server/stdio_test.dart index 4bf31761..07eadc36 100644 --- a/test/server/stdio_test.dart +++ b/test/server/stdio_test.dart @@ -343,6 +343,14 @@ void main() { 'method': 'ping', }, ), + ( + field: 'id', + message: { + 'jsonrpc': '2.0', + 'id': 1.5, + 'method': 'ping', + }, + ), ( field: 'id', message: { @@ -351,6 +359,19 @@ void main() { 'method': 'ping', }, ), + ( + field: 'progressToken', + message: { + 'jsonrpc': '2.0', + 'id': 'with-bad-meta', + 'method': 'ping', + 'params': { + '_meta': { + 'progressToken': 1.5, + }, + }, + }, + ), ( field: 'progressToken', message: { @@ -375,6 +396,17 @@ void main() { }, }, ), + ( + field: 'progressToken', + message: { + 'jsonrpc': '2.0', + 'method': 'notifications/progress', + 'params': { + 'progressToken': 1.5, + 'progress': 0, + }, + }, + ), ( field: 'progressToken', message: { diff --git a/test/shared/progress_test.dart b/test/shared/progress_test.dart index 32404899..07ffef74 100644 --- a/test/shared/progress_test.dart +++ b/test/shared/progress_test.dart @@ -264,26 +264,26 @@ void main() { expect(msg3, isA()); }); - test('Server sends progress with finite numeric progress token', () async { + test('Server sends progress with integer progress token', () async { protocol.setRequestHandler( - 'test/numeric-token-task', + 'test/integer-token-task', (request, extra) async { await extra.sendProgress(10, total: 100, message: 'Starting'); return TestResult(value: 'success'); }, (id, params, meta) => JsonRpcRequest( id: id, - method: 'test/numeric-token-task', + method: 'test/integer-token-task', params: params, meta: meta, ), ); - const progressToken = 12345.5; + const progressToken = 12345; transport.receiveMessage( const JsonRpcRequest( id: 99, - method: 'test/numeric-token-task', + method: 'test/integer-token-task', meta: {'progressToken': progressToken}, ), ); @@ -304,7 +304,7 @@ void main() { expect(msg2, isA()); }); - test('RequestHandlerExtra ignores non-finite progress token', () async { + test('RequestHandlerExtra ignores malformed progress token', () async { final sentNotifications = []; final extra = RequestHandlerExtra( signal: BasicAbortController().signal, @@ -327,6 +327,29 @@ void main() { expect(sentNotifications, isEmpty); }); + test('RequestHandlerExtra ignores fractional progress token', () async { + final sentNotifications = []; + final extra = RequestHandlerExtra( + signal: BasicAbortController().signal, + requestId: 101, + meta: const {'progressToken': 123.5}, + sendNotification: (notification, {relatedTask}) async { + sentNotifications.add(notification); + }, + sendRequest: ( + JsonRpcRequest request, + T Function(Map) resultFactory, + RequestOptions options, + ) async { + return resultFactory({}); + }, + ); + + await extra.sendProgress(10); + + expect(sentNotifications, isEmpty); + }); + test('RequestHandlerExtra rejects non-increasing progress', () async { final sentNotifications = []; final extra = RequestHandlerExtra( diff --git a/test/shared/protocol_test.dart b/test/shared/protocol_test.dart index 9cb05b3d..d65d86b5 100644 --- a/test/shared/protocol_test.dart +++ b/test/shared/protocol_test.dart @@ -505,17 +505,16 @@ void main() { expect((await requestFuture).value, 'ok'); }); - test('public notification preserves finite numeric relatedRequestId', - () async { + test('public notification preserves integer relatedRequestId', () async { await protocol.connect(transport); await protocol.notification( const JsonRpcNotification(method: 'test/notification'), - relatedRequestId: 1.5, + relatedRequestId: 15, ); expect(transport.sentMessages.single, isA()); - expect(transport.relatedRequestIds.single, 1.5); + expect(transport.relatedRequestIds.single, 15); }); test('routes nested cancellation notifications for string request IDs', @@ -658,8 +657,7 @@ void main() { expect(result.value, 'response-data'); }); - test('dispatches finite numeric progress tokens from request metadata', - () async { + test('dispatches integer progress tokens from request metadata', () async { await protocol.connect(transport); final progressUpdates = []; @@ -668,7 +666,7 @@ void main() { const JsonRpcRequest( id: 0, method: 'test/method', - meta: {'progressToken': 1.5}, + meta: {'progressToken': 15}, ), (json) => TestResult(value: json['value'] as String), RequestOptions( @@ -680,12 +678,12 @@ void main() { expect(transport.sentMessages, hasLength(1)); final sentRequest = transport.sentMessages.single as JsonRpcRequest; - expect(sentRequest.meta?['progressToken'], 1.5); + expect(sentRequest.meta?['progressToken'], 15); transport.receiveMessage( JsonRpcProgressNotification( progressParams: const ProgressNotification( - progressToken: 1.5, + progressToken: 15, progress: 50, total: 100, message: 'halfway', @@ -1261,6 +1259,27 @@ void main() { expect(transport.sentMessages, isEmpty); }); + test( + 'rejects fractional request progress tokens when progress handler is set', + () async { + await protocol.connect(transport); + + await expectLater( + protocol.request( + const JsonRpcRequest( + id: 0, + method: 'test/method', + meta: {'progressToken': 1.5}, + ), + (json) => TestResult(value: json['value'] as String), + RequestOptions(onprogress: (_) {}), + ), + throwsA(isA()), + ); + + expect(transport.sentMessages, isEmpty); + }); + test('keeps task-augmented progress tokens until terminal task status', () async { await protocol.connect(transport); diff --git a/test/shared/transport_api_compatibility_test.dart b/test/shared/transport_api_compatibility_test.dart index 9a2f2ab0..00473749 100644 --- a/test/shared/transport_api_compatibility_test.dart +++ b/test/shared/transport_api_compatibility_test.dart @@ -59,7 +59,8 @@ void main() { expect(transport.lastRelatedRequestId, isNull); }); - test('request-id-aware transports preserve non-integer relatedRequestId', + test( + 'request-id-aware transports preserve string and integer relatedRequestId', () async { final transport = FullRequestIdAwareTransport(); @@ -74,10 +75,10 @@ void main() { await transport.sendPreservingRequestId( const JsonRpcNotification(method: 'test/notification'), - relatedRequestId: 1.5, + relatedRequestId: 15, ); - expect(transport.lastRequestIdAwareRelatedRequestId, 1.5); + expect(transport.lastRequestIdAwareRelatedRequestId, 15); }); }); } diff --git a/test/types_edge_cases_test.dart b/test/types_edge_cases_test.dart index 6ac933c1..aff5633d 100644 --- a/test/types_edge_cases_test.dart +++ b/test/types_edge_cases_test.dart @@ -181,6 +181,7 @@ void main() { for (final requestId in [ null, true, + 123.5, double.nan, double.infinity, {}, @@ -202,9 +203,14 @@ void main() { } expect( - () => const CancelledNotificationParams( - requestId: double.nan, - ).toJson(), + () => const CancelledNotificationParams(requestId: 123.5).toJson(), + throwsA( + isA() + .having((e) => e.message, 'message', contains('requestId')), + ), + ); + expect( + () => const CancelledNotificationParams(requestId: double.nan).toJson(), throwsA( isA() .having((e) => e.message, 'message', contains('requestId')), @@ -212,8 +218,8 @@ void main() { ); }); - test('preserves string and finite number requestId wire values', () { - for (final requestId in [123, 123.5, 'request-123']) { + test('preserves string and integer requestId wire values', () { + for (final requestId in [123, 'request-123']) { final notification = JsonRpcCancelledNotification.fromJson({ 'jsonrpc': '2.0', 'method': 'notifications/cancelled', @@ -341,6 +347,7 @@ void main() { for (final progressToken in [ null, false, + 123.5, double.nan, double.infinity, {}, @@ -366,10 +373,16 @@ void main() { } expect( - () => const ProgressNotification( - progressToken: double.nan, - progress: 1, - ).toJson(), + () => const ProgressNotification(progressToken: 123.5, progress: 1) + .toJson(), + throwsA( + isA() + .having((e) => e.message, 'message', contains('progressToken')), + ), + ); + expect( + () => const ProgressNotification(progressToken: double.nan, progress: 1) + .toJson(), throwsA( isA() .having((e) => e.message, 'message', contains('progressToken')), @@ -377,8 +390,8 @@ void main() { ); }); - test('preserves string and finite number progressToken wire values', () { - for (final progressToken in [123, 123.5, 'progress-123']) { + test('preserves string and integer progressToken wire values', () { + for (final progressToken in [123, 'progress-123']) { final notification = JsonRpcProgressNotification.fromJson({ 'jsonrpc': '2.0', 'method': 'notifications/progress', @@ -496,6 +509,7 @@ void main() { for (final id in [ null, false, + 123.5, double.nan, double.infinity, {}, @@ -514,6 +528,19 @@ void main() { ); } + expect( + () => const JsonRpcRequest( + id: 123.5, + method: 'unknown/request', + ).toJson(), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('JsonRpcRequest.id'), + ), + ), + ); expect( () => const JsonRpcRequest( id: double.nan, @@ -529,8 +556,8 @@ void main() { ); }); - test('preserves string and finite number request ids', () { - for (final id in [123, 123.5, 'request-123']) { + test('preserves string and integer request ids', () { + for (final id in [123, 'request-123']) { final message = JsonRpcMessage.fromJson({ 'jsonrpc': '2.0', 'id': id, @@ -547,6 +574,7 @@ void main() { for (final token in [ null, false, + 123.5, double.nan, double.infinity, {}, @@ -570,6 +598,21 @@ void main() { ), ); } + + expect( + () => const JsonRpcRequest( + id: 'request-1', + method: 'unknown/request', + meta: {'progressToken': 123.5}, + ).toJson(), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('_meta.progressToken'), + ), + ), + ); }); test('rejects malformed request _meta wire values', () { @@ -604,9 +647,8 @@ void main() { ); }); - test('preserves string and finite number request progressToken wire values', - () { - for (final token in [123, 123.5, 'progress-123']) { + test('preserves string and integer request progressToken wire values', () { + for (final token in [123, 'progress-123']) { final message = JsonRpcMessage.fromJson({ 'jsonrpc': '2.0', 'id': 'request-1', @@ -665,6 +707,7 @@ void main() { test('rejects malformed response id wire values', () { for (final id in [ false, + 123.5, double.nan, double.infinity, {}, @@ -715,6 +758,7 @@ void main() { for (final id in [ null, false, + 123.5, double.nan, double.infinity, {},