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:
2026-04-22 09:06:47 +01:00
parent a37caa2219
commit 6e9ed99b3a

View File

@@ -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(