From 016030f3e4694e9c0940257a8e3ace766c31dca9 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 1 Sep 2026 21:19:06 -0500 Subject: [PATCH] A1 (#167): pin all three foreground-service regimes, and the boundary between two of them `ConversionForegroundType.current()` has three arms and the JVM suite executed one. `robolectric.properties` pins everything to `sdk=36`, and `@Config` appears nowhere in `app/src/test`, so 3 lines and 3 of 4 branches were cold. The instrumented test is not a substitute, and the reason is specific rather than general. `ConversionWorkerTest.foregroundTypeMatchesTheRunningApiLevel` asserts against whichever API the leg is, so it covers one arm per leg and never the other two -- and the legs that would cover 33 and 34 are the ones #122 wedges. From docs/coverage-read-findings.md, an API 33 run reported `received: 60` with `failed: unknown`: the regime was exercised and that leg could not have said so if it had broken. This runs all three deterministically in the same ./gradlew invocation. Four classes, not three. 35 shares its answer with 36 and looks redundant; it is the whole point. Relaxing `>= VANILLA_ICE_CREAM` to `>` is invisible at every level except exactly 35 -- measured, not assumed: that mutation failed ForegroundTypeApi35Test alone, while swapping DATA_SYNC and MEDIA_PROCESSING failed 34, 35 and 36. Without the 35 class the first mutation survives the suite. 546 -> 550 JVM tests, 0 failures. No production code changed. Co-Authored-By: Claude Opus 5 (1M context) --- .../work/ForegroundTypeRegimeTest.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/src/test/java/org/libremediaconverter/work/ForegroundTypeRegimeTest.kt diff --git a/app/src/test/java/org/libremediaconverter/work/ForegroundTypeRegimeTest.kt b/app/src/test/java/org/libremediaconverter/work/ForegroundTypeRegimeTest.kt new file mode 100644 index 0000000..1b1fd0e --- /dev/null +++ b/app/src/test/java/org/libremediaconverter/work/ForegroundTypeRegimeTest.kt @@ -0,0 +1,88 @@ +package org.libremediaconverter.work + +import android.content.pm.ServiceInfo +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * [ConversionForegroundType.current] answers differently on each of the three API regimes, and + * until this file only one of them was ever executed. + * + * `app/src/test/resources/robolectric.properties` pins the whole JVM suite to `sdk=36`, so every + * Robolectric test that reaches a `ForegroundInfo` takes the `mediaProcessing` arm and no other. + * The 33 and 34 arms were cold: 3 lines and 3 of 4 branches, measured on `main` at `d354f64`. + * + * **The instrumented test is not a substitute, and the reason is specific.** + * `ConversionWorkerTest.foregroundTypeMatchesTheRunningApiLevel` asserts against whichever API the + * leg happens to be — one arm per leg, never the other two — and the legs that would cover 33 and + * 34 are the ones issue #122 wedges. `docs/coverage-read-findings.md` records an API 33 run that + * reported `received: 60` and `failed: unknown`: the regime *was* exercised, and that leg could + * not have said so if it had broken. Four `@Config` classes here pin all three arms + * deterministically, in the same `./gradlew` invocation as everything else. + * + * `minSdk` is 33, so none of these is dead code — each is a device someone is running the app on. + * + * **SDK 35 is in the list for the boundary, not for the answer.** It shares its answer with 36, + * which would make it look redundant. It is not: relaxing `>= VANILLA_ICE_CREAM` to `>` is invisible + * at every level except exactly 35, so without this class that mutation survives the suite. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class ForegroundTypeApi33Test { + + /** + * Zero rather than a named constant because there is no constant to name: API 33 does not + * require a type, and `mediaProcessing` does not exist here to pass. `ForegroundInfo` reads 0 + * as "no type at all", which is what this regime wants. + */ + @Test + fun `api 33 asks for no foreground service type`() { + assertEquals(0, ConversionForegroundType.current()) + } +} + +/** + * API 34 makes a type mandatory and still has no `mediaProcessing`, so `dataSync` is the only + * sensible fit. See [ForegroundTypeApi33Test] for why this file exists. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class ForegroundTypeApi34Test { + + @Test + fun `api 34 falls back to dataSync, the only type that fits`() { + assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, ConversionForegroundType.current()) + } +} + +/** + * The first level with `mediaProcessing`, and therefore the one that tells `>=` from `>`. + * See [ForegroundTypeApi33Test]. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class ForegroundTypeApi35Test { + + @Test + fun `api 35 is the first level that takes mediaProcessing`() { + assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, ConversionForegroundType.current()) + } +} + +/** + * The level the rest of the suite runs at, asserted here rather than assumed — it is the one arm + * that was already covered, and leaving it out would make this file look like it is about the old + * levels rather than about all three regimes. See [ForegroundTypeApi33Test]. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [36]) +class ForegroundTypeApi36Test { + + @Test + fun `api 36 keeps mediaProcessing`() { + assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, ConversionForegroundType.current()) + } +} -- 2.47.3