---
name: Tests and fixtures
type: architecture-map-page
slice: 03-audio-transcription
last_verified: 2026/05/09
---

# Tests and fixtures

> **Where you are:** [Architecture map](../README.md) → [Audio + Transcription](README.md) → Tests and fixtures

**Plain English summary.** This slice has three categories of test. **Inline unit tests** under `#[cfg(test)] mod tests` exercise every public type (capture monitor detection, resampler duration preservation, RmsVadChunker state machine, LocalAgreement invariants, buffer trim bounds, model-manager hash and resume paths). **Integration tests** under `crates/transcription/tests/` exercise whisper-rs end-to-end with env-var-gated fixtures so CI without a model checkpoint exits quietly. **Test-only fixtures** include in-tree `tokio::net::TcpListener` HTTP servers that simulate Range / 5xx / no-Range-honour servers for the download stack.

## At a glance

- Crate: `lumotia-transcription` (mostly) and `lumotia-audio` (inline only).
- Paths:
  - `crates/audio/src/{capture,decode,wav,resample,streaming_resample}.rs` — inline `#[cfg(test)]` modules.
  - `crates/transcription/src/{model_manager,local_engine,transcriber,streaming/*}.rs` — inline `#[cfg(test)]` modules.
  - `crates/transcription/tests/whisper_rs_smoke.rs` — env-gated load + transcribe.
  - `crates/transcription/tests/jfk_bench.rs` — env-gated benchmark with cold/warm RTF.
  - `crates/transcription/tests/thread_sweep.rs` — env-gated thread-count scaling sweep.
- External deps (dev): `tempfile 3`, `tokio` (rt, sync, net, io-util, macros), `num_cpus 1` (label-only).

## Inline unit tests (selected highlights)

### `lumotia-audio`

- `capture.rs:570` — `monitor_pattern_detection` for the PulseAudio / PipeWire heuristics.
- `decode.rs:262` — `mid_stream_io_error_propagates_instead_of_returning_partial_audio`. RB-09 regression: a flaky `MediaSource` (`FlakyCursor`, `decode.rs:203`) injects an I/O error after the probe header and asserts the function errors instead of returning `Ok(partial)`.
- `wav.rs:170` — `wav_writer_survives_crash`. Uses `std::mem::forget` to skip the hound finaliser, simulating a process kill, then asserts the flushed prefix is recoverable via `read_wav`.
- `wav.rs:235` — `read_wav_surfaces_truncated_sample_stream_errors`. 2026-04-22 regression: the previous `filter_map(|s| s.ok())` swallowed errors silently.
- `streaming_resample.rs:182` — `streaming_48k_to_16k_preserves_duration`. Pushes 1 s of 48 kHz audio in 700-sample chunks (forcing the residual-buffer path) and asserts the output is within 50 ms of 1 s at 16 kHz.
- `resample.rs:84` — `resample_preserves_approximate_duration`.

### `lumotia-transcription`

- `streaming/rms_vad.rs:371`–`:734` — comprehensive state-machine coverage: silence, hysteresis, onset gating, max-chunk continuity (`max_chunk_split_preserves_audio_contiguity`), padded-final-frame regression (`flush_preserves_hit_max_chunk_from_padded_final_frame`, `flush_preserves_end_of_utterance_chunk_from_padded_final_frame` — both 2026-04-22 CRITICAL C2 fixes), idempotent flush.
- `streaming/commit_policy.rs:212`–`:402` — LocalAgreement-n correctness: text-only equality, non-shrinkage invariant, defensive shorter-pass handling (`shorter_pass_after_commit_does_not_panic`, regression for the `latest[committed_count..]` panic), n=3 behaviour.
- `streaming/buffer_trim.rs:56`–`:206` — defensive non-finite handling, integration test against `LocalAgreement::last_committed_end_secs`, the long-session bound proof (`trim_bounds_buffer_over_long_session`).
- `model_manager.rs:339`–`:615` — three TcpListener-backed fixtures (`spawn_range_server`, `spawn_no_range_server`, `spawn_500_server`) drive `download_file` through the resume path, the ResumeUnsupported path (server returns 200 to a Range request), the 5xx rejection path, and the SHA-mismatch cleanup path.
- `local_engine.rs:218` — `engine_reports_not_available_before_loading`.
- `transcriber.rs:54` — `transcriber_trait_is_object_safe` (compile-time witness).

## Env-gated integration tests (`crates/transcription/tests/`)

All three skip silently if the env vars are unset, so `cargo test` on a fresh machine is a no-op for them.

### `whisper_rs_smoke.rs`

- Env vars: `LUMOTIA_WHISPER_TEST_MODEL` (path to a ggml/gguf Whisper model).
- Loads the context, builds a state, calls `set_initial_prompt("Wren, CORBEL, ADHD")` (proves the API still exists), runs inference on 1 s of silence, exercises `full_n_segments` and `get_segment(i).to_str()`. Smoke test only; assertion is "did this run without panicking".

### `jfk_bench.rs`

- Env vars: `LUMOTIA_WHISPER_TEST_MODEL`, `LUMOTIA_WHISPER_TEST_AUDIO` (path to a 16 kHz mono 16-bit PCM WAV — assertions in the test enforce this format on the fixture, not the runtime).
- Reports cold-load time, cold-transcribe RTF, warm-transcribe RTF, peak RSS read from `/proc/{pid}/status`. Hardcodes `set_n_threads(6)` for both runs (so a sweep across thread counts is a separate test, not this one).

### `thread_sweep.rs`

- Same env vars as `jfk_bench.rs`. Adds `LUMOTIA_POWER_STATE_OVERRIDE` (set internally per panel to `ac` or `battery`) so `inference_thread_count` returns its predicted pick for each combination of (power state × GPU offload) and the empirical RTF table can be compared against the helper's choice.
- Runs the JFK clip at `n_threads = 1, 2, 4, physical, logical` (plus `8` if logical ≥ 8), takes the min of two runs per setting, prints a four-panel table:
  1. AC, CPU.
  2. AC, GPU (Vulkan).
  3. battery, CPU.
  4. battery, GPU (Vulkan).
- Uses `num_cpus::get()` and `num_cpus::get_physical()` purely for the printed labels (`thread_sweep.rs:35`). The runtime helper `inference_thread_count(Workload::Whisper, gpu_offloaded)` does the actual prediction.
- `vulkan_runtime_ok` snapshot taken once via `cfg!(feature = "whisper-vulkan") && vulkan_loader_available()`.

## Watch-outs

- **All three integration tests are quiet on missing env vars.** A green `cargo test` does NOT mean the Whisper path works; it means "either the path works or you didn't set the env vars". CI configurations must explicitly set `LUMOTIA_WHISPER_TEST_MODEL` to exercise these.
- **`jfk_bench.rs` hardcodes `n_threads = 6`.** Production code uses `inference_thread_count(Workload::Whisper, gpu_offloaded)` which is power-aware. Bench numbers are not directly comparable to runtime numbers without rerunning with the helper-picked thread count.
- **`thread_sweep.rs` mutates `LUMOTIA_POWER_STATE_OVERRIDE` via `env::set_var`.** Globally process-wide. Concurrent tests in the same `cargo test` invocation that read `LUMOTIA_POWER_STATE_OVERRIDE` will race. The test removes the var at the end (`thread_sweep.rs:94`).
- **`tempfile 3` is a dev-dep, not a runtime dep.** Production code never creates temp files.
- **`num_cpus = "1"` (the version, not "one CPU").** Test-only labelling; production uses the slice 5 helper.
- **In-tree HTTP fixtures use ephemeral ports (`bind 127.0.0.1:0`) and run on a tokio task.** No port-collision risk, but a paranoid sandbox that blocks raw TCP loopback would break these tests.
- **`ModelFile`'s `&'static str` fields force the test helper `leak()` (`model_manager.rs:462`).** Each test leaks a few small strings. Acceptable for one-shot tests; do not copy this pattern into production code.

## See also

- [Transcription model manager](transcription-model-manager.md) — the production code these download tests guard.
- [Transcription whisper](transcription-whisper.md) — the production code these integration tests guard.
- [Transcription streaming](transcription-streaming.md) — exhaustive unit-test coverage lives alongside this code.
- `docs/whisper-ecosystem/brief.md` — the source of the items (`#6, #8, #19, #21, #24, #25`) repeatedly referenced in test docstrings.
- `docs/code-review-2026-04-22.md` — the audit pass that produced the C2, RB-09, and `read_wav` regressions captured here.
