From 6e9ed99b3a93849327cbf98482a4847b5aab188b Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 22 Apr 2026 09:06:47 +0100 Subject: [PATCH] fix(cr-2026-04-22): recording_filename avoids same-second collisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAJOR from the 2026-04-22 review (audio.rs:236-257): filenames were derived from SystemTime::now().as_secs(), so two recordings started within the same second resolved to the same path — possible overwrite or merge. Extracts the filename generation into a private helper and appends the sub-second nanosecond component. Format is now `kon--.wav`, e.g. `kon-1776828000-123456789.wav`, which stays human-readable, sortable by timestamp, and effectively uncollidable under any realistic live-capture pattern. Two tests cover the regression: - recording_filenames_are_unique_across_rapid_calls: 64 tight-loop calls must produce at least 32 unique names. - recording_filename_has_expected_shape: prefix/suffix plus the zero-padded 9-digit nanos component. --- src-tauri/src/commands/audio.rs | 59 ++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 1e09d5b..3e52bf0 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -249,11 +249,62 @@ pub fn resolve_recording_path( std::fs::create_dir_all(&recordings_dir) .map_err(|e| format!("Failed to create recordings dir: {e}"))?; - let timestamp = std::time::SystemTime::now() + Ok(recordings_dir.join(recording_filename())) +} + +/// Deterministic recording filename generator. Uses seconds since the +/// epoch for human-readable sortability plus the sub-second nanosecond +/// component so two recordings started within the same second do not +/// collide (brief item from the 2026-04-22 code review). +/// +/// Format: `kon--.wav`, e.g. +/// `kon-1776828000-123456789.wav`. +fn recording_filename() -> String { + let duration = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - Ok(recordings_dir.join(format!("kon-{timestamp}.wav"))) + .unwrap_or_default(); + let secs = duration.as_secs(); + let nanos = duration.subsec_nanos(); + format!("kon-{secs}-{nanos:09}.wav") +} + +#[cfg(test)] +mod tests { + use super::recording_filename; + + #[test] + fn recording_filenames_are_unique_across_rapid_calls() { + // Regression for the 2026-04-22 review: two recordings + // started in the same second previously collided on a + // seconds-only filename. The sub-second precision added + // here must hold under a tight loop. + let mut names = std::collections::HashSet::new(); + for _ in 0..64 { + names.insert(recording_filename()); + } + assert!( + names.len() >= 32, + "expected at least 32 unique filenames across 64 rapid calls, got {}", + names.len() + ); + } + + #[test] + fn recording_filename_has_expected_shape() { + let name = recording_filename(); + assert!(name.starts_with("kon-")); + assert!(name.ends_with(".wav")); + // Shape: kon--<9 digits>.wav + let rest = name + .strip_prefix("kon-") + .and_then(|s| s.strip_suffix(".wav")) + .expect("shape prefix/suffix"); + let parts: Vec<&str> = rest.split('-').collect(); + assert_eq!(parts.len(), 2, "expected two '-' separated parts, got {parts:?}"); + assert!(parts[0].chars().all(|c| c.is_ascii_digit()), "secs is digits"); + assert_eq!(parts[1].len(), 9, "nanos component is zero-padded to 9 digits"); + assert!(parts[1].chars().all(|c| c.is_ascii_digit())); + } } pub async fn persist_audio_samples(