From bc0745027591fe64f10eb4da788deb131fd6b674 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 28 May 2026 12:04:50 -0500 Subject: [PATCH 1/4] fix(openapi): add type discriminator to DestinationUpdate union (#920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trigger: the DestinationUpdate union lost its discriminator in 8be278c5 (May 2025), when `type` wasn't on PATCH. Server has since added `type` acceptance and RFC 7396 merge-patch (bd7701b5), making the missing discriminator an active bug — Hookdeck's permissive `config: {}` variant greedily matched any partial-config payload, the SDK sent it through with no field remap (camelCase reached the server), and merge-patch silently no-op'd. All non-Webhook partial config updates affected. Spec: `type` becomes a required, enum-locked discriminator on every DestinationUpdate* variant; 17 new `*ConfigUpdate`/`*CredentialsUpdate` companions model the now-explicit partial PATCH shape; Hookdeck's `config: {}` removed. Impact: * Breaking for typed SDK consumers: `update()` must include `type` after regen. Server is more lenient (still accepts PATCH without `type`), so raw HTTP callers keep working but are spec-noncompliant. * SDK regen left to the bot — separate PR. CI compile-check on this PR will fail until that lands; tests were validated locally against a 1.4.1 regen. spec-sdk-tests (knock-on): `type` added to 46 update sites; paginator shape fixed (supersedes fix/spec-sdk-tests-paginator-shape); SDK retries enabled; cleanup parallelized; cleanup-error logging hardened. Test status (managed Outpost, local regen'd SDK): 133→152 passing, 14→0 failing. spec-sdk-tests have no CI today; follow-up PR will add one. Co-authored-by: Claude Opus 4.7 (1M context) --- docs/apis/openapi.yaml | 301 +++++++++++++++++- .../tests/destinations/aws-kinesis.test.ts | 8 +- .../tests/destinations/aws-s3.test.ts | 8 +- .../tests/destinations/aws-sqs.test.ts | 8 +- .../destinations/azure-servicebus.test.ts | 8 +- .../tests/destinations/gcp-pubsub.test.ts | 9 +- .../tests/destinations/hookdeck.test.ts | 7 +- .../tests/destinations/rabbitmq.test.ts | 8 +- .../destinations/webhook-merge-patch.test.ts | 26 +- .../tests/destinations/webhook.test.ts | 7 +- spec-sdk-tests/tests/events.test.ts | 8 +- spec-sdk-tests/tests/tenants.test.ts | 4 +- spec-sdk-tests/utils/sdk-client.ts | 10 + 13 files changed, 367 insertions(+), 45 deletions(-) diff --git a/docs/apis/openapi.yaml b/docs/apis/openapi.yaml index 0832890e4..5bfb6fea7 100644 --- a/docs/apis/openapi.yaml +++ b/docs/apis/openapi.yaml @@ -1909,8 +1909,25 @@ components: kafka: "#/components/schemas/DestinationCreateKafka" # Type-Specific Destination Update Schemas (for Request Bodies) + # Type-Specific Partial Schemas for PATCH Request Bodies + # All fields are optional — RFC 7396 JSON merge-patch semantics apply: + # omit a field to leave it unchanged; include a field to update it in place. + WebhookConfigUpdate: + type: object + description: Partial Webhook config for PATCH updates (RFC 7396 merge-patch). + properties: + url: + type: string + format: url + description: The URL to send the webhook events to. + example: "https://example.com/webhooks/user" + custom_headers: + type: string + description: JSON string of custom HTTP headers to include with every webhook request. + example: '{"x-api-key":"secret123","x-tenant-id":"customer-456"}' WebhookCredentialsUpdate: type: object + description: Partial Webhook credentials for PATCH updates (RFC 7396 merge-patch). properties: secret: type: string @@ -1925,17 +1942,207 @@ components: rotate_secret: type: boolean description: Set to true to rotate the secret. The current secret becomes the previous_secret, and a new secret is generated. `previous_secret_invalid_at` defaults to 24h if not provided. + AWSSQSConfigUpdate: + type: object + description: Partial AWS SQS config for PATCH updates (RFC 7396 merge-patch). + properties: + endpoint: + type: string + format: url + description: Optional. Custom AWS endpoint URL (e.g., for LocalStack or specific regions). + example: "https://sqs.us-east-1.amazonaws.com" + queue_url: + type: string + format: url + description: The URL of the SQS queue. + example: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue" + AWSSQSCredentialsUpdate: + type: object + description: Partial AWS SQS credentials for PATCH updates (RFC 7396 merge-patch). + properties: + key: + type: string + description: AWS Access Key ID. + secret: + type: string + description: AWS Secret Access Key. + session: + type: string + description: Optional AWS Session Token (for temporary credentials). + RabbitMQConfigUpdate: + type: object + description: Partial RabbitMQ config for PATCH updates (RFC 7396 merge-patch). + properties: + server_url: + type: string + description: RabbitMQ server address (host:port). + exchange: + type: string + description: The exchange to publish messages to. + tls: + type: string + enum: ["true", "false"] + description: Whether to use TLS connection (amqps). + RabbitMQCredentialsUpdate: + type: object + description: Partial RabbitMQ credentials for PATCH updates (RFC 7396 merge-patch). + properties: + username: + type: string + description: RabbitMQ username. + password: + type: string + description: RabbitMQ password. + HookdeckCredentialsUpdate: + type: object + description: Partial Hookdeck credentials for PATCH updates (RFC 7396 merge-patch). + properties: + token: + type: string + description: Hookdeck authentication token. + AWSKinesisConfigUpdate: + type: object + description: Partial AWS Kinesis config for PATCH updates (RFC 7396 merge-patch). + properties: + stream_name: + type: string + description: The name of the AWS Kinesis stream. + region: + type: string + description: The AWS region where the Kinesis stream is located. + endpoint: + type: string + format: url + description: Optional. Custom AWS endpoint URL (e.g., for LocalStack or VPC endpoints). + partition_key_template: + type: string + description: Optional. JMESPath template to extract the partition key from the event payload. + AWSKinesisCredentialsUpdate: + type: object + description: Partial AWS Kinesis credentials for PATCH updates (RFC 7396 merge-patch). + properties: + key: + type: string + description: AWS Access Key ID. + secret: + type: string + description: AWS Secret Access Key. + session: + type: string + description: Optional AWS Session Token (for temporary credentials). + AzureServiceBusConfigUpdate: + type: object + description: Partial Azure Service Bus config for PATCH updates (RFC 7396 merge-patch). + properties: + name: + type: string + description: The name of the Azure Service Bus queue or topic to publish messages to. + AzureServiceBusCredentialsUpdate: + type: object + description: Partial Azure Service Bus credentials for PATCH updates (RFC 7396 merge-patch). + properties: + connection_string: + type: string + description: The connection string for the Azure Service Bus namespace. + AWSS3ConfigUpdate: + type: object + description: Partial AWS S3 config for PATCH updates (RFC 7396 merge-patch). + properties: + bucket: + type: string + description: The name of your AWS S3 bucket. + region: + type: string + pattern: "^[a-z]{2}-[a-z]+-[0-9]+$" + description: The AWS region where your bucket is located. + key_template: + type: string + description: JMESPath expression for generating S3 object keys. + storage_class: + type: string + description: The storage class for the S3 objects. + AWSS3CredentialsUpdate: + type: object + description: Partial AWS S3 credentials for PATCH updates (RFC 7396 merge-patch). + properties: + key: + type: string + description: AWS Access Key ID. + secret: + type: string + description: AWS Secret Access Key. + session: + type: string + description: Optional AWS Session Token (for temporary credentials). + GCPPubSubConfigUpdate: + type: object + description: Partial GCP Pub/Sub config for PATCH updates (RFC 7396 merge-patch). + properties: + project_id: + type: string + description: The GCP project ID. + topic: + type: string + description: The Pub/Sub topic name. + endpoint: + type: string + description: Optional. Custom endpoint URL (e.g., localhost:8085 for emulator). + GCPPubSubCredentialsUpdate: + type: object + description: Partial GCP Pub/Sub credentials for PATCH updates (RFC 7396 merge-patch). + properties: + service_account_json: + type: string + description: Service account key JSON. The entire JSON key file content as a string. + KafkaConfigUpdate: + type: object + description: Partial Kafka config for PATCH updates (RFC 7396 merge-patch). + properties: + brokers: + type: string + description: Comma-separated list of Kafka broker addresses. + topic: + type: string + description: The Kafka topic to publish messages to. + sasl_mechanism: + type: string + enum: [plain, scram-sha-256, scram-sha-512] + description: SASL authentication mechanism. + tls: + type: string + enum: ["true", "false"] + description: Whether to enable TLS for the connection. + partition_key_template: + type: string + description: Optional JMESPath template to extract the partition key from the event payload. + KafkaCredentialsUpdate: + type: object + description: Partial Kafka credentials for PATCH updates (RFC 7396 merge-patch). + properties: + username: + type: string + description: SASL username for authentication. + password: + type: string + description: SASL password for authentication. + DestinationUpdateWebhook: type: object x-docs-type: "Webhook" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [webhook] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "webhook" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/WebhookConfig" # URL is required here, but PATCH means it's optional in the request + $ref: "#/components/schemas/WebhookConfigUpdate" credentials: $ref: "#/components/schemas/WebhookCredentialsUpdate" delivery_metadata: @@ -1977,15 +2184,21 @@ components: type: object x-docs-type: "AWS SQS" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [aws_sqs] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "aws_sqs" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/AWSSQSConfig" # queue_url is required here, but PATCH means it's optional + $ref: "#/components/schemas/AWSSQSConfigUpdate" credentials: - $ref: "#/components/schemas/AWSSQSCredentials" # key/secret required here, but PATCH means optional + $ref: "#/components/schemas/AWSSQSCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2025,15 +2238,21 @@ components: type: object x-docs-type: "RabbitMQ" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [rabbitmq] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "rabbitmq" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/RabbitMQConfig" # server_url/exchange required here, but PATCH means optional + $ref: "#/components/schemas/RabbitMQConfigUpdate" credentials: - $ref: "#/components/schemas/RabbitMQCredentials" # username/password required here, but PATCH means optional + $ref: "#/components/schemas/RabbitMQCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2073,14 +2292,20 @@ components: type: object x-docs-type: "Hookdeck Event Gateway" # Properties duplicated from DestinationUpdateBase + # Hookdeck has no updatable `config`. + required: [type] properties: + type: + type: string + enum: [hookdeck] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "hookdeck" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" - config: {} # Empty config, cannot be updated credentials: - $ref: "#/components/schemas/HookdeckCredentials" # token required here, but PATCH means optional + $ref: "#/components/schemas/HookdeckCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2120,15 +2345,21 @@ components: type: object x-docs-type: "AWS Kinesis" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [aws_kinesis] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "aws_kinesis" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/AWSKinesisConfig" # stream_name/region required here, but PATCH means optional + $ref: "#/components/schemas/AWSKinesisConfigUpdate" credentials: - $ref: "#/components/schemas/AWSKinesisCredentials" # key/secret required here, but PATCH means optional + $ref: "#/components/schemas/AWSKinesisCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2168,15 +2399,21 @@ components: type: object x-docs-type: "Azure Service Bus" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [azure_servicebus] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "azure_servicebus" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/AzureServiceBusConfig" # name required here, but PATCH means optional + $ref: "#/components/schemas/AzureServiceBusConfigUpdate" credentials: - $ref: "#/components/schemas/AzureServiceBusCredentials" # connection_string required here, but PATCH means optional + $ref: "#/components/schemas/AzureServiceBusCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2217,15 +2454,21 @@ components: type: object x-docs-type: "AWS S3" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [aws_s3] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "aws_s3" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/AWSS3Config" # bucket/region required here, but PATCH means optional + $ref: "#/components/schemas/AWSS3ConfigUpdate" credentials: - $ref: "#/components/schemas/AWSS3Credentials" # key/secret required here, but PATCH means optional + $ref: "#/components/schemas/AWSS3CredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2265,15 +2508,21 @@ components: type: object x-docs-type: "GCP PubSub" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [gcp_pubsub] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "gcp_pubsub" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/GCPPubSubConfig" # project_id/topic required here, but PATCH means optional + $ref: "#/components/schemas/GCPPubSubConfigUpdate" credentials: - $ref: "#/components/schemas/GCPPubSubCredentials" # service_account_json required here, but PATCH means optional + $ref: "#/components/schemas/GCPPubSubCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2313,15 +2562,21 @@ components: type: object x-docs-type: "Apache Kafka" # Properties duplicated from DestinationUpdateBase + required: [type] properties: + type: + type: string + enum: [kafka] + description: Destination type discriminator. Must equal the existing destination's type — type itself cannot be changed via PATCH. + example: "kafka" topics: $ref: "#/components/schemas/Topics" filter: $ref: "#/components/schemas/Filter" config: - $ref: "#/components/schemas/KafkaConfig" # brokers/topic/sasl_mechanism required here, but PATCH means optional + $ref: "#/components/schemas/KafkaConfigUpdate" credentials: - $ref: "#/components/schemas/KafkaCredentials" # username/password required here, but PATCH means optional + $ref: "#/components/schemas/KafkaCredentialsUpdate" delivery_metadata: type: object additionalProperties: @@ -2370,6 +2625,18 @@ components: - $ref: "#/components/schemas/DestinationUpdateGCPPubSub" - $ref: "#/components/schemas/DestinationUpdateRabbitMQ" - $ref: "#/components/schemas/DestinationUpdateKafka" + discriminator: + propertyName: type + mapping: + webhook: "#/components/schemas/DestinationUpdateWebhook" + aws_sqs: "#/components/schemas/DestinationUpdateAWSSQS" + rabbitmq: "#/components/schemas/DestinationUpdateRabbitMQ" + hookdeck: "#/components/schemas/DestinationUpdateHookdeck" + aws_kinesis: "#/components/schemas/DestinationUpdateAWSKinesis" + azure_servicebus: "#/components/schemas/DestinationUpdateAzureServiceBus" + aws_s3: "#/components/schemas/DestinationUpdateAWSS3" + gcp_pubsub: "#/components/schemas/DestinationUpdateGCPPubSub" + kafka: "#/components/schemas/DestinationUpdateKafka" # Event Schemas PublishRequest: type: object diff --git a/spec-sdk-tests/tests/destinations/aws-kinesis.test.ts b/spec-sdk-tests/tests/destinations/aws-kinesis.test.ts index 949d59ff9..07717fca6 100644 --- a/spec-sdk-tests/tests/destinations/aws-kinesis.test.ts +++ b/spec-sdk-tests/tests/destinations/aws-kinesis.test.ts @@ -246,7 +246,7 @@ describe('AWS Kinesis Destinations - Contract Tests (SDK-based validation)', () try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -321,12 +321,13 @@ describe('AWS Kinesis Destinations - Contract Tests (SDK-based validation)', () try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_kinesis', topics: ['user.created', 'user.updated'], }); @@ -341,6 +342,7 @@ describe('AWS Kinesis Destinations - Contract Tests (SDK-based validation)', () // See TEST_STATUS.md for detailed analysis it.skip('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_kinesis', config: { streamName: 'updated-stream', }, @@ -355,6 +357,7 @@ describe('AWS Kinesis Destinations - Contract Tests (SDK-based validation)', () it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_kinesis', credentials: { key: 'AKIAIOSFODNN7UPDATED', secret: 'updatedSecretKey', @@ -368,6 +371,7 @@ describe('AWS Kinesis Destinations - Contract Tests (SDK-based validation)', () let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'aws_kinesis', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/destinations/aws-s3.test.ts b/spec-sdk-tests/tests/destinations/aws-s3.test.ts index f4aa62c2d..64bfe8e6f 100644 --- a/spec-sdk-tests/tests/destinations/aws-s3.test.ts +++ b/spec-sdk-tests/tests/destinations/aws-s3.test.ts @@ -246,7 +246,7 @@ describe('AWS S3 Destinations - Contract Tests (SDK-based validation)', () => { try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -321,12 +321,13 @@ describe('AWS S3 Destinations - Contract Tests (SDK-based validation)', () => { try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_s3', topics: ['user.created', 'user.updated'], }); @@ -338,6 +339,7 @@ describe('AWS S3 Destinations - Contract Tests (SDK-based validation)', () => { it('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_s3', config: { bucket: 'updated-bucket', }, @@ -352,6 +354,7 @@ describe('AWS S3 Destinations - Contract Tests (SDK-based validation)', () => { it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_s3', credentials: { key: 'AKIAIOSFODNN7UPDATED', secret: 'updatedSecretKey', @@ -365,6 +368,7 @@ describe('AWS S3 Destinations - Contract Tests (SDK-based validation)', () => { let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'aws_s3', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/destinations/aws-sqs.test.ts b/spec-sdk-tests/tests/destinations/aws-sqs.test.ts index c2631551e..84769d799 100644 --- a/spec-sdk-tests/tests/destinations/aws-sqs.test.ts +++ b/spec-sdk-tests/tests/destinations/aws-sqs.test.ts @@ -213,7 +213,7 @@ describe('AWS SQS Destinations - Contract Tests (SDK-based validation)', () => { try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -286,12 +286,13 @@ describe('AWS SQS Destinations - Contract Tests (SDK-based validation)', () => { try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_sqs', topics: ['user.created', 'user.updated'], }); @@ -303,6 +304,7 @@ describe('AWS SQS Destinations - Contract Tests (SDK-based validation)', () => { it('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_sqs', config: { queueUrl: 'https://sqs.us-west-2.amazonaws.com/123456789012/updated-queue', }, @@ -319,6 +321,7 @@ describe('AWS SQS Destinations - Contract Tests (SDK-based validation)', () => { it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'aws_sqs', credentials: { key: 'AKIAIOSFODNN7UPDATED', secret: 'updatedSecretKey', @@ -332,6 +335,7 @@ describe('AWS SQS Destinations - Contract Tests (SDK-based validation)', () => { let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'aws_sqs', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/destinations/azure-servicebus.test.ts b/spec-sdk-tests/tests/destinations/azure-servicebus.test.ts index 75b13ab4d..8fb13c675 100644 --- a/spec-sdk-tests/tests/destinations/azure-servicebus.test.ts +++ b/spec-sdk-tests/tests/destinations/azure-servicebus.test.ts @@ -213,7 +213,7 @@ describe('Azure Service Bus Destinations - Contract Tests (SDK-based validation) try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -286,12 +286,13 @@ describe('Azure Service Bus Destinations - Contract Tests (SDK-based validation) try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'azure_servicebus', topics: ['user.created', 'user.updated'], }); @@ -303,6 +304,7 @@ describe('Azure Service Bus Destinations - Contract Tests (SDK-based validation) it('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'azure_servicebus', config: { name: 'updated-queue', }, @@ -317,6 +319,7 @@ describe('Azure Service Bus Destinations - Contract Tests (SDK-based validation) it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'azure_servicebus', credentials: { connectionString: 'Endpoint=sb://updated.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=updatedkey', @@ -330,6 +333,7 @@ describe('Azure Service Bus Destinations - Contract Tests (SDK-based validation) let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'azure_servicebus', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/destinations/gcp-pubsub.test.ts b/spec-sdk-tests/tests/destinations/gcp-pubsub.test.ts index 815d94d1c..1dc1884df 100644 --- a/spec-sdk-tests/tests/destinations/gcp-pubsub.test.ts +++ b/spec-sdk-tests/tests/destinations/gcp-pubsub.test.ts @@ -330,7 +330,7 @@ describe('GCP Pub/Sub Destinations - Contract Tests (SDK-based validation)', () try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -458,12 +458,13 @@ describe('GCP Pub/Sub Destinations - Contract Tests (SDK-based validation)', () try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'gcp_pubsub', topics: ['user.created', 'user.updated'], }); @@ -475,6 +476,7 @@ describe('GCP Pub/Sub Destinations - Contract Tests (SDK-based validation)', () it('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'gcp_pubsub', config: { topic: 'updated-topic-name', }, @@ -489,6 +491,7 @@ describe('GCP Pub/Sub Destinations - Contract Tests (SDK-based validation)', () it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'gcp_pubsub', credentials: { serviceAccountJson: '{"type":"service_account","projectId":"updated"}', }, @@ -501,6 +504,7 @@ describe('GCP Pub/Sub Destinations - Contract Tests (SDK-based validation)', () let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'gcp_pubsub', topics: '*', }); } catch (error: any) { @@ -522,6 +526,7 @@ describe('GCP Pub/Sub Destinations - Contract Tests (SDK-based validation)', () let errorThrown = false; try { await client.updateDestination(destinationId, { + type: 'gcp_pubsub', config: { // Missing required fields }, diff --git a/spec-sdk-tests/tests/destinations/hookdeck.test.ts b/spec-sdk-tests/tests/destinations/hookdeck.test.ts index 38bc6b2c5..9c859dab9 100644 --- a/spec-sdk-tests/tests/destinations/hookdeck.test.ts +++ b/spec-sdk-tests/tests/destinations/hookdeck.test.ts @@ -177,7 +177,7 @@ describe('Hookdeck Destinations - Contract Tests (SDK-based validation)', () => try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -250,12 +250,13 @@ describe('Hookdeck Destinations - Contract Tests (SDK-based validation)', () => try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'hookdeck', topics: ['user.created', 'user.updated'], }); @@ -267,6 +268,7 @@ describe('Hookdeck Destinations - Contract Tests (SDK-based validation)', () => it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'hookdeck', credentials: { token: 'hk_updated_token', }, @@ -279,6 +281,7 @@ describe('Hookdeck Destinations - Contract Tests (SDK-based validation)', () => let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'hookdeck', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/destinations/rabbitmq.test.ts b/spec-sdk-tests/tests/destinations/rabbitmq.test.ts index 88d355a27..e0be73829 100644 --- a/spec-sdk-tests/tests/destinations/rabbitmq.test.ts +++ b/spec-sdk-tests/tests/destinations/rabbitmq.test.ts @@ -246,7 +246,7 @@ describe('RabbitMQ Destinations - Contract Tests (SDK-based validation)', () => try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -321,12 +321,13 @@ describe('RabbitMQ Destinations - Contract Tests (SDK-based validation)', () => try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'rabbitmq', topics: ['user.created', 'user.updated'], }); @@ -338,6 +339,7 @@ describe('RabbitMQ Destinations - Contract Tests (SDK-based validation)', () => it('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'rabbitmq', config: { exchange: 'updated-exchange', }, @@ -352,6 +354,7 @@ describe('RabbitMQ Destinations - Contract Tests (SDK-based validation)', () => it('should update destination credentials', async () => { const updated = await client.updateDestination(destinationId, { + type: 'rabbitmq', credentials: { username: 'newuser', password: 'newpass', @@ -365,6 +368,7 @@ describe('RabbitMQ Destinations - Contract Tests (SDK-based validation)', () => let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'rabbitmq', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/destinations/webhook-merge-patch.test.ts b/spec-sdk-tests/tests/destinations/webhook-merge-patch.test.ts index 40339334f..fbd5a4c39 100644 --- a/spec-sdk-tests/tests/destinations/webhook-merge-patch.test.ts +++ b/spec-sdk-tests/tests/destinations/webhook-merge-patch.test.ts @@ -18,12 +18,11 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { } }); - after(async () => { - for (const id of createdDestinations) { - try { - await client.deleteDestination(id); - } catch {} - } + after(async function () { + this.timeout(30000); + await Promise.all( + createdDestinations.map((id) => client.deleteDestination(id).catch(() => {})) + ); try { await client.deleteTenant(); } catch {} @@ -42,6 +41,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ metadata: { env: 'prod' } }); const updated = await client.updateDestination(id, { + type: 'webhook', metadata: { env: 'prod', team: 'platform' }, }); @@ -52,6 +52,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ metadata: { env: 'prod' } }); const updated = await client.updateDestination(id, { + type: 'webhook', metadata: { env: 'staging' }, }); @@ -62,6 +63,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ metadata: { env: 'prod', region: 'us-east-1' } }); const updated = await client.updateDestination(id, { + type: 'webhook', metadata: { env: 'prod', region: null }, }); @@ -73,6 +75,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ metadata: { env: 'prod' } }); const updated = await client.updateDestination(id, { + type: 'webhook', metadata: null, }); @@ -87,6 +90,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ metadata: { env: 'prod' } }); const updated = await client.updateDestination(id, { + type: 'webhook', metadata: {}, }); @@ -97,6 +101,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ metadata: { env: 'prod' } }); const updated = await client.updateDestination(id, { + type: 'webhook', topics: ['*'], }); @@ -109,6 +114,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { }); const updated = await client.updateDestination(id, { + type: 'webhook', metadata: { keep: 'v', remove: null, update: 'new', add: 'v' }, }); @@ -124,6 +130,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ deliveryMetadata: { source: 'outpost' } }); const updated = await client.updateDestination(id, { + type: 'webhook', deliveryMetadata: { source: 'outpost', version: '1.0' }, }); @@ -136,6 +143,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { }); const updated = await client.updateDestination(id, { + type: 'webhook', deliveryMetadata: { source: 'outpost', version: null }, }); @@ -146,6 +154,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ deliveryMetadata: { source: 'outpost' } }); const updated = await client.updateDestination(id, { + type: 'webhook', deliveryMetadata: null, }); @@ -161,6 +170,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { const id = await createDest({ deliveryMetadata: { source: 'outpost' } }); const updated = await client.updateDestination(id, { + type: 'webhook', deliveryMetadata: {}, }); @@ -177,6 +187,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { }); const updated = await client.updateDestination(id, { + type: 'webhook', filter: { body: { status: 'active' } }, }); @@ -189,6 +200,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { }); const updated = await client.updateDestination(id, { + type: 'webhook', filter: {}, }); @@ -205,6 +217,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { }); const updated = await client.updateDestination(id, { + type: 'webhook', filter: null, }); @@ -221,6 +234,7 @@ describe('Webhook Destinations - Merge-Patch Semantics (RFC 7396)', () => { }); const updated = await client.updateDestination(id, { + type: 'webhook', topics: ['*'], }); diff --git a/spec-sdk-tests/tests/destinations/webhook.test.ts b/spec-sdk-tests/tests/destinations/webhook.test.ts index 2828cc580..0dad9a3d1 100644 --- a/spec-sdk-tests/tests/destinations/webhook.test.ts +++ b/spec-sdk-tests/tests/destinations/webhook.test.ts @@ -180,7 +180,7 @@ describe('Webhook Destinations - Contract Tests (SDK-based validation)', () => { try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); @@ -253,12 +253,13 @@ describe('Webhook Destinations - Contract Tests (SDK-based validation)', () => { try { await client.deleteDestination(destinationId); } catch (error) { - console.warn('Failed to cleanup destination:', error); + console.warn('Failed to cleanup destination:', error instanceof Error ? error.message : String(error)); } }); it('should update destination topics', async () => { const updated = await client.updateDestination(destinationId, { + type: 'webhook', topics: ['user.created', 'user.updated'], }); @@ -270,6 +271,7 @@ describe('Webhook Destinations - Contract Tests (SDK-based validation)', () => { it('should update destination config', async () => { const updated = await client.updateDestination(destinationId, { + type: 'webhook', config: { url: 'https://updated.example.com/webhook', }, @@ -286,6 +288,7 @@ describe('Webhook Destinations - Contract Tests (SDK-based validation)', () => { let errorThrown = false; try { await client.updateDestination('non-existent-id-12345', { + type: 'webhook', topics: ['test'], }); } catch (error: any) { diff --git a/spec-sdk-tests/tests/events.test.ts b/spec-sdk-tests/tests/events.test.ts index 4868fb1d9..b101e3cb9 100644 --- a/spec-sdk-tests/tests/events.test.ts +++ b/spec-sdk-tests/tests/events.test.ts @@ -168,7 +168,7 @@ describe('Events (PR #491)', () => { limit: 5, }); expect(response).to.not.be.undefined; - expect(response?.models).to.be.an('array'); + expect(response?.result?.models).to.be.an('array'); }); it('should list events by tenant', async function () { @@ -191,7 +191,7 @@ describe('Events (PR #491)', () => { const response = await sdk.events.list({ tenantId: client.getTenantId(), }); - return response?.models || []; + return response?.result?.models || []; }, 30000, 5000 @@ -209,7 +209,7 @@ describe('Events (PR #491)', () => { const response = await sdk.events.list({ tenantId: client.getTenantId(), }); - const events = response?.models || []; + const events = response?.result?.models || []; if (events.length === 0) { console.warn('No events found - skipping single event test'); @@ -253,7 +253,7 @@ describe('Events (PR #491)', () => { destinationId: destinationId, eventId, }); - return response?.models ?? []; + return response?.result?.models ?? []; }, 45000, 5000 diff --git a/spec-sdk-tests/tests/tenants.test.ts b/spec-sdk-tests/tests/tenants.test.ts index cb04cef57..c44c2f41d 100644 --- a/spec-sdk-tests/tests/tenants.test.ts +++ b/spec-sdk-tests/tests/tenants.test.ts @@ -19,8 +19,8 @@ describe('Tenants - List with request object', () => { const result = await sdk.tenants.list({ limit: 5 }); expect(result).to.not.be.undefined; - expect(result?.models).to.be.an('array'); - (result?.models ?? []).forEach((t: { id?: string }, i: number) => { + expect(result?.result?.models).to.be.an('array'); + (result?.result?.models ?? []).forEach((t: { id?: string }, i: number) => { expect(t, `tenant[${i}]`).to.be.an('object'); if (t.id != null) expect(t.id, `tenant[${i}].id`).to.be.a('string'); }); diff --git a/spec-sdk-tests/utils/sdk-client.ts b/spec-sdk-tests/utils/sdk-client.ts index 85199a191..0e959f469 100644 --- a/spec-sdk-tests/utils/sdk-client.ts +++ b/spec-sdk-tests/utils/sdk-client.ts @@ -46,6 +46,16 @@ export class SdkClient { serverURL: baseURL, apiKey: config.apiKey || process.env.API_KEY || '', timeoutMs: config.timeout || 10000, + retryConfig: { + strategy: 'backoff', + backoff: { + initialInterval: 250, + maxInterval: 4000, + exponent: 2, + maxElapsedTime: 20000, + }, + retryConnectionErrors: true, + }, }); } From ea0ff25925bb81bb741ff7d415f00030d2b83865 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Fri, 29 May 2026 00:07:27 +0700 Subject: [PATCH 2/4] fix(spec): expose event.data on Attempt when include=event.data (#917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(spec-sdk): fix paginator response shape (response.result.models) Speakeasy CLI 1.741.7 → 1.753.0 (commit 3f311e0d, 2026-03-13) changed the generated TS SDK return type for list operations from the flat paginated body to a PageIterator wrapper: ListEventsResponse now wraps the body in `result`, so callers access `response.result.models` instead of `response.models`. The shape change was a side-effect of the Speakeasy version bump and wasn't surfaced in the SDK regen PR changelog (which only flagged the unrelated `request` query-param restyle). Tests in spec-sdk-tests/tests/{events,tenants}.test.ts still used the pre-bump accessor and failed to compile against the current SDK. Co-Authored-By: Claude Opus 4.7 (1M context) * test(spec-sdk): assert event.data is exposed when include=event.data Co-Authored-By: Claude Opus 4.7 (1M context) * fix(spec): order EventFull before EventSummary in Attempt.event oneOf Speakeasy's TS generator emits zod unions in declaration order. The non-strict EventSummary matched first and silently stripped event.data from the parsed attempt — losing the payload added by include=event.data. Declaring EventFull first lets it match when data is present, with EventSummary as the fallback. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- docs/apis/openapi.yaml | 2 +- spec-sdk-tests/tests/events.test.ts | 34 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/apis/openapi.yaml b/docs/apis/openapi.yaml index 5bfb6fea7..7c78213fe 100644 --- a/docs/apis/openapi.yaml +++ b/docs/apis/openapi.yaml @@ -2802,8 +2802,8 @@ components: event: nullable: true oneOf: - - $ref: "#/components/schemas/EventSummary" - $ref: "#/components/schemas/EventFull" + - $ref: "#/components/schemas/EventSummary" description: The associated event object. Only present when include=event or include=event.data. destination: nullable: true diff --git a/spec-sdk-tests/tests/events.test.ts b/spec-sdk-tests/tests/events.test.ts index b101e3cb9..fe6e08324 100644 --- a/spec-sdk-tests/tests/events.test.ts +++ b/spec-sdk-tests/tests/events.test.ts @@ -265,5 +265,39 @@ describe('Events (PR #491)', () => { expect(attempt.status).to.equal('success', 'Delivery to mock.hookdeck.com should succeed'); console.log(`Event ${eventId} generated attempt; status: ${attempt.status}`); }); + + it('listAttempts with include=event.data should expose event.data on the parsed attempt', async function () { + this.timeout(60000); + + const sdk: Outpost = client.getSDK(); + + const payload = { marker: `include-event-data-${Date.now()}` }; + const publishResponse = await sdk.publish({ + tenantId: client.getTenantId(), + topic: TEST_TOPICS[0], + data: payload, + }); + const eventId = publishResponse.id; + + const attempts = await pollForAttempts( + async () => { + const response = await sdk.destinations.listAttempts({ + tenantId: client.getTenantId(), + destinationId: destinationId, + eventId, + include: ['event.data', 'response_data'], + }); + return response?.result?.models ?? []; + }, + 45000, + 5000 + ); + + expect(attempts.length).to.be.at.least(1, 'Expected at least one attempt'); + const attempt = attempts[0]; + expect(attempt.responseData, 'response_data should be populated when include=response_data').to.exist; + expect(attempt.event, 'event should be populated when include=event.data').to.exist; + expect((attempt.event as { data?: unknown }).data, 'event.data should be populated when include=event.data').to.deep.equal(payload); + }); }); }); From 030a980999e9da3ce88ec3d6b6c2e2395e18b3c7 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Fri, 29 May 2026 00:07:58 +0700 Subject: [PATCH 3/4] chore(sdks): drop x-speakeasy-pagination overlay from all SDK sources (#919) The shared pagination-config-overlay added x-speakeasy-pagination to list endpoints, which Speakeasy then converted into PageIterator wrappers (TS), res.Next() helpers (Go), and res.next() chaining (Python). In TS the wrapper changed the response shape to response.result.models, which is a noisy regression for the common single-page case. In Go and Python the helper saves one line at the cost of hiding which request params get carried across the cursor call. Drop the overlay from all three sources for symmetric, explicit DX: callers paginate manually using res.pagination.next on the flat response. SDK regeneration is intentionally left out of this commit so the workflow change can be reviewed in isolation. Co-authored-by: Claude Opus 4.7 (1M context) --- .speakeasy/workflow.yaml | 3 - sdks/schemas/pagination-config-overlay.yaml | 85 --------------------- 2 files changed, 88 deletions(-) delete mode 100644 sdks/schemas/pagination-config-overlay.yaml diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index 574cb3e3b..edb8a2bdc 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -8,7 +8,6 @@ sources: - location: ./sdks/schemas/speakeasy-modifications-overlay.yaml - location: ./sdks/schemas/security-collapse-overlay.yaml - location: ./sdks/schemas/error-types.yaml - - location: ./sdks/schemas/pagination-config-overlay.yaml registry: location: registry.speakeasyapi.dev/hookdeck-dev/outpost/outpost-api Outpost API (Go): @@ -18,7 +17,6 @@ sources: - location: ./sdks/schemas/speakeasy-modifications-overlay.yaml - location: ./sdks/schemas/security-collapse-overlay.yaml - location: ./sdks/schemas/error-types.yaml - - location: ./sdks/schemas/pagination-config-overlay.yaml - location: ./sdks/schemas/go-array-params-overlay.yaml registry: location: registry.speakeasyapi.dev/hookdeck-dev/outpost/outpost-api @@ -29,7 +27,6 @@ sources: - location: ./sdks/schemas/speakeasy-modifications-overlay.yaml - location: ./sdks/schemas/security-collapse-overlay.yaml - location: ./sdks/schemas/error-types.yaml - - location: ./sdks/schemas/pagination-config-overlay.yaml - location: ./sdks/schemas/python-pagination-fixes-overlay.yaml registry: location: registry.speakeasyapi.dev/hookdeck-dev/outpost/outpost-api diff --git a/sdks/schemas/pagination-config-overlay.yaml b/sdks/schemas/pagination-config-overlay.yaml deleted file mode 100644 index ea0d59e48..000000000 --- a/sdks/schemas/pagination-config-overlay.yaml +++ /dev/null @@ -1,85 +0,0 @@ -overlay: 1.0.0 -x-speakeasy-jsonpath: rfc9535 -info: - title: Pagination Config - version: 0.0.1 - x-speakeasy-metadata: - type: pagination-config - description: "Configures cursor-based pagination for all list endpoints (shared across all SDKs)" -extends: ../../docs/apis/openapi.yaml -actions: - # Pagination config: GET /tenants - - target: $["paths"]["/tenants"]["get"] - update: - x-speakeasy-pagination: - type: cursor - inputs: - - name: next - in: parameters - type: cursor - - name: limit - in: parameters - type: limit - outputs: - nextCursor: $.pagination.next - results: $.models - x-speakeasy-metadata: - type: pagination-config - description: "Configure cursor-based pagination for tenants listing" - - # Pagination config: GET /events - - target: $["paths"]["/events"]["get"] - update: - x-speakeasy-pagination: - type: cursor - inputs: - - name: next - in: parameters - type: cursor - - name: limit - in: parameters - type: limit - outputs: - nextCursor: $.pagination.next - results: $.models - x-speakeasy-metadata: - type: pagination-config - description: "Configure cursor-based pagination for events listing" - - # Pagination config: GET /attempts - - target: $["paths"]["/attempts"]["get"] - update: - x-speakeasy-pagination: - type: cursor - inputs: - - name: next - in: parameters - type: cursor - - name: limit - in: parameters - type: limit - outputs: - nextCursor: $.pagination.next - results: $.models - x-speakeasy-metadata: - type: pagination-config - description: "Configure cursor-based pagination for attempts listing" - - # Pagination config: GET /tenants/.../destinations/.../attempts - - target: $["paths"]["/tenants/{tenant_id}/destinations/{destination_id}/attempts"]["get"] - update: - x-speakeasy-pagination: - type: cursor - inputs: - - name: next - in: parameters - type: cursor - - name: limit - in: parameters - type: limit - outputs: - nextCursor: $.pagination.next - results: $.models - x-speakeasy-metadata: - type: pagination-config - description: "Configure cursor-based pagination for destination attempts listing" From 4675bc9c1957d3ae3e01ca0bdb4f7f7f01cc2ecd Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 28 May 2026 18:25:47 +0100 Subject: [PATCH 4/4] test(spec-sdk): revert paginator wrapper accessors after pagination overlay drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trigger: #919 (merged into umbrella) drops the x-speakeasy-pagination overlay, so the TS SDK no longer wraps list responses in PageIterator. events.list()/tenants.list() return EventPaginatedResult/TenantPaginatedResult directly — accessor is `response.models`, not `response.result.models`. Tests in #920 had been adapted to the wrapper shape; this realigns them back to the flat shape post-regen. Local run after fresh TS SDK regen: 153 passing / 0 failing / 15 pending. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec-sdk-tests/tests/events.test.ts | 10 +++++----- spec-sdk-tests/tests/tenants.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec-sdk-tests/tests/events.test.ts b/spec-sdk-tests/tests/events.test.ts index fe6e08324..8113b68b2 100644 --- a/spec-sdk-tests/tests/events.test.ts +++ b/spec-sdk-tests/tests/events.test.ts @@ -168,7 +168,7 @@ describe('Events (PR #491)', () => { limit: 5, }); expect(response).to.not.be.undefined; - expect(response?.result?.models).to.be.an('array'); + expect(response?.models).to.be.an('array'); }); it('should list events by tenant', async function () { @@ -191,7 +191,7 @@ describe('Events (PR #491)', () => { const response = await sdk.events.list({ tenantId: client.getTenantId(), }); - return response?.result?.models || []; + return response?.models || []; }, 30000, 5000 @@ -209,7 +209,7 @@ describe('Events (PR #491)', () => { const response = await sdk.events.list({ tenantId: client.getTenantId(), }); - const events = response?.result?.models || []; + const events = response?.models || []; if (events.length === 0) { console.warn('No events found - skipping single event test'); @@ -253,7 +253,7 @@ describe('Events (PR #491)', () => { destinationId: destinationId, eventId, }); - return response?.result?.models ?? []; + return response?.models ?? []; }, 45000, 5000 @@ -287,7 +287,7 @@ describe('Events (PR #491)', () => { eventId, include: ['event.data', 'response_data'], }); - return response?.result?.models ?? []; + return response?.models ?? []; }, 45000, 5000 diff --git a/spec-sdk-tests/tests/tenants.test.ts b/spec-sdk-tests/tests/tenants.test.ts index c44c2f41d..cb04cef57 100644 --- a/spec-sdk-tests/tests/tenants.test.ts +++ b/spec-sdk-tests/tests/tenants.test.ts @@ -19,8 +19,8 @@ describe('Tenants - List with request object', () => { const result = await sdk.tenants.list({ limit: 5 }); expect(result).to.not.be.undefined; - expect(result?.result?.models).to.be.an('array'); - (result?.result?.models ?? []).forEach((t: { id?: string }, i: number) => { + expect(result?.models).to.be.an('array'); + (result?.models ?? []).forEach((t: { id?: string }, i: number) => { expect(t, `tenant[${i}]`).to.be.an('object'); if (t.id != null) expect(t.id, `tenant[${i}].id`).to.be.a('string'); });