From 0a614dfbc9a0b86570378151ca39584ce02e70a6 Mon Sep 17 00:00:00 2001 From: Durga Date: Mon, 13 Apr 2026 12:52:12 -0400 Subject: [PATCH] Fix spread syntax runtime error on non-iterable values Replace unsafe non-null assertions and truthy guards with Array.isArray checks before spreading webhook subscription properties in reduceWebhooks. Use flatMap with nullish coalescing in esbuild watcher deleteContexts. Remove fragile Map.get()! in app-logs sources. Add tests for reduceWebhooks property-branch merging, covering the case where the existing subscription's property array is initially undefined. Co-Authored-By: Claude Opus 4.6 --- .../transform/app_config_webhook.test.ts | 30 +++++++++++++++++++ .../transform/app_config_webhook.ts | 10 +++---- .../app/src/cli/services/app-logs/sources.ts | 3 +- .../dev/app-events/app-watcher-esbuild.ts | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.test.ts b/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.test.ts index 93ef8ee8eba..660a65fdaa5 100644 --- a/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.test.ts +++ b/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.test.ts @@ -282,4 +282,34 @@ describe('reduceWebhooks', () => { }, ]) }) + + test('with property set, merges compliance_topics for subscriptions with the same uri', () => { + // Given + const subscriptions = [ + {uri: 'https://example.com', compliance_topics: ['customers/redact']}, + {uri: 'https://example.com', compliance_topics: ['customers/data_request']}, + ] + + // When + const result = reduceWebhooks(subscriptions, 'compliance_topics') + + // Then + expect(result).toEqual([ + {uri: 'https://example.com', compliance_topics: ['customers/redact', 'customers/data_request']}, + ]) + }) + + test('with property set, initializes the property array when existing subscription has none', () => { + // Given + const subscriptions = [ + {uri: 'https://example.com'}, + {uri: 'https://example.com', compliance_topics: ['customers/redact']}, + ] + + // When + const result = reduceWebhooks(subscriptions, 'compliance_topics') + + // Then + expect(result).toEqual([{uri: 'https://example.com', compliance_topics: ['customers/redact']}]) + }) }) diff --git a/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.ts b/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.ts index 45a4b6ba079..b34112362fa 100644 --- a/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.ts +++ b/packages/app/src/cli/models/extensions/specifications/transform/app_config_webhook.ts @@ -80,15 +80,15 @@ export function reduceWebhooks( return subscriptions.reduce((accumulator, subscription) => { const existingSubscription = findSubscription(accumulator, subscription) if (existingSubscription) { - if (property && subscription?.[property]?.length) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - existingSubscription[property]?.push(...subscription[property]!) + if (property && Array.isArray(subscription?.[property]) && subscription[property].length) { + existingSubscription[property] ??= [] + existingSubscription[property].push(...subscription[property]) } else { - if (subscription.topics) { + if (Array.isArray(subscription.topics)) { existingSubscription.topics ??= [] existingSubscription.topics.push(...subscription.topics) } - if (subscription.compliance_topics) { + if (Array.isArray(subscription.compliance_topics)) { existingSubscription.compliance_topics ??= [] existingSubscription.compliance_topics.push(...subscription.compliance_topics) } diff --git a/packages/app/src/cli/services/app-logs/sources.ts b/packages/app/src/cli/services/app-logs/sources.ts index 7eb85f64540..3bf3c19ba34 100644 --- a/packages/app/src/cli/services/app-logs/sources.ts +++ b/packages/app/src/cli/services/app-logs/sources.ts @@ -15,7 +15,8 @@ export function sources(app: AppInterface) { sourcesByNamespace.set(sourceNamespace, []) } - sourcesByNamespace.set(sourceNamespace, [...sourcesByNamespace.get(sourceNamespace)!, source]) + const existing = sourcesByNamespace.get(sourceNamespace) ?? [] + sourcesByNamespace.set(sourceNamespace, [...existing, source]) } }) diff --git a/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts b/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts index edf39052082..afceb30f4c8 100644 --- a/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts +++ b/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts @@ -116,7 +116,7 @@ export class ESBuildContextManager { } async deleteContexts(extensions: ExtensionInstance[]) { - const promises = extensions.map((ext) => this.contexts[ext.uid]?.map((context) => context.dispose())).flat() + const promises = extensions.flatMap((ext) => this.contexts[ext.uid]?.map((context) => context.dispose()) ?? []) await Promise.all(promises) extensions.forEach((ext) => { const {[ext.uid]: _, ...rest} = this.contexts