@@ -12,6 +12,10 @@ import WatchConnectivity
1212
1313// MARK: - WatchServiceTests
1414
15+ /// Tests create `DefaultWatchService` instances that run background tasks on the cooperative thread
16+ /// pool. Running tests in parallel causes thread contention that can push async chains past the
17+ /// CI 1-second per-test time limit, so the suite is serialized.
18+ @Suite ( . serialized)
1519@MainActor
1620struct WatchServiceTests { // swiftlint:disable:this type_body_length
1721 // MARK: Properties
@@ -201,15 +205,22 @@ struct WatchServiceTests { // swiftlint:disable:this type_body_length
201205 stateService. connectToWatchSubject. send ( ( " 1 " , true ) )
202206 }
203207
208+ // Capture the context at callback time rather than reading the shared property afterwards.
209+ // A task from the first sync could otherwise overwrite it between the resume() call and the
210+ // assertion below.
211+ var capturedContext : [ String : Any ] ?
204212 await withContinuationTimeout { resume in
205- watchSession. updateApplicationContextClosure = { _ in resume ( ) }
213+ watchSession. updateApplicationContextClosure = { context in
214+ capturedContext = context
215+ resume ( )
216+ }
206217
207218 // Now update with shouldConnect = false — the existing session will be used.
208219 stateService. connectToWatchByUserId [ " 1 " ] = false
209220 stateService. connectToWatchSubject. send ( ( " 1 " , false ) )
210221 }
211222
212- let dto = try decodedDTO ( from: watchSession . updateApplicationContextReceivedApplicationContext )
223+ let dto = try decodedDTO ( from: capturedContext )
213224 #expect( dto. state == . needSetup)
214225 }
215226
@@ -345,12 +356,22 @@ struct WatchServiceTests { // swiftlint:disable:this type_body_length
345356 let decryptCountAfterSetup = decryptCallCount
346357 #expect( decryptCountAfterSetup > 0 )
347358
348- // Lock the vault and trigger another sync.
359+ // Lock the vault and trigger another sync (no updateApplicationContext expected) .
349360 vaultTimeoutService. isClientLocked [ " 1 " ] = true
350361 stateService. connectToWatchSubject. send ( ( " 1 " , true ) )
351- try await Task . sleep ( nanoseconds: 10_000_000 )
352362
353- // Confirm no additional decrypt attempts.
363+ // Use a barrier sync to guarantee sequential processing: unlock the vault and set
364+ // shouldConnect = false so the next sync sends .needSetup without decrypting ciphers.
365+ // By the time the barrier callback fires, the locked sync above has already been processed.
366+ await withContinuationTimeout { resume in
367+ watchSession. updateApplicationContextClosure = { _ in resume ( ) }
368+
369+ vaultTimeoutService. isClientLocked [ " 1 " ] = false
370+ stateService. connectToWatchByUserId [ " 1 " ] = false
371+ stateService. connectToWatchSubject. send ( ( " 1 " , false ) )
372+ }
373+
374+ // Confirm no additional decrypt attempts occurred during the locked sync.
354375 #expect( decryptCallCount == decryptCountAfterSetup)
355376 }
356377
@@ -365,12 +386,16 @@ struct WatchServiceTests { // swiftlint:disable:this type_body_length
365386 vaultTimeoutService. isClientLocked [ " 1 " ] = true
366387 stateService. connectToWatchByUserId [ " 1 " ] = true
367388 stateService. connectToWatchSubject. send ( ( " 1 " , true ) )
368- try await Task . sleep ( nanoseconds: 10_000_000 )
369- #expect( watchSession. updateApplicationContextCallsCount == 0 )
370389
371390 // Unlock the vault — the publisher emits, triggering a fresh sync.
391+ // The locked and unlock events are processed sequentially by the service's listener loop,
392+ // so the unlock sync fires after the locked sync. Asserting callsCount == 1 inside the
393+ // closure confirms the locked sync produced no calls before this one.
372394 await withContinuationTimeout { resume in
373- watchSession. updateApplicationContextClosure = { _ in resume ( ) }
395+ watchSession. updateApplicationContextClosure = { _ in
396+ #expect( watchSession. updateApplicationContextCallsCount == 1 )
397+ resume ( )
398+ }
374399
375400 cipherService. ciphersSubject. send ( [ . fixture( ) ] )
376401 vaultTimeoutService. isClientLocked [ " 1 " ] = false
0 commit comments