Every existing test throws with a message, so the right-hand side had never been evaluated anywhere in the suite. A Throwable carrying none is not exotic — RuntimeException(), IOException() and most platform exceptions raised without an argument all have a null message, and a native engine that dies is exactly where one comes from.
Why the worker case survived three waves — measured, not reasoned
ConversionStateMappingTest's "a failure with nothing said still says something" looks like it covers :316. It does not: it drives the read side, map(FAILED, Data.EMPTY), and that side has a fallback of its own (ConversionViewModel.kt:147-149):
Mutate the worker to .orEmpty() and it writes KEY_ERROR to "", which the ViewModel turns straight back into the same constant. A test asserting on the resulting Failed state stays green while the worker's fallback is broken.
I checked this rather than assuming it. With :316 mutated, the full suite reported:
MessagelessFailureTest > a conversion that fails without a message still reports one FAILED
587 tests completed, 1 failed
Exactly one of 587, and it is the new one. Every existing test — including the one that appears to cover this — stayed green.
So the worker case reads KEY_ERROR off the worker's own Result, before anything downstream can repair it. The two save cases have no such second line (both write _state.value directly), so the state is right there — and both also assert pending still travels, since a fallback that dropped the handle would leave the file unreachable from the very screen that just said the save failed.
One class, against the ticket's suggestion of three
#193 proposed putting each case beside the behaviour it neighbours. They are together instead: one rule at three layers, and the masking above needs explaining once rather than three times. FailedSaveRetryTest already sets the precedent for both ViewModels in one file; this adds one worker to that shape.
Acceptance: mutations run and restored
site
mutation
result
ConversionWorker:316
?: GENERIC_FAILURE_MESSAGE → .orEmpty()
1 of 587 red (this file)
ConversionViewModel:631
?: SAVE_FAILED_MESSAGE → .orEmpty()
red
JoinViewModel:416
?: SAVE_FAILED_MESSAGE → .orEmpty()
red
Each to .orEmpty(), never to a different constant — that would only prove the test reads a constant, which is the vacuous shape wave 3 rejected twice.
Verification
assembleDebug + testDebugUnitTest (full suite) + compileDebugAndroidTestKotlin + ktlintCheck + detekt + lintDebug — all green, production tree verified clean of mutation leftovers.
Closes #193.
## The gap
Three sites, all `ci == 0` before this, all the same rule:
```
work/ConversionWorker.kt:316 cause.message ?: GENERIC_FAILURE_MESSAGE
convert/ConversionViewModel.kt:631 e.message ?: SAVE_FAILED_MESSAGE
join/JoinViewModel.kt:416 e.message ?: SAVE_FAILED_MESSAGE
```
Every existing test throws *with* a message, so the right-hand side had never been evaluated anywhere in the suite. A `Throwable` carrying none is not exotic — `RuntimeException()`, `IOException()` and most platform exceptions raised without an argument all have a null message, and a native engine that dies is exactly where one comes from.
## Why the worker case survived three waves — measured, not reasoned
`ConversionStateMappingTest`'s *"a failure with nothing said still says something"* looks like it covers `:316`. It does not: it drives the **read** side, `map(FAILED, Data.EMPTY)`, and that side has a fallback of its own (`ConversionViewModel.kt:147-149`):
```kotlin
update.outputData.getString(ConversionWorker.KEY_ERROR)
?.takeIf { it.isNotBlank() }
?: ConversionWorker.GENERIC_FAILURE_MESSAGE
```
Mutate the worker to `.orEmpty()` and it writes `KEY_ERROR to ""`, which the ViewModel turns straight back into the same constant. **A test asserting on the resulting `Failed` state stays green while the worker's fallback is broken.**
I checked this rather than assuming it. With `:316` mutated, the full suite reported:
```
MessagelessFailureTest > a conversion that fails without a message still reports one FAILED
587 tests completed, 1 failed
```
**Exactly one of 587**, and it is the new one. Every existing test — including the one that appears to cover this — stayed green.
So the worker case reads `KEY_ERROR` off the worker's own `Result`, before anything downstream can repair it. The two save cases have no such second line (both write `_state.value` directly), so the state is right there — and both also assert `pending` still travels, since a fallback that dropped the handle would leave the file unreachable from the very screen that just said the save failed.
## One class, against the ticket's suggestion of three
#193 proposed putting each case beside the behaviour it neighbours. They are together instead: one rule at three layers, and the masking above needs explaining once rather than three times. `FailedSaveRetryTest` already sets the precedent for both ViewModels in one file; this adds one worker to that shape.
## Acceptance: mutations run and restored
| site | mutation | result |
|---|---|---|
| `ConversionWorker:316` | `?: GENERIC_FAILURE_MESSAGE` → `.orEmpty()` | **1 of 587 red** (this file) |
| `ConversionViewModel:631` | `?: SAVE_FAILED_MESSAGE` → `.orEmpty()` | **red** |
| `JoinViewModel:416` | `?: SAVE_FAILED_MESSAGE` → `.orEmpty()` | **red** |
Each to `.orEmpty()`, never to a different constant — that would only prove the test reads a constant, which is the vacuous shape wave 3 rejected twice.
## Verification
`assembleDebug` + `testDebugUnitTest` (full suite) + `compileDebugAndroidTestKotlin` + `ktlintCheck` + `detekt` + `lintDebug` — all green, production tree verified clean of mutation leftovers.
🤖 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 #193.
The gap
Three sites, all
ci == 0before this, all the same rule:Every existing test throws with a message, so the right-hand side had never been evaluated anywhere in the suite. A
Throwablecarrying none is not exotic —RuntimeException(),IOException()and most platform exceptions raised without an argument all have a null message, and a native engine that dies is exactly where one comes from.Why the worker case survived three waves — measured, not reasoned
ConversionStateMappingTest's "a failure with nothing said still says something" looks like it covers:316. It does not: it drives the read side,map(FAILED, Data.EMPTY), and that side has a fallback of its own (ConversionViewModel.kt:147-149):Mutate the worker to
.orEmpty()and it writesKEY_ERROR to "", which the ViewModel turns straight back into the same constant. A test asserting on the resultingFailedstate stays green while the worker's fallback is broken.I checked this rather than assuming it. With
:316mutated, the full suite reported:Exactly one of 587, and it is the new one. Every existing test — including the one that appears to cover this — stayed green.
So the worker case reads
KEY_ERRORoff the worker's ownResult, before anything downstream can repair it. The two save cases have no such second line (both write_state.valuedirectly), so the state is right there — and both also assertpendingstill travels, since a fallback that dropped the handle would leave the file unreachable from the very screen that just said the save failed.One class, against the ticket's suggestion of three
#193 proposed putting each case beside the behaviour it neighbours. They are together instead: one rule at three layers, and the masking above needs explaining once rather than three times.
FailedSaveRetryTestalready sets the precedent for both ViewModels in one file; this adds one worker to that shape.Acceptance: mutations run and restored
ConversionWorker:316?: GENERIC_FAILURE_MESSAGE→.orEmpty()ConversionViewModel:631?: SAVE_FAILED_MESSAGE→.orEmpty()JoinViewModel:416?: SAVE_FAILED_MESSAGE→.orEmpty()Each to
.orEmpty(), never to a different constant — that would only prove the test reads a constant, which is the vacuous shape wave 3 rejected twice.Verification
assembleDebug+testDebugUnitTest(full suite) +compileDebugAndroidTestKotlin+ktlintCheck+detekt+lintDebug— all green, production tree verified clean of mutation leftovers.🤖 Generated with Claude Code