@@ -11,6 +11,42 @@ private extension NSLock {
1111 }
1212}
1313
14+ private final class DoubleCallbackPingWebSocketTask : WebSocketTasking , @unchecked Sendable {
15+ private let callbacks : [ Error ? ]
16+
17+ init ( callbacks: [ Error ? ] ) {
18+ self . callbacks = callbacks
19+ }
20+
21+ var state : URLSessionTask . State { . running }
22+
23+ func resume( ) { }
24+
25+ func cancel( with closeCode: URLSessionWebSocketTask . CloseCode , reason: Data ? ) {
26+ _ = ( closeCode, reason)
27+ }
28+
29+ func send( _ message: URLSessionWebSocketTask . Message ) async throws {
30+ _ = message
31+ }
32+
33+ func sendPing( pongReceiveHandler: @escaping @Sendable ( Error ? ) -> Void ) {
34+ for callback in self . callbacks {
35+ pongReceiveHandler ( callback)
36+ }
37+ }
38+
39+ func receive( ) async throws -> URLSessionWebSocketTask . Message {
40+ throw URLError ( . badServerResponse)
41+ }
42+
43+ func receive(
44+ completionHandler: @escaping @Sendable ( Result < URLSessionWebSocketTask . Message , Error > ) -> Void )
45+ {
46+ completionHandler ( . failure( URLError ( . badServerResponse) ) )
47+ }
48+ }
49+
1450private final class FakeGatewayWebSocketTask : WebSocketTasking , @unchecked Sendable {
1551 private let lock = NSLock ( )
1652 private let helloAuth : [ String : Any ] ?
@@ -193,6 +229,25 @@ private actor SeqGapProbe {
193229
194230@Suite ( . serialized)
195231struct GatewayNodeSessionTests {
232+ @Test
233+ func websocketPingIgnoresDuplicateSuccessCallbacks( ) async throws {
234+ let task = DoubleCallbackPingWebSocketTask ( callbacks: [ nil , nil ] )
235+ try await WebSocketTaskBox ( task: task) . sendPing ( )
236+ }
237+
238+ @Test
239+ func websocketPingIgnoresDuplicateCallbacksAfterFirstError( ) async throws {
240+ let firstError = URLError ( . networkConnectionLost)
241+ let task = DoubleCallbackPingWebSocketTask ( callbacks: [ firstError, nil ] )
242+
243+ do {
244+ try await WebSocketTaskBox ( task: task) . sendPing ( )
245+ Issue . record ( " sendPing unexpectedly succeeded " )
246+ } catch let error as URLError {
247+ #expect( error. code == firstError. code)
248+ }
249+ }
250+
196251 @Test
197252 func scannedSetupCodePrefersBootstrapAuthOverStoredDeviceToken( ) async throws {
198253 let tempDir = FileManager . default. temporaryDirectory
0 commit comments