fix: give client GameTests a virtual clock/dispatcher instead of real threads - #10
Merged
Conversation
# Conflicts: # AGENTS.md
- ArchieFluidSlot.Serializer's encodeNullableSerializableElement/ decodeNullableSerializableElement (PR #9's structured-encoding fix) need @OptIn(ExperimentalSerializationApi::class), matching the existing pattern elsewhere in the codebase (NetworkChannel.kt, NBT.kt, ...). - AClientGameTestHarness.run()'s new TestCoroutineScheduler.advanceTimeBy(...) call needs @OptIn(ExperimentalCoroutinesApi::class). Also merges origin/1.21.x (now includes the just-merged PR #9) into this branch, resolving one AGENTS.md conflict: replaced the "known open issue, cause not confirmed" flakiness note PR #9 landed with this branch's own "confirmed fixed" note, rather than keeping both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KP2048
marked this pull request as ready for review
August 7, 2026 02:36
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
Follow-up to #9's "known open issue" note: gives client GameTests a virtual clock/dispatcher instead of real threads/wall-clock time, fixing the ~1-in-5 intermittent failure rate documented there.
Root cause
ComposeScreen/ComposeContainerScreenback their coroutine scope withDispatchers.Defaultand real wall-clock time. A composable'sdelay(...)(e.g. a dialog's close animation) genuinely raced real thread scheduling and frame delivery against the harness's tick-based polling - a different test failing each run, no logged exception.Fix
AClientGameTestHarness.run()now installs aComposeTestClockOverride(common/.../gui/ComposeScreen.kt) around each@ClientGameTest: aStandardTestDispatcherplus a per-framescheduler.advanceTimeBy(50)pump called fromrenderNodes(), matching how real Jetpack Compose's own test tooling (ComposeTestRule/runComposeUiTest) avoids this class of race by construction.Real gotcha hit and fixed along the way: the first attempt used
advanceUntilIdle()instead ofadvanceTimeBy(bounded). Composables can run legitimately infinitedelay()loops (TextFieldCore's blinking-cursorLaunchedEffect), andadvanceUntilIdle()only returns once truly nothing is scheduled anywhere - never, for an unboundedly-recurring loop. That hung the render thread permanently the moment any text field was on screen, and every subsequent test in that run then failed too (client's main-thread executor queue never got to run again). Switched to a fixed, bounded per-pump increment instead.Keeping the dependency out of the shipped jar
kotlinx-coroutines-testis dev/test-only:compileOnlyincommon/build.gradle.kts,runtimeLibrary(...)(present for local runs likerunGametestClient, never bundled - and the config that correctly handles NeoForge's classloader-layer artifact transform, not plainruntimeOnly) in the loader modules.ComposeScreenitself never referenceskotlinx.coroutines.test.*symbols directly - it only holds a plainCoroutineDispatcher?and a(() -> Unit)?pump callback (already-bundled core/stdlib types) - so a real player's game, which never has the test dependency on its classpath, never needs to resolve it.Also in this PR
origin/1.21.x(now including fix: NBT serialization crash in ArchieFluidSlot/Storage and ArchieEnergyStorage #9) - resolved oneAGENTS.mdconflict by replacing fix: NBT serialization crash in ArchieFluidSlot/Storage and ArchieEnergyStorage #9's "known open issue, cause not confirmed" note with this PR's "confirmed fixed" one.ArchieFluidSlot.Serializer'sencodeNullableSerializableElement/decodeNullableSerializableElement(from fix: NBT serialization crash in ArchieFluidSlot/Storage and ArchieEnergyStorage #9) need@OptIn(ExperimentalSerializationApi::class), matching the existing pattern elsewhere (NetworkChannel.kt,NBT.kt);AClientGameTestHarness.run()'s newadvanceTimeBy(...)call needs@OptIn(ExperimentalCoroutinesApi::class).Test plan
:common:compileKotlin,:fabric:compileKotlin,:neoforge:compileKotlinall clean, no warningskotlinx-coroutines-testconfirmed resolving correctly viaruntimeLibraryConfigurationneoforge:runGametestClient: 8 consecutive clean runs, 131 individual tests total, zero failures - including the previously-hanging text field test and the originally-flakyConfirmDialog/RadioGrouptestsadvanceUntilIdle()attempt hung the entire suite on the text field test; verified theadvanceTimeByfix resolves it before re-running the full batch🤖 Generated with Claude Code