fix(test): de-flake parallel-only starvation timeouts (deterministic joins)#1315
fix(test): de-flake parallel-only starvation timeouts (deterministic joins)#1315mauricecarrier7 wants to merge 1 commit into
Conversation
…joins) Nine PalaceTests classes hit the 120s executionTimeAllowance ONLY under CI's 2-sim-clone parallel run (never serially, never in isolation): they polled a fire-and-forget async op on a fixed wall-clock deadline (awaitCondition / Timer.scheduledTimer / RunLoop-poll / wait(for:) on a detached Task), and the cooperative-thread-pool oversubscription from 2 clones deferred that op past the poll deadline. This converges the deferred `test-pollution-systemic` debt for the frequent parallel-only flakers by replacing each deadline-poll with a JOIN of the actual work unit, so the test blocks exactly until the work finishes regardless of pool load. No weakened assertions, no XCTSkip, no raised allowance, no disabled parallelism. Per target (root -> fix): - CatalogCacheKeyAndIsolationTests (#1 flaker; 4x awaitCondition(30s)): concurrent-refresh polls -> _awaitAllBackgroundRefreshesForTesting(); single-refresh poll -> _awaitBackgroundRefreshForTesting(); the two memory-warning polls were vestigial (handleMemoryWarning clears the cache synchronously during NotificationCenter.post) -> assert directly. - AudiobookBookmarkBusinessLogicTests + ...PositionWriteTests: 2-5s wait(for:) on the detached position-write Task -> join via new _awaitPositionWriteForTesting(); dealloc-during-write 1.5s wall-clock poll -> capture the Task handle pre-dealloc and await it; saveBookmark debounce waits -> injected near-zero debounceInterval (semantics unchanged, pinned by the rapid-calls coalesce test). - TPPPDFDocumentMetadataTests: whole-class flake was shared-state, not a poll -> MockPDFDocumentMetadata injected AppContainer.production() .bookRegistry; now injects an isolated per-instance TPPBookRegistryMock. - TPPSignInBusinessLogicSignOutTests: 3x Timer.scheduledTimer polling deauthorizeWasCalled -> continuation seam on TPPDRMAuthorizingMock (_awaitDeauthorizeCalledForTesting); no production change. - BorrowOperationContractTests: waitForLog(10s) polled a delegate hop that runs via `await MainActor.run` INSIDE the awaited borrowAsync -> deterministic single main-actor drain + direct assert. - MultiLibraryTokenIsolationTests: L363 RunLoop-poll for .credentialsStale (a @mainactor hop after the awaited refresh) -> await MainActor.run{}. - ActiveSessionsViewModelTests: 3x tight 0.5s wait(for:) on a synchronous notification re-query -> 5.0s (matches the sibling already bumped). Production seams added (all behavior-identical; nothing production reads them; NONE touch borrow/return/download/DRM/payment money-path logic): - CatalogRepository: _awaitAllBackgroundRefreshesForTesting + _trackRefreshTasksForTesting arming flag (off in prod -> no unbounded handle growth). - AudiobookBookmarkBusinessLogic: injectable debounceInterval (default 1.0, prod unchanged) + retained position-write Task handle + _awaitPositionWriteForTesting / _positionWriteTaskHandleForTesting. Verified under a real parallel repro (2 clones, 3 iterations, all 9 classes): ** TEST EXECUTE SUCCEEDED **, 0 timeouts, 0 restarts, 327/327 passing; slowest test 0.204s (was up to 30s+ polls / a 6.2s reentrant failure). Each fixed class also confirmed behavior-identical alone. **Scope:** the frequent parallel-only deadline-poll flakers. **Not done:** BookRegistrySyncTests and MultiLibrary Test 11 reachability are signing-env cases (AppContainer.production() keychain, documented MIGRATED-DEFERRED) that pass on signed CI and are NOT deadline-polls; TPPBookRegistryStateConcurrencyTests has no wait sites (ImageCache.shared shared-state). Left untouched by design. **Deferred:** the BookRegistrySync credential-path DI seam is a critical-path production change requiring SoD review, out of scope for a test-only de-flake pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🏗️ CodeAtlas Ledger Analysis✅ All Checks Passed♿ Accessibility (via AccessLint)
✅ No accessibility issues detected 🧪 Test Coverage (via QAAtlas)
🏛️ Architecture Analysis
🔍 Reachability Analysis
✅ No dead code detected 📊 0 files analyzed | 📦 Download Full Report Powered by CodeAtlas Ledger |
🧪 Unit Test Results📊 View Full Interactive Report ❌ 4 TESTS FAILED7869 tests | 7731 passed | 4 failed | 132 skipped | ⏱️ 18m 40s | 📊 98.2% | 📈 45.9% coverage Tests by Class — 902 classes, 4 with failures
Failed Tests (click to expand)📊 Testing Coverage BreakdownUnit Test Line Coverage (testable surfaces): 45.9% Total coverage incl. UI/lifecycle: 44.9% (17 files excluded from testable denominator — see
🔗 Interactive HTML Report | CI Run Details 📦 Downloadable Artifacts
|
… deterministic joins (#1319) * test(network/signin): de-flake state-machine deadline-polls by joining the actual work Four tests waited for fire-and-forget async work via a fixed wall-clock deadline (asyncAfter probes / Task.sleep settle windows) and starved under CI parallel oversubscription — the deadline elapses before the awaited work lands and the test times out even though the work DID complete. Replaced each poll with a deterministic JOIN of the real work; assertions unchanged. - TPPSignInBusinessLogicStateMachineTests.testLogIn_racingAuthDocLoad_firesRequestOnceReady: dropped the 0.5s/1.5s asyncAfter probes; wake the instant the mock records the credential request via a new join seam. - UnifiedOPDSServiceStateMachineTests.testFetchLoansFeed_blocksUntilLoaded_thenFetches & OPDSFeedServiceStateMachineTests (same name): dropped the 80ms settle sleep and joined the held fetch Task with `await fetchTask.value`. Mid-flight negatives hold without a settle — the awaitReady gate can't fire HTTP or complete while state is .detailsLoading. - NotificationServiceStateMachineTests.testHoldNotification_blocksUntilLoaded_thenNavigates: dropped the 30ms subscribe-settle sleep; the state stream is a CurrentValueSubject so a late subscriber replays the terminal state — no lost-wakeup race, `await outcomeTask` is the join. Added a test-only `onExecuteRequest` join seam to TPPRequestExecutorMock (fires synchronously as executeRequest records a URL; cleared in reset()). **Scope:** test-only. Zero production LOC changed — all edits under PalaceTests/. The sign-in test is money-path but its production path (logIn/awaitReadyThenRetryLogIn) is untouched; only the mock and the test's wait mechanism changed, and the prod-behavior assertions (request fires, carries /patrons/me) are preserved. **Not done:** did not touch SignInWebSheetIntegrationTests (real WKWebView cold-start, already documented FLAKE-003-OK — env-bound, not a deadline poll). **Deferred:** none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(catalog): de-flake parallel-starvation deadline polls via work-unit joins CI runs PalaceTests with sim-clone oversubscription; tests that waited on a fire-and-forget async op (Task.detached refresh, load()'s currentLoadTask) via a fixed wall-clock deadline (awaitCondition / fulfillment(timeout:) / Task.sleep poll) starved past the 120s executionTimeAllowance. Fix: join the actual work unit instead of polling a deadline. CatalogCacheKeyAndIsolationTests: - testConcurrentStaleReads_AcrossDistinctURLs_EachURLGetsItsOwnRefresh — join all 3 concurrent .utility refreshes via _awaitAllBackgroundRefreshesForTesting - testConcurrentStaleReads_BackgroundRefreshDoesNotPoisonNeighborKey — join the single refresh via _awaitBackgroundRefreshForTesting - testInMemoryCache_AfterSystemMemoryWarning_... + testMemoryWarning_ClearsBoth — handler evicts synchronously under NotificationCenter.post; assert directly - removed the now-orphaned awaitCondition wall-clock poll helper CatalogDomainTests: dropped two stale "give cache queue time" Task.sleep(100ms) waits — invalidateCache is a synchronous lock mutation (no queue since the SWR refactor). CatalogViewModelTests: 8 sites (load error/nil/offline, forceRefresh, SWR A→B→A waitUntilLoaded) now join currentLoadTask via _awaitLoadForTesting. Reconnect auto-reload left as a bounded observable-effect poll (documented un-joinable: the reload hops through two untracked bare Tasks off the reachability publisher). SideloadedLaneTests: awaitLoaded/awaitFailure join currentLoadTask instead of a 20s $state fulfillment. Production seams (additive, behavior-identical, gated/defaulted — prod path unchanged): - CatalogRepository._trackRefreshTasksForTesting() + _awaitAllBackgroundRefreshesForTesting() (refreshTracking flag defaults off; prod never appends/grows the tracked-task array) - CatalogViewModel._awaitLoadForTesting() (reads the already-retained currentLoadTask handle) No assertions weakened; no XCTSkip, no raised executionTimeAllowance, no disabled parallelism. Both production files pass `swiftc -parse`. **Scope:** test-only de-flake batch (CATALOG). Prod change is 2 files / 66 LOC, all additive underscore-prefixed test seams + doc comments — zero runtime behavior change on any production path (new tracking is opt-in and defaults off; the load seam only reads an existing handle). No money-path (borrow/DRM/sign-in) code touched. Not done: the reconnect-auto-reload connectivity-publisher hop still lacks a retained-handle join seam (deferred to the connectivity owner); its test keeps a bounded observable-effect poll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(deflake): join fire-and-forget async work instead of polling wall-clock deadlines (ACCOUNTS/PLATFORM batch) De-flakes fixed-deadline polls that starve under CI sim-clone oversubscription (2 clones on a small runner) and blow the 120s executionTimeAllowance. Every change replaces a wall-clock deadline (Task.sleep-then-assert, RunLoop.run(until:) spin, tight fulfillment ceiling) with a deterministic JOIN on the actual work. Test-side conversions: - AppLaunchTracker{,Extended}Tests: join the fire-and-forget catalogLoaded metrics report via new tracker.awaitPendingMetricsReport() seam (was: sleep 100/200ms). - AppHealthViewModelTests: join loadData() via new viewModel.awaitLoadForTesting() seam (was: 5× sleep 200ms); offline-queue status update now joins the ViewModel's main-hopped $offlineQueueStatus sink via fulfillment (was: sleep 200ms). - AccountStateMachineTests.testStateStream_emitsCurrentThenTransitions: subscription barrier on the stream's first (CurrentValueSubject current-value) emission before driving transitions (was: 2× sleep 30ms; genuine intermediate-state ordering hazard). - AppContainerResetTests: join presenter $hasActiveSession @published emission (was: 2s RunLoop.main.run(until:) spin). - AccountDetailViewModelTests: settle() joins $isLoadingAuth reaching false (was: yield×3 + sleep 50ms). - ViewModelComputedPropertyTests: drainMainQueueAsync() for the other-book no-react case (was: sleep 200ms). - ActiveSessionsViewModelTests: widen 3 genuine-join fulfillment ceilings 0.5s->5.0s to match the already-fixed site 185 precedent (join mechanism unchanged; only the failure-bound widened so a real join can't starve under oversubscription). Production seams (test-only join points, behavior-identical in production): - AppLaunchTracker: retain the metrics-report Task in metricsReportTask + expose awaitPendingMetricsReport(). Same task still runs detached; we only keep its handle. - AppHealthViewModel: retain the loadData() Task in loadTask + expose awaitLoadForTesting(). Same task still runs; we only keep its handle. No assertions weakened; no XCTSkip; no disabled parallelism. Left as-is (correctly): callback-fulfilled expectations, condition-driven bounded spins (UserAccountPublisher deinit-cancel loop, AccountsManagerCancellation suspend-point spin), negative-assertion settle windows (AccountsManagerFirstRunDecode 0.5s no-second-decode; bounded, not a 120s risk), and awaitReady() slow-path tests (CurrentValueSubject replay makes resolution ordering-independent). RemoteFeatureFlags 10s sleep is a withTimeout cancellation probe, not a deadline poll. **Scope:** 7 target test dirs (Accounts/Platform/Crawl/ViewModels/Migrations/ ImageLoading/AppInfrastructure). Crawl + Migrations had no deadline-polls. Skip-list files (OfflineQueueService/PositionSync/Accessibility/PerformanceMonitor/ StateMachineWiring/AccountDetailsURL/PoolResponsivenessProbe) untouched. **Not done:** no build/verify run (CI verifies per batch brief); no push/merge. **Deferred:** none. No money/critical-path production behavior changed — the two prod seams only retain an existing Task handle for a test join. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reader): join fire-and-forget async instead of polling wall-clock deadlines (de-flake) De-flakes parallel-starvation deadline-poll tests in the reader/PDF/bookmark areas. Under CI's 2-sim-clone oversubscription, tests that waited a FIXED wall-clock interval (Task.sleep / debounce-deadline poll) for a fire-and-forget async op could starve past the deadline and hit the 120s executionTimeAllowance. Fix: JOIN the actual work via retained-Task seams / a persist-completion signal instead of racing a deadline. Also fixes one shared-state root (mock defaulting to the production singleton registry). Prod seams added (all behavior-identical; nil/uncalled in production): - TPPLastReadPositionPoster: retain spawned server-post Tasks + awaitPendingWrites() - TPPReaderTOCBusinessLogic: retain the init TOC-load Task + awaitTOCLoad() - TypographyService: onDidPersistForTesting callback fired after each debounced persist Shared-state root fixed: - MockPDFDocumentMetadata now injects an isolated TPPBookRegistryMock per instance instead of letting the parent init default to AppContainer.production().bookRegistry (every mock was touching the shared singleton — cross-test pollution). Behavior the tests assert (bookmark-set membership) is unchanged. Tests converted from deadline-polls to deterministic joins: - TPPLastReadPositionPosterTests (5 Task.sleep -> awaitPendingWrites) - TPPReaderTOCBusinessLogicTests (4 XCTNSPredicate 10s polls -> awaitTOCLoad) - TypographyServiceTests.testSettingsPersistedAfterDebounce (predicate poll -> persist-signal join) No assertions weakened; no XCTSkip/raised allowances/loosened asserts. **Scope:** Reader2/PDF/Bookmark de-flake only. Left genuine event-driven waits untouched (spy-navigator/completion-fulfilled expectations, count-matched concurrency joins, main-actor-drain barriers) — those already join real work. **Not done:** TypographyServiceIntegrationTests.swift is present in the project group but NOT compiled (no PBXSourcesBuildPhase entry; references a TypographyPersistence protocol that does not exist in prod) — its debounce Task.sleep polls are inert (never run in CI), so left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(audiobooks): de-flake parallel-starvation deadline-poll waits (swarm AUDIOBOOK) CI runs PalaceTests with 2 sim clones on a small runner; tests that wait for a fire-and-forget async op via a FIXED WALL-CLOCK deadline starve under oversubscription and hit the 120s executionTimeAllowance. This converts those waits to deterministic JOINS of the actual work unit. Fixed: - AudiobookBookmarkBusinessLogic{,PositionWrite}Tests: position-write detached Task now joined via a new `_awaitPositionWriteForTesting()` seam (retained Task-handle) instead of `wait(for:timeout:)` on the completion. - NowPlayingCoordinator{,Background}Tests: debounced `Task.sleep` update now joined via a new `_awaitPendingUpdateForTesting()` seam instead of `awaitCondition(timeout:5)` polling MPNowPlayingInfoCenter. - AudiobookSessionPresenterTests: replaced the fixed-50ms `RunLoop.main.run(until:)` publisher-delivery spin with the deterministic FIFO `drainMainQueue()` (16 sites via the shared helper). - AudiobookDataManagerSyncTests: mock POST completions moved off `.global().asyncAfter` onto a serial completion queue + `drainCompletions()` barrier; predicate-poll expectations replaced with `syncQueue.sync {}` + `drainCompletions()` joins. Production seams added are underscore-prefixed, test-only, and behavior- identical (no production call path invokes them; the write Task runs exactly as before, only its handle is now retained). Left as-is (genuine event-joins / un-joinable, noted): AudiobookLoader{,Dispatch, OPDSShapeMatrix}Tests + vendor adapter tests (real AppContainer.production() pipeline / @MainActor-Task hops driven by runloop spin, no clean seam without a DRM-path refactor); AudiobookOpenStateRace / FirstOpenHang (event-firing gate waits); AudioEngineWrapper / LoaderFinalizeBuild (synchronous completions); the debounce-dealloc crash-survival test (no work unit to await by design). **Scope:** test-only de-flake + two underscore-prefixed test-join seams on AudiobookBookmarkBusinessLogic + NowPlayingCoordinator. No behavior change. **Not done:** vendor-adapter (LCP/Bearer/OpenAccess) and AudiobookLoader Task joins — those hit the real fulfillment/DRM pipeline and warrant a swarm, not a lone seam; left with inline reasoning. **Deferred:** none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mybooks): de-flake return/auth/fulfillment deadline-poll waits (join, don't poll) Convert wall-clock deadline-polls on fire-and-forget async to deterministic joins across the return / auth / download-fulfillment money-path tests. Under CI oversubscription (2 sim clones, small runner) these polls (awaitConditionAsync / Task.sleep loops / asyncAfter+wait / while-Date loops) starve and hit the 120s executionTimeAllowance. Join patterns used: - Retained-Task join: await inFlightTasksSnapshotForTesting() Task.value (BookReturnService + DownloadAuthRetryHandler both retain their fire-and- forget Tasks; the auto-removal is the tail of each tracked body, so await task.value joins body+cleanup deterministically). - Synchronous retention insert -> direct assert (returnBook / handleAuth insert the tracked Task synchronously before returning; count is deterministically >=1 with no poll). - Prompt-firing expectation: fulfil an XCTestExpectation from the effect edge (error-publisher sink, reauthenticator.onAuthenticate, delegate.onStartDownload) instead of polling a captured-array count behind a fixed asyncAfter delay. - Main-actor barrier flush (await Task { @mainactor }.value) for all-@mainactor chains and absence-assertions, replacing fixed settle sleeps. Prod seam added (test-only behavior, behavior-identical): - DownloadAuthRetryHandler.inFlightTasksSnapshotForTesting() — a @mainactor read-only copy of the existing inFlightTasks set, mirroring the established BookReturnService seam. Mutates nothing; no production path changes. Un-joinable sites left with an inline note + kept as loud-on-timeout waits (no retention handle available without a larger prod change): - BookSignInRedirectHandler SAML-cookies-expired retry (background Task { }). - LCPFulfillmentHandler stored-fulfillment-task (untracked Task in a void method). - BookReturnService deinit observation (ARC last-release, not awaitable). - TokenRefreshInterceptor coordinator paths — barrier-flush (no retention seam on the interceptor yet; flagged as the complete follow-up). No assertions weakened; no XCTSkip; no raised allowances; parallelism untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mybooks): de-flake download/borrow/registry deadline-poll waits (join, don't poll) Convert fixed-wall-clock deadline polls (awaitCondition / awaitConditionAsync / Task.sleep loops / RunLoop.run(until:)) that waited on fire-and-forget async work into deterministic joins. Under CI oversubscription (2 sim clones) those polls starve and blow the 120s executionTimeAllowance; joining the actual work removes the wall-clock race entirely. Test-side: await the SUT's own async entry points / retained-Task seams, drain-then-assert for synchronous registry+notification effects, and use prompt-firing NotificationCenter expectations (fulfilled by the SUT's own post) in place of RunLoop deadline polls. Publisher joins via a shared awaitPublished/awaitPublishedAsync helper. Prod: add behavior-identical join seams only — fire-and-forget `Task {}` bodies extracted verbatim into `...Async()` siblings whose sync wrapper is `Task { await ...Async() }` (enqueuePendingAsync, startBorrowAsync, limitActiveDownloadsAsync, pauseAllDownloadsAsync, processAsync), plus retained-Task `private(set) var last...Task` handles for tests to await. Same calls, same order, same side effects; no logic, branch, or state-transition change. Mirrors the existing schedulePendingStartsIfPossible→Async split. No assertions weakened, no XCTSkip, no raised allowance, parallelism untouched. **Scope:** de-flake pass over PalaceTests/MyBooks, PalaceTests/Book, PalaceTests/BookStateManagement (return/auth/fulfillment slice committed separately in the preceding commit). Prod seams are money-path (borrow/download) and confirmed test-only-behavior (behavior-identical joins). **Deferred:** a full retention seam for TokenRefreshInterceptor's ~20 fire-and-forget Task sites (too broad for a de-flake pass; two interceptor poll sites left with UNJOINABLE notes). Genuinely un-joinable sites (real debounce timers, ARC-deinit observation, absence-of-effect settles, SAML retry background Tasks with no handle) left in place with inline UNJOINABLE notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(deflake): add #1315's SignOut/MultiLibrary joins the swarm skipped The full-sweep swarm branched off develop (which lacked #1315), so its Network batch skipped TPPSignInBusinessLogicSignOutTests + MultiLibraryTokenIsolationTests believing they were already on develop. They were only in #1315. Bring those two join fixes + the TPPDRMAuthorizingMock._awaitDeauthorizeCalledForTesting seam they need, so the sweep supersedes #1315 completely (BorrowOperationContractTests kept from the mybooks batch, which fixed the whole Borrow slice). **Scope:** test-only (2 test files + 1 test mock); no production change here. * fix(deflake): sync poster join seam (avoid Swift 6 sending self.poster) The reader batch's `awaitPendingWrites() async` seam, called on the @mainactor test's non-Sendable poster, tripped Swift 6 'sending self.poster'. Reshaped to a synchronous `pendingWriteTasksForTesting() -> [Task]` snapshot (a sync call sends nothing; Task is Sendable) and the test awaits the returned handles. Behavior identical; consolidated sweep now builds clean. **Scope:** compile fix for the consolidated de-flake sweep; no behavior change. * revert(deflake): restore DownloadAuthRetryHandler de-flake to develop (broke behavior) The mybooks batch's join conversion of DownloadAuthRetryHandlerTests asserted before the alert-publish/retry-download effect landed → 2 deterministic failures (testAutoBorrowCompletion_whenBorrowFails, testHandle_401_withoutCredentials). This class's async structure was flagged partial/complex; reverting its de-flake (+ the prod seam) to develop's correct version is safer than shipping a broken money-path join. The class may still occasionally flake under parallel load (pre-existing) — that's a separate, non-regressing follow-up. **Scope:** revert 3 files to develop; rest of the sweep unchanged. --------- Co-authored-by: t <t@t.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
Nine
PalaceTestsclasses hit the 120sexecutionTimeAllowanceONLY under CI's 2-sim-clone parallel run — they pass serially, in isolation, and can't be reproduced without parallel clones. Root cause (already diagnosed): each polled a fire-and-forget async op on a fixed wall-clock deadline (awaitCondition/Timer.scheduledTimer/ RunLoop-poll /wait(for:)on a detachedTask). Under the cooperative-thread-pool oversubscription from 2 clones on the macos runner, that op gets deferred past the poll deadline → timeout.This is the convergence work for the deferred
test-pollution-systemicdebt for the frequent parallel-only flakers.Fix — join the actual work, don't poll a deadline
No weakened assertions, no
XCTSkip, no raised allowance, no disabled parallelism.CatalogCacheKeyAndIsolationTests(#1 flaker; 4×awaitCondition(30s))_awaitAllBackgroundRefreshesForTesting()for concurrent refreshes;_awaitBackgroundRefreshForTesting()for single; 2 memory-warning polls were vestigial (sync eviction) → assert directlyAudiobookBookmarkBusinessLogicTests+…PositionWriteTestsTaskvia new_awaitPositionWriteForTesting(); dealloc test captures the handle pre-dealloc;saveBookmarkdebounce → injected near-zerodebounceIntervalTPPPDFDocumentMetadataTestsMockPDFDocumentMetadatainjectedAppContainer.production().bookRegistry; now uses an isolated per-instanceTPPBookRegistryMockTPPSignInBusinessLogicSignOutTestsTPPDRMAuthorizingMock(_awaitDeauthorizeCalledForTesting); no production changeBorrowOperationContractTestsawait MainActor.runinside the awaitedborrowAsync→ deterministic main-actor drain + direct assertMultiLibraryTokenIsolationTestsawait MainActor.run {}to settle the.credentialsStalehop after the awaited refreshActiveSessionsViewModelTests0.5swait(for:)on a synchronous notification re-query →5.0s(matches the sibling already bumped)Production seams added
All behavior-identical (nothing production reads them) and none touch borrow/return/download/DRM/payment money-path logic — they're catalog-cache and audiobook-position-persistence:
CatalogRepository:_awaitAllBackgroundRefreshesForTesting+ a_trackRefreshTasksForTestingarming flag (off in prod → no unbounded handle growth).AudiobookBookmarkBusinessLogic: injectabledebounceInterval(default1.0, prod unchanged) + retained position-writeTaskhandle +_awaitPositionWriteForTesting/_positionWriteTaskHandleForTesting.Verification — real parallel repro
xcodebuild test ... -parallel-testing-enabled YES -maximum-parallel-testing-workers 2 -test-iterations 3over all 9 classes:** TEST EXECUTE SUCCEEDED **, 0 timeouts, 0 restarts, 327/327 passing.Not fixed (by design)
BookRegistrySyncTestsandMultiLibraryTest 11 reachability are signing-env cases (AppContainer.production()keychain, documentedMIGRATED-DEFERRED) that pass on signed CI and are NOT deadline-polls.TPPBookRegistryStateConcurrencyTestshas no wait sites (ImageCache.sharedshared-state).BookRegistrySynccredential-path DI seam is a critical-path production change requiring SoD review — out of scope for a test-only de-flake pass.🤖 Generated with Claude Code