From 8804839c6c53dc4d216d22372711395633fb4e5c Mon Sep 17 00:00:00 2001 From: Shawn Shi Date: Sun, 26 Jul 2026 12:28:21 -0400 Subject: [PATCH 1/2] Fix flaky debounce test that failed on CI after merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coalescesBursts raced a fixed 120ms sleep against a 50ms debounce timer, which flaked on CI's shared runner (count == 0, onChange hadn't fired yet by the deadline) — this was flagged as timing- sensitive during PR #10's review but only surfaced once CI actually ran the suite on a real runner. Signal from inside onChange via an AsyncStream continuation instead, matching the pattern WorkspaceReloadCoordinatorTests already uses, so the test waits for the actual event instead of a wall-clock guess. A .timeLimit trait bounds the wait if onChange regresses to never firing. Co-Authored-By: Claude Sonnet 4.6 --- .../Watcher/WorkspaceWatcherTests.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift b/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift index 810c96a..dd0d0f5 100644 --- a/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift +++ b/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift @@ -187,17 +187,19 @@ struct WorkspaceChangeFilterTests { @Suite("WatcherContext debounce") struct WatcherContextDebounceTests { - @Test("Coalesces rapid relevant events into a single onChange") + @Test("Coalesces rapid relevant events into a single onChange", .timeLimit(.minutes(1))) func coalescesBursts() async throws { let filter = WorkspaceChangeFilter( allowedPaths: ["/Projects/App/App.xcodeproj/project.pbxproj"] ) let count = OSAllocatedUnfairLock(initialState: 0) + let (changeStream, changeContinuation) = AsyncStream.makeStream(of: Void.self) let context = WatcherContext( filter: filter, debounceInterval: 0.05, onChange: { count.withLock { $0 += 1 } + changeContinuation.yield(()) } ) @@ -206,7 +208,12 @@ struct WatcherContextDebounceTests { context.handle(paths: ["/Projects/App/.git/HEAD"]) context.handle(paths: ["/Projects/App/App.xcodeproj/project.pbxproj"]) - try await Task.sleep(for: .milliseconds(120)) + // Wait for the debounced onChange to actually fire instead of racing a fixed + // sleep against CI scheduling jitter, which flaked in CI (count == 0). + for await _ in changeStream { + break + } + changeContinuation.finish() #expect(count.withLock { $0 } == 1) context.cancelPending() } From a7fb4d34564074f64cf5202fbaf661f2a9a22eac Mon Sep 17 00:00:00 2001 From: Shawn Shi Date: Mon, 27 Jul 2026 22:47:52 -0400 Subject: [PATCH 2/2] Add a quiet window after the first onChange to catch multi-fire regressions Asserting immediately on the first signal would let a coalescing regression (onChange firing more than once) slip through if the second fire landed after the #expect. The added wait is safe against the same CI race that caused the original flake: a slow runner can only delay a fire, never manufacture a spurious extra one, so this can only fail on a genuine regression, not reintroduce count == 0. Co-Authored-By: Claude Sonnet 4.6 --- .../Watcher/WorkspaceWatcherTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift b/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift index dd0d0f5..503e3da 100644 --- a/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift +++ b/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift @@ -213,6 +213,11 @@ struct WatcherContextDebounceTests { for await _ in changeStream { break } + // A slow runner can only delay a fire, never manufacture a spurious extra + // one, so this quiet window is safe against the same CI race: it only + // catches a real regression (coalescing broken, onChange fires more than + // once), it can't itself flake count down to 0. + try await Task.sleep(for: .milliseconds(150)) changeContinuation.finish() #expect(count.withLock { $0 } == 1) context.cancelPending()