use super::*;
#[test]
fn build_desired_appends_extension_and_mode() {
let a = clip("id-a", "Song A", "alice");
let clips = [&a];
let desired = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
assert_eq!(desired.len(), 1);
assert!(
desired[0].path.ends_with(".flac"),
"path: {}",
desired[0].path
);
assert_eq!(desired[0].format, AudioFormat::Flac);
assert_eq!(desired[0].modes, vec![SourceMode::Mirror]);
assert!(!desired[0].trashed);
assert!(!desired[0].private);
let lineage = LineageContext::own_root(&a);
assert_eq!(desired[0].meta_hash, crate::hash::meta_hash(&a, &lineage));
assert_eq!(desired[0].art_hash, art_hash(&a));
assert_eq!(desired[0].lineage, lineage);
}
#[test]
fn build_desired_carries_the_trashed_flag_from_the_clip() {
let mut gone = clip("id-gone", "Removed", "alice");
gone.is_trashed = true;
let live = clip("id-live", "Kept", "alice");
let clips = [&gone, &live];
let desired = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
assert!(desired[0].trashed, "a trashed clip is marked trashed");
assert!(!desired[1].trashed, "a live clip is not");
}
#[test]
fn build_desired_uses_supplied_lineage_context() {
use crate::lineage::ResolveStatus;
let a = clip("child-1", "Remix", "alice");
let clips = [&a];
let lineage = LineageContext {
root_id: "root-1".to_owned(),
root_title: "Original".to_owned(),
root_date: String::new(),
parent_id: "root-1".to_owned(),
edge_type: None,
status: ResolveStatus::Resolved,
track: 0,
track_total: 0,
};
let contexts: HashMap<String, LineageContext> =
[(a.id.clone(), lineage.clone())].into_iter().collect();
let desired = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&contexts,
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
assert!(
desired[0].path.contains("/Original/"),
"path: {}",
desired[0].path
);
assert_eq!(desired[0].lineage, lineage);
assert_eq!(desired[0].meta_hash, crate::hash::meta_hash(&a, &lineage));
}
#[test]
fn lineage_is_stable_when_a_later_resolution_fails() {
use crate::graph::LineageStore;
use crate::lineage::{Resolution, ResolveStatus, RootInfo};
let root = Clip {
id: "root-break".into(),
title: "Break Through".into(),
clip_type: "gen".into(),
handle: "alice".into(),
display_name: "alice".into(),
..Default::default()
};
let child = Clip {
id: "child-remix".into(),
title: "Remix".into(),
clip_type: "gen".into(),
task: "cover".into(),
cover_clip_id: "root-break".into(),
edited_clip_id: "root-break".into(),
handle: "alice".into(),
display_name: "alice".into(),
..Default::default()
};
let clips = [&root, &child];
let contexts_of = |store: &LineageStore| -> HashMap<String, LineageContext> {
clips
.iter()
.map(|c| (c.id.clone(), store.context_for(c)))
.collect()
};
let mut roots = HashMap::new();
for id in ["root-break", "child-remix"] {
roots.insert(
id.to_owned(),
RootInfo {
root_id: "root-break".into(),
root_title: "Break Through".into(),
status: ResolveStatus::Resolved,
},
);
}
let resolution = Resolution {
roots,
gap_filled: Vec::new(),
bridges: Vec::new(),
};
let mut store = LineageStore::new();
store.update(&[root.clone(), child.clone()], &resolution, "t1");
let cycle1 = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&contexts_of(&store),
&store.colliding_root_titles(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
let child1 = cycle1.iter().find(|d| d.clip.id == "child-remix").unwrap();
assert!(
child1.path.contains("/Break Through/"),
"the remix should folder under its root album, got {}",
child1.path
);
let cycle2 = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&contexts_of(&store),
&store.colliding_root_titles(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
for (a, b) in cycle1.iter().zip(&cycle2) {
assert_eq!(a.path, b.path, "album path drifted for {}", a.clip.id);
assert_eq!(
a.meta_hash, b.meta_hash,
"meta_hash drifted for {}",
a.clip.id
);
}
let own = LineageContext::own_root(&child);
assert_ne!(
crate::hash::meta_hash(&child, &own),
child1.meta_hash,
"own-root fallback must differ from the store-driven hash"
);
}
#[test]
fn build_desired_disambiguates_collisions() {
let a = clip("id-a", "Same", "alice");
let b = clip("id-b", "Same", "alice");
let clips = [&a, &b];
let desired = build_desired(
&clips,
AudioFormat::Mp3,
&modes_for(&clips, SourceMode::Copy),
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
assert_ne!(desired[0].path, desired[1].path);
assert!(desired.iter().all(|d| d.path.ends_with(".mp3")));
assert!(desired.iter().all(|d| d.modes == vec![SourceMode::Copy]));
}
#[test]
fn build_desired_uses_forward_slashes() {
let a = clip("id-a", "Song A", "alice");
let clips = [&a];
let desired = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
assert!(!desired[0].path.contains('\\'));
assert!(desired[0].path.contains('/'));
}
#[test]
fn rel_to_string_normalises_os_separators_to_forward_slash() {
let mut path = PathBuf::new();
for part in ["alice", "Album Name", "alice-Song [clipaaaa]"] {
path.push(part);
}
let rendered = rel_to_string(&path);
assert_eq!(rendered, "alice/Album Name/alice-Song [clipaaaa]");
assert!(
!rendered.contains('\\'),
"rendered a platform separator: {rendered}"
);
}
#[test]
fn cover_album_and_stem_paths_use_forward_slashes() {
let a = art_clip("clipaaaa-1234");
let clips = [&a];
let desired = build_desired(
&clips,
AudioFormat::Flac,
&modes_for(&clips, SourceMode::Mirror),
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
let d = &desired[0];
let cover = d
.artifacts
.iter()
.find(|art| art.kind == ArtifactKind::CoverJpg)
.expect("an art-bearing clip yields a cover.jpg");
assert!(
cover.path.contains('/') && !cover.path.contains('\\'),
"cover: {}",
cover.path
);
let albums =
crate::desired::album_desired(&desired, false, false, WebpEncodeSettings::default());
let folder_jpg = albums[0]
.folder_jpg
.as_ref()
.expect("the album has a folder.jpg");
assert!(
folder_jpg.path.contains('/') && !folder_jpg.path.contains('\\'),
"folder.jpg: {}",
folder_jpg.path
);
let base = d.path.strip_suffix(".flac").expect("a .flac audio path");
let stems = clip_stems(
base,
&[Stem {
id: "stemvocal-9f8e".to_owned(),
label: "Vocals".to_owned(),
url: "https://cdn.suno.ai/stem.mp3".to_owned(),
}],
StemFormat::Mp3,
CharacterSet::Unicode,
);
assert!(
stems[0].path.contains('/') && !stems[0].path.contains('\\'),
"stem: {}",
stems[0].path
);
}
#[test]
fn build_desired_one_pass_disambiguates_and_stamps_modes() {
let a = clip("lib-1", "Song", "alice");
let b = clip("pl-1", "Song", "alice");
let clips = [&a, &b];
let mut modes = HashMap::new();
modes.insert("lib-1".to_owned(), vec![SourceMode::Copy]);
modes.insert(
"pl-1".to_owned(),
vec![SourceMode::Mirror, SourceMode::Copy],
);
let desired = build_desired(
&clips,
AudioFormat::Flac,
&modes,
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&NamingConfig::default(),
);
assert_eq!(desired.len(), 2);
assert_ne!(desired[0].path, desired[1].path);
assert_eq!(desired[1].modes, vec![SourceMode::Mirror, SourceMode::Copy]);
}
#[test]
fn build_desired_respects_custom_naming_config() {
use crate::naming::CharacterSet;
let a = clip("abcdefgh-1234", "Song A", "alice");
let clips = [&a];
let custom = NamingConfig {
template: "{title}/{id8}".to_owned(),
character_set: CharacterSet::Ascii,
..NamingConfig::default()
};
let desired = build_desired(
&clips,
AudioFormat::Flac,
&HashMap::from([("abcdefgh-1234".to_owned(), vec![SourceMode::Mirror])]),
&no_contexts(),
&no_collisions(),
ArtifactToggles::default(),
&custom,
);
assert!(
desired[0].path.starts_with("Song A/"),
"path: {}",
desired[0].path
);
assert!(desired[0].path.contains(&a.id[..8]));
}