diff --git a/.changeset/e2e-test-budgets.md b/.changeset/e2e-test-budgets.md new file mode 100644 index 0000000000..6b3f86684e --- /dev/null +++ b/.changeset/e2e-test-budgets.md @@ -0,0 +1,5 @@ +--- +'@workflow/core': patch +--- + +Fix flaky timing-sensitive tests: stall-proof budgets for the events-consumer deferred-check tests and a sibling-matched budget for the TTL-expiration abort e2e test. diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 18b0650099..9600a99e6b 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -4188,7 +4188,11 @@ describe('e2e', () => { test( 'distributedAbortController - TTL expiration triggers signal', - { timeout: 30_000 }, + // Same budget as the sibling distributedAbortController tests: the 3s + // TTL is trivial, but cold starts plus queue backlog on a fresh prod + // deployment routinely pushed run start + stream delivery past the + // tighter 30s this test used to get. + { timeout: 60_000 }, async () => { const controllerId = `test-expire-${Math.random().toString(36).slice(2)}`; diff --git a/packages/core/src/events-consumer.test.ts b/packages/core/src/events-consumer.test.ts index 7134d268bd..943fa2ef84 100644 --- a/packages/core/src/events-consumer.test.ts +++ b/packages/core/src/events-consumer.test.ts @@ -736,7 +736,12 @@ describe('EventsConsumer', () => { }); }); - describe('duplicate event classes', () => { + // The deferred check reaches its outcome through a multi-stage timer chain + // (promise queue → setTimeout(0) → idle poll → delay timer), and loaded CI + // runners with coarse timers — Windows especially — can starve that chain + // for whole seconds. The polls below return as soon as their assertions + // hold, so a generous test budget costs healthy runs nothing. + describe('duplicate event classes', { timeout: 30_000 }, () => { // Nothing here waits on the window for its result — a duplicate is stepped // over in the pass that offers it — so run at the shortest legal delay and // let the assertions that a check did NOT fire be cheap. @@ -761,8 +766,11 @@ describe('EventsConsumer', () => { // and the negatives alongside them are then evaluated at the moment the // check is known to have fired, which is what the assertions mean. function afterDeferredCheck(assertions: () => void): Promise { + // The timeout bounds a stalled runner, not the expected path: a healthy + // run satisfies the assertions within a few windows. 2s (the previous + // bound) was regularly starved through on Windows CI runners. return vi.waitFor(assertions, { - timeout: MIN_DEFERRED_CHECK_DELAY_MS * 200, + timeout: 15_000, interval: MIN_DEFERRED_CHECK_DELAY_MS, }); } @@ -961,7 +969,7 @@ describe('EventsConsumer', () => { expect(onDuplicateEvent).not.toHaveBeenCalled(); }); - await vi.waitFor(() => { + await afterDeferredCheck(() => { expect(onUnconsumedEvent).toHaveBeenCalledWith(events[3]); }); }); @@ -989,7 +997,7 @@ describe('EventsConsumer', () => { expect(onDuplicateEvent).not.toHaveBeenCalled(); }); - await vi.waitFor(() => { + await afterDeferredCheck(() => { expect(onUnconsumedEvent).toHaveBeenCalledWith(events[2]); }); });