import { validate } from '@scalar/openapi-parser' import * as TestApp from '../test/App.js' import * as Mcp from './apps/mcp.js' type JsonObject = Record const methods = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace'] as const function array(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : [] } function object(value: unknown): JsonObject | undefined { if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined return value as JsonObject } function operations(document: JsonObject) { return ['paths', 'webhooks'].flatMap((surface) => { const paths = object(document[surface]) ?? {} return Object.entries(paths).flatMap(([path, item]) => { const pathItem = object(item) ?? {} return methods.flatMap((method) => { const operation = object(pathItem[method]) const name = surface === 'paths' ? path : `${surface}:${path}` return operation ? ([[name, method, operation, pathItem]] as const) : [] }) }) }) } function jsonMedia(content: unknown) { const entries = object(content) if (!entries || !Object.hasOwn(entries, 'application/json')) return { present: false, schema: undefined } return { present: true, schema: object(object(entries['application/json'])?.['schema']), } } function resolvePointer(document: JsonObject, pointer: string): unknown { if (!pointer.startsWith('#/')) return undefined return pointer .slice(2) .split('/') .map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~')) .reduce((value, part) => object(value)?.[part], document) } function resolveObject(document: JsonObject, value: unknown): JsonObject | undefined { const current = object(value) if (!current || typeof current['$ref'] !== 'string') return current return object(resolvePointer(document, current['$ref'])) } function references(value: unknown): readonly string[] { if (Array.isArray(value)) return value.flatMap(references) const current = object(value) if (!current) return [] return Object.entries(current).flatMap(([key, item]) => key === '$ref' && typeof item === 'string' ? [item] : references(item), ) } function missingFieldExamples( document: JsonObject, value: unknown, path: string, audit = false, seen: ReadonlySet = new Set(), ): readonly string[] { if (Array.isArray(value)) return value.flatMap((item) => missingFieldExamples(document, item, path, audit, seen)) const current = object(value) if (!current) return [] const documented = 'example' in current || 'examples' in current const missing = audit && !documented ? [path] : [] if (typeof current['$ref'] === 'string') { if (seen.has(current['$ref'])) return [] return missingFieldExamples( document, resolvePointer(document, current['$ref']), path, audit, new Set([...seen, current['$ref']]), ) } const properties = object(current['properties']) ?? {} return [ ...missing, ...Object.entries(properties).flatMap(([name, schema]) => missingFieldExamples(document, schema, `${path}.${name}`, true, seen), ), ...['allOf', 'anyOf', 'items', 'oneOf'].flatMap((name) => missingFieldExamples(document, current[name], path, false, seen), ), ] } async function document() { const app = TestApp.create({ webhook: { supportedChainIds: [4217] } }) const fetch = app.fetch.bind(app) app.route('/', Mcp.mcp({ fetch })) const response = await app.request('/openapi.json') expect(response.status).toBe(200) const value = (await response.json()) as JsonObject expect(operations(value).length).toBeGreaterThan(0) return value } describe('OpenAPI contract', () => { test('uses unique operation ids and valid component references', async () => { const spec = await document() const validation = await validate(spec) const routes = operations(spec) const ids = routes.map(([, , operation]) => operation['operationId']) expect(ids.every((id) => typeof id === 'string' && id.length > 0)).toBe(true) expect(new Set(ids).size).toBe(ids.length) expect(references(spec).filter((reference) => !resolvePointer(spec, reference))).toStrictEqual( [], ) expect(validation.errors ?? []).toStrictEqual([]) expect(validation.valid).toBe(true) }) test('names JSON request and success models', async () => { const spec = await document() const unnamed: string[] = [] for (const [path, method, operation] of operations(spec)) { const request = jsonMedia(resolveObject(spec, operation['requestBody'])?.['content']) // MPP and MCP bodies are owned by their external protocols. const protocolOwned = path.startsWith('/v1/mpp/') || path === '/mcp' if (request.present && !request.schema && !protocolOwned) unnamed.push(`${method} ${path} request schema`) else if (request.schema && !protocolOwned && typeof request.schema['$ref'] !== 'string') unnamed.push(`${method} ${path} request`) const responses = object(operation['responses']) ?? {} for (const [status, response] of Object.entries(responses)) { if (!status.startsWith('2')) continue const media = jsonMedia(resolveObject(spec, response)?.['content']) if (media.present && !media.schema && !protocolOwned) unnamed.push(`${method} ${path} response ${status} schema`) else if (media.schema && !protocolOwned && typeof media.schema['$ref'] !== 'string') unnamed.push(`${method} ${path} response ${status}`) } } expect(unnamed).toStrictEqual([]) }) test('documents examples in JSON success models', async () => { const spec = await document() const missing: string[] = [] for (const [path, method, operation, pathItem] of operations(spec)) { for (const parameter of [ ...array(pathItem['parameters']), ...array(operation['parameters']), ]) { const resolved = resolveObject(spec, parameter) const schema = object(resolved?.['schema']) if (resolved && !schema) missing.push(`${method} ${path} parameter ${String(resolved['name'])} schema`) else if ( resolved && schema && !('example' in resolved) && !('examples' in resolved) && !('example' in schema) && !('examples' in schema) ) missing.push(`${method} ${path} parameter ${String(resolved?.['name'])}`) } const responses = object(operation['responses']) ?? {} for (const [status, response] of Object.entries(responses)) { if (!status.startsWith('2')) continue const media = jsonMedia(resolveObject(spec, response)?.['content']) // MCP owns this external protocol envelope rather than Tempo's REST model conventions. if (path === '/mcp') continue if (media.present && !media.schema) missing.push(`${method} ${path} ${status} schema`) else if (media.schema) missing.push(...missingFieldExamples(spec, media.schema, `${method} ${path} ${status}`)) } } expect([...new Set(missing)]).toStrictEqual([]) }) })