diff --git a/.changeset/add-sleep-in-loop-e2e-test.md b/.changeset/add-sleep-in-loop-e2e-test.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/add-sleep-in-loop-e2e-test.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index d2206cc324..f67e3335f3 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -1860,6 +1860,24 @@ describe('e2e', () => { } ); + test( + 'sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration', + { timeout: 60_000 }, + async () => { + const run = await start(await e2e('sleepInLoopWorkflow'), []); + const returnValue = await run.returnValue; + + // 3 iterations with 3s sleep between each pair = 2 sleeps, ~6s total + // Use 2.5s threshold per sleep to allow jitter + expect(returnValue.timestamps).toHaveLength(3); + const delta1 = returnValue.timestamps[1] - returnValue.timestamps[0]; + const delta2 = returnValue.timestamps[2] - returnValue.timestamps[1]; + expect(delta1).toBeGreaterThan(2_500); + expect(delta2).toBeGreaterThan(2_500); + expect(returnValue.totalElapsed).toBeGreaterThan(5_000); + } + ); + test( 'sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)', { timeout: 60_000 }, diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index e1d1fc9b32..b20a32f0ef 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -1380,6 +1380,42 @@ async function addNumbers(a: number, b: number) { return a + b; } +/** + * Validates that sleep() inside a loop with step calls actually delays + * execution on each iteration (i.e., sleeps are honored on replay, not skipped). + * + * Reproduces the scenario from a user report claiming that: + * for (let i = 0; i < N; i++) { + * await someStep(); + * await sleep(duration); + * } + * ...fires all iterations instantly with zero delay. + */ +async function noopStep(iteration: number) { + 'use step'; + return { iteration, ts: Date.now() }; +} + +export async function sleepInLoopWorkflow() { + 'use workflow'; + const iterations = 3; + const sleepMs = 3_000; // 3s between iterations (2 sleeps total) + const timestamps: number[] = []; + + for (let i = 0; i < iterations; i++) { + const result = await noopStep(i); + timestamps.push(result.ts); + if (i < iterations - 1) { + await sleep(sleepMs); + } + } + + const totalElapsed = timestamps[timestamps.length - 1] - timestamps[0]; + return { timestamps, totalElapsed }; +} + +////////////////////////////////////////////////////////// + /** * Control workflow: sleep + sequential steps (no hooks). * Proves that void sleep().then() does NOT interfere with sequential steps