11use cap_audio:: DecodedAudio ;
22use cap_project:: { AudioGapSummary , AudioMeta , RecordingMeta , StudioRecordingMeta } ;
3- use std:: { path:: PathBuf , sync:: Arc } ;
3+ use std:: {
4+ path:: { Path , PathBuf } ,
5+ sync:: Arc ,
6+ } ;
47
58#[ derive( Clone , Default ) ]
69pub ( crate ) struct CompletedAudioSegment {
@@ -50,6 +53,14 @@ fn audio_identities(meta: &StudioRecordingMeta) -> Vec<SegmentAudioIdentity> {
5053 }
5154}
5255
56+ fn same_project_path ( left : & Path , right : & Path ) -> bool {
57+ left == right
58+ || matches ! (
59+ ( left. canonicalize( ) , right. canonicalize( ) ) ,
60+ ( Ok ( left) , Ok ( right) ) if left == right
61+ )
62+ }
63+
5364#[ derive( Clone ) ]
5465pub struct CompletedAudioHandoff {
5566 project_path : PathBuf ,
@@ -63,7 +74,10 @@ impl CompletedAudioHandoff {
6374 expected_finalized_metadata : & RecordingMeta ,
6475 segments : Vec < CompletedAudioSegment > ,
6576 ) -> Result < Self , String > {
66- if source_metadata. project_path != expected_finalized_metadata. project_path {
77+ if !same_project_path (
78+ & source_metadata. project_path ,
79+ & expected_finalized_metadata. project_path ,
80+ ) {
6781 return Err ( "Completed audio belongs to a different project" . into ( ) ) ;
6882 }
6983 let source = audio_identities (
@@ -107,7 +121,7 @@ impl CompletedAudioHandoff {
107121 recording_meta : & RecordingMeta ,
108122 meta : & StudioRecordingMeta ,
109123 ) -> Option < Vec < CompletedAudioSegment > > {
110- ( self . project_path == recording_meta. project_path
124+ ( same_project_path ( & self . project_path , & recording_meta. project_path )
111125 && self . identities == audio_identities ( meta) )
112126 . then_some ( self . segments )
113127 }
@@ -227,6 +241,98 @@ pub(crate) mod tests {
227241 assert ! ( matched[ 1 ] . mic. is_none( ) ) ;
228242 }
229243
244+ #[ test]
245+ fn completed_cache_accepts_equivalent_project_paths_and_preserves_pcm ( ) {
246+ let directory = tempfile:: tempdir ( ) . unwrap ( ) ;
247+ let entry = directory. path ( ) . join ( "editor-entry" ) ;
248+ std:: fs:: create_dir ( & entry) . unwrap ( ) ;
249+ let mut original = metadata ( ) ;
250+ original. project_path = entry. join ( ".." ) ;
251+ let mut finalized = original. clone ( ) ;
252+ finalized. project_path = directory. path ( ) . canonicalize ( ) . unwrap ( ) ;
253+ assert_ne ! ( original. project_path, finalized. project_path) ;
254+ let audio = audio ( ) ;
255+ for expected in [ & original, & finalized] {
256+ let cache = CompletedAudioHandoff :: from_completed_tracks (
257+ & original,
258+ expected,
259+ completed ( & audio) ,
260+ )
261+ . unwrap ( ) ;
262+ let matched = cache
263+ . into_matching ( & finalized, finalized. studio_meta ( ) . unwrap ( ) )
264+ . unwrap ( ) ;
265+ assert ! ( Arc :: ptr_eq( matched[ 0 ] . mic. as_ref( ) . unwrap( ) , & audio) ) ;
266+ }
267+ }
268+
269+ #[ test]
270+ fn silent_completed_cache_accepts_canonical_editor_paths ( ) {
271+ let directory = tempfile:: tempdir ( ) . unwrap ( ) ;
272+ let entry = directory. path ( ) . join ( "editor-entry" ) ;
273+ std:: fs:: create_dir ( & entry) . unwrap ( ) ;
274+ for count in [ 1 , 2 ] {
275+ let mut original = metadata ( ) ;
276+ original. project_path = entry. join ( ".." ) ;
277+ let segments = segments_mut ( & mut original) ;
278+ segments. truncate ( count) ;
279+ for segment in segments {
280+ segment. mic = None ;
281+ segment. system_audio = None ;
282+ }
283+ let cache = CompletedAudioHandoff :: from_completed_tracks (
284+ & original,
285+ & original,
286+ vec ! [ CompletedAudioSegment :: default ( ) ; count] ,
287+ )
288+ . unwrap ( ) ;
289+ let mut finalized = original. clone ( ) ;
290+ finalized. project_path = directory. path ( ) . canonicalize ( ) . unwrap ( ) ;
291+ let matched = cache
292+ . into_matching ( & finalized, finalized. studio_meta ( ) . unwrap ( ) )
293+ . unwrap ( ) ;
294+ assert_eq ! ( matched. len( ) , count) ;
295+ assert ! (
296+ matched
297+ . iter( )
298+ . all( |segment| segment. mic. is_none( ) && segment. system_audio. is_none( ) )
299+ ) ;
300+ }
301+ }
302+
303+ #[ test]
304+ fn completed_cache_rejects_different_or_unresolvable_project_paths ( ) {
305+ let directory = tempfile:: tempdir ( ) . unwrap ( ) ;
306+ let mut original = metadata ( ) ;
307+ original. project_path = directory. path ( ) . join ( "original.cap" ) ;
308+ std:: fs:: create_dir ( & original. project_path ) . unwrap ( ) ;
309+ let mut other = original. clone ( ) ;
310+ other. project_path = directory. path ( ) . join ( "other.cap" ) ;
311+ std:: fs:: create_dir ( & other. project_path ) . unwrap ( ) ;
312+ let audio = audio ( ) ;
313+ for missing in [ false , true ] {
314+ if missing {
315+ original. project_path = directory. path ( ) . join ( "missing-original.cap" ) ;
316+ other. project_path = directory. path ( ) . join ( "missing-other.cap" ) ;
317+ }
318+ assert ! (
319+ CompletedAudioHandoff :: from_completed_tracks( & original, & other, completed( & audio) )
320+ . is_err( )
321+ ) ;
322+ let cache = CompletedAudioHandoff :: from_completed_tracks (
323+ & original,
324+ & original,
325+ completed ( & audio) ,
326+ )
327+ . unwrap ( ) ;
328+ assert ! (
329+ cache
330+ . into_matching( & other, other. studio_meta( ) . unwrap( ) )
331+ . is_none( )
332+ ) ;
333+ }
334+ }
335+
230336 #[ test]
231337 fn completed_cache_rejects_every_changed_track_identity ( ) {
232338 let original = metadata ( ) ;
0 commit comments