Fix flaky webhook delivery integration test (fixed sleep -> poll) (#22699)

# 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)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22699?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-07-09 10:58:07 +02:00
committed by GitHub
parent e38d587602
commit 67fa0cc93c
@@ -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);
});
});