use std::path::PathBuf; use std::time::Duration; use doctate_server::transcribe; use doctate_server::transcribe::ffmpeg::remux_faststart; use doctate_server::transcribe::recovery::scan_and_enqueue; use doctate_server::transcribe::whisper::{transcribe, WhisperError}; use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; fn fixture(name: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures") .join(name) } #[tokio::test] async fn ffmpeg_remux_produces_valid_output() { let input = fixture("sample.m4a"); assert!(input.exists(), "fixture missing: {}", input.display()); let output = remux_faststart(&input) .await .expect("remux failed"); let meta = std::fs::metadata(output.path()).expect("output missing"); assert!(meta.len() > 0, "output file is empty"); // Sanity check: ffprobe should still recognize it as an m4a/mp4 container. let probe = std::process::Command::new("ffprobe") .arg("-hide_banner") .arg("-loglevel") .arg("error") .arg("-show_entries") .arg("format=format_name") .arg("-of") .arg("default=nw=1:nk=1") .arg(output.path()) .output() .expect("ffprobe spawn failed"); assert!(probe.status.success(), "ffprobe failed"); let fmt = String::from_utf8_lossy(&probe.stdout); assert!(fmt.contains("mp4") || fmt.contains("m4a"), "unexpected format: {fmt}"); } #[tokio::test] async fn ffmpeg_remux_fails_on_missing_input() { let input = PathBuf::from("/nonexistent/does-not-exist.m4a"); let result = remux_faststart(&input).await; assert!(result.is_err(), "expected error for missing input"); } #[tokio::test] async fn whisper_client_posts_multipart_and_returns_text() { let server = MockServer::start().await; let expected = "Hallo Welt, das ist ein Test."; Mock::given(method("POST")) .and(path("/asr")) .and(query_param("output", "txt")) .and(query_param("language", "de")) .respond_with(ResponseTemplate::new(200).set_body_string(expected)) .expect(1) .mount(&server) .await; let client = reqwest::Client::new(); let text = transcribe(&client, &server.uri(), &fixture("sample.m4a"), Duration::from_secs(10)) .await .expect("transcribe failed"); assert_eq!(text, expected); } #[tokio::test] async fn whisper_client_returns_status_error_on_500() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/asr")) .respond_with(ResponseTemplate::new(500).set_body_string("boom")) .mount(&server) .await; let client = reqwest::Client::new(); let err = transcribe(&client, &server.uri(), &fixture("sample.m4a"), Duration::from_secs(10)) .await .expect_err("expected error"); match err { WhisperError::Status { status, body } => { assert_eq!(status, 500); assert_eq!(body, "boom"); } other => panic!("unexpected error variant: {other:?}"), } } #[tokio::test] async fn whisper_client_times_out() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/asr")) .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2))) .mount(&server) .await; let client = reqwest::Client::new(); let err = transcribe( &client, &server.uri(), &fixture("sample.m4a"), Duration::from_millis(100), ) .await .expect_err("expected timeout"); assert!(matches!(err, WhisperError::Http(_)), "expected Http error, got {err:?}"); } #[tokio::test] async fn recovery_enqueues_only_pending_recordings() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); // User 1, open case with two .m4a — one pending, one already transcribed. let case_a = root.join("dr_a/open/aaaa"); std::fs::create_dir_all(&case_a).unwrap(); std::fs::write(case_a.join("2026-04-10T10-00-00Z.m4a"), b"x").unwrap(); std::fs::write(case_a.join("2026-04-11T10-00-00Z.m4a"), b"x").unwrap(); std::fs::write(case_a.join("2026-04-11T10-00-00Z.transcript.txt"), b"done").unwrap(); // User 2, open case with one pending .m4a. let case_b = root.join("dr_b/open/bbbb"); std::fs::create_dir_all(&case_b).unwrap(); std::fs::write(case_b.join("2026-04-12T10-00-00Z.m4a"), b"x").unwrap(); // done/ cases must be ignored (not re-transcribed). let case_c = root.join("dr_a/done/cccc"); std::fs::create_dir_all(&case_c).unwrap(); std::fs::write(case_c.join("2026-04-09T10-00-00Z.m4a"), b"x").unwrap(); let (tx, mut rx) = transcribe::channel(); scan_and_enqueue(root, &tx).await; drop(tx); // close channel so the loop below terminates let mut received = Vec::new(); while let Some(job) = rx.recv().await { received.push(job.audio_path); } assert_eq!(received.len(), 2, "got: {received:?}"); // Oldest first: assert!(received[0].ends_with("2026-04-10T10-00-00Z.m4a")); assert!(received[1].ends_with("2026-04-12T10-00-00Z.m4a")); }