//! Filesystem-fixture builders for case directories, recordings, //! oneliner state files, and close markers. //! //! All functions use synchronous `std::fs` (tests run inside //! `#[tokio::test]` but these writes are short and their output is //! consumed sequentially, so there is no win from async I/O here). A //! few of the oneliner_api tests use `tokio::fs` directly because they //! need to run inside the same async flow; that's fine — the two //! coexist. use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; use doctate_common::oneliners::OnelinerState; use doctate_common::{ONELINER_FILENAME, RECORDING_META_SUFFIX, RecordingMeta, Transcript}; use doctate_server::paths::CLOSE_MARKER; use filetime::FileTime; /// Create `///` and return its path. This is /// the only "has to be on disk for the route to see it" fixture — /// recordings and derived artefacts layer on top. pub fn seed_case(data_path: &Path, slug: &str, case_id: &str) -> PathBuf { let dir = data_path.join(slug).join(case_id); std::fs::create_dir_all(&dir).unwrap(); dir } /// Write `.m4a` with placeholder bytes, plus optionally a /// `.json` sidecar that mirrors the transcribe outcome. /// /// `transcript`: /// - `None` → no sidecar written (recording stays in `Pending` state). /// - `Some(text)` with whitespace-only `text` → sidecar with /// `Transcript::Silent` (matches the old `transcript.txt = ""` rule). /// - `Some(text)` non-empty → sidecar with `Transcript::Content`. /// /// Returns the audio filename (no path) so the caller can feed it back /// to the delete endpoint. pub fn seed_recording(case_dir: &Path, stem: &str, transcript: Option<&str>) -> String { let filename = format!("{stem}.m4a"); std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap(); if let Some(text) = transcript { seed_recording_meta(case_dir, stem, transcript_for(text), None); } filename } /// Like [`seed_recording`] but the sidecar carries a real duration /// (`duration_seconds: Some(42)`). Used by tests that assert the /// duration is rendered or cleaned up. pub fn seed_recording_with_sidecars(case_dir: &Path, stem: &str, transcript: &str) -> String { let filename = format!("{stem}.m4a"); std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap(); seed_recording_meta(case_dir, stem, transcript_for(transcript), Some(42)); filename } /// Write a fully-specified `.json` next to an already-seeded /// `.m4a`. Use when a test needs precise control over the /// transcript variant or duration; the higher-level helpers above /// cover the common cases. pub fn seed_recording_meta( case_dir: &Path, stem: &str, transcript: Transcript, duration_seconds: Option, ) { let meta = RecordingMeta { transcript, duration_seconds, }; let bytes = serde_json::to_vec(&meta).unwrap(); std::fs::write( case_dir.join(format!("{stem}.{RECORDING_META_SUFFIX}")), bytes, ) .unwrap(); } /// Map a free-text transcript to the `Transcript` variant the worker /// would have produced for it: whitespace-only → `Silent`, otherwise /// `Content`. Mirrors the old `transcript.txt = "" ⇒ Silent` rule so /// existing tests keep their meaning. fn transcript_for(text: &str) -> Transcript { if text.trim().is_empty() { Transcript::Silent } else { Transcript::Content { text: text.to_owned(), } } } /// Write an m4a with an artificial `mtime` in the past (relative to /// now). Used by the retention-sweep tests to simulate "recorded N days /// ago" without a clock abstraction. pub fn seed_recording_with_age(case_dir: &Path, stem: &str, age: Duration) { let filename = format!("{stem}.m4a"); let path = case_dir.join(&filename); std::fs::write(&path, b"audio").unwrap(); let target = SystemTime::now() - age; filetime::set_file_mtime(&path, FileTime::from_system_time(target)).unwrap(); } /// Persist `OnelinerState::Ready { text }` with a fixed `generated_at`. /// Used by UI-rendering tests that care about the displayed text, not /// the timestamp. pub fn seed_oneliner_ready(case_dir: &Path, text: &str) { let state = OnelinerState::Ready { text: text.to_owned(), generated_at: "2026-04-18T10:00:00Z".into(), }; seed_oneliner_state(case_dir, &state); } /// Persist an arbitrary [`OnelinerState`] as `oneliner.json`. pub fn seed_oneliner_state(case_dir: &Path, state: &OnelinerState) { let bytes = serde_json::to_vec(state).unwrap(); std::fs::write(case_dir.join(ONELINER_FILENAME), bytes).unwrap(); } /// Write a `.closed` marker with a caller-chosen `closed_at` string. /// Tests that need a specific age pass a historical RFC3339 stamp; a /// helper for that is [`past_rfc3339`]. pub fn write_closed_marker(case_dir: &Path, closed_at: &str) { let json = format!(r#"{{"closed_at":"{closed_at}"}}"#); std::fs::write(case_dir.join(CLOSE_MARKER), json).unwrap(); } /// Format "now - age" as RFC3339 UTC. Used to mint historical /// `closed_at` strings for retention-purge tests. pub fn past_rfc3339(age: Duration) -> String { use time::format_description::well_known::Rfc3339; let t = time::OffsetDateTime::now_utc() - time::Duration::seconds(age.as_secs() as i64); t.format(&Rfc3339).unwrap() }