use super::*;
use crate::desired::album_desired;
use crate::hash::{art_url_hash, webp_art_hash};
use crate::reconcile::album::delete_album_artifact_action;
use crate::vocab::WebpEncodeSettings;
#[test]
fn folder_art_missing_on_disk_forces_rewrite() {
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", ""),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, false, false, WebpEncodeSettings::default());
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", &art_url_hash("art-a"))),
folder_webp: None,
folder_mp4: None,
},
);
let mut local: HashMap<String, LocalFile> = HashMap::new();
local.insert("c/al/folder.jpg".to_owned(), LocalFile::default());
let actions = plan_album_artifacts(&desired, &albums, true, &local);
assert_eq!(actions.len(), 1, "missing folder art must be rewritten");
assert!(matches!(
&actions[0],
Action::WriteArtifact {
kind: ArtifactKind::FolderJpg,
..
}
));
}
#[test]
fn folder_art_present_on_disk_no_churn() {
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", ""),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, false, false, WebpEncodeSettings::default());
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", &art_url_hash("art-a"))),
folder_webp: None,
folder_mp4: None,
},
);
let mut local: HashMap<String, LocalFile> = HashMap::new();
local.insert("c/al/folder.jpg".to_owned(), present(5000));
let actions = plan_album_artifacts(&desired, &albums, true, &local);
assert!(
actions.is_empty(),
"present folder art with matching hash must not churn"
);
}
fn album_clip(id: &str, play_count: u64, created_at: &str, image: &str, video: &str) -> Clip {
Clip {
id: id.to_string(),
title: "Song".to_string(),
image_large_url: image.to_string(),
video_cover_url: video.to_string(),
play_count,
created_at: created_at.to_string(),
..Default::default()
}
}
fn album_member(clip: Clip, root_id: &str, path: &str) -> Desired {
let mut lineage = LineageContext::own_root(&clip);
lineage.root_id = root_id.to_string();
Desired {
clip,
lineage,
path: path.to_string(),
format: AudioFormat::Flac,
meta_hash: "m".to_string(),
art_hash: "a".to_string(),
modes: vec![SourceMode::Mirror],
trashed: false,
private: false,
artifacts: Vec::new(),
stems: None,
}
}
fn stored(path: &str, hash: &str) -> ArtifactState {
ArtifactState {
path: path.to_string(),
hash: hash.to_string(),
}
}
#[test]
fn plan_writes_folder_art_when_store_empty() {
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", "vid-a"),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, true, false, WebpEncodeSettings::default());
let actions = plan_album_artifacts(&desired, &BTreeMap::new(), true, &HashMap::new());
assert_eq!(
actions,
vec![
Action::WriteArtifact {
kind: ArtifactKind::FolderJpg,
path: "c/al/folder.jpg".to_string(),
source_url: "art-a".to_string(),
hash: art_url_hash("art-a"),
owner_id: "root".to_string(),
content: None,
},
Action::WriteArtifact {
kind: ArtifactKind::FolderWebp,
path: "c/al/cover.webp".to_string(),
source_url: "vid-a".to_string(),
hash: webp_art_hash("vid-a", &WebpEncodeSettings::default()),
owner_id: "root".to_string(),
content: None,
},
]
);
}
#[test]
fn plan_skips_when_hash_and_path_match() {
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", ""),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, false, false, WebpEncodeSettings::default());
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", &art_url_hash("art-a"))),
folder_webp: None,
folder_mp4: None,
},
);
assert!(plan_album_artifacts(&desired, &albums, true, &HashMap::new()).is_empty());
}
#[test]
fn plan_rewrites_when_path_drifts_even_if_hash_matches() {
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", ""),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, false, false, WebpEncodeSettings::default());
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("old/folder.jpg", &art_url_hash("art-a"))),
folder_webp: None,
folder_mp4: None,
},
);
let actions = plan_album_artifacts(&desired, &albums, true, &HashMap::new());
assert_eq!(actions.len(), 1);
assert!(matches!(
&actions[0],
Action::WriteArtifact { path, .. } if path == "c/al/folder.jpg"
));
}
#[test]
fn h1_most_played_flip_to_same_art_writes_nothing() {
let run1 = vec![
album_member(
album_clip("a", 9, "t0", "same-art", ""),
"root",
"c/al/a.flac",
),
album_member(
album_clip("b", 1, "t1", "same-art", ""),
"root",
"c/al/b.flac",
),
];
let desired1 = album_desired(&run1, false, false, WebpEncodeSettings::default());
let write1 = plan_album_artifacts(&desired1, &BTreeMap::new(), true, &HashMap::new());
assert_eq!(write1.len(), 1);
let mut albums = BTreeMap::new();
if let Action::WriteArtifact {
path,
hash,
owner_id,
..
} = &write1[0]
{
albums.insert(
owner_id.clone(),
AlbumArt {
folder_jpg: Some(stored(path, hash)),
folder_webp: None,
folder_mp4: None,
},
);
}
let run2 = vec![
album_member(
album_clip("a", 1, "t0", "same-art", ""),
"root",
"c/al/a.flac",
),
album_member(
album_clip("b", 9, "t1", "same-art", ""),
"root",
"c/al/b.flac",
),
];
let desired2 = album_desired(&run2, false, false, WebpEncodeSettings::default());
assert!(plan_album_artifacts(&desired2, &albums, true, &HashMap::new()).is_empty());
}
#[test]
fn h1_flip_to_different_art_writes_exactly_one() {
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", &art_url_hash("old-art"))),
folder_webp: None,
folder_mp4: None,
},
);
let members = vec![
album_member(
album_clip("a", 1, "t0", "old-art", ""),
"root",
"c/al/a.flac",
),
album_member(
album_clip("b", 9, "t1", "new-art", ""),
"root",
"c/al/b.flac",
),
];
let desired = album_desired(&members, false, false, WebpEncodeSettings::default());
let actions = plan_album_artifacts(&desired, &albums, true, &HashMap::new());
assert_eq!(actions.len(), 1);
assert!(matches!(
&actions[0],
Action::WriteArtifact { hash, .. } if *hash == art_url_hash("new-art")
));
}
#[test]
fn one_write_per_album_regardless_of_clip_count() {
let members: Vec<Desired> = (0..200)
.map(|i| {
album_member(
album_clip(
&format!("clip-{i:03}"),
i as u64,
&format!("t{i:03}"),
&format!("art-{i:03}"),
&format!("vid-{i:03}"),
),
"root",
&format!("c/al/clip-{i:03}.flac"),
)
})
.collect();
let desired = album_desired(&members, true, false, WebpEncodeSettings::default());
assert_eq!(desired.len(), 1);
let actions = plan_album_artifacts(&desired, &BTreeMap::new(), true, &HashMap::new());
assert_eq!(actions.len(), 2);
assert_eq!(
actions
.iter()
.filter(|a| matches!(a, Action::WriteArtifact { .. }))
.count(),
2
);
}
#[test]
fn emptied_album_deletes_only_when_can_delete() {
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", "h")),
folder_webp: Some(stored("c/al/cover.webp", "hw")),
folder_mp4: Some(stored("c/al/cover.mp4", "hm")),
},
);
let desired: Vec<AlbumDesired> = Vec::new();
assert!(plan_album_artifacts(&desired, &albums, false, &HashMap::new()).is_empty());
let actions = plan_album_artifacts(&desired, &albums, true, &HashMap::new());
assert_eq!(
actions,
vec![
Action::DeleteArtifact {
kind: ArtifactKind::FolderJpg,
path: "c/al/folder.jpg".to_string(),
owner_id: "root".to_string(),
},
Action::DeleteArtifact {
kind: ArtifactKind::FolderWebp,
path: "c/al/cover.webp".to_string(),
owner_id: "root".to_string(),
},
Action::DeleteArtifact {
kind: ArtifactKind::FolderMp4,
path: "c/al/cover.mp4".to_string(),
owner_id: "root".to_string(),
},
]
);
}
#[test]
fn disappeared_webp_source_deletes_only_that_kind_when_gated() {
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", &art_url_hash("art-a"))),
folder_webp: Some(stored("c/al/cover.webp", &art_url_hash("vid-a"))),
folder_mp4: None,
},
);
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", "vid-a"),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, false, false, WebpEncodeSettings::default());
assert!(plan_album_artifacts(&desired, &albums, false, &HashMap::new()).is_empty());
let actions = plan_album_artifacts(&desired, &albums, true, &HashMap::new());
assert_eq!(
actions,
vec![Action::DeleteArtifact {
kind: ArtifactKind::FolderWebp,
path: "c/al/cover.webp".to_string(),
owner_id: "root".to_string(),
}]
);
}
#[test]
fn disappeared_raw_cover_deletes_only_that_kind_when_gated() {
let mut albums = BTreeMap::new();
albums.insert(
"root".to_string(),
AlbumArt {
folder_jpg: Some(stored("c/al/folder.jpg", &art_url_hash("art-a"))),
folder_webp: Some(stored(
"c/al/cover.webp",
&webp_art_hash("vid-a", &WebpEncodeSettings::default()),
)),
folder_mp4: Some(stored("c/al/cover.mp4", &art_url_hash("vid-a"))),
},
);
let members = vec![album_member(
album_clip("a", 1, "t0", "art-a", "vid-a"),
"root",
"c/al/a.flac",
)];
let desired = album_desired(&members, true, false, WebpEncodeSettings::default());
assert!(plan_album_artifacts(&desired, &albums, false, &HashMap::new()).is_empty());
let actions = plan_album_artifacts(&desired, &albums, true, &HashMap::new());
assert_eq!(
actions,
vec![Action::DeleteArtifact {
kind: ArtifactKind::FolderMp4,
path: "c/al/cover.mp4".to_string(),
owner_id: "root".to_string(),
}]
);
}
#[test]
fn plan_album_artifacts_is_deterministically_ordered() {
let members = vec![
album_member(
album_clip("a", 1, "t0", "art-a", "vid-a"),
"r2",
"c/al2/a.flac",
),
album_member(
album_clip("b", 1, "t0", "art-b", "vid-b"),
"r1",
"c/al1/b.flac",
),
];
let desired = album_desired(&members, true, true, WebpEncodeSettings::default());
let actions = plan_album_artifacts(&desired, &BTreeMap::new(), true, &HashMap::new());
let keys: Vec<(&str, ArtifactKind)> = actions
.iter()
.map(|a| match a {
Action::WriteArtifact { owner_id, kind, .. } => (owner_id.as_str(), *kind),
_ => unreachable!(),
})
.collect();
assert_eq!(
keys,
vec![
("r1", ArtifactKind::FolderJpg),
("r1", ArtifactKind::FolderWebp),
("r1", ArtifactKind::FolderMp4),
("r2", ArtifactKind::FolderJpg),
("r2", ArtifactKind::FolderWebp),
("r2", ArtifactKind::FolderMp4),
]
);
}
#[test]
fn delete_album_artifact_gate_needs_the_verdict_and_a_path() {
assert_eq!(
delete_album_artifact_action("r1", ArtifactKind::FolderJpg, "r1/folder.jpg", true),
Some(Action::DeleteArtifact {
kind: ArtifactKind::FolderJpg,
path: "r1/folder.jpg".to_owned(),
owner_id: "r1".to_owned(),
})
);
assert_eq!(
delete_album_artifact_action("r1", ArtifactKind::FolderJpg, "r1/folder.jpg", false),
None
);
assert_eq!(
delete_album_artifact_action("r1", ArtifactKind::FolderJpg, "", true),
None
);
}