//! 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::ONELINER_FILENAME; use doctate_common::oneliners::OnelinerState; 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 /// `.transcript.txt`. `stem` must be the full RFC3339-like /// filename stem (e.g. `2026-04-15T10-00-00Z`); pass the same prefix /// you want to read back later. 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 { std::fs::write(case_dir.join(format!("{stem}.transcript.txt")), text).unwrap(); } filename } /// Like [`seed_recording`] but additionally writes the `.duration.txt` /// sidecar — used by the delete-recording tests that assert every /// sidecar is cleaned up. pub fn seed_recording_with_sidecars(case_dir: &Path, stem: &str, transcript: &str) -> String { let filename = seed_recording(case_dir, stem, Some(transcript)); std::fs::write(case_dir.join(format!("{stem}.duration.txt")), "42").unwrap(); filename } /// 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() }