Skip to content
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4863ff7
chore: Fix flaky FlagSynchronizerSpec polling tests
devin-ai-integration[bot] Mar 19, 2026
2907884
chore: Address review feedback on polling test assertions
devin-ai-integration[bot] Mar 19, 2026
5e84839
chore: trigger CI re-run (1 of 3)
devin-ai-integration[bot] Mar 19, 2026
96376d6
chore: trigger CI re-run (2 of 3)
devin-ai-integration[bot] Mar 19, 2026
0433d4e
chore: trigger CI re-run (3 of 3)
devin-ai-integration[bot] Mar 19, 2026
dbc5045
chore: Fix 'does not stop polling' test to detect restart regressions
devin-ai-integration[bot] Mar 19, 2026
341b70d
chore: Add didSignal guard to remaining polling tests
devin-ai-integration[bot] Mar 19, 2026
cae3bd6
chore: Simplify polling test fixes — guard-only approach for starts/d…
devin-ai-integration[bot] Mar 19, 2026
cd3c610
chore: Fix flaky 'event reported while polling' test
devin-ai-integration[bot] Mar 19, 2026
3684736
chore: trigger CI re-run (1 of 3)
devin-ai-integration[bot] Mar 19, 2026
6ced240
chore: trigger CI re-run (2 of 3)
devin-ai-integration[bot] Mar 19, 2026
be6a5d2
chore: trigger CI re-run (3 of 3)
devin-ai-integration[bot] Mar 19, 2026
8e87633
chore: trigger CI re-run (2 of 3)
devin-ai-integration[bot] Mar 19, 2026
c953ee8
chore: Add upper bounds check to 'stops polling' test
devin-ai-integration[bot] Mar 19, 2026
2beb73e
chore: trigger CI stress test (1 of 3)
devin-ai-integration[bot] Mar 19, 2026
3a938ce
chore: trigger CI stress test (2 of 3)
devin-ai-integration[bot] Mar 19, 2026
71176ca
chore: trigger CI stress test (3 of 3)
devin-ai-integration[bot] Mar 19, 2026
630525f
chore: trigger CI stress test round 2 (1 of 3)
devin-ai-integration[bot] Mar 19, 2026
1e1a793
chore: trigger CI stress test round 2 (2 of 3)
devin-ai-integration[bot] Mar 19, 2026
5d47632
chore: trigger CI stress test round 2 (3 of 3)
devin-ai-integration[bot] Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,12 @@ final class FlagSynchronizerSpec: QuickSpec {
}
it("stops polling") {
let semaphore = DispatchSemaphore(value: 0)
var didSignal = false

DispatchQueue.global().async {
testContext = TestContext(streamingMode: .polling, useReport: false) { _ in
// Stop polling inside the callback to prevent further
// timer ticks from racing with assertions.
guard !didSignal else { return }
didSignal = true
testContext.flagSynchronizer.isOnline = false
semaphore.signal()
}
Expand All @@ -131,8 +132,13 @@ final class FlagSynchronizerSpec: QuickSpec {

expect(testContext.flagSynchronizer.isOnline) == false
expect(testContext.flagSynchronizer.streamingMode) == .polling
expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1
let countAfterStop = testContext.serviceMock.getFeatureFlagsCallCount
expect(countAfterStop) >= 1
expect(testContext.serviceMock.createEventSourceCallCount) == 0

// Wait briefly to confirm no further polling occurs.
Thread.sleep(forTimeInterval: 1.5)
expect(testContext.serviceMock.getFeatureFlagsCallCount) == countAfterStop
}
}
context("offline to online") {
Expand All @@ -149,9 +155,12 @@ final class FlagSynchronizerSpec: QuickSpec {
}
it("starts polling") {
let semaphore = DispatchSemaphore(value: 0)
var didSignal = false

DispatchQueue.global().async {
testContext = TestContext(streamingMode: .polling, useReport: false) { _ in
guard !didSignal else { return }
didSignal = true
semaphore.signal()
}
testContext.flagSynchronizer.isOnline = true
Expand All @@ -165,7 +174,7 @@ final class FlagSynchronizerSpec: QuickSpec {
// polling starts by requesting flags
expect(testContext.flagSynchronizer.isOnline) == true
expect(testContext.flagSynchronizer.streamingMode) == .polling
expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1
expect(testContext.serviceMock.getFeatureFlagsCallCount) >= 1
expect(testContext.serviceMock.createEventSourceCallCount) == 0

testContext.flagSynchronizer.isOnline = false
Expand All @@ -185,9 +194,12 @@ final class FlagSynchronizerSpec: QuickSpec {
}
it("does not stop polling") {
let semaphore = DispatchSemaphore(value: 0)
var didSignal = false

DispatchQueue.global().async {
testContext = TestContext(streamingMode: .polling, useReport: false) { _ in
guard !didSignal else { return }
didSignal = true
semaphore.signal()
}
testContext.flagSynchronizer.isOnline = true
Expand All @@ -200,10 +212,10 @@ final class FlagSynchronizerSpec: QuickSpec {
runLoop.run(mode: .default, before: .distantFuture)
}

// setting the same value shouldn't make another flag request
// setting the same value shouldn't restart polling
expect(testContext.flagSynchronizer.isOnline) == true
expect(testContext.flagSynchronizer.streamingMode) == .polling
expect(testContext.serviceMock.getFeatureFlagsCallCount) == 1
expect(testContext.serviceMock.getFeatureFlagsCallCount) >= 1
expect(testContext.serviceMock.createEventSourceCallCount) == 0

testContext.flagSynchronizer.isOnline = false
Expand Down Expand Up @@ -809,10 +821,23 @@ final class FlagSynchronizerSpec: QuickSpec {
}
context("event reported while polling") {
it("reports an event error") {
waitUntil(timeout: .seconds(5)) { done in
testContext = TestContext(streamingMode: .polling, useReport: false) { _ in done() }
let semaphore = DispatchSemaphore(value: 0)
var didSignal = false

DispatchQueue.global().async {
testContext = TestContext(streamingMode: .polling, useReport: false) { _ in
guard !didSignal else { return }
didSignal = true
semaphore.signal()
}
testContext.flagSynchronizer.isOnline = true
}

let runLoop = RunLoop.current
while semaphore.wait(timeout: .now()) == .timedOut {
runLoop.run(mode: .default, before: .distantFuture)
}

waitUntil { done in
testContext.flagSynchronizer.onSyncComplete = { result in
if case .error(let errorResult) = result {
Expand Down Expand Up @@ -885,10 +910,17 @@ final class FlagSynchronizerSpec: QuickSpec {
runLoop.run(mode: .default, before: .distantFuture)
}

// Verify polling stopped by capturing the count, waiting,
// and confirming no additional requests were made.
expect(testContext.flagSynchronizer.isOnline) == false
expect(testContext.flagSynchronizer.streamingMode) == .polling
expect(testContext.serviceMock.getFeatureFlagsCallCount) == 2
let countAfterStop = testContext.serviceMock.getFeatureFlagsCallCount
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there is still a race condition here. Similar to other cases we don't know that we will not get one more request to getFeatureFlags. Is there any way to know that the polling has finished stopping?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Yes, there is a window where one more getFeatureFlags call could sneak through — between isOnline = false in the callback and the countAfterStop read. That's why countAfterStop uses >= 2 instead of == 2: it tolerates in-flight calls.

The key assertion is the second one: after the 1.5-second sleep, getFeatureFlagsCallCount == countAfterStop. Whatever the count was right after stopping (2, 3, etc.), it shouldn't change during the sleep. If it does, polling didn't actually stop.

So the test doesn't claim to know the exact count at stop time — it just verifies the count stabilizes, which proves polling stopped.

Also: the CI failure on this push was a different flaky test (event_reported_while_polling) with the same root cause — waitUntil callback called multiple times by polling timer ticks. I've fixed that one too (same semaphore + didSignal guard pattern) in the latest push.

expect(countAfterStop) >= 2
expect(testContext.serviceMock.createEventSourceCallCount) == 0

// Wait briefly to confirm no further polling occurs.
Thread.sleep(forTimeInterval: 1.5)
expect(testContext.serviceMock.getFeatureFlagsCallCount) == countAfterStop
}
}
}
Expand Down
Loading