From c3059f7929f404db992f1101cdf8cc427a45b4d0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 23:24:05 +0000 Subject: [PATCH] Keep the SDK-only caveat out of variant summaries The summary of a derived fragment operation is what the developer site renders as the page title and H1. Prefixing the SDK-only caveat onto it made "SDK-only logical operation. HTTP clients must call the base path; the URL fragment is not sent." the title of the platform-chat-create-stream page instead of the operation's own name. The variant summary now falls back to the parent summary unprefixed, and a variant can declare its own summary and description so a fragment operation can carry a title distinct from its parent's. The caveat still leads the description, where it reads as prose rather than a heading; it describes the fragment path itself, so it applies even when the variant overrides the description. Co-authored-by: Chris Freeman --- src/source-spec-transformer.js | 44 ++++++++-- tests/source-spec-transformer.test.js | 116 +++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 11 deletions(-) diff --git a/src/source-spec-transformer.js b/src/source-spec-transformer.js index c686c77a..ce185197 100644 --- a/src/source-spec-transformer.js +++ b/src/source-spec-transformer.js @@ -189,10 +189,12 @@ const platformSdkGroupPattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/; const platformSdkMethodPattern = /^[a-z][A-Za-z0-9]*$/; const platformSdkFragmentPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/; const platformSdkVariantKeys = new Set([ + 'description', 'fragment', 'method', 'request-body-overrides', 'response-media-type', + 'summary', ]); const schemaRefPrefix = '#/components/schemas/'; const eventStreamMediaType = 'text/event-stream'; @@ -483,12 +485,30 @@ function validateEventStreamSchemaRef(operation, mediaType, location) { } } -function prefixSdkOnlyDocs(operation) { - operation.summary = operation.summary - ? `${sdkOnlyOperationPrefix} ${operation.summary}` - : sdkOnlyOperationPrefix; - operation.description = operation.description - ? `${sdkOnlyOperationPrefix} ${operation.description}` +/** + * Applies a variant's own docs and the SDK-only caveat to a derived operation. + * + * The summary is what the developer site renders as the page title and H1, so the + * caveat has to stay out of it — prefixing it there replaced the title of + * platform-chat-create-stream with boilerplate. A variant supplies its own summary + * when it needs a title distinct from the parent's; otherwise the parent summary + * carries over unprefixed. + * + * The caveat still leads the description, where it is prose rather than a heading. + * It describes the fragment path itself, not the source text, so it applies even + * when the variant overrides the description. + */ +function applySdkVariantDocs(operation, variant) { + if (variant.summary !== undefined) { + operation.summary = variant.summary; + } + + const description = + variant.description !== undefined + ? variant.description + : operation.description; + operation.description = description + ? `${sdkOnlyOperationPrefix} ${description}` : sdkOnlyOperationPrefix; } @@ -827,6 +847,16 @@ function transformPlatformOperations(spec) { `Platform operation ${variantLocation} has missing response-media-type`, ); } + for (const key of ['summary', 'description']) { + if ( + variant[key] !== undefined && + (typeof variant[key] !== 'string' || variant[key].length === 0) + ) { + throw new Error( + `Platform operation ${variantLocation} has invalid ${key} ${JSON.stringify(variant[key])}; expected non-empty string`, + ); + } + } if (declaredMediaTypes.has(variant['response-media-type'])) { throw new Error( `Platform operation ${variantLocation} declares duplicate response media type ${JSON.stringify(variant['response-media-type'])}`, @@ -905,7 +935,7 @@ function transformPlatformOperations(spec) { for (const variant of variants) { const variantOperation = structuredClone(operation); variantOperation.operationId = variant.operationId; - prefixSdkOnlyDocs(variantOperation); + applySdkVariantDocs(variantOperation, variant); selectResponseMediaType(variantOperation, variant['response-media-type']); wrapServerSentEventSchema(spec, variantOperation); applyRequestBodyOverrides( diff --git a/tests/source-spec-transformer.test.js b/tests/source-spec-transformer.test.js index 5da68fe2..c2f4e800 100644 --- a/tests/source-spec-transformer.test.js +++ b/tests/source-spec-transformer.test.js @@ -688,8 +688,7 @@ describe('OpenAPI YAML Transformer', () => { 'x-glean-experimental': { id: 'experiment' }, 'x-speakeasy-group': 'chat', 'x-speakeasy-name-override': 'createStream', - summary: - 'SDK-only logical operation. HTTP clients must call the base path; the URL fragment is not sent. Create a chat response', + summary: 'Create a chat response', description: 'SDK-only logical operation. HTTP clients must call the base path; the URL fragment is not sent. Run an assistant turn.', }); @@ -1448,7 +1447,7 @@ describe('OpenAPI YAML Transformer', () => { ).toBeUndefined(); }); - test('transformPlatformSpec prefixes missing variant summary and description', () => { + test('transformPlatformSpec keeps the SDK-only caveat out of the variant summary', () => { const spec = { components: { schemas: { @@ -1503,11 +1502,120 @@ describe('OpenAPI YAML Transformer', () => { const prefix = 'SDK-only logical operation. HTTP clients must call the base path; the URL fragment is not sent.'; - expect(spec.paths['/api/chat#stream'].post.summary).toBe(prefix); + expect(spec.paths['/api/chat#stream'].post.summary).toBeUndefined(); expect(spec.paths['/api/chat#stream'].post.description).toBe(prefix); expect(spec.paths['/api/chat'].post.summary).toBeUndefined(); }); + test('transformPlatformSpec applies variant summary and description overrides', () => { + const spec = { + components: { + schemas: { + ChatRequest: { + type: 'object', + properties: { stream: { type: 'boolean' } }, + }, + ChatEvent: { type: 'object' }, + }, + }, + paths: { + '/api/chat': { + post: { + operationId: 'platform-chat-create', + summary: 'Create a chat response', + description: 'Run an assistant turn.', + 'x-glean-sdk': { + group: 'chat', + method: 'create', + 'response-media-type': 'application/json', + 'request-body-overrides': { stream: false }, + variants: [ + { + fragment: 'stream', + method: 'createStream', + summary: 'Create a streaming chat response', + description: 'Run an assistant turn as server-sent events.', + 'response-media-type': 'text/event-stream', + 'request-body-overrides': { stream: true }, + }, + ], + }, + requestBody: { + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/ChatRequest' }, + }, + }, + }, + responses: { + 200: { + content: { + 'application/json': {}, + 'text/event-stream': { + schema: { $ref: '#/components/schemas/ChatEvent' }, + }, + }, + }, + }, + }, + }, + }, + }; + + transformPlatformSpec(spec); + + const createStream = spec.paths['/api/chat#stream'].post; + expect(createStream.summary).toBe('Create a streaming chat response'); + expect(createStream.description).toBe( + 'SDK-only logical operation. HTTP clients must call the base path; the URL fragment is not sent. Run an assistant turn as server-sent events.', + ); + expect(spec.paths['/api/chat'].post.summary).toBe('Create a chat response'); + expect(spec.paths['/api/chat'].post.description).toBe( + 'Run an assistant turn.', + ); + expect(createStream).not.toHaveProperty('x-glean-sdk'); + }); + + test('transformPlatformSpec rejects a non-string variant summary', () => { + expect(() => + transformPlatformSpec({ + components: { + schemas: { ChatEvent: { type: 'object' } }, + }, + paths: { + '/api/chat': { + post: { + operationId: 'platform-chat-create', + 'x-glean-sdk': { + group: 'chat', + method: 'create', + 'response-media-type': 'application/json', + variants: [ + { + fragment: 'stream', + method: 'createStream', + summary: '', + 'response-media-type': 'text/event-stream', + }, + ], + }, + responses: { + 200: { + content: { + 'application/json': {}, + 'text/event-stream': { + schema: { $ref: '#/components/schemas/ChatEvent' }, + }, + }, + }, + }, + }, + }, + }, + }), + ).toThrow('has invalid summary ""; expected non-empty string'); + }); + test('transformPlatformSpec rejects operations without x-glean-sdk metadata', () => { expect(() => transformPlatformSpec({