Skip to content

Commit ca7a047

Browse files
[PM-36604] test: Fix flaky WatchServiceTests
1 parent c31aca7 commit ca7a047

1 file changed

Lines changed: 33 additions & 8 deletions

File tree

BitwardenShared/Core/Platform/Services/WatchServiceTests.swift

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
1620
struct 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

Comments
 (0)