refactor(65): deterministic SyncScheduler timing — kill the CI flake#46
Merged
Conversation
Inject `any Clock<Duration>` (default `ContinuousClock`) so tests can drive time forward without relying on real wall-clock sleeps. Production semantics unchanged — 300 s default interval, same sleep-first loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add ManualClock — a zero-dependency test Clock<Duration> that suspends callers on continuations until the test calls advance(by:). The key fix is waitForSleeper(): tests now await the moment the scheduler loop has actually suspended in clock.sleep(until:) before advancing time, closing the TOCTOU window where advance() ran before the loop task started. SyncSpy.makeSlow() now uses a proper CheckedContinuation rather than a 60-second real sleep, so cleanup is immediate. Verified: 10 consecutive passes on FK-Lane-B (E07B654D) with parallel execution enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-flake # Conflicts: # FenixKanban.xcodeproj/project.pbxproj # FenixKanbanTests/Features/Sync/SyncSchedulerTests.swift
…markers A prior aborted scripted resolution had git-added the conflicted file, so checkout --ours pulled markers from the index. File restored from the branch tip (the ManualClock rewrite), suite verified green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-flake # Conflicts: # FenixKanban.xcodeproj/project.pbxproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
any Clock<Duration>intoSyncScheduler(defaultContinuousClock— production unchanged); loop now callsclock.sleep(for:)instead ofTask.sleep(for:).ManualClocktest helper: suspends callers onCheckedContinuations, exposeswaitForSleeper()(blocks until the loop actually enters sleep) +advance(by:)(wakes all due sleepers, yields 8× for MainActor work); NSLock-based, zero external dependencies.SyncSchedulerTeststo useManualClock—waitForSleeper()closes the TOCTOU window whereadvance(by:)ran before the loop task had even calledsleep(), which was the root cause of the flake.SyncSpy.makeSlow()to block on aCheckedContinuation+releaseAll()instead of a 60 s real wall-clock sleep; cleanup is now instantaneous.Root cause of the flake
SyncScheduler.startLoop()creates an unstructuredTask {}(queued but not started). The test immediately calledawait Task.sleep(for: .milliseconds(200))hoping to give it time. Under parallel load (Xcode spawns 2 clone processes per suite; Swift Testing randomises ordering), the loop task's continuation was starved on the cooperative thread pool — it never ran during the 200 ms window, sospy.callCount == 0. The fix is not "wait longer" but "don't start checking until we know the loop is blocked in sleep."Test plan
SyncSchedulerTests— 10 consecutive passes on FK-Lane-B (E07B654D) with parallel execution enabled; each run: 8 tests × 2 clones = 16 cases, all green.SyncSchedulerTestsall pass; pre-existing failures inKeychainHelperTests/FizzySyncEngine*are unrelated and present on develop.CODE_SIGNING_ALLOWED=NO) —** BUILD SUCCEEDED **, zero warnings.repo.sh check) — PASS.Mission: #28 · Status log: #65
🤖 Generated with Claude Code