Files
doctate/server/tests/silent_case_empty_test.rs
T
Brummel 813fb896ff refactor: consolidate bulk actions, URL assembly, and case-artefact filenames
Pulls three pattern groups into shared helpers so a rename or wire-format
tweak edits one file instead of 10+:

- BulkAction enum in doctate-common replaces the "close"/"analyze"/"reset"
  string literals that were duplicated between the bulk handler and 3
  attack/CSRF test files. FromStr preserves the exact "Unbekannte Aktion"
  error shape; the handler match is now exhaustive over the enum.
- join_url helper in doctate-common absorbs 7 identical
  trim_end_matches+format! call sites across client-core, client-desktop,
  server/transcribe (ollama, whisper), and server/analyze (llm).
- server/tests/common/artefacts.rs re-exports ONELINER_FILENAME
  (doctate-common), DOCUMENT_FILE + ANALYSIS_INPUT_FILE
  (doctate-server::analyze), and CLOSE_MARKER (doctate-server::paths) so
  test files reference the canonical name instead of inlining literals.

Also lifts client-desktop local duplication:
- paths.rs: project_path helper collapses 4 identical ProjectDirs chains
- main.rs: or_die helper replaces 4 eprintln!+exit(1) blocks
- app.rs: named RecordingContext struct replaces (Uuid, String) tuple
  at 3 sites around the ffmpeg-flush finalization path

Verification: 397 tests pass (baseline was 389; +8 for new unit tests on
BulkAction and join_url), 0 failed, 4 ignored (unchanged). clippy clean.
2026-04-22 12:09:46 +02:00

107 lines
4.0 KiB
Rust

//! Regression test: a case whose recordings are all `TranscriptState::Silent`
//! (whisper classified as silence) must produce an `OnelinerState::Empty`
//! automatically, so the UI doesn't stick on "Generating" forever.
//!
//! The historical bug path: whisper wrote a 0-byte `.transcript.txt`,
//! `update_oneliner` saw no content and returned early without persisting
//! any state, and the UI's `compute_oneliner_display` collapsed the
//! Silent file onto "transcribed → Generating" (missing state). The fix
//! makes `update_oneliner` settle the case to `OnelinerState::Empty`
//! when no content transcript exists and no recording is still Pending.
mod common;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use doctate_common::oneliners::OnelinerState;
use doctate_server::events;
use doctate_server::gazetteer::Gazetteer;
use doctate_server::{
AnalyzeBusy, OnelinerHealBusy, PipelineState, TranscribeBusy, WorkerBusy, analyze, transcribe,
};
use tempfile::tempdir;
use wiremock::matchers::{method, path as wm_path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::{ONELINER_FILENAME, TestConfig};
/// Seed an m4a plus a silent (0-byte) transcript sidecar — the on-disk
/// shape of a recording that whisper classified as silence.
fn seed_silent_recording(case_dir: &Path, stem: &str) {
std::fs::write(case_dir.join(format!("{stem}.m4a")), b"audio").unwrap();
std::fs::write(case_dir.join(format!("{stem}.transcript.txt")), b"").unwrap();
}
#[tokio::test]
async fn silent_only_case_settles_to_empty_without_llm_call() {
let data = tempdir().unwrap();
let slug = "dr_test";
let user_root = data.path().join(slug);
let case_dir = user_root.join("case-silent");
std::fs::create_dir_all(&case_dir).unwrap();
seed_silent_recording(&case_dir, "2026-04-16T10-00-00Z");
// Ollama must NOT be called — a silent-only case has no content to
// summarize. `.expect(0)` fails the test (on MockServer drop) if any
// request arrived.
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(wm_path("/api/chat"))
.respond_with(ResponseTemplate::new(200))
.expect(0)
.mount(&mock)
.await;
let config = TestConfig::new()
.with_data_path(data.path().to_path_buf())
.with_ollama(mock.uri())
.build();
let vocab = Arc::new(Gazetteer::empty());
let events_tx = events::channel();
let http_client = reqwest::Client::new();
let (tx_a, _rx_a) = analyze::channel();
let (tx_t, _rx_t) = transcribe::channel();
let heal_busy: WorkerBusy = Arc::new(AtomicBool::new(false));
let pipeline = PipelineState {
analyze_busy: AnalyzeBusy(Arc::new(AtomicBool::new(false))),
analyze_tx: tx_a,
transcribe_busy: TranscribeBusy(Arc::new(AtomicBool::new(false))),
transcribe_tx: tx_t,
oneliner_heal_busy: OnelinerHealBusy(heal_busy.clone()),
};
pipeline
.heal_orphans_if_idle(&user_root, slug, &http_client, &config, &vocab, &events_tx)
.await;
// Heal spawn drops the busy flag on completion — poll until idle.
let start = std::time::Instant::now();
while heal_busy.load(Ordering::Acquire) {
if start.elapsed() > Duration::from_secs(5) {
panic!("oneliner heal did not finish within 5s");
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
// Oneliner is persisted as `Empty`. Without the fix, the file would
// either not exist or be mid-generation.
let path = case_dir.join(ONELINER_FILENAME);
assert!(
path.exists(),
"expected oneliner.json to be written for silent-only case"
);
let bytes = std::fs::read(&path).unwrap();
let state: OnelinerState = serde_json::from_slice(&bytes).unwrap();
assert!(
matches!(state, OnelinerState::Empty { .. }),
"expected OnelinerState::Empty, got {state:?}"
);
// Mock.drop() runs here — `.expect(0)` panics if Ollama was touched.
}