diff --git a/packages/core/test/spec.types.test.ts b/packages/core/test/spec.types.test.ts index d7be7cb2f..d26a4cd70 100644 --- a/packages/core/test/spec.types.test.ts +++ b/packages/core/test/spec.types.test.ts @@ -763,6 +763,302 @@ const sdkTypeChecks = { } }; +// --------------------------------------------------------------------------- +// Key-level assertions: verify that each SDK type and its corresponding spec +// type expose exactly the same set of named property keys. This catches cases +// where a Zod schema marks a field as `.optional()` but the spec does not (or +// vice-versa), which the mutual-assignability checks above cannot detect +// because optional fields satisfy structural subtyping in both directions. +// --------------------------------------------------------------------------- + +/** Strip index signatures, keeping only explicitly-named keys. */ +type KnownKeys = keyof { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + +/** + * Assert that A and B have exactly the same set of known (named) keys. + * Resolves to `true` on match; a descriptive error type on mismatch. + */ +type AssertExactKeys< + A, + B, + Extra extends PropertyKey = Exclude, KnownKeys>, + Missing extends PropertyKey = Exclude, KnownKeys> +> = [Extra, Missing] extends [never, never] ? true : { _brand: 'KeyMismatch'; extra: Extra; missing: Missing }; + +/** Constraint: T must resolve to `true`. */ +type Assert = T; + +/* + * Excluded from key-level assertions (23 entries): + * + * Union types — KnownKeys cannot meaningfully enumerate their members (15): + * ClientRequest, ServerRequest, ClientNotification, ServerNotification, + * ClientResult, ServerResult, JSONRPCMessage, JSONRPCResponse, ContentBlock, + * SamplingMessageContentBlock, ElicitRequestParams, PrimitiveSchemaDefinition, + * SingleSelectEnumSchema, MultiSelectEnumSchema, EnumSchema + * + * Primitive type aliases — no object keys to compare (8): + * JSONValue, JSONArray, Role, LoggingLevel, ProgressToken, RequestId, + * Cursor, TaskStatus + */ + +// -- Simple types (96) -- + +type _K_RequestParams = Assert>; +type _K_NotificationParams = Assert>; +type _K_CancelledNotificationParams = Assert>; +type _K_InitializeRequestParams = Assert>; +type _K_ProgressNotificationParams = Assert>; +type _K_ResourceRequestParams = Assert>; +type _K_ReadResourceRequestParams = Assert>; +type _K_SubscribeRequestParams = Assert>; +type _K_UnsubscribeRequestParams = Assert>; +type _K_ResourceUpdatedNotificationParams = Assert< + AssertExactKeys +>; +type _K_GetPromptRequestParams = Assert>; +type _K_CallToolRequestParams = Assert>; +type _K_SetLevelRequestParams = Assert>; +type _K_LoggingMessageNotificationParams = Assert< + AssertExactKeys +>; +type _K_CreateMessageRequestParams = Assert>; +type _K_CompleteRequestParams = Assert>; +type _K_ElicitRequestFormParams = Assert>; +type _K_ElicitRequestURLParams = Assert>; +type _K_PaginatedRequestParams = Assert>; +type _K_BaseMetadata = Assert>; +type _K_Implementation = Assert>; +type _K_PaginatedResult = Assert>; +type _K_ListRootsResult = Assert>; +type _K_Root = Assert>; +type _K_ElicitResult = Assert>; +type _K_CompleteResult = Assert>; +type _K_Request = Assert>; +type _K_Result = Assert>; +type _K_JSONRPCRequest = Assert>; +type _K_JSONRPCNotification = Assert>; +type _K_EmptyResult = Assert>; +type _K_Notification = Assert>; +type _K_ResourceTemplateReference = Assert>; +// @ts-expect-error Genuine mismatch: SDK PromptReference is missing 'title' from spec +type _K_PromptReference = Assert>; +type _K_ToolAnnotations = Assert>; +type _K_Tool = Assert>; +type _K_ListToolsResult = Assert>; +type _K_CallToolResult = Assert>; +type _K_ListResourcesResult = Assert>; +type _K_ListResourceTemplatesResult = Assert>; +type _K_ReadResourceResult = Assert>; +type _K_ResourceContents = Assert>; +type _K_TextResourceContents = Assert>; +type _K_BlobResourceContents = Assert>; +type _K_Resource = Assert>; +// @ts-expect-error Genuine mismatch: SDK PromptArgument is missing 'title' from spec +type _K_PromptArgument = Assert>; +type _K_Prompt = Assert>; +type _K_ListPromptsResult = Assert>; +type _K_GetPromptResult = Assert>; +type _K_TextContent = Assert>; +type _K_ImageContent = Assert>; +type _K_AudioContent = Assert>; +type _K_EmbeddedResource = Assert>; +type _K_ResourceLink = Assert>; +type _K_PromptMessage = Assert>; +type _K_BooleanSchema = Assert>; +type _K_StringSchema = Assert>; +type _K_NumberSchema = Assert>; +type _K_UntitledSingleSelectEnumSchema = Assert< + AssertExactKeys +>; +type _K_TitledSingleSelectEnumSchema = Assert< + AssertExactKeys +>; +type _K_UntitledMultiSelectEnumSchema = Assert< + AssertExactKeys +>; +type _K_TitledMultiSelectEnumSchema = Assert>; +type _K_LegacyTitledEnumSchema = Assert>; +type _K_JSONRPCErrorResponse = Assert>; +type _K_JSONRPCResultResponse = Assert>; +type _K_InitializeResult = Assert>; +type _K_ClientCapabilities = Assert>; +type _K_ServerCapabilities = Assert>; +type _K_SamplingMessage = Assert>; +type _K_Icon = Assert>; +type _K_Icons = Assert>; +type _K_ModelHint = Assert>; +type _K_ModelPreferences = Assert>; +type _K_ToolChoice = Assert>; +type _K_ToolUseContent = Assert>; +type _K_ToolResultContent = Assert>; +type _K_Annotations = Assert>; +type _K_TaskAugmentedRequestParams = Assert>; +type _K_ToolExecution = Assert>; +type _K_TaskMetadata = Assert>; +type _K_RelatedTaskMetadata = Assert>; +type _K_Task = Assert>; +type _K_CreateTaskResult = Assert>; +type _K_GetTaskResult = Assert>; +type _K_ListTasksResult = Assert>; +type _K_CancelTaskResult = Assert>; +type _K_GetTaskPayloadResult = Assert>; +type _K_TaskStatusNotificationParams = Assert< + AssertExactKeys +>; +type _K_JSONObject = Assert>; +type _K_MetaObject = Assert>; +// @ts-expect-error Genuine mismatch: SDK RequestMetaObject has extra 'io.modelcontextprotocol/related-task' not in spec +type _K_RequestMetaObject = Assert>; +type _K_ParseError = Assert>; +type _K_InvalidRequestError = Assert>; +type _K_MethodNotFoundError = Assert>; +type _K_InvalidParamsError = Assert>; +type _K_InternalError = Assert>; + +// -- WithJSONRPC-wrapped notification types (11) -- +// SDK notification types do not include `jsonrpc` — the spec types do. We wrap +// with WithJSONRPC<> to add the missing field before comparing keys. + +type _K_ElicitationCompleteNotification = Assert< + AssertExactKeys, SpecTypes.ElicitationCompleteNotification> +>; +type _K_CancelledNotification = Assert, SpecTypes.CancelledNotification>>; +type _K_ProgressNotification = Assert, SpecTypes.ProgressNotification>>; +type _K_ToolListChangedNotification = Assert< + AssertExactKeys, SpecTypes.ToolListChangedNotification> +>; +type _K_ResourceListChangedNotification = Assert< + AssertExactKeys, SpecTypes.ResourceListChangedNotification> +>; +type _K_PromptListChangedNotification = Assert< + AssertExactKeys, SpecTypes.PromptListChangedNotification> +>; +type _K_RootsListChangedNotification = Assert< + AssertExactKeys, SpecTypes.RootsListChangedNotification> +>; +type _K_ResourceUpdatedNotification = Assert< + AssertExactKeys, SpecTypes.ResourceUpdatedNotification> +>; +type _K_LoggingMessageNotification = Assert< + AssertExactKeys, SpecTypes.LoggingMessageNotification> +>; +type _K_InitializedNotification = Assert, SpecTypes.InitializedNotification>>; +type _K_TaskStatusNotification = Assert, SpecTypes.TaskStatusNotification>>; + +// -- WithJSONRPCRequest-wrapped request types (21) -- +// SDK request types do not include `jsonrpc` or `id` — the spec types do. We +// wrap with WithJSONRPCRequest<> to add the missing fields before comparing keys. + +type _K_SubscribeRequest = Assert, SpecTypes.SubscribeRequest>>; +type _K_UnsubscribeRequest = Assert, SpecTypes.UnsubscribeRequest>>; +type _K_PaginatedRequest = Assert, SpecTypes.PaginatedRequest>>; +type _K_ListRootsRequest = Assert, SpecTypes.ListRootsRequest>>; +type _K_ElicitRequest = Assert, SpecTypes.ElicitRequest>>; +type _K_CompleteRequest = Assert, SpecTypes.CompleteRequest>>; +type _K_ListToolsRequest = Assert, SpecTypes.ListToolsRequest>>; +type _K_CallToolRequest = Assert, SpecTypes.CallToolRequest>>; +type _K_SetLevelRequest = Assert, SpecTypes.SetLevelRequest>>; +type _K_PingRequest = Assert, SpecTypes.PingRequest>>; +type _K_ListResourcesRequest = Assert, SpecTypes.ListResourcesRequest>>; +type _K_ListResourceTemplatesRequest = Assert< + AssertExactKeys, SpecTypes.ListResourceTemplatesRequest> +>; +type _K_ReadResourceRequest = Assert, SpecTypes.ReadResourceRequest>>; +type _K_ListPromptsRequest = Assert, SpecTypes.ListPromptsRequest>>; +type _K_GetPromptRequest = Assert, SpecTypes.GetPromptRequest>>; +type _K_CreateMessageRequest = Assert, SpecTypes.CreateMessageRequest>>; +type _K_InitializeRequest = Assert, SpecTypes.InitializeRequest>>; +type _K_GetTaskPayloadRequest = Assert< + AssertExactKeys, SpecTypes.GetTaskPayloadRequest> +>; +type _K_ListTasksRequest = Assert, SpecTypes.ListTasksRequest>>; +type _K_CancelTaskRequest = Assert, SpecTypes.CancelTaskRequest>>; +type _K_GetTaskRequest = Assert, SpecTypes.GetTaskRequest>>; + +// -- TypedResultResponse-wrapped types (21) -- +// The spec defines typed *ResultResponse interfaces that pair JSONRPCResultResponse +// with a specific result. We compare TypedResultResponse against the +// spec's combined type. + +type _K_InitializeResultResponse = Assert< + AssertExactKeys, SpecTypes.InitializeResultResponse> +>; +type _K_PingResultResponse = Assert, SpecTypes.PingResultResponse>>; +type _K_ListResourcesResultResponse = Assert< + AssertExactKeys, SpecTypes.ListResourcesResultResponse> +>; +type _K_ListResourceTemplatesResultResponse = Assert< + AssertExactKeys, SpecTypes.ListResourceTemplatesResultResponse> +>; +type _K_ReadResourceResultResponse = Assert< + AssertExactKeys, SpecTypes.ReadResourceResultResponse> +>; +type _K_SubscribeResultResponse = Assert, SpecTypes.SubscribeResultResponse>>; +type _K_UnsubscribeResultResponse = Assert, SpecTypes.UnsubscribeResultResponse>>; +type _K_ListPromptsResultResponse = Assert< + AssertExactKeys, SpecTypes.ListPromptsResultResponse> +>; +type _K_GetPromptResultResponse = Assert, SpecTypes.GetPromptResultResponse>>; +type _K_ListToolsResultResponse = Assert, SpecTypes.ListToolsResultResponse>>; +type _K_CallToolResultResponse = Assert, SpecTypes.CallToolResultResponse>>; +type _K_CreateTaskResultResponse = Assert< + AssertExactKeys, SpecTypes.CreateTaskResultResponse> +>; +type _K_GetTaskResultResponse = Assert, SpecTypes.GetTaskResultResponse>>; +type _K_GetTaskPayloadResultResponse = Assert< + AssertExactKeys, SpecTypes.GetTaskPayloadResultResponse> +>; +type _K_CancelTaskResultResponse = Assert< + AssertExactKeys, SpecTypes.CancelTaskResultResponse> +>; +type _K_ListTasksResultResponse = Assert, SpecTypes.ListTasksResultResponse>>; +type _K_SetLevelResultResponse = Assert, SpecTypes.SetLevelResultResponse>>; +type _K_CreateMessageResultResponse = Assert< + AssertExactKeys, SpecTypes.CreateMessageResultResponse> +>; +type _K_CompleteResultResponse = Assert, SpecTypes.CompleteResultResponse>>; +type _K_ListRootsResultResponse = Assert, SpecTypes.ListRootsResultResponse>>; +type _K_ElicitResultResponse = Assert, SpecTypes.ElicitResultResponse>>; + +// -- Name mismatches (2) -- +// SDK exports these under different names than the spec. + +type _K_CreateMessageResult = Assert>; +type _K_ResourceTemplate = Assert>; + +// Types excluded from the key-parity completeness guard: union types and primitive aliases +// that cannot have meaningful AssertExactKeys assertions. +const KEY_PARITY_EXCLUDED = [ + // Union types (15) + 'ClientRequest', + 'ServerRequest', + 'ClientNotification', + 'ServerNotification', + 'ClientResult', + 'ServerResult', + 'JSONRPCMessage', + 'JSONRPCResponse', + 'ContentBlock', + 'SamplingMessageContentBlock', + 'ElicitRequestParams', + 'PrimitiveSchemaDefinition', + 'SingleSelectEnumSchema', + 'MultiSelectEnumSchema', + 'EnumSchema', + // Primitive aliases (8) + 'JSONValue', + 'JSONArray', + 'Role', + 'LoggingLevel', + 'ProgressToken', + 'RequestId', + 'Cursor', + 'TaskStatus' +]; + // This file is .gitignore'd, and fetched by `npm run fetch:spec-types` (called by `npm run test`) const SPEC_TYPES_FILE = path.resolve(__dirname, '../src/types/spec.types.ts'); const SDK_TYPES_FILE = path.resolve(__dirname, '../src/types/types.ts'); @@ -778,6 +1074,10 @@ function extractExportedTypes(source: string): string[] { return matches.map(m => m[1]!); } +function extractKeyParityTypes(source: string): string[] { + return [...source.matchAll(/^type _K_(\w+)\s*=/gm)].map(m => m[1]!); +} + describe('Spec Types', () => { const specTypes = extractExportedTypes(fs.readFileSync(SPEC_TYPES_FILE, 'utf8')); const sdkTypes = extractExportedTypes(fs.readFileSync(SDK_TYPES_FILE, 'utf8')); @@ -807,6 +1107,14 @@ describe('Spec Types', () => { expect(missingTests).toHaveLength(0); }); + it('should have key-parity assertions for all non-excluded compatibility tests', () => { + const thisSource = fs.readFileSync(__filename, 'utf8'); + const checked = new Set(extractKeyParityTypes(thisSource)); + const excluded = new Set(KEY_PARITY_EXCLUDED); + const missing = Object.keys(sdkTypeChecks).filter(name => !checked.has(name) && !excluded.has(name)); + expect(missing).toHaveLength(0); + }); + describe('Missing SDK Types', () => { it.each(MISSING_SDK_TYPES)('%s should not be present in MISSING_SDK_TYPES if it has a compatibility test', type => { expect(sdkTypeChecks[type as keyof typeof sdkTypeChecks]).toBeUndefined();