use super::*;
#[test]
fn trashed_but_complete_clip_is_downloadable_yet_still_deletes() {
let mut trashed = clip("a");
trashed.status = "complete".to_string();
trashed.is_trashed = true;
assert!(crate::is_downloadable(&trashed));
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.clip = trashed;
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(
plan.actions,
vec![Action::Delete {
path: "a.flac".to_string(),
clip_id: "a".to_string(),
}]
);
}
#[test]
fn trashed_clip_deletes_local_file() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(
plan.actions,
vec![Action::Delete {
path: "a.flac".to_string(),
clip_id: "a".to_string(),
}]
);
}
#[test]
fn trashed_clip_not_in_manifest_skips() {
let manifest = Manifest::new();
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.trashed = true;
let plan = reconcile(&manifest, &[d], &HashMap::new(), &mirror_ok());
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "a".to_string()
}]
);
}
#[test]
fn private_clip_is_kept() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.private = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "a".to_string()
}]
);
}
#[test]
fn private_beats_trashed_never_deletes() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.trashed = true;
d.private = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn copy_held_trashed_clip_is_not_deleted() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.modes = vec![SourceMode::Copy];
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "a".to_string()
}]
);
}
#[test]
fn absent_clip_deleted_when_all_mirrors_enumerated() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(
plan.actions,
vec![Action::Delete {
path: "gone.flac".to_string(),
clip_id: "gone".to_string(),
}]
);
}
#[test]
fn absent_clip_kept_when_any_mirror_not_enumerated() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
let sources = vec![
SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: true,
},
SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
},
];
let plan = reconcile(&manifest, &[], &HashMap::new(), &sources);
assert_eq!(plan.deletes(), 0);
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "gone".to_string()
}]
);
}
#[test]
fn empty_listing_cannot_cause_deletion() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
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.skips(), 1);
}
#[test]
fn no_mirror_sources_means_no_deletion() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
let copy_only = vec![SourceStatus {
mode: SourceMode::Copy,
fully_enumerated: true,
}];
assert_eq!(
reconcile(&manifest, &[], &HashMap::new(), ©_only).deletes(),
0
);
assert_eq!(reconcile(&manifest, &[], &HashMap::new(), &[]).deletes(), 0);
}
#[test]
fn copy_source_with_unenumerated_mirror_still_suppresses_deletion() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
let sources = vec![
SourceStatus {
mode: SourceMode::Copy,
fully_enumerated: true,
},
SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: false,
},
];
assert_eq!(
reconcile(&manifest, &[], &HashMap::new(), &sources).deletes(),
0
);
}
#[test]
fn area_authoritative_requires_all_conditions() {
assert!(area_authoritative(true, false, false));
assert!(!area_authoritative(false, false, false));
assert!(!area_authoritative(true, true, false));
assert!(!area_authoritative(true, false, true));
assert!(!area_authoritative(false, true, true));
}
#[test]
fn area_fully_enumerated_applies_empty_mirror_guard() {
assert!(area_fully_enumerated(true, false, SourceMode::Mirror));
assert!(!area_fully_enumerated(true, true, SourceMode::Mirror));
assert!(area_fully_enumerated(true, true, SourceMode::Copy));
assert!(area_fully_enumerated(true, false, SourceMode::Copy));
assert!(!area_fully_enumerated(false, false, SourceMode::Mirror));
assert!(!area_fully_enumerated(false, true, SourceMode::Copy));
}
#[test]
fn narrows_downloads_only_when_no_deletion_and_no_full_library() {
assert!(narrows_downloads(false, false));
assert!(!narrows_downloads(true, false));
assert!(!narrows_downloads(false, true));
assert!(!narrows_downloads(true, true));
}
#[test]
fn narrowing_never_coexists_with_deletion() {
for can_delete in [false, true] {
for lib_auth in [false, true] {
assert!(
!(narrows_downloads(can_delete, lib_auth) && can_delete),
"truncate must imply !can_delete"
);
}
}
}
#[test]
fn copy_held_clip_in_desired_is_never_a_deletion_candidate() {
let mut manifest = Manifest::new();
manifest.insert("keep", entry("keep.flac", AudioFormat::Flac, "m", "art"));
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
let mut held = desired("keep", "keep.flac", AudioFormat::Flac, "m", "art");
held.modes = vec![SourceMode::Copy];
let local: HashMap<String, LocalFile> = [
("keep".to_string(), present(100)),
("gone".to_string(), present(100)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &[held], &local, &mirror_ok());
assert!(plan.actions.contains(&Action::Skip {
clip_id: "keep".to_string()
}));
assert!(plan.actions.contains(&Action::Delete {
path: "gone.flac".to_string(),
clip_id: "gone".to_string(),
}));
assert!(
!plan
.actions
.iter()
.any(|a| matches!(a, Action::Delete { clip_id, .. } if clip_id == "keep"))
);
}
#[test]
fn orphan_with_preserve_marker_is_kept() {
let mut manifest = Manifest::new();
manifest.insert(
"gone",
preserved_entry("gone.flac", AudioFormat::Flac, "m", "art"),
);
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "gone".to_string()
}]
);
}
#[test]
fn trashed_clip_with_preserve_marker_is_kept() {
let mut manifest = Manifest::new();
manifest.insert(
"a",
preserved_entry("a.flac", AudioFormat::Flac, "m", "art"),
);
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn trashed_clip_kept_when_a_mirror_is_not_enumerated() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
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.skips(), 1);
}
#[test]
fn trashed_clip_kept_when_sources_empty() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.trashed = true;
let plan = reconcile(&manifest, &[d], &local_present("a"), &[]);
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn failed_copy_listing_suppresses_orphan_deletion() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("gone.flac", AudioFormat::Flac, "m", "art"));
let sources = vec![
SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: true,
},
SourceStatus {
mode: SourceMode::Copy,
fully_enumerated: false,
},
];
let plan = reconcile(&manifest, &[], &HashMap::new(), &sources);
assert_eq!(plan.deletes(), 0);
}
#[test]
fn failed_copy_listing_suppresses_trashed_deletion() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut d = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
d.trashed = true;
let sources = vec![
SourceStatus {
mode: SourceMode::Mirror,
fully_enumerated: true,
},
SourceStatus {
mode: SourceMode::Copy,
fully_enumerated: false,
},
];
let plan = reconcile(&manifest, &[d], &local_present("a"), &sources);
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn empty_path_entry_never_deletes() {
let mut manifest = Manifest::new();
manifest.insert("gone", entry("", AudioFormat::Flac, "m", "art"));
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(
plan.actions,
vec![Action::Skip {
clip_id: "gone".to_string()
}]
);
}
#[test]
fn delete_suppressed_when_path_aliases_rename_target() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("old/a.flac", AudioFormat::Flac, "m", "art"));
manifest.insert("b", entry("new/a.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired("a", "new/a.flac", AudioFormat::Flac, "m", "art")];
let local: HashMap<String, LocalFile> = [
("a".to_string(), present(100)),
("b".to_string(), present(100)),
]
.into_iter()
.collect();
let plan = reconcile(&manifest, &d, &local, &mirror_ok());
assert!(plan.actions.contains(&Action::Rename {
from: "old/a.flac".to_string(),
to: "new/a.flac".to_string(),
}));
assert!(
!plan
.actions
.iter()
.any(|a| matches!(a, Action::Delete { path, .. } if path == "new/a.flac"))
);
assert!(plan.actions.contains(&Action::Skip {
clip_id: "b".to_string()
}));
}
#[test]
fn delete_suppressed_when_path_aliases_download_target() {
let mut manifest = Manifest::new();
manifest.insert("b", entry("shared.flac", AudioFormat::Flac, "m", "art"));
let d = vec![desired("a", "shared.flac", AudioFormat::Flac, "m", "art")];
let plan = reconcile(&manifest, &d, &HashMap::new(), &mirror_ok());
assert!(
!plan
.actions
.iter()
.any(|a| matches!(a, Action::Delete { .. }))
);
assert_eq!(plan.downloads(), 1);
}
#[test]
fn delete_suppressed_when_path_case_aliases_download_target() {
let mut manifest = Manifest::new();
manifest.insert(
"b",
entry("Creator/Song.flac", AudioFormat::Flac, "m", "art"),
);
let d = vec![desired(
"a",
"Creator/song.flac",
AudioFormat::Flac,
"m",
"art",
)];
let plan = reconcile(&manifest, &d, &HashMap::new(), &mirror_ok());
assert!(
!plan
.actions
.iter()
.any(|a| matches!(a, Action::Delete { .. })),
"case-only alias was not suppressed: {:?}",
plan.actions
);
assert_eq!(plan.downloads(), 1);
assert!(plan.actions.contains(&Action::Skip {
clip_id: "b".to_string()
}));
}
#[test]
fn delete_suppressed_when_path_nfc_aliases_download_target() {
let nfc = "Creator/\u{00e9}toile.flac"; let nfd = "Creator/e\u{0301}toile.flac"; let mut manifest = Manifest::new();
manifest.insert("b", entry(nfd, AudioFormat::Flac, "m", "art"));
let d = vec![desired("a", nfc, AudioFormat::Flac, "m", "art")];
let plan = reconcile(&manifest, &d, &HashMap::new(), &mirror_ok());
assert!(
!plan
.actions
.iter()
.any(|a| matches!(a, Action::Delete { .. })),
"NFC/NFD alias was not suppressed: {:?}",
plan.actions
);
assert_eq!(plan.downloads(), 1);
}
#[test]
fn delete_artifact_suppressed_when_path_aliases_rename_target() {
let mut actions = vec![
Action::Rename {
from: "old/song.flac".to_string(),
to: "new/cover.jpg".to_string(),
},
Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: "new/cover.jpg".to_string(),
owner_id: "a".to_string(),
},
];
suppress_path_aliasing(&mut actions);
assert!(
!actions
.iter()
.any(|a| matches!(a, Action::DeleteArtifact { .. })),
"a sidecar delete must not alias a rename target"
);
assert!(actions.contains(&Action::Skip {
clip_id: "a".to_string()
}));
assert!(actions.contains(&Action::Rename {
from: "old/song.flac".to_string(),
to: "new/cover.jpg".to_string(),
}));
}
#[test]
fn delete_artifact_suppressed_when_path_aliases_write_artifact_target() {
let mut actions = vec![
Action::WriteArtifact {
kind: ArtifactKind::FolderJpg,
path: "creator/album/folder.jpg".to_string(),
source_url: "https://art/large.jpg".to_string(),
hash: "h".to_string(),
owner_id: "root".to_string(),
content: None,
},
Action::DeleteArtifact {
kind: ArtifactKind::FolderJpg,
path: "creator/album/folder.jpg".to_string(),
owner_id: "root-old".to_string(),
},
];
suppress_path_aliasing(&mut actions);
assert!(
!actions
.iter()
.any(|a| matches!(a, Action::DeleteArtifact { .. }))
);
assert!(actions.contains(&Action::Skip {
clip_id: "root-old".to_string()
}));
}
#[test]
fn duplicate_trashed_does_not_defeat_copy_sibling() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut copy_entry = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
copy_entry.modes = vec![SourceMode::Copy];
let mut trashed_entry = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
trashed_entry.modes = vec![SourceMode::Mirror];
trashed_entry.trashed = true;
let plan = reconcile(
&manifest,
&[copy_entry, trashed_entry],
&local_present("a"),
&mirror_ok(),
);
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn duplicate_trashed_does_not_defeat_private_sibling() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut private_entry = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
private_entry.private = true;
let mut trashed_entry = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
trashed_entry.trashed = true;
let plan = reconcile(
&manifest,
&[private_entry, trashed_entry],
&local_present("a"),
&mirror_ok(),
);
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}
#[test]
fn duplicate_trashed_deletes_only_when_all_trashed() {
let mut manifest = Manifest::new();
manifest.insert("a", entry("a.flac", AudioFormat::Flac, "m", "art"));
let mut first = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
first.trashed = true;
let mut second = desired("a", "a.flac", AudioFormat::Flac, "m", "art");
second.trashed = true;
let plan = reconcile(
&manifest,
&[first, second],
&local_present("a"),
&mirror_ok(),
);
assert_eq!(plan.deletes(), 1);
}
#[test]
fn absent_private_clip_protected_by_preserve_marker() {
let mut manifest = Manifest::new();
manifest.insert(
"a",
preserved_entry("a.flac", AudioFormat::Flac, "m", "art"),
);
let plan = reconcile(&manifest, &[], &HashMap::new(), &mirror_ok());
assert_eq!(plan.deletes(), 0);
assert_eq!(plan.skips(), 1);
}