Closes#60. Child 4 of 8 decomposing #52. Two new JVM test files, no production change — the diff is app/src/test only.
What was untested
AdvancedPicker is the one leaf on the converter screen that is not stateless: expanded is its own rememberSaveable, and the Container/Video/Audio chip rows live inside AnimatedVisibility(visible = expanded). ValidationError is invoked after that block, so an invalid spec explains itself and offers one-tap fixes while the section is collapsed. That is the only route out of an invalid spec for a user who never opened Advanced — and since the only way to reach an invalid spec is through Advanced, hiding the way out behind the same toggle looks locally sensible. Folding the two if blocks into one compiles, and until now nothing went red.
AdvancedPickerTest — 9 tests
the gate in both directions: collapsed → the panel and all three row tags absent; expanded → all present; collapsed again → absent (the exit transition outlives the click, so the second absence is waited for rather than asserted straight away)
the toggle's label swaps Advanced / Hide advanced
each of the four colliding chip labels clicked through its own row tag: Copy in the video row and Copy in the audio row reach different callbacks, and MP3 in the container row does not report as an audio codec
the error card, every assertion made with the toggle untouched, and asserting the panel is absent in the same test so a future expanded = true default cannot quietly satisfy it
a suggestion chip applies its own spec, clicked from the collapsed section — index 1 of 2, with the count pinned first so a shrunken list says so instead of producing an opaque matcher error
a valid spec renders no error card, open or closed
open and collapsed both survive StateRestorationTester.emulateSavedInstanceStateRestore()
The invalid specs come from ContainerCapabilities.validate(spec, probe) rather than a hand-built Validation.Invalid, so the messages and the suggestions are the real pairing — "This would produce an empty file — keep at least one track." and "WebM cannot hold H.264 video." are asserted as literals against what validate produced.
AdvancedPanelSavedStateTest — 2 tests, and a finding
This is the DestinationSaverTest split that #60 assigned to this child: StateRestorationTester saves into an in-memory map, so it discriminates rememberSaveable from remember and stops there.
Driving a real SaveableStateRegistry through the composition turned up something the ticket's framing did not expect. The saved representation is not the Boolean.rememberSaveable { mutableStateOf(false) } passes no stateSaver, so autoSaver saves the MutableStateitself — the registry hands back an androidx.compose.runtime.ParcelableSnapshotMutableState. That survives a rotation only because mutableStateOf on Android returns a Parcelable implementation; the same call on a plain JVM does not. So the thing standing between an open panel and a rotation that closes it is a platform-specific detail of a factory function nothing in this repo names directly, and an in-memory map cannot tell the two apart. The test writes what the registry produced into a real Bundle, round-trips it through a real Parcel, and asserts the panel comes back open.
canBeSaved = { true } is passed to the registry on purpose: a predicate mirroring Bundle acceptance would be a hand-written copy of the thing under test, and a false from it drops the entry silently — the same symptom as the remember regression. The type is checked on the way out instead.
Mutations — both run, output verbatim
1. ValidationError call moved inside the AnimatedVisibility block (ConverterScreen.kt:365)
AdvancedPickerTest > a codec the container cannot hold explains itself while the section is collapsed FAILED
java.lang.AssertionError: Assert failed: The component with Text + InputText + EditableText contains 'WebM cannot hold H.264 video.' (ignoreCase: false) is not displayed!
AdvancedPickerTest > a suggestion chip applies its own spec without the section ever being opened FAILED
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [MP4 · Copy + Opus])
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.validationError.suggestion:1')
AdvancedPickerTest > an empty output explains itself while the section is collapsed FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.validationError')
ConverterLeafTagsTest stays green under this mutation, as it should — it calls ValidationError directly rather than through the picker, so it cannot see where the call site sits.
2. rememberSaveable -> remember on expanded (ConverterScreen.kt:300) — this one reddens both classes:
AdvancedPickerTest > an open panel is still open after recreation FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.advanced.panel')
AdvancedPanelSavedStateTest > the open panel survives a real Parcel, not just an in-memory map FAILED
java.lang.AssertionError: the panel should register exactly one saved value expected:<1> but was:<0>
AdvancedPanelSavedStateTest > the panel registers its open state with the registry, and nothing else FAILED
java.lang.AssertionError: expected:<1> but was:<0>
Both mutations were reverted and the gate re-run green afterwards.
Not covered, so it is a decision
The chips' selected state. Which chip reads as selected for a given spec is asserted nowhere here; the tests click chips and check the callback. A selection-rendering test wants the FilterChip semantics and belongs with the other pickers rather than split across two files.
describe in its own right. It is exercised through the suggestion chip labels only. Its NONE-drops-the-track branches have no direct test.
The Container.entries / VideoCodec.entries / AudioCodec.entries completeness — that every enum constant gets a chip. The row tags make it reachable; nothing asserts the counts.
Anything needing a ConversionViewModel or a ConversionState. That is #61 and #62.
Closes #60. Child 4 of 8 decomposing #52. Two new JVM test files, **no production change** — the diff is `app/src/test` only.
## What was untested
`AdvancedPicker` is the one leaf on the converter screen that is not stateless: `expanded` is its own `rememberSaveable`, and the Container/Video/Audio chip rows live inside `AnimatedVisibility(visible = expanded)`. `ValidationError` is invoked **after** that block, so an invalid spec explains itself and offers one-tap fixes *while the section is collapsed*. That is the only route out of an invalid spec for a user who never opened Advanced — and since the only way to reach an invalid spec is through Advanced, hiding the way out behind the same toggle looks locally sensible. Folding the two `if` blocks into one compiles, and until now nothing went red.
## `AdvancedPickerTest` — 9 tests
- the gate in both directions: collapsed → the panel and all three row tags absent; expanded → all present; collapsed again → absent (the exit transition outlives the click, so the second absence is waited for rather than asserted straight away)
- the toggle's label swaps `Advanced` / `Hide advanced`
- each of the four colliding chip labels clicked through its own row tag: `Copy` in the video row and `Copy` in the audio row reach different callbacks, and `MP3` in the container row does not report as an audio codec
- the error card, **every assertion made with the toggle untouched**, and asserting the panel is absent in the same test so a future `expanded = true` default cannot quietly satisfy it
- a suggestion chip applies its own spec, clicked from the collapsed section — index 1 of 2, with the count pinned first so a shrunken list says so instead of producing an opaque matcher error
- a valid spec renders no error card, open or closed
- open and collapsed both survive `StateRestorationTester.emulateSavedInstanceStateRestore()`
The invalid specs come from `ContainerCapabilities.validate(spec, probe)` rather than a hand-built `Validation.Invalid`, so the messages and the suggestions are the real pairing — `"This would produce an empty file — keep at least one track."` and `"WebM cannot hold H.264 video."` are asserted as literals against what `validate` produced.
## `AdvancedPanelSavedStateTest` — 2 tests, and a finding
This is the `DestinationSaverTest` split that #60 assigned to this child: `StateRestorationTester` saves into an in-memory map, so it discriminates `rememberSaveable` from `remember` and stops there.
Driving a real `SaveableStateRegistry` through the composition turned up something the ticket's framing did not expect. **The saved representation is not the `Boolean`.** `rememberSaveable { mutableStateOf(false) }` passes no `stateSaver`, so `autoSaver` saves the `MutableState` *itself* — the registry hands back an `androidx.compose.runtime.ParcelableSnapshotMutableState`. That survives a rotation only because `mutableStateOf` on Android returns a `Parcelable` implementation; the same call on a plain JVM does not. So the thing standing between an open panel and a rotation that closes it is a platform-specific detail of a factory function nothing in this repo names directly, and an in-memory map cannot tell the two apart. The test writes what the registry produced into a real `Bundle`, round-trips it through a real `Parcel`, and asserts the panel comes back open.
`canBeSaved = { true }` is passed to the registry on purpose: a predicate mirroring `Bundle` acceptance would be a hand-written copy of the thing under test, and a `false` from it *drops* the entry silently — the same symptom as the `remember` regression. The type is checked on the way out instead.
## Mutations — both run, output verbatim
**1. `ValidationError` call moved inside the `AnimatedVisibility` block** (`ConverterScreen.kt:365`)
```
AdvancedPickerTest > a codec the container cannot hold explains itself while the section is collapsed FAILED
java.lang.AssertionError: Assert failed: The component with Text + InputText + EditableText contains 'WebM cannot hold H.264 video.' (ignoreCase: false) is not displayed!
AdvancedPickerTest > a suggestion chip applies its own spec without the section ever being opened FAILED
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [MP4 · Copy + Opus])
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.validationError.suggestion:1')
AdvancedPickerTest > an empty output explains itself while the section is collapsed FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.validationError')
```
`ConverterLeafTagsTest` stays green under this mutation, as it should — it calls `ValidationError` directly rather than through the picker, so it cannot see where the call site sits.
**2. `rememberSaveable` -> `remember` on `expanded`** (`ConverterScreen.kt:300`) — this one reddens **both** classes:
```
AdvancedPickerTest > an open panel is still open after recreation FAILED
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.advanced.panel')
AdvancedPanelSavedStateTest > the open panel survives a real Parcel, not just an in-memory map FAILED
java.lang.AssertionError: the panel should register exactly one saved value expected:<1> but was:<0>
AdvancedPanelSavedStateTest > the panel registers its open state with the registry, and nothing else FAILED
java.lang.AssertionError: expected:<1> but was:<0>
```
Both mutations were reverted and the gate re-run green afterwards.
## Not covered, so it is a decision
- **The chips' `selected` state.** Which chip reads as selected for a given `spec` is asserted nowhere here; the tests click chips and check the callback. A selection-rendering test wants the `FilterChip` semantics and belongs with the other pickers rather than split across two files.
- **`describe` in its own right.** It is exercised through the suggestion chip labels only. Its `NONE`-drops-the-track branches have no direct test.
- **The `Container.entries` / `VideoCodec.entries` / `AudioCodec.entries` completeness** — that every enum constant gets a chip. The row tags make it reachable; nothing asserts the counts.
- Anything needing a `ConversionViewModel` or a `ConversionState`. That is #61 and #62.
## Gate
`./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue` — BUILD SUCCESSFUL.
🤖 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 #60. Child 4 of 8 decomposing #52. Two new JVM test files, no production change — the diff is
app/src/testonly.What was untested
AdvancedPickeris the one leaf on the converter screen that is not stateless:expandedis its ownrememberSaveable, and the Container/Video/Audio chip rows live insideAnimatedVisibility(visible = expanded).ValidationErroris invoked after that block, so an invalid spec explains itself and offers one-tap fixes while the section is collapsed. That is the only route out of an invalid spec for a user who never opened Advanced — and since the only way to reach an invalid spec is through Advanced, hiding the way out behind the same toggle looks locally sensible. Folding the twoifblocks into one compiles, and until now nothing went red.AdvancedPickerTest— 9 testsAdvanced/Hide advancedCopyin the video row andCopyin the audio row reach different callbacks, andMP3in the container row does not report as an audio codecexpanded = truedefault cannot quietly satisfy itStateRestorationTester.emulateSavedInstanceStateRestore()The invalid specs come from
ContainerCapabilities.validate(spec, probe)rather than a hand-builtValidation.Invalid, so the messages and the suggestions are the real pairing —"This would produce an empty file — keep at least one track."and"WebM cannot hold H.264 video."are asserted as literals against whatvalidateproduced.AdvancedPanelSavedStateTest— 2 tests, and a findingThis is the
DestinationSaverTestsplit that #60 assigned to this child:StateRestorationTestersaves into an in-memory map, so it discriminatesrememberSaveablefromrememberand stops there.Driving a real
SaveableStateRegistrythrough the composition turned up something the ticket's framing did not expect. The saved representation is not theBoolean.rememberSaveable { mutableStateOf(false) }passes nostateSaver, soautoSaversaves theMutableStateitself — the registry hands back anandroidx.compose.runtime.ParcelableSnapshotMutableState. That survives a rotation only becausemutableStateOfon Android returns aParcelableimplementation; the same call on a plain JVM does not. So the thing standing between an open panel and a rotation that closes it is a platform-specific detail of a factory function nothing in this repo names directly, and an in-memory map cannot tell the two apart. The test writes what the registry produced into a realBundle, round-trips it through a realParcel, and asserts the panel comes back open.canBeSaved = { true }is passed to the registry on purpose: a predicate mirroringBundleacceptance would be a hand-written copy of the thing under test, and afalsefrom it drops the entry silently — the same symptom as therememberregression. The type is checked on the way out instead.Mutations — both run, output verbatim
1.
ValidationErrorcall moved inside theAnimatedVisibilityblock (ConverterScreen.kt:365)ConverterLeafTagsTeststays green under this mutation, as it should — it callsValidationErrordirectly rather than through the picker, so it cannot see where the call site sits.2.
rememberSaveable->rememberonexpanded(ConverterScreen.kt:300) — this one reddens both classes:Both mutations were reverted and the gate re-run green afterwards.
Not covered, so it is a decision
selectedstate. Which chip reads as selected for a givenspecis asserted nowhere here; the tests click chips and check the callback. A selection-rendering test wants theFilterChipsemantics and belongs with the other pickers rather than split across two files.describein its own right. It is exercised through the suggestion chip labels only. ItsNONE-drops-the-track branches have no direct test.Container.entries/VideoCodec.entries/AudioCodec.entriescompleteness — that every enum constant gets a chip. The row tags make it reachable; nothing asserts the counts.ConversionViewModelor aConversionState. That is #61 and #62.Gate
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue— BUILD SUCCESSFUL.🤖 Generated with Claude Code