ConversionWorker has WorkerCancellationTest and DeniedForegroundStartTest. Its twin had only the retry case — a join whose foreground start is denied retries instead of failing terminally already existed — so two of ConcatWorker's three failure exits were cold: the CancellationException arm (:92, :95-96) and FOREGROUND_DENIED (:105-106).
Four tests, added to the files that own each rule rather than to a new ConcatWorker file. That matches how this suite is organised: a file per rule, tested across both workers.
The cancellation seam is the part worth reviewing
The conversion twin cancels inside the engine, which is honest there because ConversionDependencies has a seam for it. ConcatWorker calls ConcatEngine directly and has none — it is native, and nothing here gets past it — so the cancellation is injected at the only other point inside the try: setForeground.
That is a real shape, not a contrivance. A job cancelled while WorkManager is promoting it to the foreground is precisely when that window is open, and the catch arm cannot tell where in the try the cancellation came from. The mechanism is the one DeniedForegroundStartTest already documents, carrying a different exception.
FailedFuture moved to WorkerStubs.kt
Two tests now inject two different failures through it, and Kotlin will not take two file-private top-level classes of one name in one package. WorkerStubs.kt's own rule — scaffolding more than one test needs — is what decided where it goes.
Mutations — four run, four red, each isolated
mutation
reddens
cancellation arm → Result.failure
propagation test only
drop staged.delete() on cancellation
cancellation-partial test only
FOREGROUND_DENIED → Result.retry()
past-the-bound test only
drop staged.delete() on the Throwable path
give-up-partial test only
Both delete tests write a partial first, so a missing delete() cannot pass by asking whether a file nobody wrote is absent.
Coverage
:92, :95-96 and :105-106 now covered. Missed branches 4 → 3. What remains is exactly what the ticket scoped out: the two input guards (e2e-covered by UnopenableUriTest and ConcatWorkerTest), the ConcatEngine success path (native), and getForegroundInfo — #88's named exemption, deliberately not re-covered here.
Local gate green: ktlintCheck, detekt, testDebugUnitTest, compileDebugAndroidTestKotlin.
Closes #138.
`ConversionWorker` has `WorkerCancellationTest` and `DeniedForegroundStartTest`. Its twin had only the retry case — `a join whose foreground start is denied retries instead of failing terminally` already existed — so **two of ConcatWorker's three failure exits were cold**: the `CancellationException` arm (`:92, :95-96`) and `FOREGROUND_DENIED` (`:105-106`).
Four tests, added to the files that own each rule rather than to a new ConcatWorker file. That matches how this suite is organised: a file per rule, tested across both workers.
### The cancellation seam is the part worth reviewing
The conversion twin cancels **inside the engine**, which is honest there because `ConversionDependencies` has a seam for it. `ConcatWorker` calls `ConcatEngine` directly and has none — it is native, and nothing here gets past it — so the cancellation is injected at the only other point inside the `try`: `setForeground`.
That is a real shape, not a contrivance. A job cancelled while WorkManager is promoting it to the foreground is precisely when that window is open, and the `catch` arm cannot tell where in the `try` the cancellation came from. The mechanism is the one `DeniedForegroundStartTest` already documents, carrying a different exception.
### `FailedFuture` moved to `WorkerStubs.kt`
Two tests now inject two different failures through it, and Kotlin will not take two file-private top-level classes of one name in one package. `WorkerStubs.kt`'s own rule — scaffolding more than one test needs — is what decided where it goes.
### Mutations — four run, four red, each isolated
| mutation | reddens |
|---|---|
| cancellation arm → `Result.failure` | propagation test only |
| drop `staged.delete()` on cancellation | cancellation-partial test only |
| `FOREGROUND_DENIED` → `Result.retry()` | past-the-bound test only |
| drop `staged.delete()` on the `Throwable` path | give-up-partial test only |
Both delete tests write a partial **first**, so a missing `delete()` cannot pass by asking whether a file nobody wrote is absent.
### Coverage
`:92`, `:95-96` and `:105-106` now covered. Missed branches 4 → **3**. What remains is exactly what the ticket scoped out: the two input guards (e2e-covered by `UnopenableUriTest` and `ConcatWorkerTest`), the `ConcatEngine` success path (native), and `getForegroundInfo` — **#88's named exemption**, deliberately not re-covered here.
Local gate green: `ktlintCheck`, `detekt`, `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 #138.
ConversionWorkerhasWorkerCancellationTestandDeniedForegroundStartTest. Its twin had only the retry case —a join whose foreground start is denied retries instead of failing terminallyalready existed — so two of ConcatWorker's three failure exits were cold: theCancellationExceptionarm (:92, :95-96) andFOREGROUND_DENIED(:105-106).Four tests, added to the files that own each rule rather than to a new ConcatWorker file. That matches how this suite is organised: a file per rule, tested across both workers.
The cancellation seam is the part worth reviewing
The conversion twin cancels inside the engine, which is honest there because
ConversionDependencieshas a seam for it.ConcatWorkercallsConcatEnginedirectly and has none — it is native, and nothing here gets past it — so the cancellation is injected at the only other point inside thetry:setForeground.That is a real shape, not a contrivance. A job cancelled while WorkManager is promoting it to the foreground is precisely when that window is open, and the
catcharm cannot tell where in thetrythe cancellation came from. The mechanism is the oneDeniedForegroundStartTestalready documents, carrying a different exception.FailedFuturemoved toWorkerStubs.ktTwo tests now inject two different failures through it, and Kotlin will not take two file-private top-level classes of one name in one package.
WorkerStubs.kt's own rule — scaffolding more than one test needs — is what decided where it goes.Mutations — four run, four red, each isolated
Result.failurestaged.delete()on cancellationFOREGROUND_DENIED→Result.retry()staged.delete()on theThrowablepathBoth delete tests write a partial first, so a missing
delete()cannot pass by asking whether a file nobody wrote is absent.Coverage
:92,:95-96and:105-106now covered. Missed branches 4 → 3. What remains is exactly what the ticket scoped out: the two input guards (e2e-covered byUnopenableUriTestandConcatWorkerTest), theConcatEnginesuccess path (native), andgetForegroundInfo— #88's named exemption, deliberately not re-covered here.Local gate green:
ktlintCheck,detekt,testDebugUnitTest,compileDebugAndroidTestKotlin.🤖 Generated with Claude Code