Ten fixes from the audit in #7, each its own commit with its own tests. 242 JVM unit tests, 0 failures (up from 180), detekt 0 findings, lint 0 errors, 0 warnings, 1 hint — the pre-existing UsableSpace informational, which belongs to a defect deliberately not fixed here.
Commit
Defect
cfd705a
D2 staged output the user never saved is deleted, instead of waiting for the OS
ec969c4
D3 reattach to conversions and joins the ViewModel did not start
ce4d0ff
D6 the selected tab survives the recreation targetSdk 37 guarantees
dbfc463
D4 a half-written destination is deleted instead of left under the user's name
97fdc49
D14 the native boundary is guarded against what it actually throws
7db3200
D11 the JDK claim is corrected and the backup rules decided
5c27801
D13 work the system refused to start is retried, not failed terminally
dcdcbfd
D10 a cancelled conversion stays cancelled
2a68f03
D8 every job gets a staging path of its own
159320d
D9 a finished file is named after the job that made it
The one to read first
D13 is the reason this branch exists. Work interrupted by process death was failing terminally — confirmed on a Pixel 10 Pro XL, API 37, on a natural dispatch 119 s after kill -9, with reschedule = false and empty output Data. setForeground sat outsidedoWork's try, so a ForegroundServiceStartNotAllowedException bypassed the retry, the error message and staged.delete() all at once. MediaProbe.probe was outside for the same reason.
A denied start now returns Result.retry(), bounded at 10 attempts — derived from the default backoff read out of WorkRequest.kt (30 s, doubling, clamped at 5 h) ≈ 8 h 30 m. The exception is matched on its exact class, never on IllegalStateException, which plenty of ordinary muxer failures also are.
Test infrastructure this adds
Robolectric 4.16.1, pinned — org.robolectric is outside the componentSelection prerelease guard, so 4.+ would have taken 4.17-beta-3. It needs testOptions { unitTests.isIncludeAndroidResources = true } (the module had no testOptions at all), --enable-native-access=ALL-UNNAMED for Java 25, and robolectric.properties with sdk=36 — Robolectric defaults to the manifest's targetSdk of 37, no android-all jar exists for 37, and the class fails to initialise before any test body runs.
Both ViewModels now resolve through the existing ConversionDependencies seam rather than constructing their own OutputPublisher, which is what makes them testable at all.
Known gaps
D15 and D16 were introduced or exposed by this work and are not fixed — both recorded in #7. D16 is the notable one: D13's bound reports through a FAILED job, and D3's reattachment excludes FAILED, so a user not watching at the eleventh attempt still sees an empty screen. Neither commit owns that gap.
Nothing here ran on hardware. All 242 tests are JVM; every androidTest was compile-verified only. Per CLAUDE.md, API 37 needs a manual check on the Pixel 10 Pro XL before release — and D13 in particular was only ever observable there.
hasSpaceFor still reads File.usableSpace. The lint hint is untouched on purpose: the device pass contradicted that finding's premise (see D1 in #7), so its fix is parked pending a near-full-disk measurement.
The merge commits name scratch/integrate-d2-d3, the branch the fixes were integrated on before it was renamed. Cosmetic; the ten commits above are the substance.
Ten fixes from the audit in #7, each its own commit with its own tests. **242 JVM unit tests, 0 failures** (up from 180), detekt 0 findings, lint `0 errors, 0 warnings, 1 hint` — the pre-existing `UsableSpace` informational, which belongs to a defect deliberately not fixed here.
| Commit | Defect |
|---|---|
| `cfd705a` | **D2** staged output the user never saved is deleted, instead of waiting for the OS |
| `ec969c4` | **D3** reattach to conversions and joins the ViewModel did not start |
| `ce4d0ff` | **D6** the selected tab survives the recreation targetSdk 37 guarantees |
| `dbfc463` | **D4** a half-written destination is deleted instead of left under the user's name |
| `97fdc49` | **D14** the native boundary is guarded against what it actually throws |
| `7db3200` | **D11** the JDK claim is corrected and the backup rules decided |
| `5c27801` | **D13** work the system refused to start is retried, not failed terminally |
| `dcdcbfd` | **D10** a cancelled conversion stays cancelled |
| `2a68f03` | **D8** every job gets a staging path of its own |
| `159320d` | **D9** a finished file is named after the job that made it |
## The one to read first
**D13** is the reason this branch exists. Work interrupted by process death was failing terminally — confirmed on a Pixel 10 Pro XL, API 37, on a natural dispatch 119 s after `kill -9`, with `reschedule = false` and empty output `Data`. `setForeground` sat *outside* `doWork`'s `try`, so a `ForegroundServiceStartNotAllowedException` bypassed the retry, the error message and `staged.delete()` all at once. `MediaProbe.probe` was outside for the same reason.
A denied start now returns `Result.retry()`, bounded at 10 attempts — derived from the default backoff read out of `WorkRequest.kt` (30 s, doubling, clamped at 5 h) ≈ 8 h 30 m. The exception is matched on its exact class, never on `IllegalStateException`, which plenty of ordinary muxer failures also are.
## Test infrastructure this adds
Robolectric 4.16.1, **pinned** — `org.robolectric` is outside the `componentSelection` prerelease guard, so `4.+` would have taken `4.17-beta-3`. It needs `testOptions { unitTests.isIncludeAndroidResources = true }` (the module had no `testOptions` at all), `--enable-native-access=ALL-UNNAMED` for Java 25, and `robolectric.properties` with `sdk=36` — Robolectric defaults to the manifest's `targetSdk` of 37, no `android-all` jar exists for 37, and the class fails to initialise before any test body runs.
Both ViewModels now resolve through the existing `ConversionDependencies` seam rather than constructing their own `OutputPublisher`, which is what makes them testable at all.
## Known gaps
- **D15 and D16 were introduced or exposed by this work and are not fixed** — both recorded in #7. D16 is the notable one: D13's bound reports through a FAILED job, and D3's reattachment excludes FAILED, so a user not watching at the eleventh attempt still sees an empty screen. Neither commit owns that gap.
- **Nothing here ran on hardware.** All 242 tests are JVM; every `androidTest` was compile-verified only. Per `CLAUDE.md`, **API 37 needs a manual check on the Pixel 10 Pro XL before release** — and D13 in particular was only ever observable there.
- **`hasSpaceFor` still reads `File.usableSpace`.** The lint hint is untouched on purpose: the device pass contradicted that finding's premise (see D1 in #7), so its fix is parked pending a near-full-disk measurement.
- The merge commits name `scratch/integrate-d2-d3`, the branch the fixes were integrated on before it was renamed. Cosmetic; the ten commits above are the substance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two more fixes pushed — this branch now carries twelve, at 257 JVM tests, 0 failures (up from 242, and from 180 when the work started). detekt 0, lint 0 errors, 0 warnings, 1 hint.
Commit
Defect
b86df47
D5 an unknown input size is told apart from an empty file
c2e6344
D7 WorkManager keeps sole ownership of the progress notification
D5 — InputFile.sizeBytes is now Long?, so "nobody told me" is absence rather than 0L. The two byte-identical copies of queryFile became one InputQuery, which asks twice: OpenableColumns.SIZE, then openFileDescriptor(uri, "r").statSize. That second question is the real behaviour change — it turns the confirmed file:// case measured on the Pixel from unknown into known.
Where the size genuinely cannot be determined, the check reserves headroom only, which is the same number the defect produced by accident. What changed is that it is now the answer to a question that was asked, so the next person can decide differently without first discovering that 0L meant two things.
D7 — publishProgress returns early on isStopped and publishes through setForegroundAsync rather than NotificationManager.notify(1001, …). The initial setForeground in doWork is deliberately left as a suspending call inside the try: converting it for symmetry would move ForegroundServiceStartNotAllowedException into an unobserved future and silently undo 5c27801.
The device's orphan notification now reproduces on the JVM — and must resurrect nothing expected:<0> but was:<1> before the fix. That was previously only observable on hardware, on attempt 3 of 12.
Two limits worth knowing
Work enqueued before b86df47 carries KEY_SIZE_BYTES = 0present for an unmeasurable input, so the vacuous check survives for that job. Not fixable after the fact; self-clearing as WorkManager ages the queue out.
ConverterScreen's "Size unknown" has no test. The repo has no Compose test in either source set, so one would be new infrastructure rather than a test.
Rebased on main after #6 and #7 merged; re-gated green on the merged result.
Two more fixes pushed — this branch now carries **twelve**, at **257 JVM tests, 0 failures** (up from 242, and from 180 when the work started). detekt 0, lint `0 errors, 0 warnings, 1 hint`.
| Commit | Defect |
|---|---|
| `b86df47` | **D5** an unknown input size is told apart from an empty file |
| `c2e6344` | **D7** WorkManager keeps sole ownership of the progress notification |
**D5** — `InputFile.sizeBytes` is now `Long?`, so "nobody told me" is absence rather than `0L`. The two byte-identical copies of `queryFile` became one `InputQuery`, which asks twice: `OpenableColumns.SIZE`, then `openFileDescriptor(uri, "r").statSize`. That second question is the real behaviour change — it turns the confirmed `file://` case measured on the Pixel from unknown into known.
Where the size genuinely cannot be determined, the check reserves headroom only, which is **the same number the defect produced by accident**. What changed is that it is now the answer to a question that was asked, so the next person can decide differently without first discovering that `0L` meant two things.
**D7** — `publishProgress` returns early on `isStopped` and publishes through `setForegroundAsync` rather than `NotificationManager.notify(1001, …)`. The initial `setForeground` in `doWork` is deliberately left as a *suspending* call inside the `try`: converting it for symmetry would move `ForegroundServiceStartNotAllowedException` into an unobserved future and silently undo `5c27801`.
**The device's orphan notification now reproduces on the JVM** — `and must resurrect nothing expected:<0> but was:<1>` before the fix. That was previously only observable on hardware, on attempt 3 of 12.
## Two limits worth knowing
- **Work enqueued before `b86df47`** carries `KEY_SIZE_BYTES = 0` *present* for an unmeasurable input, so the vacuous check survives for that job. Not fixable after the fact; self-clearing as WorkManager ages the queue out.
- **`ConverterScreen`'s "Size unknown" has no test.** The repo has no Compose test in either source set, so one would be new infrastructure rather than a test.
Rebased on `main` after #6 and #7 merged; re-gated green on the merged result.
🤖 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.
Ten fixes from the audit in #7, each its own commit with its own tests. 242 JVM unit tests, 0 failures (up from 180), detekt 0 findings, lint
0 errors, 0 warnings, 1 hint— the pre-existingUsableSpaceinformational, which belongs to a defect deliberately not fixed here.cfd705aec969c4ce4d0ffdbfc46397fdc497db32005c27801dcdcbfd2a68f03159320dThe one to read first
D13 is the reason this branch exists. Work interrupted by process death was failing terminally — confirmed on a Pixel 10 Pro XL, API 37, on a natural dispatch 119 s after
kill -9, withreschedule = falseand empty outputData.setForegroundsat outsidedoWork'stry, so aForegroundServiceStartNotAllowedExceptionbypassed the retry, the error message andstaged.delete()all at once.MediaProbe.probewas outside for the same reason.A denied start now returns
Result.retry(), bounded at 10 attempts — derived from the default backoff read out ofWorkRequest.kt(30 s, doubling, clamped at 5 h) ≈ 8 h 30 m. The exception is matched on its exact class, never onIllegalStateException, which plenty of ordinary muxer failures also are.Test infrastructure this adds
Robolectric 4.16.1, pinned —
org.robolectricis outside thecomponentSelectionprerelease guard, so4.+would have taken4.17-beta-3. It needstestOptions { unitTests.isIncludeAndroidResources = true }(the module had notestOptionsat all),--enable-native-access=ALL-UNNAMEDfor Java 25, androbolectric.propertieswithsdk=36— Robolectric defaults to the manifest'stargetSdkof 37, noandroid-alljar exists for 37, and the class fails to initialise before any test body runs.Both ViewModels now resolve through the existing
ConversionDependenciesseam rather than constructing their ownOutputPublisher, which is what makes them testable at all.Known gaps
androidTestwas compile-verified only. PerCLAUDE.md, API 37 needs a manual check on the Pixel 10 Pro XL before release — and D13 in particular was only ever observable there.hasSpaceForstill readsFile.usableSpace. The lint hint is untouched on purpose: the device pass contradicted that finding's premise (see D1 in #7), so its fix is parked pending a near-full-disk measurement.scratch/integrate-d2-d3, the branch the fixes were integrated on before it was renamed. Cosmetic; the ten commits above are the substance.🤖 Generated with Claude Code
Two more fixes pushed — this branch now carries twelve, at 257 JVM tests, 0 failures (up from 242, and from 180 when the work started). detekt 0, lint
0 errors, 0 warnings, 1 hint.b86df47c2e6344D5 —
InputFile.sizeBytesis nowLong?, so "nobody told me" is absence rather than0L. The two byte-identical copies ofqueryFilebecame oneInputQuery, which asks twice:OpenableColumns.SIZE, thenopenFileDescriptor(uri, "r").statSize. That second question is the real behaviour change — it turns the confirmedfile://case measured on the Pixel from unknown into known.Where the size genuinely cannot be determined, the check reserves headroom only, which is the same number the defect produced by accident. What changed is that it is now the answer to a question that was asked, so the next person can decide differently without first discovering that
0Lmeant two things.D7 —
publishProgressreturns early onisStoppedand publishes throughsetForegroundAsyncrather thanNotificationManager.notify(1001, …). The initialsetForegroundindoWorkis deliberately left as a suspending call inside thetry: converting it for symmetry would moveForegroundServiceStartNotAllowedExceptioninto an unobserved future and silently undo5c27801.The device's orphan notification now reproduces on the JVM —
and must resurrect nothing expected:<0> but was:<1>before the fix. That was previously only observable on hardware, on attempt 3 of 12.Two limits worth knowing
b86df47carriesKEY_SIZE_BYTES = 0present for an unmeasurable input, so the vacuous check survives for that job. Not fixable after the fact; self-clearing as WorkManager ages the queue out.ConverterScreen's "Size unknown" has no test. The repo has no Compose test in either source set, so one would be new infrastructure rather than a test.Rebased on
mainafter #6 and #7 merged; re-gated green on the merged result.🤖 Generated with Claude Code