onInputPicked does not reach Ready on the calling thread. It hops twice — withContext(pickDispatcher) { InputQuery.describe(...) } and then the probe — and pickDispatcher defaults to Dispatchers.IO, a real background thread Compose's idling knows nothing about. So deliver() returned with the state still Idle, and asserting immediately was a race the test usually won.
It lost five times on CI in one day, on PRs whose diffs were instrumented tests and documentation and could not reach it. Two came alongside #125's deadlock and could be argued as fallout; three did not, including one that failed in 1m07s with 629 tests completed, 1 failed and nothing else wrong.
The fix
waitUntil polls through waitForIdle, draining the main looper each time round, so it sees the recomposition the IO hop eventually posts back.
Injecting the dispatcher would be better and is not available here.pickDispatcher is a constructor parameter precisely so a test can pin it — but this test composes the real ConverterScreen, which resolves its own ViewModel through viewModel(). The seam sits one layer below the launcher edge this class exists to cover, so reaching for it would mean not testing that edge.
Evidence
Per the note #218 left — prefer a mutation that must go red to a repetition count when a fix is for something intermittent — the risk here is that adding a wait makes the test vacuous. So:
Mutation: transpose the two launcher callbacks at ConverterScreen.kt:70 and :83, the exact defect this class guards.
LauncherWiringTest > a picked document is loaded as input rather than saved to FAILED
3 tests completed, 1 failed
Still bites. A transposed callback leaves the screen in Idle forever, so it fails on the timeout with the meaning it had before.
Supporting evidence: eight consecutive green runs of the class.
Closes #220.
`onInputPicked` does not reach `Ready` on the calling thread. It hops twice — `withContext(pickDispatcher) { InputQuery.describe(...) }` and then the probe — and `pickDispatcher` defaults to `Dispatchers.IO`, a real background thread Compose's idling knows nothing about. So `deliver()` returned with the state still `Idle`, and asserting immediately was a race the test usually won.
It lost **five times on CI in one day**, on PRs whose diffs were instrumented tests and documentation and could not reach it. Two came alongside #125's deadlock and could be argued as fallout; three did not, including one that failed in 1m07s with `629 tests completed, 1 failed` and nothing else wrong.
## The fix
`waitUntil` polls through `waitForIdle`, draining the main looper each time round, so it sees the recomposition the IO hop eventually posts back.
**Injecting the dispatcher would be better and is not available here.** `pickDispatcher` is a constructor parameter precisely so a test can pin it — but this test composes the real `ConverterScreen`, which resolves its own ViewModel through `viewModel()`. The seam sits one layer *below* the launcher edge this class exists to cover, so reaching for it would mean not testing that edge.
## Evidence
Per the note #218 left — *prefer a mutation that must go red to a repetition count when a fix is for something intermittent* — the risk here is that adding a wait makes the test vacuous. So:
**Mutation:** transpose the two launcher callbacks at `ConverterScreen.kt:70` and `:83`, the exact defect this class guards.
```
LauncherWiringTest > a picked document is loaded as input rather than saved to FAILED
3 tests completed, 1 failed
```
Still bites. A transposed callback leaves the screen in `Idle` forever, so it fails on the timeout with the meaning it had before.
Supporting evidence: eight consecutive green runs of the class.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #220.
onInputPickeddoes not reachReadyon the calling thread. It hops twice —withContext(pickDispatcher) { InputQuery.describe(...) }and then the probe — andpickDispatcherdefaults toDispatchers.IO, a real background thread Compose's idling knows nothing about. Sodeliver()returned with the state stillIdle, and asserting immediately was a race the test usually won.It lost five times on CI in one day, on PRs whose diffs were instrumented tests and documentation and could not reach it. Two came alongside #125's deadlock and could be argued as fallout; three did not, including one that failed in 1m07s with
629 tests completed, 1 failedand nothing else wrong.The fix
waitUntilpolls throughwaitForIdle, draining the main looper each time round, so it sees the recomposition the IO hop eventually posts back.Injecting the dispatcher would be better and is not available here.
pickDispatcheris a constructor parameter precisely so a test can pin it — but this test composes the realConverterScreen, which resolves its own ViewModel throughviewModel(). The seam sits one layer below the launcher edge this class exists to cover, so reaching for it would mean not testing that edge.Evidence
Per the note #218 left — prefer a mutation that must go red to a repetition count when a fix is for something intermittent — the risk here is that adding a wait makes the test vacuous. So:
Mutation: transpose the two launcher callbacks at
ConverterScreen.kt:70and:83, the exact defect this class guards.Still bites. A transposed callback leaves the screen in
Idleforever, so it fails on the timeout with the meaning it had before.Supporting evidence: eight consecutive green runs of the class.
🤖 Generated with Claude Code