//! 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. 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::config::Config; 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}; /// 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 mut cfg = Config::test_default(); cfg.data_path = data.path().to_path_buf(); cfg.ollama_url = mock.uri(); let config = Arc::new(cfg); 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.json"); 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. }