Wait for the pick the launcher test is about #241

Merged
JMR-dev merged 1 commits from fix/launcher-wiring-waits-for-the-pick into main 2026-09-06 06:41:55 +00:00
JMR-dev commented 2026-09-06 06:34:20 +00:00 (Migrated from github.com)

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

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)
Sign in to join this conversation.