Two commits, one per ticket. #134 is an enabler with no consumer of its own, so it ships with the first thing that consumes it — that is also the only way its "done means" is checkable.
#134 — move the fake providers to scaffolding, let them answer wrongly
FakeSafProvider lived inside OutputPublisherPublishTest.kt and could only answer correctly: its row was always (file.name, file.length()). Moved to FakeProviders.kt in the same package, following StagingCleanupSupport.kt and ParkedPickDispatcher.kt. UnreliableOutputStream stays behind — it serves one test, which is the line WorkerStubs.kt draws.
Added RowShape: eight ways a provider can answer a metadata query. Column granularity is deliberate — OutputPublisher reads only SIZE, InputQuery reads both and reaches different answers depending on which is bad — and so is keeping null, missing, negative, no-row and throws distinct rather than folding them into one "bad" case.
No production change. OutputPublisherPublishTest, OutputPublisherStagingTest and UnknownInputSizeTest pass unchanged.
#137 — pin what InputQuery makes of a metadata row
Nothing had ever handed InputQuery a row. firstRow's body, displayNameOrNull and sizeOrNull had never executed in the JVM suite.
Nine tests. What they pin is not "reads a cursor" — that passes against almost any implementation — but the rule the class exists for: a size nobody could determine must arrive as null, never 0.hasSpaceFor(0) is only "is there 128 MB free".
Mutations run — three bite, one does not
mutation
result
drop takeIf { it >= 0 } from sizeOrNull
red — negative-size test
drop !isNull(it) from sizeOrNull
red — null-size test
drop the runCatching in firstRow
red — throwing-provider test
drop !isNull(it) from displayNameOrNull
green — does not bite
That last row is in the test's KDoc as a named exemption, not papered over. Measured: MatrixCursor.getString on a null cell returns null, while getLong returns 0. So the guard is load-bearing on the size path — it is exactly what stops a null becoming a real number — and unfalsifiable on the name path, where getString already yields null. It stays regardless: Cursor.getString's contract makes throwing-on-null implementation-defined, so a real provider may do what MatrixCursor does not.
Numbers
InputQuery.kt now has no never-executed lines (was 90, 104-105, 107-108). Suite 456 → 465 tests. Branch coverage 63.8% → 65.6%; line 84.9% → 85.0%. The coverage move is not the acceptance — the mutation table is.
Local gate green: ktlintCheck, detekt, lintDebug, testDebugUnitTest, compileDebugAndroidTestKotlin.
Closes #134. Closes #137.
Two commits, one per ticket. #134 is an enabler with no consumer of its own, so it ships with the first thing that consumes it — that is also the only way its "done means" is checkable.
### #134 — move the fake providers to scaffolding, let them answer wrongly
`FakeSafProvider` lived inside `OutputPublisherPublishTest.kt` and could only answer *correctly*: its row was always `(file.name, file.length())`. Moved to `FakeProviders.kt` in the same package, following `StagingCleanupSupport.kt` and `ParkedPickDispatcher.kt`. `UnreliableOutputStream` stays behind — it serves one test, which is the line `WorkerStubs.kt` draws.
Added `RowShape`: eight ways a provider can answer a metadata query. Column granularity is deliberate — `OutputPublisher` reads only `SIZE`, `InputQuery` reads both and reaches different answers depending on which is bad — and so is keeping null, missing, negative, no-row and throws distinct rather than folding them into one "bad" case.
No production change. `OutputPublisherPublishTest`, `OutputPublisherStagingTest` and `UnknownInputSizeTest` pass unchanged.
### #137 — pin what `InputQuery` makes of a metadata row
Nothing had ever handed `InputQuery` a row. `firstRow`'s body, `displayNameOrNull` and `sizeOrNull` had **never executed** in the JVM suite.
Nine tests. What they pin is not "reads a cursor" — that passes against almost any implementation — but the rule the class exists for: **a size nobody could determine must arrive as `null`, never `0`.** `hasSpaceFor(0)` is only "is there 128 MB free".
### Mutations run — three bite, one does not
| mutation | result |
|---|---|
| drop `takeIf { it >= 0 }` from `sizeOrNull` | **red** — negative-size test |
| drop `!isNull(it)` from `sizeOrNull` | **red** — null-size test |
| drop the `runCatching` in `firstRow` | **red** — throwing-provider test |
| drop `!isNull(it)` from `displayNameOrNull` | **green — does not bite** |
That last row is in the test's KDoc as a **named exemption**, not papered over. Measured: `MatrixCursor.getString` on a null cell returns `null`, while `getLong` returns **`0`**. So the guard is load-bearing on the size path — it is exactly what stops a null becoming a real number — and unfalsifiable on the name path, where `getString` already yields null. It stays regardless: `Cursor.getString`'s contract makes throwing-on-null *implementation-defined*, so a real provider may do what `MatrixCursor` does not.
### Numbers
`InputQuery.kt` now has **no never-executed lines** (was 90, 104-105, 107-108). Suite 456 → 465 tests. Branch coverage 63.8% → **65.6%**; line 84.9% → 85.0%. The coverage move is not the acceptance — the mutation table is.
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 #134. Closes #137.
Two commits, one per ticket. #134 is an enabler with no consumer of its own, so it ships with the first thing that consumes it — that is also the only way its "done means" is checkable.
#134 — move the fake providers to scaffolding, let them answer wrongly
FakeSafProviderlived insideOutputPublisherPublishTest.ktand could only answer correctly: its row was always(file.name, file.length()). Moved toFakeProviders.ktin the same package, followingStagingCleanupSupport.ktandParkedPickDispatcher.kt.UnreliableOutputStreamstays behind — it serves one test, which is the lineWorkerStubs.ktdraws.Added
RowShape: eight ways a provider can answer a metadata query. Column granularity is deliberate —OutputPublisherreads onlySIZE,InputQueryreads both and reaches different answers depending on which is bad — and so is keeping null, missing, negative, no-row and throws distinct rather than folding them into one "bad" case.No production change.
OutputPublisherPublishTest,OutputPublisherStagingTestandUnknownInputSizeTestpass unchanged.#137 — pin what
InputQuerymakes of a metadata rowNothing had ever handed
InputQuerya row.firstRow's body,displayNameOrNullandsizeOrNullhad never executed in the JVM suite.Nine tests. What they pin is not "reads a cursor" — that passes against almost any implementation — but the rule the class exists for: a size nobody could determine must arrive as
null, never0.hasSpaceFor(0)is only "is there 128 MB free".Mutations run — three bite, one does not
takeIf { it >= 0 }fromsizeOrNull!isNull(it)fromsizeOrNullrunCatchinginfirstRow!isNull(it)fromdisplayNameOrNullThat last row is in the test's KDoc as a named exemption, not papered over. Measured:
MatrixCursor.getStringon a null cell returnsnull, whilegetLongreturns0. So the guard is load-bearing on the size path — it is exactly what stops a null becoming a real number — and unfalsifiable on the name path, wheregetStringalready yields null. It stays regardless:Cursor.getString's contract makes throwing-on-null implementation-defined, so a real provider may do whatMatrixCursordoes not.Numbers
InputQuery.ktnow has no never-executed lines (was 90, 104-105, 107-108). Suite 456 → 465 tests. Branch coverage 63.8% → 65.6%; line 84.9% → 85.0%. The coverage move is not the acceptance — the mutation table is.Local gate green:
ktlintCheck,detekt,lintDebug,testDebugUnitTest,compileDebugAndroidTestKotlin.🤖 Generated with Claude Code