From 67fa0cc93ce33caa0627c51ada90155592fe71d7 Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 9 Jul 2026 10:58:07 +0200 Subject: [PATCH] Fix flaky webhook delivery integration test (fixed sleep -> poll) (#22699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Context Part of a CI flakiness sweep. `webhooks.integration-spec.ts` › "should deliver webhook successfully when safe mode is disabled" intermittently fails with `expect(receiver.receivedPayloads.length).toBe(1) ... Received: 0` on unrelated PRs (example: run 28959576750, shard 3). # Root cause The test asserted delivery after a fixed 100ms sleep. Delivery actually crosses: a fire-and-forget `EventEmitter2.emit` (the GraphQL response returns before the job is even enqueued) → BullMQ hop 1 (`CallWebhookJobsJob`, which also recomputes the just-invalidated `flatWebhookMaps` cache) → BullMQ hop 2 (`CallWebhookJob`) → HTTP POST to the in-test receiver. Two Redis round trips plus a cache rebuild routinely exceed 100ms on loaded CI runners. The global `waitForAllJobsToFinish` only runs in `afterEach`, after the assertion. # Fix - Poll the receiver with the existing `expectEventually` helper (30s deadline, 100ms interval) instead of sleeping, and give the test an explicit 60s timeout (suite default is 20s). Worst case the test fails slower; it can no longer fail while delivery is merely in flight. - Bonus bug found during adversarial review of this fix: the `finally` cleanup deleted config key `HTTP_TOOL_SAFE_MODE_ENABLED` while the test creates `OUTBOUND_HTTP_SAFE_MODE_ENABLED`, silently leaving outbound safe mode disabled in the DB for every suite that runs after this one. Fixed the key. Duplicate-delivery risk was checked: `CallWebhookJob.handle` never throws (errors swallowed), so `retryLimit: 3` can't produce a second payload that would break `toBe(1)`. Test-only change, 1 file, +15/-9. --- _Generated by [Claude Code](https://claude.ai/code/session_01AtD2wWm3EthV6t3Hs31QyB)_ Review in cubic --- .../developers/webhooks.integration-spec.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/twenty-server/test/integration/metadata/suites/developers/webhooks.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/developers/webhooks.integration-spec.ts index fe731d183c..b6ccfa7fb6 100644 --- a/packages/twenty-server/test/integration/metadata/suites/developers/webhooks.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/developers/webhooks.integration-spec.ts @@ -9,6 +9,7 @@ import { updateWebhook, } from 'test/integration/metadata/suites/utils/webhook-test.util'; import { makeAdminPanelAPIRequest } from 'test/integration/twenty-config/utils/make-admin-panel-api-request.util'; +import { expectEventually } from 'test/integration/utils/expect-eventually.util'; import { v4 as uuidv4 } from 'uuid'; import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input'; @@ -329,21 +330,26 @@ describe('webhooksResolver (e2e)', () => { expect(createPersonResponse.body.errors).toBeUndefined(); createdPersonId = createPersonResponse.body.data.createPerson.id; - await new Promise((resolve) => setTimeout(resolve, 100)); - - expect(receiver.receivedPayloads.length).toBe(1); - expect(receiver.receivedPayloads[0]).toMatchObject({ - targetUrl: `http://127.0.0.1:${WEBHOOK_RECEIVER_PORT}/webhook`, - eventName: 'person.created', - }); + // Delivery crosses two BullMQ hops behind a fire-and-forget event + // emit, so poll the receiver instead of guessing at the latency. + await expectEventually( + () => { + expect(receiver.receivedPayloads.length).toBe(1); + expect(receiver.receivedPayloads[0]).toMatchObject({ + targetUrl: `http://127.0.0.1:${WEBHOOK_RECEIVER_PORT}/webhook`, + eventName: 'person.created', + }); + }, + { timeoutMs: 30_000, intervalMs: 100 }, + ); } finally { await receiver.close(); await makeAdminPanelAPIRequest({ query: DELETE_CONFIG_VARIABLE_MUTATION, - variables: { key: 'HTTP_TOOL_SAFE_MODE_ENABLED' }, + variables: { key: 'OUTBOUND_HTTP_SAFE_MODE_ENABLED' }, }).catch(() => {}); jest.useFakeTimers(); } - }); + }, 60_000); }); });