Repository navigation
Conversation
…ntil joins eventually in PladderTestSupport
`waitForSleeper(at:)` polled `deadlines` a millisecond at a time; a sleep now resumes the waiters it satisfies, and `waitForSleepers(_:)` does the same for a count, replacing four `waitUntil { clock.sleeperCount >= 2 }`. Both still end by themselves after two seconds, as a Gate does.
`waitUntil` moves from DictationCoordinatorTests to PladderTestSupport, and the coordinator tests' own `eventually` goes in favour of the shared one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`FakeCapture.stop`, `FakeRefiner.refine` and the counting engine's warm pass slept 50 to 200 ms so a cancel, a press or a release would land while they ran. Each now waits on a Gate the test opens once it has seen that state. The warm pass's wait cannot be cancelled, like a CoreML pass in flight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, not a 10 ms poll Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of fifty The stand-in cancel slept 5 ms and the test ran fifty times so the ensure would sometimes reach the actor first. It now waits on a Gate and marks the download stopped only after it, so an ensure that does not wait for the cancel loses its download whether it reaches the actor before the cancel or during its wind-down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defaulted to ContinuousClock, so the app's call site is unchanged. The two tests that slept four times the delay to be sure an arm had given up now drive it with ManualClock. Off the release path: only `recordingStarted`, called at key-down, reads the clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d load instead of sleeping 20 ms `abandoned` becomes `private(set)` so the test, through `@testable import`, can wait for the first load to finish and try to store its model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each is a negative path, where nothing happens that a test could wait for, or a cancellable sleep standing in for a download that the cancel must interrupt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task
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.
Part of #73, point 4.
Where a sleep in the tests stood in for ordering, it is now a
GateorManualClock. Where it has to be real time, it now says why. Real-time sleeps inTests/: 20 → 13. Counted withgrep -rnE 'Task.sleep|Thread.sleep|usleep|clock.sleep' Tests, leaving out theclock.sleeperCountmatches andPasteTestSupport'sManualClockforwarder.No test was renamed and no assertion was removed. What changed is how the tests wait.
Every real-time sleep, before → after
DictationCoordinatorTestsFakeCapture.stop,stopDelaystopGate: Gate?.aPressRightAfterACancelKeepsItsUtterance,aPressCancelledWhileItWaitsLeavesTheMicrophoneAloneandaPressRightAfterAMonitorSwapStartsADictationopen it once they have seen the next press reach.recordingDictationCoordinatorTestsFakeRefiner.refine,delaygate: Gate?.polishingStateIsPublishedWhileTheModelRunschecks.polishing, then opens itDictationCoordinatorTestsCountingEngine.transcribe,delaywarmGate: Gate?, used only on the warm pass (samples.count == 8_000).releaseDuringWarmupStillInsertspresses, waits forcalls == [8_000], releases, then opens.Gate.waitcannot be cancelled, so it still models a CoreML pass that cannot be stopped. The oldtry? Task.sleepcould be cancelled, so the release used to cut it shortDictationCoordinatorTestswaitUntil, 10 ms pollPladderTestSupport/TestSupport.swift, labelled "Polls in real time: the fakes' counters are not observable."DictationCoordinatorTestslocaleventually, 10 ms pollPladderTestSupport.eventuallyDictationCoordinatorTeststheHoldIsTimedByTheEventsOwnInstants, 100 msEngineLoaderTestsaSupersededLoadLeavesTheNextEngineAlone, 50 msEngineLoaderTestsGatedEngine.waitUntilLoaded, 10 ms pollload()opens aGatethatwaitUntilLoadedwaits onOrderedTasksTestsDownloads.cancel, 5 ms, test looped 50×cancelwaits on aGateand marks the download stopped only after it; one runOrderedTasksTestseachPieceStartsAfterThePreviousOneFinished, 10 msOutputMuteControllerTestspastTheDelay()OutputMuteControllertakes aclock(defaultContinuousClock()). The two tests that used it run onManualClockand calladvanceModelFilesTestscancelStopsTheDownloadAndLeavesNothingBehind, 3 sModelFilesTestscancelReachesTheChecksum,Thread.sleep1 msS1MiniTestsaLoadInterruptedByUnloadIsDropped, 20 msS1MiniPolisher.abandonedis nowprivate(set); the test runs_ = await polisher.abandoned?.valueS1MiniTestsaLoadAfterUnloadWaitsForTheAbandonedOneToBeFreed, 30 msTranscriptPolisherBudgetTestsslowCallObservationLoopTestssettle()PasteFlowTestsaReadBeforeCmdVIsNotThePaste, 20 msTestSupportGate's safety timerManualClock.swifteventually, 1 ms pollTestSupport.swiftnext towaitUntilManualClock.waitFor, newGate's does, and is cancelled as soon as the wait is metManualClock.waitForSleeper(at:)used to poll througheventually. It now waits for an event:sleep(until:)wakes any waiter whose condition it meets. The newwaitForSleepers(_ count:)(at leastcount) replaces the fourwaitUntil { clock.sleeperCount >= 2 }. The== 1and== 3polls are left as they are, because "at least" would weaken them.Not counted above, and not changed:
uncancellableSleepinDictationCoordinatorTests(aDispatchQueue.asyncAfter) behindFakeStreamingEngine'sfeedDelay/beginDelay/abandonDelay/livePassDelay, andEchoEngine'sdelay. The grep does not find them and the issue's 20 did not include them. They are the next candidates.Where the plan was wrong
OrderedTasksTests: "one deterministic iteration". A correctOrderedTasksnow always passes. A broken one does not always fail. Withenqueuechanged so it no longer waits for the previous piece,aCancelThenEnsureLeavesTheDownloadRunningfailed 29 of 30 runs on its own, and fewer in a full parallel run. The ensure task sometimes reaches the actor only after the test has opened the gate, and that cannot be detected without a sleep.eachPieceStartsAfterThePreviousOneFinished(row 10) failed every time under the same change, so that is the test that reliably catches it. I kept one run rather than the 50× loop.GatedEngine.waitUntilLoaded. The plan said to store continuations; I used aGateinstead. Its 2 s safety timer means a regression fails instead of hanging until the one-minute suite limit.OutputMuteControllerTests. The two tests run onManualClock. The rest keep the 10 ms realtestDelaythrough the default clock, because some of them assert that a start returns without sleeping, and with aManualClockthose would need a sleeper that never comes.pastTheDelay()was also redundant before: both tests already awaited the start task, which only returns after its sleep.Production code touched
OutputMuteController:clock: any Clock<Duration> = ContinuousClock()ininit, used for the arm delay. The default keepsAppModel's call unchanged. Off the release path: onlyrecordingStarted(session:)reads the clock, and the coordinator calls it at key-down fromarmOutputMute(). The release side,recordingEnded(session:), does not touch the clock. Its spawn, theTask.detachedinrestoreOutputDevice()thatendRecording()creates beforerecordingStoppedis emitted, is unchanged. Nothing betweenrecordingStoppedandinsertedchanged, so the benchmark was not run.S1MiniPolisher.abandoned:private→private(set). The tests read it through@testable import PladderRefine.Verification
swift build: no warnings inSources/.swift test: three runs in a row, all green, on the final tree and on every commit of the branch.for i in $(seq 20); do swift test --skip-build --filter CoordinatorHandOverTests || break; done: 20/20 green, 0.42–0.44 s each.--filter, 10/10 green each:DictationCoordinatorTests,PolishToggleTests,ToggleHotkeyTests,EscapeTests,CoordinatorClockTests,EngineLoaderTests,OrderedTasksTests,OutputMuteControllerTests,ModelFilesTests,S1MiniLoadTests,PasteFlowTests(useswaitForSleeper),CorrectionLearnerTests(useswaitUntil).OutputMuteController,tapAndReleaseBeforeTheDelayNeverMutesfails 3/3.S1MiniPolisher.finishLoad,aLoadInterruptedByUnloadIsDroppedfails 3/3.OrderedTasks, see above.Suite times reported by
swift test, three runs each:Core's time is set by its slowest tests, the streaming fakes' 300 ms delays, which this PR leaves alone. System got faster because
waitForSleeperno longer polls.🤖 Generated with Claude Code