S2 + S3: the two OutputPublisher seams, and where the second one actually goes #151

Merged
JMR-dev merged 1 commits from test/outputpublisher-seams into main 2026-08-27 13:54:31 +00:00
JMR-dev commented 2026-08-27 03:58:10 +00:00 (Migrated from github.com)

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

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