//! Regression test: a case whose recordings are all `.m4a.failed` //! (permanent transcription failures — ffmpeg corrupt audio or Whisper //! 4xx) must produce an `OnelinerState::Empty` automatically, so the UI //! has a durable terminal state instead of relying on the on-render //! `compute_oneliner_display` fallback. //! //! The historical bug path: `.m4a.failed`-only cases were skipped by the //! recovery scan (`has_any_transcript` looked for `*.transcript.txt` //! only), so `update_oneliner` never ran, so no `oneliner.json` was //! written. The UI masked the symptom with its `non_failed_count == 0` //! fallback — but a future refactor of that fallback would regress the //! case. The fix makes `has_any_transcript` count `.m4a.failed` too, so //! `update_oneliner` runs, sees no content transcripts, and settles the //! case to `Empty` through the same terminal branch used for silent-only //! cases. mod common; 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}; #[tokio::test] async fn failed_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-failed"); std::fs::create_dir_all(&case_dir).unwrap(); // Seed: only a `.m4a.failed` file. No transcript, no plain .m4a. std::fs::write(case_dir.join("2026-04-15T20-30-00Z.m4a.failed"), b"x").unwrap(); // Ollama must NOT be called — nothing to summarize for a failed-only // case. `.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; } let path = case_dir.join(ONELINER_FILENAME); assert!( path.exists(), "expected oneliner.json to be written for failed-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. }