A provider that has gone away throws FileNotFoundException from inside the call — a destination the provider will not open does not stay behind as an empty file already drives that. A provider that is present and declines returns null, and no fake provider can be asked to produce that on demand.
openDestination is the seam. The test asserts the failure names the destination, which is what separates the ?: error(...) from an NPE thrown inside use and surfacing as "Conversion failed." with a null message.
#143 — the sweep's re-read, and a correction to the ticket
The seam #143 proposed does not reach the branch. I cut entriesIn(dir) as suggested, wrote the race test, and the mutation came back green: overriding the listing fires before the entries are snapshotted, so StagingSweep.collectable is handed the new timestamp, the file is never proposed for deletion, and the guard is never exercised. Deleting the guard outright changed nothing.
The race being modelled is a file that was collectable when the snapshot was taken and is not by the time the delete comes round — one worker resuming in this same process. So the seam has to sit at the snapshot, not around listFiles(). snapshot(listing) does, and deleting the guard now reddens the test. The KDoc on the seam records why it is where it is, so the next person does not repeat the move.
Mutations — three run after the move, three red
mutation
reddens
null stream returns silently
null-return test
null stream via !! instead of ?: error
null-return test
sweep deletes unconditionally
race test
Coverage
OutputPublisher.kt now has no never-executed lines at all. Two partial branches remain, and both are named exemptions rather than gaps:
:216 — runCatching { … }.getOrNull() ?: false in destinationIsKnownEmpty
Both are the failure arm of a runCatching whose body cannot be made to throw through any public entry point. Same shape as the size >= 0 exemption recorded in #149 — worth stating rather than leaving as an implied gap for the next coverage read to re-find.
Local gate green: ktlintCheck, detekt, lintDebug, testDebugUnitTest, compileDebugAndroidTestKotlin.
Closes #142. Closes #143.
> **Stacked on #149**, which is itself stacked on #144. All three touch `OutputPublisher`; merging in order retargets each cleanly.
### #142 — `openOutputStream` refuses two ways
A provider that has gone away throws `FileNotFoundException` from inside the call — `a destination the provider will not open does not stay behind as an empty file` already drives that. A provider that is **present and declines** returns null, and no fake provider can be asked to produce that on demand.
`openDestination` is the seam. The test asserts the failure **names the destination**, which is what separates the `?: error(...)` from an NPE thrown inside `use` and surfacing as "Conversion failed." with a null message.
### #143 — the sweep's re-read, and a correction to the ticket
**The seam #143 proposed does not reach the branch.** I cut `entriesIn(dir)` as suggested, wrote the race test, and the mutation came back **green**: overriding the listing fires *before* the entries are snapshotted, so `StagingSweep.collectable` is handed the new timestamp, the file is never proposed for deletion, and the guard is never exercised. Deleting the guard outright changed nothing.
The race being modelled is a file that **was** collectable when the snapshot was taken and is not by the time the delete comes round — one worker resuming in this same process. So the seam has to sit at the snapshot, not around `listFiles()`. `snapshot(listing)` does, and deleting the guard now reddens the test. The KDoc on the seam records why it is where it is, so the next person does not repeat the move.
### Mutations — three run after the move, three red
| mutation | reddens |
|---|---|
| null stream returns silently | null-return test |
| null stream via `!!` instead of `?: error` | null-return test |
| sweep deletes unconditionally | race test |
### Coverage
**`OutputPublisher.kt` now has no never-executed lines at all.** Two partial branches remain, and both are named exemptions rather than gaps:
- `:216` — `runCatching { … }.getOrNull() ?: false` in `destinationIsKnownEmpty`
- `:304` — `runCatching { canonicalFile }.getOrDefault(absoluteFile)`
Both are the failure arm of a `runCatching` whose body cannot be made to throw through any public entry point. Same shape as the `size >= 0` exemption recorded in #149 — worth stating rather than leaving as an implied gap for the next coverage read to re-find.
Local gate green: `ktlintCheck`, `detekt`, `lintDebug`, `testDebugUnitTest`, `compileDebugAndroidTestKotlin`.
🤖 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 #142. Closes #143.
#142 —
openOutputStreamrefuses two waysA provider that has gone away throws
FileNotFoundExceptionfrom inside the call —a destination the provider will not open does not stay behind as an empty filealready drives that. A provider that is present and declines returns null, and no fake provider can be asked to produce that on demand.openDestinationis the seam. The test asserts the failure names the destination, which is what separates the?: error(...)from an NPE thrown insideuseand surfacing as "Conversion failed." with a null message.#143 — the sweep's re-read, and a correction to the ticket
The seam #143 proposed does not reach the branch. I cut
entriesIn(dir)as suggested, wrote the race test, and the mutation came back green: overriding the listing fires before the entries are snapshotted, soStagingSweep.collectableis handed the new timestamp, the file is never proposed for deletion, and the guard is never exercised. Deleting the guard outright changed nothing.The race being modelled is a file that was collectable when the snapshot was taken and is not by the time the delete comes round — one worker resuming in this same process. So the seam has to sit at the snapshot, not around
listFiles().snapshot(listing)does, and deleting the guard now reddens the test. The KDoc on the seam records why it is where it is, so the next person does not repeat the move.Mutations — three run after the move, three red
!!instead of?: errorCoverage
OutputPublisher.ktnow has no never-executed lines at all. Two partial branches remain, and both are named exemptions rather than gaps::216—runCatching { … }.getOrNull() ?: falseindestinationIsKnownEmpty:304—runCatching { canonicalFile }.getOrDefault(absoluteFile)Both are the failure arm of a
runCatchingwhose body cannot be made to throw through any public entry point. Same shape as thesize >= 0exemption recorded in #149 — worth stating rather than leaving as an implied gap for the next coverage read to re-find.Local gate green:
ktlintCheck,detekt,lintDebug,testDebugUnitTest,compileDebugAndroidTestKotlin.🤖 Generated with Claude Code