@@ -124,29 +124,111 @@ describe('ExecutionSignalHub', () => {
124124 expect ( mockSubscribe ) . toHaveBeenCalledWith ( 'execution:signal:execution-new' , 'execution:cancel' )
125125 } )
126126
127- it . each ( [ 'error' , 'end' ] ) (
128- 'rejects readiness waiters on %s and allows a fresh attempt' ,
129- async ( event ) => {
130- connection . status = 'connect'
131- const hub = getExecutionSignalHub ( )
132- const handler = vi . fn ( )
133- const subscription = hub . subscribe ( 'execution-1' , handler )
134- const rejected = expect ( subscription ) . rejects . toThrow ( 'Execution signal subscription failed:' )
127+ it ( 'keeps waiting through recoverable connection errors' , async ( ) => {
128+ connection . status = 'connecting'
129+ const hub = getExecutionSignalHub ( )
130+ const handler = vi . fn ( )
131+ const subscription = hub . subscribe ( 'execution-1' , handler )
132+ const settled = vi . fn ( )
133+ void subscription . then ( settled , settled )
135134
136- connection . status = 'end'
137- connection . client ?. emit ( event , new Error ( 'connection failed' ) )
138- await rejected
139- expect ( mockSubscribe ) . not . toHaveBeenCalled ( )
140- expect ( connection . client ?. listenerCount ( 'ready' ) ) . toBe ( 1 )
141- expect ( connection . client ?. listenerCount ( 'error' ) ) . toBe ( 1 )
142- expect ( connection . client ?. listenerCount ( 'end' ) ) . toBe ( 0 )
135+ connection . client ?. emit ( 'error' , new Error ( 'ECONNREFUSED' ) )
136+ connection . status = 'reconnecting'
137+ connection . client ?. emit ( 'close' )
138+ await Promise . resolve ( )
139+ expect ( settled ) . not . toHaveBeenCalled ( )
140+ expect ( mockSubscribe ) . not . toHaveBeenCalled ( )
143141
144- connection . status = 'ready'
145- connection . client ?. emit ( 'ready' )
146- await hub . subscribe ( 'execution-1' , handler )
147- expect ( mockSubscribe ) . toHaveBeenCalledOnce ( )
148- }
149- )
142+ connection . status = 'ready'
143+ connection . client ?. emit ( 'ready' )
144+ await subscription
145+ expect ( mockSubscribe ) . toHaveBeenCalledOnce ( )
146+ connection . client ?. emit ( 'message' , 'execution:signal:execution-1' , 'cancelled' )
147+ expect ( handler ) . toHaveBeenCalledWith ( 'cancelled' )
148+ } )
149+
150+ it ( 'rejects readiness waiters when the subscriber stops reconnecting' , async ( ) => {
151+ connection . status = 'connect'
152+ const hub = getExecutionSignalHub ( )
153+ const subscription = hub . subscribe ( 'execution-1' , vi . fn ( ) )
154+ const rejected = expect ( subscription ) . rejects . toThrow ( 'Redis subscriber connection ended' )
155+
156+ connection . status = 'end'
157+ connection . client ?. emit ( 'end' )
158+ await rejected
159+ expect ( mockSubscribe ) . not . toHaveBeenCalled ( )
160+ expect ( connection . client ?. listenerCount ( 'ready' ) ) . toBe ( 1 )
161+ expect ( connection . client ?. listenerCount ( 'error' ) ) . toBe ( 1 )
162+ expect ( connection . client ?. listenerCount ( 'end' ) ) . toBe ( 0 )
163+ } )
164+
165+ it ( 'keeps a new channel independent of an existing channel reconnect failure' , async ( ) => {
166+ const hub = getExecutionSignalHub ( )
167+ connection . client ?. emit ( 'ready' )
168+ const existingHandler = vi . fn ( )
169+ await hub . subscribe ( 'execution-existing' , existingHandler )
170+ mockSubscribe . mockClear ( )
171+ connection . status = 'reconnecting'
172+ connection . client ?. emit ( 'close' )
173+ const newHandler = vi . fn ( )
174+ const subscription = hub . subscribe ( 'execution-new' , newHandler )
175+ let rejectReconnect ! : ( error : Error ) => void
176+ let acknowledgeNew ! : ( count : number ) => void
177+ mockSubscribe . mockReturnValueOnce (
178+ new Promise < number > ( ( _resolve , reject ) => {
179+ rejectReconnect = reject
180+ } )
181+ )
182+ mockSubscribe . mockReturnValueOnce (
183+ new Promise < number > ( ( resolve ) => {
184+ acknowledgeNew = resolve
185+ } )
186+ )
187+
188+ connection . status = 'ready'
189+ connection . client ?. emit ( 'ready' )
190+ await vi . waitFor ( ( ) => expect ( mockSubscribe ) . toHaveBeenCalledTimes ( 2 ) )
191+ expect ( mockSubscribe ) . toHaveBeenNthCalledWith (
192+ 1 ,
193+ 'execution:signal:execution-existing' ,
194+ 'execution:cancel'
195+ )
196+ expect ( mockSubscribe ) . toHaveBeenNthCalledWith (
197+ 2 ,
198+ 'execution:signal:execution-new' ,
199+ 'execution:cancel'
200+ )
201+
202+ rejectReconnect ( new Error ( 'Command timed out' ) )
203+ await vi . waitFor ( ( ) => expect ( existingHandler ) . toHaveBeenCalledWith ( 'unavailable' ) )
204+ expect ( newHandler ) . not . toHaveBeenCalled ( )
205+ acknowledgeNew ( 3 )
206+ await subscription
207+ connection . client ?. emit ( 'message' , 'execution:signal:execution-new' , 'cancelled' )
208+ expect ( newHandler ) . toHaveBeenCalledExactlyOnceWith ( 'cancelled' )
209+ } )
210+
211+ it ( 'preserves the pending acknowledgement when Redis reconnects before it arrives' , async ( ) => {
212+ const hub = getExecutionSignalHub ( )
213+ connection . client ?. emit ( 'ready' )
214+ let acknowledge ! : ( count : number ) => void
215+ mockSubscribe . mockReturnValueOnce (
216+ new Promise < number > ( ( resolve ) => {
217+ acknowledge = resolve
218+ } )
219+ )
220+ const handler = vi . fn ( )
221+ const subscription = hub . subscribe ( 'execution-new' , handler )
222+ connection . status = 'reconnecting'
223+ connection . client ?. emit ( 'close' )
224+ connection . status = 'ready'
225+ connection . client ?. emit ( 'ready' )
226+
227+ expect ( mockSubscribe ) . toHaveBeenCalledOnce ( )
228+ acknowledge ( 2 )
229+ await subscription
230+ expect ( handler ) . not . toHaveBeenCalled ( )
231+ } )
150232
151233 it ( 'bounds the readiness wait and removes failed handlers before a later ready event' , async ( ) => {
152234 vi . useFakeTimers ( )
@@ -159,7 +241,11 @@ describe('ExecutionSignalHub', () => {
159241 'Timed out waiting for Redis subscriber readiness'
160242 )
161243
162- await Promise . all ( [ rejected , vi . advanceTimersByTimeAsync ( 5000 ) ] )
244+ const timeout = vi . advanceTimersByTimeAsync ( 4000 ) . then ( ( ) => {
245+ connection . client ?. emit ( 'error' , new Error ( 'ECONNREFUSED' ) )
246+ return vi . advanceTimersByTimeAsync ( 1000 )
247+ } )
248+ await Promise . all ( [ rejected , timeout ] )
163249 expect ( mockSubscribe ) . not . toHaveBeenCalled ( )
164250 expect ( connection . client ?. listenerCount ( 'ready' ) ) . toBe ( 1 )
165251 expect ( connection . client ?. listenerCount ( 'error' ) ) . toBe ( 1 )
0 commit comments