af3377a6bc
Finishes the lift of shared helpers into `tests/common/`. Covered: - health, web, oneliners_api, upload (31 tests; upload exercises the new `multipart_upload_body` helper driven by doctate_common field constants) - sse_integration, sse_cleanup, transcribe, oneliner_heal_decoupled, silent_case_empty, failed_only_case_empty_oneliner, transient_failure_retries (24 tests) Also applied cargo fmt across the test tree and fixed one clippy needless_borrows_for_generic_args warning in analyze_test. All 387 tests pass; 3 ignored (as before). Side effect: health_test previously used a hardcoded `/tmp/doctate-test` data path, which parallel `cargo test` runs could collide on. The migration replaces it with the common unique-tmpdir pattern, removing a latent flake.
107 lines
4.0 KiB
Rust
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::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.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.
|
|
}
|