fix(cr-2026-04-22): recording_filename avoids same-second collisions
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-<secs>-<nanos_in_sec>.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.
This commit is contained in:
@@ -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-<secs>-<nanos_in_sec>.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-<digits>-<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(
|
||||
|
||||
Reference in New Issue
Block a user