Raise two more starvation-prone test deadlines

Both surfaced on the CI runs for this PR and #1487, both in tests neither
PR touches, both the same shape as the two already fixed here: a deadline
sized for the work rather than for a runner executing many suites at once.

VoiceNotePlaybackControllerTests waited 5s for a @MainActor Task that
playback schedules for the session acquire and its failure path. When that
Task is not scheduled in time the helper reports *two* failures — the wait,
and the `!isPlaying` the un-run failure path has not reset yet — which
reads like a playback bug rather than a starved scheduler. That is exactly
what CI showed.

GeoRelayDirectoryTests waited 10s for work the directory runs in
Task.detached(priority: .utility); utility priority competes with every
other suite. The retry-scheduling case timed out at exactly 10.06s with the
retry never scheduled, reading like a missing retry rather than a starved
background task.

Raised to 30s each with the reasoning recorded at the helper. Both helpers
return as soon as their condition holds, so nothing slows down when tests
pass — only the genuine-failure case takes longer to report.

Verified 3x with all cores saturated: both suites clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-26 22:00:04 +01:00
co-authored by Claude Opus 5
parent 0fd17b093a
commit 44fadeeaba
2 changed files with 23 additions and 2 deletions
@@ -580,8 +580,16 @@ final class GeoRelayDirectoryTests: XCTestCase {
/// constrained CI runners (2-core, serialized testing) can starve the /// constrained CI runners (2-core, serialized testing) can starve the
/// detached utility-priority fetch task for seconds before it runs, and /// detached utility-priority fetch task for seconds before it runs, and
/// a successful wait returns as soon as the condition becomes true. /// a successful wait returns as soon as the condition becomes true.
/// Default deliberately far larger than the work being awaited.
///
/// The directory performs its fetch in a `Task.detached(priority: .utility)`,
/// and utility priority competes with every other suite on a CI runner. At
/// ten seconds the retry-scheduling test timed out at exactly 10.06s with
/// the retry never scheduled which reads like a missing retry rather than
/// a starved background task. Returning as soon as the condition holds means
/// a longer deadline only extends the genuine-failure case.
private func waitUntil( private func waitUntil(
timeout: TimeInterval = 10.0, timeout: TimeInterval = 30.0,
condition: @escaping @MainActor () async -> Bool condition: @escaping @MainActor () async -> Bool
) async -> Bool { ) async -> Bool {
let deadline = Date().addingTimeInterval(timeout) let deadline = Date().addingTimeInterval(timeout)
@@ -59,11 +59,24 @@ struct VoiceNotePlaybackControllerTests {
return url return url
} }
/// Waits for an async settle, then asserts.
///
/// The deadline is deliberately far larger than the work it waits on. Every
/// condition here depends on a `@MainActor` Task that playback schedules
/// (the session acquire and its failure path), and on a CI runner executing
/// many suites in parallel that Task can simply not be scheduled for
/// seconds. At five seconds this timed out on CI and reported *two*
/// failures the wait itself, and the `!isPlaying` that the un-run failure
/// path had not yet reset which reads like a playback bug rather than a
/// starved scheduler.
///
/// A generous deadline costs nothing when the condition holds, since this
/// returns as soon as it does; it only extends the genuine-failure case.
private func waitUntil( private func waitUntil(
_ condition: () -> Bool, _ condition: () -> Bool,
sourceLocation: SourceLocation = #_sourceLocation sourceLocation: SourceLocation = #_sourceLocation
) async { ) async {
let deadline = ContinuousClock.now.advanced(by: .seconds(5)) let deadline = ContinuousClock.now.advanced(by: .seconds(30))
while !condition(), ContinuousClock.now < deadline { while !condition(), ContinuousClock.now < deadline {
await Task.yield() await Task.yield()
try? await Task.sleep(nanoseconds: 1_000_000) try? await Task.sleep(nanoseconds: 1_000_000)