refactor(specTypeSchema): drop as unknown as casts; add allowlist drift guard (#1993)
This commit is contained in:
parent
96db044fe9
commit
4fbcfcd176
2 changed files with 32 additions and 4 deletions
|
|
@ -238,9 +238,9 @@ type SpecTypeInputs = {
|
|||
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
|
||||
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
|
||||
|
||||
const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
|
||||
const _specTypeSchemas: Record<string, StandardSchemaV1> = {};
|
||||
const _isSpecType: Record<string, (value: unknown) => boolean> = {};
|
||||
function register(key: string, schema: z.ZodTypeAny): void {
|
||||
function register(key: string, schema: z.ZodType): void {
|
||||
const name = key.slice(0, -'Schema'.length);
|
||||
_specTypeSchemas[name] = schema;
|
||||
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
|
||||
|
|
@ -271,7 +271,7 @@ for (const [key, schema] of Object.entries(authSchemas)) {
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as unknown as SchemaRecord;
|
||||
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas as SchemaRecord);
|
||||
|
||||
/**
|
||||
* Type predicates for every MCP spec type, keyed by type name.
|
||||
|
|
@ -293,4 +293,4 @@ export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as
|
|||
* const blocks = mixed.filter(isSpecType.ContentBlock);
|
||||
* ```
|
||||
*/
|
||||
export const isSpecType: GuardRecord = Object.freeze(_isSpecType) as unknown as GuardRecord;
|
||||
export const isSpecType: GuardRecord = Object.freeze(_isSpecType as GuardRecord);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth.js';
|
||||
import * as schemas from '../../src/types/schemas.js';
|
||||
import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema.js';
|
||||
import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema.js';
|
||||
import type {
|
||||
|
|
@ -146,3 +147,30 @@ describe('SpecTypeName / SpecTypes (type-level)', () => {
|
|||
expectTypeOf<SpecTypes['ResourceTemplate']>().toEqualTypeOf<ResourceTemplateType>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SPEC_SCHEMA_KEYS allowlist', () => {
|
||||
// Mirrors the exclusion comment in specTypeSchema.ts. If this list grows, confirm the new
|
||||
// entry has no public type in types.ts before adding it here; otherwise add it to the allowlist.
|
||||
const INTERNAL_HELPER_SCHEMAS: readonly string[] = [
|
||||
'ListChangedOptionsBaseSchema',
|
||||
'BaseRequestParamsSchema',
|
||||
'NotificationsParamsSchema',
|
||||
'ClientTasksCapabilitySchema',
|
||||
'ServerTasksCapabilitySchema'
|
||||
];
|
||||
|
||||
it('covers every public protocol schema in schemas.ts (drift guard)', () => {
|
||||
// PascalCase filters out helper functions like getRequestSchema/getResultSchema.
|
||||
const allProtocolSchemas = Object.keys(schemas).filter(k => k.endsWith('Schema') && /^[A-Z]/.test(k));
|
||||
const expected = allProtocolSchemas
|
||||
.filter(k => !INTERNAL_HELPER_SCHEMAS.includes(k))
|
||||
.map(k => k.slice(0, -'Schema'.length))
|
||||
.sort();
|
||||
// Auth schemas are sourced from shared/auth.ts, not schemas.ts, so filter them out of the
|
||||
// observed side before comparing.
|
||||
const actual = Object.keys(isSpecType)
|
||||
.filter(k => !k.startsWith('OAuth') && !k.startsWith('OpenId'))
|
||||
.sort();
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue