C1 (#176): give ConcatWorker the seam ConversionWorker always had, and test what was behind it #188

Merged
JMR-dev merged 2 commits from test/concat-engine-seam into main 2026-09-02 04:44:27 +00:00
JMR-dev commented 2026-09-02 03:06:54 +00:00 (Migrated from github.com)

Closes #176. Stacked on #187. Last of the wave.

ConcatWorker constructed ConcatEngine in place while ConversionWorker reached its engines through ConversionDependencies. That asymmetry is the whole reason one worker had a tested failure path and the other had none: everything past setForeground was untested on every source set, JVM and device alike.

The repo had already measured the cost and written it down. PerJobStagingTest's KDoc records that reverting ConcatWorker to a constant staging name left all 257 tests green, because nothing could reach the line that names the file. RefusedJobTest says it from the other side — "the next thing past the count guard is ConcatEngine, which is native."

That mutation is red now.

The seam

ConversionDependencies.concat: (Context) -> ConcatJoiner, beside .hardware and .software. ConcatEngine implements the interface; its Result type stays nested in the implementation, because moving it would touch every call site to buy nothing — what a test needs is the ability to not run FFmpeg, and that is the method, not the type.

Five tests, five mutations

behaviour mutation
the engine's own reason reaches the user replace e.message with the generic string
a failure with no message still says one drop the ?: GENERIC_FAILURE_MESSAGE fallback
a failed join deletes its partial drop staged.delete() from the catch
no input array at all is refused swap in TOO_FEW_INPUTS_MESSAGE
a join reports its own staged file revert to the constant staging name

The delete test was vacuous on its first draft, and the mutation caught it. It scanned the staging directory for a "join-" prefix that StagingNames.forJob does not produce — it names files <jobId>.<ext> — so the assertion was trivially true and the mutation walked straight through. Rewritten to assert against the handle the joiner was actually given.

Two small fixes ride along

Both are the repo's own conventions, not new opinions:

  • "No input files." becomes NO_INPUTS_MESSAGE, per #158: a message the user can see is named once, so a test asserts the string the worker writes rather than a copy that can drift.
  • That arm now has a JVM test. It ran before staging and before any native code and had no business being a device-only test.

Numbers

579 → 584 JVM tests, 0 failures. ConcatWorker: 14 → 5 missed lines, 2 → 0 missed branches.
Line 2173/2348 → 2183/2352; branch numerator unchanged at 1091/1342.

The line denominator moved 2348 → 2352: that is the ConcatJoiner interface, not new untested code. Said plainly because CLAUDE.md's coverage entry has a history of explaining its own numbers wrongly.

🤖 Generated with Claude Code

Closes #176. Stacked on #187. Last of the wave. `ConcatWorker` constructed `ConcatEngine` in place while `ConversionWorker` reached its engines through `ConversionDependencies`. **That asymmetry is the whole reason one worker had a tested failure path and the other had none**: everything past `setForeground` was untested on *every* source set, JVM and device alike. The repo had already measured the cost and written it down. `PerJobStagingTest`'s KDoc records that **reverting `ConcatWorker` to a constant staging name left all 257 tests green**, because nothing could reach the line that names the file. `RefusedJobTest` says it from the other side — *"the next thing past the count guard is `ConcatEngine`, which is native."* That mutation is red now. ### The seam `ConversionDependencies.concat: (Context) -> ConcatJoiner`, beside `.hardware` and `.software`. `ConcatEngine` implements the interface; its `Result` type stays nested in the implementation, because moving it would touch every call site to buy nothing — what a test needs is the ability to *not run FFmpeg*, and that is the method, not the type. ### Five tests, five mutations | behaviour | mutation | |---|---| | the engine's own reason reaches the user | replace `e.message` with the generic string | | a failure with no message still says one | drop the `?: GENERIC_FAILURE_MESSAGE` fallback | | a failed join deletes its partial | drop `staged.delete()` from the catch | | no input array at all is refused | swap in `TOO_FEW_INPUTS_MESSAGE` | | a join reports its own staged file | revert to the constant staging name | **The delete test was vacuous on its first draft, and the mutation caught it.** It scanned the staging directory for a `"join-"` prefix that `StagingNames.forJob` does not produce — it names files `<jobId>.<ext>` — so the assertion was trivially true and the mutation walked straight through. Rewritten to assert against the handle the joiner was actually given. ### Two small fixes ride along Both are the repo's own conventions, not new opinions: - `"No input files."` becomes `NO_INPUTS_MESSAGE`, per **#158**: a message the user can see is named once, so a test asserts the string the worker writes rather than a copy that can drift. - That arm now has a JVM test. It ran before staging and before any native code and had no business being a device-only test. ### Numbers 579 → 584 JVM tests, 0 failures. `ConcatWorker`: 14 → 5 missed lines, 2 → **0** missed branches. Line 2173/2348 → **2183/2352**; branch numerator unchanged at 1091/1342. The line **denominator** moved 2348 → 2352: that is the `ConcatJoiner` interface, not new untested code. Said plainly because `CLAUDE.md`'s coverage entry has a history of explaining its own numbers wrongly. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.