use super::*;
#[test]
fn album_overrides_are_runtime_only_and_never_persist() {
let mut store = LineageStore::new();
store.update(&chain_clips(), &chain_resolution(), "now");
store.set_album_overrides(
[("a".to_owned(), "Preferred".to_owned())]
.into_iter()
.collect(),
);
let value: serde_json::Value = serde_json::to_value(&store).unwrap();
assert!(value.get("album_overrides").is_none());
let json = serde_json::to_string(&store).unwrap();
let back: LineageStore = serde_json::from_str(&json).unwrap();
assert!(back.album_overrides.is_empty());
assert_eq!(back.album_for_id("c"), "Root");
}
#[test]
fn colliding_root_titles_flags_only_shared_distinct_roots() {
let clips = vec![
Clip {
id: "r1".into(),
title: "Break Through".into(),
clip_type: "gen".into(),
..Default::default()
},
Clip {
id: "r2".into(),
title: "Break Through".into(),
clip_type: "gen".into(),
..Default::default()
},
Clip {
id: "r3".into(),
title: "Solo".into(),
clip_type: "gen".into(),
..Default::default()
},
Clip {
id: "c1".into(),
title: "Break Through".into(),
clip_type: "gen".into(),
task: "cover".into(),
cover_clip_id: "r1".into(),
edited_clip_id: "r1".into(),
..Default::default()
},
];
let mut roots = HashMap::new();
for (id, root) in [("r1", "r1"), ("r2", "r2"), ("r3", "r3"), ("c1", "r1")] {
let title = if root == "r3" {
"Solo"
} else {
"Break Through"
};
roots.insert(
id.to_owned(),
RootInfo {
root_id: root.into(),
root_title: title.into(),
status: ResolveStatus::Resolved,
},
);
}
let resolution = Resolution {
roots,
gap_filled: Vec::new(),
bridges: Vec::new(),
};
let mut store = LineageStore::new();
store.update(&clips, &resolution, "now");
let colliding = store.colliding_root_titles();
assert!(colliding.contains("Break Through"));
assert!(!colliding.contains("Solo"));
assert_eq!(colliding.len(), 1);
}
fn two_root_store(t1: &str, t2: &str) -> LineageStore {
let clips = vec![
Clip {
id: "r1".into(),
title: t1.into(),
clip_type: "gen".into(),
..Default::default()
},
Clip {
id: "r2".into(),
title: t2.into(),
clip_type: "gen".into(),
..Default::default()
},
];
let mut roots = HashMap::new();
roots.insert(
"r1".to_owned(),
RootInfo {
root_id: "r1".into(),
root_title: t1.into(),
status: ResolveStatus::Resolved,
},
);
roots.insert(
"r2".to_owned(),
RootInfo {
root_id: "r2".into(),
root_title: t2.into(),
status: ResolveStatus::Resolved,
},
);
let mut store = LineageStore::new();
store.update(
&clips,
&Resolution {
roots,
gap_filled: Vec::new(),
bridges: Vec::new(),
},
"now",
);
store
}
#[test]
fn album_override_flows_into_context_tag_hash_and_index() {
let clips = chain_clips();
let mut store = LineageStore::new();
store.update(&clips, &chain_resolution(), "now");
let cover = &clips[0]; let before_hash = crate::hash::meta_hash(cover, &store.context_for(cover));
store.set_album_overrides(
[("a".to_owned(), "Preferred Name".to_owned())]
.into_iter()
.collect(),
);
for id in ["a", "b", "c"] {
let clip = clips.iter().find(|c| c.id == id).unwrap();
let ctx = store.context_for(clip);
assert_eq!(ctx.album(&clip.title), "Preferred Name");
assert_eq!(store.album_for_id(id), "Preferred Name");
}
let ctx = store.context_for(cover);
let meta = crate::tag::TrackMetadata::from_clip(cover, &ctx);
assert_eq!(meta.album, "Preferred Name");
let after_hash = crate::hash::meta_hash(cover, &ctx);
assert_ne!(before_hash, after_hash);
}
#[test]
fn empty_album_override_is_ignored() {
let clips = chain_clips();
let mut store = LineageStore::new();
store.update(&clips, &chain_resolution(), "now");
store.set_album_overrides([("a".to_owned(), " ".to_owned())].into_iter().collect());
assert_eq!(store.album_for_id("c"), "Root");
}
#[test]
fn album_override_creates_a_collision_that_disambiguates() {
let mut store = two_root_store("Alpha", "Beta");
assert!(store.colliding_root_titles().is_empty());
store.set_album_overrides(
[("r2".to_owned(), "Alpha".to_owned())]
.into_iter()
.collect(),
);
let colliding = store.colliding_root_titles();
assert!(colliding.contains("Alpha"));
assert_eq!(colliding.len(), 1);
}
#[test]
fn album_override_resolves_a_natural_collision() {
let mut store = two_root_store("Break Through", "Break Through");
assert!(store.colliding_root_titles().contains("Break Through"));
store.set_album_overrides(
[("r2".to_owned(), "Second Wind".to_owned())]
.into_iter()
.collect(),
);
assert!(store.colliding_root_titles().is_empty());
}
fn insert_cache_only_root(store: &mut LineageStore, root_id: &str) {
store.resolution_cache.insert(
root_id.to_owned(),
CacheEntry {
root_id: root_id.to_owned(),
status: ResolveStatus::External,
algorithm_version: 1,
computed_at: "now".to_owned(),
},
);
store.refresh_eligible_roots();
}
#[test]
fn override_on_node_less_root_collides_with_a_real_root() {
let mut store = LineageStore::new();
store.update(
std::slice::from_ref(&Clip {
id: "realroot".into(),
title: "Shared".into(),
clip_type: "gen".into(),
..Default::default()
}),
&Resolution {
roots: [(
"realroot".to_owned(),
RootInfo {
root_id: "realroot".into(),
root_title: "Shared".into(),
status: ResolveStatus::Resolved,
},
)]
.into_iter()
.collect(),
gap_filled: Vec::new(),
bridges: Vec::new(),
},
"now",
);
insert_cache_only_root(&mut store, "extroot");
store.set_album_overrides(
[("extroot".to_owned(), "Shared".to_owned())]
.into_iter()
.collect(),
);
let colliding = store.colliding_root_titles();
assert!(
colliding.contains("Shared"),
"a node-less overridden root must still be seen by collision detection"
);
}
#[test]
fn two_node_less_roots_overridden_to_same_name_collide() {
let mut store = LineageStore::new();
insert_cache_only_root(&mut store, "extone");
insert_cache_only_root(&mut store, "exttwo");
store.set_album_overrides(
[
("extone".to_owned(), "Shared".to_owned()),
("exttwo".to_owned(), "Shared".to_owned()),
]
.into_iter()
.collect(),
);
assert!(store.colliding_root_titles().contains("Shared"));
}
#[test]
fn colliding_node_less_overrides_keep_album_art_paths_distinct() {
let mut store = LineageStore::new();
insert_cache_only_root(&mut store, "aaaaaaaa-root-one");
insert_cache_only_root(&mut store, "bbbbbbbb-root-two");
store.set_album_overrides(
[
("aaaaaaaa-root-one".to_owned(), "Shared".to_owned()),
("bbbbbbbb-root-two".to_owned(), "Shared".to_owned()),
]
.into_iter()
.collect(),
);
let colliding = store.colliding_root_titles();
let colliding_ids = store.colliding_clip_ids();
let clip_of = |id: &str| Clip {
id: id.to_owned(),
title: "Track".to_owned(),
display_name: "alice".to_owned(),
image_large_url: "https://art.example/large.jpg".to_owned(),
..Default::default()
};
let ctx_of = |root_id: &str| LineageContext {
root_id: root_id.to_owned(),
root_title: "Shared".to_owned(),
root_date: String::new(),
parent_id: String::new(),
edge_type: None,
status: ResolveStatus::Resolved,
track: 0,
track_total: 0,
};
let clip_a = clip_of("clipaaaa-1111");
let clip_b = clip_of("clipbbbb-2222");
let ctx_a = ctx_of("aaaaaaaa-root-one");
let ctx_b = ctx_of("bbbbbbbb-root-two");
let requests = [
crate::naming::NamingRequest {
clip: &clip_a,
lineage: &ctx_a,
},
crate::naming::NamingRequest {
clip: &clip_b,
lineage: &ctx_b,
},
];
let names = crate::naming::render_clip_names(
&requests,
&crate::naming::NamingConfig::default(),
&colliding,
&colliding_ids,
);
let desired_of = |clip: &Clip, ctx: &LineageContext, name: &crate::naming::RenderedName| {
crate::reconcile::Desired {
clip: clip.clone(),
lineage: ctx.clone(),
path: format!(
"{}.flac",
crate::desired::rel_to_string(&name.relative_path)
),
format: crate::AudioFormat::Flac,
meta_hash: String::new(),
art_hash: String::new(),
embedded_lyrics_hash: String::new(),
modes: vec![crate::vocab::SourceMode::Mirror],
trashed: false,
private: false,
artifacts: Vec::new(),
stems: None,
}
};
let desired = vec![
desired_of(&clip_a, &ctx_a, &names[0]),
desired_of(&clip_b, &ctx_b, &names[1]),
];
let albums = crate::desired::album_desired(
&desired,
false,
false,
crate::vocab::WebpEncodeSettings::default(),
);
assert_eq!(albums.len(), 2, "each distinct root is its own album");
let jpg_paths: Vec<String> = albums
.iter()
.filter_map(|a| a.folder_jpg.as_ref().map(|art| art.path.clone()))
.collect();
assert_eq!(jpg_paths.len(), 2, "both albums have a folder.jpg");
assert_ne!(
jpg_paths[0], jpg_paths[1],
"colliding roots must not share one folder.jpg path"
);
}
#[test]
fn override_on_uncached_selected_root_is_ignored_and_keeps_albums_distinct() {
let mut store = LineageStore::new();
store.update(
std::slice::from_ref(&Clip {
id: "realroot".into(),
title: "Shared".into(),
clip_type: "gen".into(),
..Default::default()
}),
&Resolution {
roots: [(
"realroot".to_owned(),
RootInfo {
root_id: "realroot".into(),
root_title: "Shared".into(),
status: ResolveStatus::Resolved,
},
)]
.into_iter()
.collect(),
gap_filled: Vec::new(),
bridges: Vec::new(),
},
"now",
);
let new_clip = Clip {
id: "newnewnew-9999".into(),
title: "Solo Track".into(),
display_name: "alice".into(),
image_large_url: "https://art.example/large.jpg".into(),
..Default::default()
};
store.set_album_overrides(
[("newnewnew-9999".to_owned(), "Shared".to_owned())]
.into_iter()
.collect(),
);
let new_ctx = store.context_for(&new_clip);
assert_eq!(new_ctx.root_id, "newnewnew-9999");
assert_eq!(new_ctx.album(&new_clip.title), "Solo Track");
assert!(store.colliding_root_titles().is_empty());
let real_clip = Clip {
id: "realroot".into(),
title: "Shared".into(),
display_name: "alice".into(),
image_large_url: "https://art.example/large.jpg".into(),
..Default::default()
};
let real_ctx = store.context_for(&real_clip);
let colliding = store.colliding_root_titles();
let colliding_ids = store.colliding_clip_ids();
let requests = [
crate::naming::NamingRequest {
clip: &real_clip,
lineage: &real_ctx,
},
crate::naming::NamingRequest {
clip: &new_clip,
lineage: &new_ctx,
},
];
let names = crate::naming::render_clip_names(
&requests,
&crate::naming::NamingConfig::default(),
&colliding,
&colliding_ids,
);
let desired_of = |clip: &Clip, ctx: &LineageContext, name: &crate::naming::RenderedName| {
crate::reconcile::Desired {
clip: clip.clone(),
lineage: ctx.clone(),
path: format!(
"{}.flac",
crate::desired::rel_to_string(&name.relative_path)
),
format: crate::AudioFormat::Flac,
meta_hash: String::new(),
art_hash: String::new(),
embedded_lyrics_hash: String::new(),
modes: vec![crate::vocab::SourceMode::Mirror],
trashed: false,
private: false,
artifacts: Vec::new(),
stems: None,
}
};
let desired = vec![
desired_of(&real_clip, &real_ctx, &names[0]),
desired_of(&new_clip, &new_ctx, &names[1]),
];
let albums = crate::desired::album_desired(
&desired,
false,
false,
crate::vocab::WebpEncodeSettings::default(),
);
let jpg_paths: Vec<String> = albums
.iter()
.filter_map(|a| a.folder_jpg.as_ref().map(|art| art.path.clone()))
.collect();
assert_eq!(jpg_paths.len(), 2, "both albums have a folder.jpg");
assert_ne!(
jpg_paths[0], jpg_paths[1],
"an uncached override must not collapse two albums onto one path"
);
}
#[test]
fn override_on_gap_filled_root_applies_to_children_and_collides() {
let child = Clip {
id: "childclip".into(),
title: "Cover".into(),
clip_type: "gen".into(),
task: "cover".into(),
cover_clip_id: "gaproot".into(),
edited_clip_id: "gaproot".into(),
..Default::default()
};
let other_root = Clip {
id: "otherroot".into(),
title: "Preferred".into(),
clip_type: "gen".into(),
..Default::default()
};
let gap_ancestor = Clip {
id: "gaproot".into(),
title: "Working Title".into(),
clip_type: "gen".into(),
..Default::default()
};
let mut roots = HashMap::new();
roots.insert(
"childclip".to_owned(),
RootInfo {
root_id: "gaproot".into(),
root_title: "Working Title".into(),
status: ResolveStatus::Resolved,
},
);
roots.insert(
"otherroot".to_owned(),
RootInfo {
root_id: "otherroot".into(),
root_title: "Preferred".into(),
status: ResolveStatus::Resolved,
},
);
let mut store = LineageStore::new();
store.update(
&[child.clone(), other_root],
&Resolution {
roots,
gap_filled: vec![gap_ancestor],
bridges: Vec::new(),
},
"now",
);
assert!(store.node("gaproot").is_some());
assert!(!store.resolution_cache.contains_key("gaproot"));
store.set_album_overrides(
[("gaproot".to_owned(), "Preferred".to_owned())]
.into_iter()
.collect(),
);
assert_eq!(store.context_for(&child).album(&child.title), "Preferred");
assert_eq!(store.album_for_id("childclip"), "Preferred");
assert!(store.colliding_root_titles().contains("Preferred"));
}
#[test]
fn eligible_root_set_is_exactly_the_cache_value_domain() {
let child = Clip {
id: "childclip".into(),
title: "Cover".into(),
clip_type: "gen".into(),
task: "cover".into(),
cover_clip_id: "gaproot".into(),
edited_clip_id: "gaproot".into(),
..Default::default()
};
let mut roots = HashMap::new();
roots.insert(
"childclip".to_owned(),
RootInfo {
root_id: "gaproot".into(),
root_title: "Working Title".into(),
status: ResolveStatus::Resolved,
},
);
let mut store = LineageStore::new();
store.update(
std::slice::from_ref(&child),
&Resolution {
roots,
gap_filled: vec![Clip {
id: "gaproot".into(),
title: "Working Title".into(),
clip_type: "gen".into(),
..Default::default()
}],
bridges: Vec::new(),
},
"now",
);
let expected: std::collections::HashSet<String> = store
.resolution_cache
.values()
.map(|entry| entry.root_id.clone())
.filter(|root_id| !root_id.is_empty())
.collect();
assert_eq!(*store.eligible_root_ids_for_test(), expected);
assert!(store.eligible_root_ids_for_test().contains("gaproot"));
assert!(!store.resolution_cache.contains_key("gaproot"));
}