use super::*;
fn cover(path: &str, hash: &str) -> ArtifactState {
ArtifactState {
path: path.to_string(),
hash: hash.to_string(),
}
}
fn art(kind: ArtifactKind, path: &str, url: &str, hash: &str) -> DesiredArtifact {
DesiredArtifact {
kind,
path: path.to_string(),
source_url: url.to_string(),
hash: hash.to_string(),
content: None,
}
}
fn text_art(kind: ArtifactKind, path: &str, body: &str) -> DesiredArtifact {
DesiredArtifact {
kind,
path: path.to_string(),
source_url: String::new(),
hash: content_hash(body),
content: Some(body.to_string()),
}
}
fn desired_arts(id: &str, arts: Vec<DesiredArtifact>) -> Desired {
Desired {
artifacts: arts,
..desired(id, &format!("{id}.flac"), AudioFormat::Flac, "m", "art")
}
}
fn entry_with_cover_jpg(id: &str, cover_path: &str, cover_hash: &str) -> ManifestEntry {
ManifestEntry {
cover_jpg: Some(cover(cover_path, cover_hash)),
..entry(&format!("{id}.flac"), AudioFormat::Flac, "m", "art")
}
}
fn write_artifacts(plan: &Plan) -> Vec<&Action> {
plan.actions
.iter()
.filter(|a| matches!(a, Action::WriteArtifact { .. }))
.collect()
}
#[test]
fn write_artifact_emitted_when_manifest_lacks_it() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"a/cover.jpg",
"https://art/a",
"h1",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.skips(), 1);
assert_eq!(
write_artifacts(&plan)[0],
&Action::WriteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_string(),
source_url: "https://art/a".to_string(),
hash: "h1".to_string(),
owner_id: "a".to_string(),
content: None,
}
);
}
#[test]
fn write_artifact_emitted_when_hash_differs() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "old"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"a/cover.jpg",
"https://art/a",
"new",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
if let Action::WriteArtifact { hash, .. } = write_artifacts(&plan)[0] {
assert_eq!(hash, "new");
} else {
panic!("expected a WriteArtifact");
}
}
#[test]
fn write_artifact_skipped_when_hash_matches() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"a/cover.jpg",
"https://art/a",
"h1",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 0);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "a".to_string()
}]
);
}
#[test]
fn removed_kind_cover_is_kept_not_deleted() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.artifact_writes(), 0);
assert_eq!(plan.deletes(), 0);
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "a".to_string()
}]
);
assert!(!plan.actions.iter().any(|a| matches!(
a,
Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
..
}
)));
}
#[test]
fn delete_artifact_never_on_incomplete_listing() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
manifest.insert("b", entry_with_cover_jpg("b", "b/cover.jpg", "h1"));
let d = vec![desired_arts("a", vec![]), desired_arts("b", vec![])];
let sources = vec![SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
}];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
("b".to_string(), present(100)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &sources);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.deletes(), 0);
}
#[test]
fn delete_artifact_never_when_entry_preserved() {
let mut manifest = Manifest::new();
let preserved = ManifestEntry {
preserve: true,
..entry_with_cover_jpg("a", "a/cover.jpg", "h1")
};
manifest.insert("a", preserved);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn co_delete_never_when_path_empty() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry_with_cover_jpg("gone", "", "h1"));
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(plan.deletes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn co_delete_absent_clip_deletes_audio_and_cover() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry_with_cover_jpg("gone", "gone/cover.jpg", "h1"));
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(plan.deletes(), 1);
assert_eq!(plan.artifact_deletes(), 1);
assert!(plan.actions.contains(&Action::Delete {
path: "gone.flac".to_string(),
clip_id: "gone".to_string(),
}));
assert!(plan.actions.contains(&Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: "gone/cover.jpg".to_string(),
owner_id: "gone".to_string(),
}));
}
#[test]
fn co_delete_absent_clip_suppressed_when_not_enumerated() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry_with_cover_jpg("gone", "gone/cover.jpg", "h1"));
let sources = vec![SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
}];
let plan = reconcile(&manifest, &[], &HashMap::new(), &sources);
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn co_delete_trashed_desired_clip_removes_audio_and_cover() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let mut d = desired_arts("a", vec![]);
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(plan.deletes(), 1);
assert_eq!(plan.artifact_deletes(), 1);
}
#[test]
fn co_delete_trashed_suppressed_when_not_enumerated() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let mut d = desired_arts("a", vec![]);
d.trashed = true;
let sources = vec![SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
}];
let plan = reconcile(&manifest, &[d], &local_present("a"), &sources);
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn co_delete_trashed_suppressed_when_preserved() {
let mut manifest = Manifest::new();
let preserved = ManifestEntry {
preserve: true,
..entry_with_cover_jpg("a", "a/cover.jpg", "h1")
};
manifest.insert("a", preserved);
let mut d = desired_arts("a", vec![]);
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn details_sidecar_written_with_inline_content_when_slot_absent() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired_arts(
"a",
vec![text_art(
ArtifactKind::DetailsTxt,
"a.details.txt",
"Title: A\n",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(
write_artifacts(&plan)[0],
&Action::WriteArtifact {
kind: ArtifactKind::DetailsTxt,
path: "a.details.txt".to_string(),
source_url: String::new(),
hash: content_hash("Title: A\n"),
owner_id: "a".to_string(),
content: Some("Title: A\n".to_string()),
}
);
}
#[test]
fn lrc_sidecar_written_with_inline_content_when_slot_absent() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let body = "[re:rs-suno]\nla la\n";
let d = vec![desired_arts(
"a",
vec![text_art(ArtifactKind::Lrc, "a.lrc", body)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(
write_artifacts(&plan)[0],
&Action::WriteArtifact {
kind: ArtifactKind::Lrc,
path: "a.lrc".to_string(),
source_url: String::new(),
hash: content_hash(body),
owner_id: "a".to_string(),
content: Some(body.to_string()),
}
);
}
#[test]
fn text_sidecars_skipped_when_hash_and_path_match() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.details_txt = Some(cover("a.details.txt", &content_hash("Title: A\n")));
e.lyrics_txt = Some(cover("a.lyrics.txt", &content_hash("la la\n")));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![
text_art(ArtifactKind::DetailsTxt, "a.details.txt", "Title: A\n"),
text_art(ArtifactKind::LyricsTxt, "a.lyrics.txt", "la la\n"),
],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 0);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn details_rewritten_when_content_hash_differs() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.details_txt = Some(cover("a.details.txt", &content_hash("Title: Old\n")));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![text_art(
ArtifactKind::DetailsTxt,
"a.details.txt",
"Title: New\n",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn lyrics_rewritten_when_content_hash_differs_though_meta_unchanged() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.lyrics_txt = Some(cover("a.lyrics.txt", &content_hash("old words\n")));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![text_art(
ArtifactKind::LyricsTxt,
"a.lyrics.txt",
"new words\n",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.retags(), 0);
}
#[test]
fn text_sidecar_relocated_when_path_differs() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.details_txt = Some(cover("old/a.details.txt", &content_hash("Title: A\n")));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![text_art(
ArtifactKind::DetailsTxt,
"new/a.details.txt",
"Title: A\n",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
if let Action::WriteArtifact { path, .. } = write_artifacts(&plan)[0] {
assert_eq!(path, "new/a.details.txt");
} else {
panic!("expected a WriteArtifact");
}
}
#[test]
fn fetched_sidecar_path_drift_emits_move() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.cover_jpg = Some(cover("old/cover.jpg", "arthash"));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"new/cover.jpg",
"https://art/large.jpg",
"arthash",
)],
)];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
("old/cover.jpg".to_string(), present(50)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(plan.artifact_moves(), 1);
assert_eq!(plan.artifact_writes(), 0);
assert!(plan.actions.contains(&Action::MoveArtifact {
kind: ArtifactKind::CoverJpg,
from: "old/cover.jpg".to_string(),
to: "new/cover.jpg".to_string(),
source_url: "https://art/large.jpg".to_string(),
hash: "arthash".to_string(),
owner_id: "a".to_string(),
}));
}
#[test]
fn sidecar_hash_drift_emits_write_not_move() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.cover_jpg = Some(cover("old/cover.jpg", "oldhash"));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"new/cover.jpg",
"https://art/large.jpg",
"newhash",
)],
)];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
("old/cover.jpg".to_string(), present(50)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(plan.artifact_moves(), 0);
assert_eq!(plan.artifact_writes(), 1);
}
#[test]
fn inline_sidecar_path_drift_stays_a_write() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.lyrics_txt = Some(cover("old/a.lyrics.txt", &content_hash("words\n")));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![text_art(
ArtifactKind::LyricsTxt,
"new/a.lyrics.txt",
"words\n",
)],
)];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
("old/a.lyrics.txt".to_string(), present(50)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(plan.artifact_moves(), 0);
assert_eq!(plan.artifact_writes(), 1);
}
#[test]
fn sidecar_move_downgrades_to_write_when_old_file_absent() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.cover_jpg = Some(cover("old/cover.jpg", "arthash"));
manifest.insert("a", e);
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"new/cover.jpg",
"https://art/large.jpg",
"arthash",
)],
)];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
(
"old/cover.jpg".to_string(),
LocalFile {
exists: false,
size: 0,
},
),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(plan.artifact_moves(), 0);
assert_eq!(plan.artifact_writes(), 1);
}
#[test]
fn move_target_suppresses_a_colliding_delete() {
let mut manifest = Manifest::new();
let mut a = entry("a.flac", AudioFormat::Flac, "m", "art");
a.cover_jpg = Some(cover("old/cover.jpg", "arthash"));
manifest.insert("a", a);
let mut b = entry("b.flac", AudioFormat::Flac, "m", "art");
b.details_txt = Some(cover("new/cover.jpg", "bh"));
manifest.insert("b", b);
let d = vec![
desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"new/cover.jpg",
"https://art/large.jpg",
"arthash",
)],
),
desired_arts("b", vec![]),
];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
("b".to_string(), present(100)),
("old/cover.jpg".to_string(), present(50)),
("new/cover.jpg".to_string(), present(50)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(plan.artifact_moves(), 1);
assert!(!plan.actions.iter().any(|a| matches!(
a,
Action::DeleteArtifact { path, .. } if path == "new/cover.jpg"
)));
}
#[test]
fn details_removed_kind_is_deleted_when_feature_off() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.details_txt = Some(cover("a.details.txt", &content_hash("Title: A\n")));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 1);
assert!(plan.actions.contains(&Action::DeleteArtifact {
kind: ArtifactKind::DetailsTxt,
path: "a.details.txt".to_string(),
owner_id: "a".to_string(),
}));
}
#[test]
fn cover_webp_retired_sidecar_is_deleted_through_the_gate() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.cover_webp = Some(cover("a.webp", "v1"));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 1);
assert!(plan.actions.contains(&Action::DeleteArtifact {
kind: ArtifactKind::CoverWebp,
path: "a.webp".to_string(),
owner_id: "a".to_string(),
}));
}
#[test]
fn cover_webp_retired_cleanup_respects_the_deletion_gate() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.cover_webp = Some(cover("a.webp", "v1"));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let not_enumerated = vec![SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
}];
let plan = reconcile(&manifest, &d, &local_present("a"), ¬_enumerated);
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.deletes(), 0);
}
#[test]
fn lyrics_removed_kind_is_kept_not_deleted() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.lyrics_txt = Some(cover("a.lyrics.txt", &content_hash("words\n")));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.deletes(), 0);
}
#[test]
fn lrc_removed_kind_is_kept_not_deleted() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.lrc = Some(cover("a.lrc", &content_hash("[re:rs-suno]\nwords\n")));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.deletes(), 0);
}
#[test]
fn video_mp4_removed_kind_is_kept_not_deleted() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.video_mp4 = Some(cover("a.mp4", "vid-hash"));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
assert_eq!(plan.deletes(), 0);
}
#[test]
fn video_mp4_written_when_manifest_lacks_it() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::VideoMp4,
"a/song.mp4",
"https://cdn/a/video.mp4",
"vid-hash",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(
write_artifacts(&plan)[0],
&Action::WriteArtifact {
kind: ArtifactKind::VideoMp4,
path: "a/song.mp4".to_string(),
source_url: "https://cdn/a/video.mp4".to_string(),
hash: "vid-hash".to_string(),
owner_id: "a".to_string(),
content: None,
}
);
}
#[test]
fn details_removed_kind_not_deleted_on_incomplete_listing() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.details_txt = Some(cover("a.details.txt", &content_hash("Title: A\n")));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let sources = vec![SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
}];
let plan = reconcile(&manifest, &d, &local_present("a"), &sources);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn details_removed_kind_not_deleted_when_preserved() {
let mut manifest = Manifest::new();
let mut e = ManifestEntry {
preserve: true,
..entry("a.flac", AudioFormat::Flac, "m", "art")
};
e.details_txt = Some(cover("a.details.txt", &content_hash("Title: A\n")));
manifest.insert("a", e);
let d = vec![desired_arts("a", vec![])];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn co_delete_orphan_removes_every_text_sidecar() {
let mut manifest = Manifest::new();
let mut e = entry("gone.flac", AudioFormat::Flac, "m", "art");
e.cover_jpg = Some(cover("gone/cover.jpg", "h1"));
e.details_txt = Some(cover("gone.details.txt", &content_hash("Title: G\n")));
e.lyrics_txt = Some(cover("gone.lyrics.txt", &content_hash("words\n")));
e.lrc = Some(cover("gone.lrc", &content_hash("[re:rs-suno]\nwords\n")));
e.video_mp4 = Some(cover("gone/song.mp4", "vid-hash"));
manifest.insert("gone", e);
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(plan.deletes(), 1);
assert_eq!(plan.artifact_deletes(), 5);
for (kind, path) in [
(ArtifactKind::CoverJpg, "gone/cover.jpg"),
(ArtifactKind::DetailsTxt, "gone.details.txt"),
(ArtifactKind::LyricsTxt, "gone.lyrics.txt"),
(ArtifactKind::Lrc, "gone.lrc"),
(ArtifactKind::VideoMp4, "gone/song.mp4"),
] {
assert!(
plan.actions.contains(&Action::DeleteArtifact {
kind,
path: path.to_string(),
owner_id: "gone".to_string(),
}),
"missing co-delete for {kind:?}"
);
}
}
#[test]
fn co_delete_trashed_removes_every_text_sidecar() {
let mut manifest = Manifest::new();
let mut e = entry("a.flac", AudioFormat::Flac, "m", "art");
e.details_txt = Some(cover("a.details.txt", &content_hash("Title: A\n")));
e.lyrics_txt = Some(cover("a.lyrics.txt", &content_hash("words\n")));
manifest.insert("a", e);
let mut d = desired_arts("a", vec![]);
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(plan.deletes(), 1);
assert_eq!(plan.artifact_deletes(), 2);
}
#[test]
fn suppress_downgrades_delete_artifact_colliding_with_write_artifact() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
manifest.insert("b", entry_with_cover_jpg("b", "shared/cover.jpg", "h1"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"shared/cover.jpg",
"https://art/a",
"h2",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert!(
!plan.actions.iter().any(
|a| matches!(a, Action::DeleteArtifact { path, .. } if path == "shared/cover.jpg")
)
);
assert!(plan.actions.contains(&Action::Delete {
path: "b.flac".to_string(),
clip_id: "b".to_string(),
}));
}
#[test]
fn suppress_downgrades_delete_artifact_colliding_with_download() {
let mut manifest = Manifest::new();
manifest.insert("b", entry_with_cover_jpg("b", "shared/x", "h1"));
let d = vec![desired("a", "shared/x", AudioFormat::Flac, "m", "art")];
let plan = reconcile(&manifest, &d, &HashMap::new(), &mirror_ok());
assert_eq!(plan.downloads(), 1);
assert!(
!plan
.actions
.iter()
.any(|a| matches!(a, Action::DeleteArtifact { path, .. } if path == "shared/x"))
);
}
#[test]
fn adding_artifacts_leaves_the_audio_plan_unchanged() {
let build = |with_art: bool| {
let mut manifest = Manifest::new();
manifest.insert("keep", entry_with_cover_jpg("keep", "keep/cover.jpg", "h1"));
manifest.insert("gone", entry_with_cover_jpg("gone", "gone/cover.jpg", "h1"));
manifest.insert(
"trash",
entry_with_cover_jpg("trash", "trash/cover.jpg", "h1"),
);
let keep = if with_art {
desired_arts(
"keep",
vec![art(
ArtifactKind::CoverJpg,
"keep/cover.jpg",
"https://art/keep",
"h1",
)],
)
} else {
desired_arts("keep", vec![])
};
let mut trash = desired_arts("trash", vec![]);
trash.trashed = true;
let local: HashMap<String, LocalFile> = ["keep", "gone", "trash"]
.iter()
.map(|id| (id.to_string(), present(100)))
.collect();
reconcile(&manifest, &[keep, trash], &local, &mirror_ok())
};
let with = build(true);
let without = build(false);
let audio = |plan: &Plan| -> Vec<Action> {
plan.actions
.iter()
.filter(|a| {
!matches!(
a,
Action::WriteArtifact { .. } | Action::DeleteArtifact { .. }
)
})
.cloned()
.collect()
};
assert_eq!(audio(&with), audio(&without));
assert_eq!(with.deletes(), without.deletes());
assert_eq!(with.deletes(), 2);
assert_eq!(with.artifact_deletes(), 2);
assert_eq!(with.artifact_writes(), 0);
}
#[test]
fn removed_kind_sidecar_kept_when_clip_is_protected_this_run() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
assert!(!manifest.get("a").unwrap().preserve);
let private = Desired {
private: true,
..desired_arts("a", vec![])
};
let plan = reconcile(&manifest, &[private], &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
let copy_held = Desired {
modes: vec![SourceMode::Copy],
..desired_arts("a", vec![])
};
let plan = reconcile(&manifest, &[copy_held], &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn write_artifact_emitted_when_path_differs_even_if_hash_matches() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "old/cover.jpg", "h1"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"new/cover.jpg",
"https://art/a",
"h1",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
assert_eq!(plan.artifact_deletes(), 0);
if let Action::WriteArtifact { path, .. } = write_artifacts(&plan)[0] {
assert_eq!(path, "new/cover.jpg");
} else {
panic!("expected a WriteArtifact");
}
}
#[test]
fn needs_write_drift_applies_hash_path_and_probe_rules() {
let local: HashMap<String, LocalFile> = [
("ok".to_string(), present(10)),
("missing".to_string(), LocalFile::default()),
("empty".to_string(), present(0)),
]
.into_iter()
.collect();
assert!(needs_write_drift(None, "h1", "ok", &local));
assert!(!needs_write_drift(Some(("h1", "ok")), "h1", "ok", &local));
assert!(needs_write_drift(Some(("h0", "ok")), "h1", "ok", &local));
assert!(needs_write_drift(
Some(("h1", "missing")),
"h1",
"missing",
&local
));
assert!(needs_write_drift(
Some(("h1", "empty")),
"h1",
"empty",
&local
));
assert!(!needs_write_drift(
Some(("h1", "unprobed")),
"h1",
"unprobed",
&local
));
}
#[test]
fn per_clip_reconcile_ignores_album_and_library_kinds() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired_arts(
"a",
vec![
art(
ArtifactKind::FolderJpg,
"a/folder.jpg",
"https://art/folder",
"hf",
),
art(
ArtifactKind::Playlist,
"a/list.m3u",
"https://art/list",
"hp",
),
art(ArtifactKind::CoverJpg, "a/cover.jpg", "https://art/a", "h1"),
],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 1);
let paths: Vec<&str> = plan
.actions
.iter()
.filter_map(|a| match a {
Action::WriteArtifact { path, .. } => Some(path.as_str()),
_ => None,
})
.collect();
assert_eq!(paths, vec!["a/cover.jpg"]);
}
#[test]
fn per_clip_reconcile_emits_nothing_for_album_only_artifacts() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::FolderWebp,
"a/folder.webp",
"https://art/folder",
"hf",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(plan.artifact_writes(), 0);
assert_eq!(plan.artifact_deletes(), 0);
}
fn local_with_missing(audio_id: &str, missing_path: &str) -> HashMap<String, LocalFile> {
let mut m = local_present(audio_id);
m.insert(missing_path.to_owned(), LocalFile::default());
m
}
fn local_with_present_artifact(audio_id: &str, artifact_path: &str) -> HashMap<String, LocalFile> {
let mut m = local_present(audio_id);
m.insert(artifact_path.to_owned(), present(50));
m
}
#[test]
fn sidecar_missing_on_disk_forces_rewrite() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"a/cover.jpg",
"https://art/a",
"h1",
)],
)];
let local = local_with_missing("a", "a/cover.jpg");
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(
plan.artifact_writes(),
1,
"missing sidecar must be rewritten"
);
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn sidecar_present_on_disk_with_matching_hash_no_churn() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"a/cover.jpg",
"https://art/a",
"h1",
)],
)];
let local = local_with_present_artifact("a", "a/cover.jpg");
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert_eq!(plan.artifact_writes(), 0, "present sidecar must not churn");
assert_eq!(plan.artifact_deletes(), 0);
}
#[test]
fn sidecar_probe_absent_falls_back_to_hash_comparison_no_write() {
let mut manifest = Manifest::new();
manifest.insert("a", entry_with_cover_jpg("a", "a/cover.jpg", "h1"));
let d = vec![desired_arts(
"a",
vec![art(
ArtifactKind::CoverJpg,
"a/cover.jpg",
"https://art/a",
"h1",
)],
)];
let plan = reconcile(&manifest, &d, &local_present("a"), &mirror_ok());
assert_eq!(
plan.artifact_writes(),
0,
"no write when probe unavailable and hash matches"
);
assert_eq!(
plan.artifact_deletes(),
0,
"missing probe must never trigger a delete"
);
}