|
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | 4 | import { EventEmitter } from 'node:events' |
5 | | -import { coldConnectionBudgetMs } from '@sim/utils/retry' |
6 | 5 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
7 | 6 |
|
8 | 7 | const { connection, mockRedisUrl, mockSubscribe, mockUnsubscribe } = vi.hoisted(() => ({ |
@@ -41,26 +40,30 @@ vi.mock('@/lib/core/config/redis', () => ({ |
41 | 40 | if (mockRedisUrl.error) throw mockRedisUrl.error |
42 | 41 | return mockRedisUrl.value |
43 | 42 | }, |
44 | | - // Realistic defaults: the readiness budget has a connect-deadline term. |
45 | | - getRedisConnectionDefaults: () => ({ connectTimeout: 10_000 }), |
| 43 | + // Realistic defaults: the readiness budget derives from these. The literals |
| 44 | + // are inherent to mocking the module that exports the real constants. |
| 45 | + getRedisConnectionDefaults: () => ({ connectTimeout: 10_000, disconnectTimeout: 2_000 }), |
46 | 46 | })) |
47 | 47 |
|
| 48 | +import { coldConnectionBudgetMs } from '@/lib/core/config/redis-budget' |
48 | 49 | import { |
| 50 | + connectExecutionSignalHub, |
49 | 51 | getExecutionSignalHub, |
50 | 52 | publishLocalExecutionSignal, |
51 | | - warmExecutionSignalHub, |
52 | 53 | } from '@/lib/execution/execution-signal' |
53 | 54 |
|
54 | 55 | /** The readiness budget production derives from the options the subscriber was built with. */ |
55 | 56 | function readyBudgetMs(): number { |
56 | 57 | const options = connection.options as { |
57 | 58 | connectTimeout: number |
58 | 59 | commandTimeout: number |
| 60 | + disconnectTimeout: number |
59 | 61 | retryStrategy: (attempt: number) => number |
60 | 62 | } |
61 | 63 | return coldConnectionBudgetMs({ |
62 | 64 | connectTimeoutMs: options.connectTimeout, |
63 | 65 | commandTimeoutMs: options.commandTimeout, |
| 66 | + disconnectTimeoutMs: options.disconnectTimeout, |
64 | 67 | reconnectDelayMs: options.retryStrategy(1), |
65 | 68 | }) |
66 | 69 | } |
@@ -401,27 +404,33 @@ describe('ExecutionSignalHub', () => { |
401 | 404 | } |
402 | 405 | }) |
403 | 406 |
|
404 | | - it('gives a subscribe that joins an in-flight warm-up its own full budget', async () => { |
| 407 | + it('gives a subscribe that joins another in-flight wait its own full budget', async () => { |
405 | 408 | vi.useFakeTimers() |
406 | 409 | try { |
407 | 410 | connection.status = 'connect' |
408 | | - const warm = warmExecutionSignalHub() |
409 | | - // Late joiner: the warm-up has almost spent its budget when this subscribe begins. |
| 411 | + const hub = getExecutionSignalHub() |
| 412 | + const first = hub.subscribe('execution-first', vi.fn()) |
| 413 | + const firstSettled = vi.fn() |
| 414 | + void first.then(firstSettled, firstSettled) |
| 415 | + // Late joiner: the first wait has almost spent its budget when this one begins. |
410 | 416 | await vi.advanceTimersByTimeAsync(readyBudgetMs() - 1000) |
411 | | - const subscription = getExecutionSignalHub().subscribe('execution-1', vi.fn()) |
412 | | - const settled = vi.fn() |
413 | | - void subscription.then(settled, settled) |
| 417 | + const second = hub.subscribe('execution-second', vi.fn()) |
| 418 | + const secondSettled = vi.fn() |
| 419 | + void second.then(secondSettled, secondSettled) |
414 | 420 |
|
415 | 421 | await vi.advanceTimersByTimeAsync(1000) |
416 | | - await expect(warm).resolves.toBe(false) |
417 | | - // The warm-up's deadline was its own; the subscribe is still waiting. |
418 | | - expect(settled).not.toHaveBeenCalled() |
| 422 | + await expect(first).rejects.toThrow('Timed out waiting for Redis subscriber readiness') |
| 423 | + // The first deadline was its own; the second is still waiting on the shared signal. |
| 424 | + expect(secondSettled).not.toHaveBeenCalled() |
419 | 425 | expect(connection.client?.listenerCount('ready')).toBe(2) |
420 | 426 |
|
421 | 427 | connection.status = 'ready' |
422 | 428 | connection.client?.emit('ready') |
423 | | - await subscription |
424 | | - expect(mockSubscribe).toHaveBeenCalledOnce() |
| 429 | + await second |
| 430 | + expect(mockSubscribe).toHaveBeenCalledWith( |
| 431 | + 'execution:signal:execution-second', |
| 432 | + 'execution:cancel' |
| 433 | + ) |
425 | 434 | } finally { |
426 | 435 | vi.useRealTimers() |
427 | 436 | } |
@@ -472,42 +481,21 @@ describe('ExecutionSignalHub', () => { |
472 | 481 | } |
473 | 482 | }) |
474 | 483 |
|
475 | | - it('warms to true once the subscriber becomes ready', async () => { |
| 484 | + it('begins connecting when asked to connect ahead of a subscription', () => { |
476 | 485 | connection.status = 'connecting' |
477 | | - const warm = warmExecutionSignalHub() |
478 | | - |
479 | | - connection.status = 'ready' |
480 | | - connection.client?.emit('ready') |
481 | | - |
482 | | - await expect(warm).resolves.toBe(true) |
483 | | - }) |
484 | | - |
485 | | - it('warms to false, not a rejection, when readiness never arrives', async () => { |
486 | | - vi.useFakeTimers() |
487 | | - try { |
488 | | - connection.status = 'connect' |
489 | | - const warm = warmExecutionSignalHub() |
490 | 486 |
|
491 | | - await vi.advanceTimersByTimeAsync(readyBudgetMs()) |
| 487 | + connectExecutionSignalHub() |
492 | 488 |
|
493 | | - await expect(warm).resolves.toBe(false) |
494 | | - expect(vi.getTimerCount()).toBe(0) |
495 | | - } finally { |
496 | | - vi.useRealTimers() |
497 | | - } |
| 489 | + // Constructing the hub is what dials; the client exists before any subscribe. |
| 490 | + expect(connection.client).toBeDefined() |
| 491 | + expect(mockSubscribe).not.toHaveBeenCalled() |
498 | 492 | }) |
499 | 493 |
|
500 | | - it('reports not-warm instead of throwing when Redis is misconfigured', async () => { |
501 | | - // Start-up hooks call this; a throw there would fail the run attempt. |
| 494 | + it('does not throw when Redis is misconfigured, leaving that to the first subscriber', () => { |
502 | 495 | mockRedisUrl.error = new Error('Cache capability selected Redis but REDIS_URL is missing') |
503 | 496 |
|
504 | | - await expect(warmExecutionSignalHub()).resolves.toBe(false) |
505 | | - }) |
506 | | - |
507 | | - it('is trivially warm when signals are process-local', async () => { |
508 | | - mockRedisUrl.value = undefined |
509 | | - |
510 | | - await expect(warmExecutionSignalHub()).resolves.toBe(true) |
| 497 | + expect(() => connectExecutionSignalHub()).not.toThrow() |
| 498 | + expect(connection.client).toBeUndefined() |
511 | 499 | }) |
512 | 500 |
|
513 | 501 | it('uses a process-local signal hub when Redis is not configured', async () => { |
|
0 commit comments