C4: ConcatWorker's cancellation and give-up arms #147

Merged
JMR-dev merged 3 commits from test/concatworker-failure-arms into test/container-capabilities-audio 2026-08-27 13:56:05 +00:00
JMR-dev commented 2026-08-27 03:33:07 +00:00 (Migrated from github.com)

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

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