|
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | 4 | import { redisConfigMockFns, resetRedisConfigMock } from '@sim/testing' |
| 5 | +import { sleep } from '@sim/utils/helpers' |
5 | 6 | import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' |
6 | 7 | import type { ExecutionEventEntry } from '@/lib/execution/event-buffer' |
7 | 8 | import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events' |
@@ -490,13 +491,49 @@ describe('execution event buffer', () => { |
490 | 491 | // Let writeTerminal's queued body actually enqueue its entry before the |
491 | 492 | // in-flight flush resolves — otherwise the scheduled loop finds nothing left |
492 | 493 | // to drain and the race under test never forms. |
493 | | - await new Promise((resolve) => setTimeout(resolve, 5)) |
| 494 | + await sleep(5) |
494 | 495 | releaseFirstFlush?.() |
495 | 496 | await terminalWrite |
496 | 497 |
|
497 | 498 | expect(observedTerminalStatuses).toContain('complete') |
498 | 499 | }) |
499 | 500 |
|
| 501 | + /** |
| 502 | + * The backlog ahead of a terminal event can exceed the budget while the |
| 503 | + * terminal event itself still fits. Discarding it alongside the backlog would |
| 504 | + * leave readers without the final status for a run that could have published |
| 505 | + * one. |
| 506 | + */ |
| 507 | + it('still publishes the terminal event when the backlog ahead of it is dropped', async () => { |
| 508 | + mockRedis.incrby.mockResolvedValue(100) |
| 509 | + const observedTerminalStatuses: string[] = [] |
| 510 | + mockRedis.eval.mockImplementation(async (script: string, ...args: unknown[]) => { |
| 511 | + if (!isFlushScript(script)) return [1, 'ok', 0, 0] |
| 512 | + const { terminalStatus, zaddArgs } = parseFlushEvalArgs(args) |
| 513 | + // Reject anything but a lone entry, standing in for a budget with only |
| 514 | + // enough headroom left for one small write. |
| 515 | + if (zaddArgs.length > 2) return [0, 'execution_redis_bytes', 64 * 1024 * 1024] |
| 516 | + observedTerminalStatuses.push(terminalStatus) |
| 517 | + for (let i = 0; i < zaddArgs.length; i += 2) { |
| 518 | + persistedEntries.push(JSON.parse(zaddArgs[i + 1] as string) as ExecutionEventEntry) |
| 519 | + } |
| 520 | + return [1, 1, 0] |
| 521 | + }) |
| 522 | + |
| 523 | + const writer = createExecutionEventWriter('exec-1') |
| 524 | + for (let i = 0; i < 5; i++) { |
| 525 | + await writer.write(makeEvent(`block-${i}`)).catch(() => {}) |
| 526 | + } |
| 527 | + |
| 528 | + await expect(writer.writeTerminal(makeEvent('terminal'), 'complete')).resolves.toMatchObject({ |
| 529 | + executionId: 'exec-1', |
| 530 | + }) |
| 531 | + expect(observedTerminalStatuses).toContain('complete') |
| 532 | + expect( |
| 533 | + persistedEntries.map((entry) => (entry.event.data as { blockId: string }).blockId) |
| 534 | + ).toContain('terminal') |
| 535 | + }) |
| 536 | + |
500 | 537 | /** |
501 | 538 | * A terminal publish that threw must not be resurrected. Leaving the status |
502 | 539 | * armed would let the next flush stamp the stream terminal for an event that |
@@ -571,7 +608,7 @@ describe('execution event buffer', () => { |
571 | 608 | const writer = createExecutionEventWriter('exec-1') |
572 | 609 | await writer.write(makeEvent('a')) |
573 | 610 |
|
574 | | - await new Promise((resolve) => setTimeout(resolve, 60)) |
| 611 | + await sleep(60) |
575 | 612 |
|
576 | 613 | await expect(writer.flush()).resolves.toBeUndefined() |
577 | 614 | }) |
|
0 commit comments