1use std::collections::{BTreeSet, HashMap, HashSet};
8
9use crate::desired::{LIKED_PLAYLIST_ID, PlaylistInput, build_playlist_desired};
10use crate::graph::LineageStore;
11use crate::model::Clip;
12use crate::reconcile::{
13 Desired, PlaylistDesired, SourceStatus, area_authoritative, area_fully_enumerated,
14 deletion_allowed,
15};
16use crate::vocab::SourceMode;
17
18pub struct AreaListing {
27 kind: AreaKind,
28 mode: SourceMode,
30 clips: Vec<Clip>,
32 authoritative_ignoring_empty: bool,
36}
37
38pub enum AreaKind {
41 Library,
42 Liked,
43 Playlist { id: String, name: String },
44}
45
46impl AreaListing {
47 pub fn listed(
53 kind: AreaKind,
54 mode: SourceMode,
55 clips: Vec<Clip>,
56 complete: bool,
57 any_filtered: bool,
58 narrowed: bool,
59 ) -> Self {
60 Self {
61 kind,
62 mode,
63 clips,
64 authoritative_ignoring_empty: area_authoritative(complete, any_filtered, narrowed),
65 }
66 }
67
68 pub fn failed(kind: AreaKind, mode: SourceMode) -> Self {
71 Self {
72 kind,
73 mode,
74 clips: Vec::new(),
75 authoritative_ignoring_empty: false,
76 }
77 }
78
79 pub fn unresolved_playlist(mode: SourceMode) -> Self {
81 Self::failed(
82 AreaKind::Playlist {
83 id: String::new(),
84 name: String::new(),
85 },
86 mode,
87 )
88 }
89
90 pub fn clips(&self) -> &[Clip] {
92 &self.clips
93 }
94}
95
96pub fn area_mode(area: &AreaListing, force_copy: bool) -> SourceMode {
98 if force_copy {
99 SourceMode::Copy
100 } else {
101 area.mode
102 }
103}
104
105#[must_use]
108pub fn area_enumerated(area: &AreaListing, force_copy: bool) -> bool {
109 area_fully_enumerated(
110 area.authoritative_ignoring_empty,
111 area.clips.is_empty(),
112 area_mode(area, force_copy),
113 )
114}
115
116#[must_use]
119pub fn library_authoritative(areas: &[AreaListing], force_copy: bool) -> bool {
120 areas
121 .iter()
122 .any(|a| matches!(a.kind, AreaKind::Library) && area_enumerated(a, force_copy))
123}
124
125#[must_use]
127pub fn source_statuses(areas: &[AreaListing], force_copy: bool) -> Vec<SourceStatus> {
128 areas
129 .iter()
130 .map(|area| SourceStatus {
131 mode: area_mode(area, force_copy),
132 fully_enumerated: area_enumerated(area, force_copy),
133 })
134 .collect()
135}
136
137#[must_use]
146pub fn adoption_enumerated(areas: &[AreaListing], force_copy: bool) -> bool {
147 library_authoritative(areas, force_copy)
148 || deletion_allowed(&source_statuses(areas, force_copy))
149}
150
151pub fn union_clips(areas: &[AreaListing]) -> Vec<Clip> {
154 let mut seen: HashSet<String> = HashSet::new();
155 let mut union: Vec<Clip> = Vec::new();
156 for area in areas {
157 for clip in &area.clips {
158 if seen.insert(clip.id.clone()) {
159 union.push(clip.clone());
160 }
161 }
162 }
163 union
164}
165
166#[must_use]
172pub fn playlists_enumerated(areas: &[AreaListing], force_copy: bool, members_intact: bool) -> bool {
173 members_intact
174 && areas.iter().all(|area| match area.kind {
175 AreaKind::Library => true,
176 AreaKind::Liked | AreaKind::Playlist { .. } => area_enumerated(area, force_copy),
177 })
178}
179
180pub fn build_scoped_playlist_desired(
192 areas: &[AreaListing],
193 desired: &[Desired],
194 store: &LineageStore,
195 protected: &mut BTreeSet<String>,
196 force_copy: bool,
197 members_intact: bool,
198) -> (Vec<PlaylistDesired>, bool) {
199 let mut owned: Vec<(String, String, Vec<Clip>)> = Vec::new();
200 for area in areas {
201 match &area.kind {
202 AreaKind::Playlist { id, name } => {
203 if !id.is_empty() && area_enumerated(area, force_copy) {
204 owned.push((id.clone(), name.clone(), area.clips.clone()));
205 } else if !id.is_empty() {
206 protected.insert(id.clone());
207 }
208 }
209 AreaKind::Liked => {
210 if area_enumerated(area, force_copy) {
211 owned.push((
212 LIKED_PLAYLIST_ID.to_owned(),
213 "Liked Songs".to_owned(),
214 area.clips.clone(),
215 ));
216 } else {
217 protected.insert(LIKED_PLAYLIST_ID.to_owned());
218 }
219 }
220 AreaKind::Library => {}
221 }
222 }
223 let rendered: BTreeSet<&str> = owned.iter().map(|(id, _, _)| id.as_str()).collect();
224 for id in store.playlists.keys() {
227 if !rendered.contains(id.as_str()) {
228 protected.insert(id.clone());
229 }
230 }
231 let inputs: Vec<PlaylistInput<'_>> = owned
232 .iter()
233 .map(|(id, name, members)| PlaylistInput {
234 id: id.as_str(),
235 name: name.as_str(),
236 members: members.as_slice(),
237 })
238 .collect();
239 (
240 build_playlist_desired(&inputs, desired),
241 playlists_enumerated(areas, force_copy, members_intact),
242 )
243}
244
245pub fn build_modes_by_id(
254 areas: &[AreaListing],
255 force_copy: bool,
256) -> HashMap<String, Vec<SourceMode>> {
257 let mut map: HashMap<String, (bool, bool)> = HashMap::new();
258 for area in areas {
259 let mode = area_mode(area, force_copy);
260 for clip in &area.clips {
261 let entry = map.entry(clip.id.clone()).or_insert((false, false));
262 match mode {
263 SourceMode::Mirror => entry.0 = true,
264 SourceMode::Copy => entry.1 = true,
265 }
266 }
267 }
268 map.into_iter()
269 .map(|(id, (mirror, copy))| {
270 let mut modes = Vec::new();
271 if mirror {
272 modes.push(SourceMode::Mirror);
273 }
274 if copy {
275 modes.push(SourceMode::Copy);
276 }
277 (id, modes)
278 })
279 .collect()
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::{
286 Action, ArtifactToggles, AudioFormat, LocalFile, Manifest, ManifestEntry, NamingConfig,
287 build_desired, narrows_downloads, reconcile,
288 };
289
290 fn tclip(id: &str) -> Clip {
291 Clip {
292 id: id.to_owned(),
293 title: "Song".to_owned(),
294 handle: "alice".to_owned(),
295 ..Default::default()
296 }
297 }
298
299 fn area(kind: AreaKind, mode: SourceMode, ids: &[&str], authoritative: bool) -> AreaListing {
300 AreaListing {
301 kind,
302 mode,
303 clips: ids.iter().map(|id| tclip(id)).collect(),
304 authoritative_ignoring_empty: authoritative,
305 }
306 }
307
308 #[test]
312 fn empty_mirror_area_is_not_enumerated() {
313 let mirror = area(AreaKind::Liked, SourceMode::Mirror, &[], true);
314 assert!(!area_enumerated(&mirror, false));
315 let copy = area(AreaKind::Liked, SourceMode::Copy, &[], true);
316 assert!(area_enumerated(©, false));
317 let full = area(AreaKind::Liked, SourceMode::Mirror, &["x"], true);
319 assert!(area_enumerated(&full, false));
320 }
321
322 #[test]
326 fn adoption_enumerated_covers_a_mirror_playlist_under_library_off() {
327 let playlist = |mode, ids: &[&str], auth| {
328 area(
329 AreaKind::Playlist {
330 id: "p".into(),
331 name: "P".into(),
332 },
333 mode,
334 ids,
335 auth,
336 )
337 };
338 assert!(adoption_enumerated(
340 &[playlist(SourceMode::Mirror, &["pl"], true)],
341 false
342 ));
343 assert!(!adoption_enumerated(
345 &[playlist(SourceMode::Copy, &["pl"], true)],
346 false
347 ));
348 assert!(!adoption_enumerated(
350 &[playlist(SourceMode::Mirror, &[], true)],
351 false
352 ));
353 assert!(!adoption_enumerated(
355 &[playlist(SourceMode::Mirror, &["pl"], false)],
356 false
357 ));
358 assert!(!adoption_enumerated(
360 &[playlist(SourceMode::Mirror, &["pl"], true)],
361 true
362 ));
363 assert!(adoption_enumerated(
365 &[area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)],
366 false,
367 ));
368 }
369
370 #[test]
373 fn library_authoritative_counts_protector_not_off() {
374 let with_protector = vec![
375 area(AreaKind::Library, SourceMode::Copy, &["lib"], true),
376 area(
377 AreaKind::Playlist {
378 id: "p".into(),
379 name: "P".into(),
380 },
381 SourceMode::Mirror,
382 &["pl"],
383 true,
384 ),
385 ];
386 assert!(library_authoritative(&with_protector, false));
387
388 let off = vec![area(
389 AreaKind::Playlist {
390 id: "p".into(),
391 name: "P".into(),
392 },
393 SourceMode::Mirror,
394 &["pl"],
395 true,
396 )];
397 assert!(!library_authoritative(&off, false));
398 }
399
400 fn verdict(areas: &[AreaListing]) -> (bool, bool, bool) {
403 let can_delete = deletion_allowed(&source_statuses(areas, false));
404 let lib_auth = library_authoritative(areas, false);
405 (
406 can_delete,
407 lib_auth,
408 narrows_downloads(can_delete, lib_auth),
409 )
410 }
411
412 fn pl_area(mode: SourceMode, ids: &[&str], authoritative: bool) -> AreaListing {
413 area(
414 AreaKind::Playlist {
415 id: "p".into(),
416 name: "P".into(),
417 },
418 mode,
419 ids,
420 authoritative,
421 )
422 }
423
424 #[test]
427 fn narrowed_playlist_mirror_disarms_deletion() {
428 let narrowed = pl_area(
429 SourceMode::Mirror,
430 &["a"],
431 area_authoritative(true, false, true),
432 );
433 assert!(!area_enumerated(&narrowed, false));
434 assert!(!deletion_allowed(&source_statuses(&[narrowed], false)));
435
436 let full = pl_area(
437 SourceMode::Mirror,
438 &["a"],
439 area_authoritative(true, false, false),
440 );
441 assert!(area_enumerated(&full, false));
442 assert!(deletion_allowed(&source_statuses(&[full], false)));
443 }
444
445 #[test]
449 fn narrowed_playlist_with_protector_neither_deletes_nor_narrows() {
450 let areas = vec![
451 area(AreaKind::Library, SourceMode::Copy, &["lib"], true),
452 pl_area(
453 SourceMode::Mirror,
454 &["pl"],
455 area_authoritative(true, false, true),
456 ),
457 ];
458 let (can_delete, lib_auth, truncate) = verdict(&areas);
459 assert!(!can_delete, "narrowed playlist mirror is disarmed");
460 assert!(lib_auth, "the protector is an authoritative library");
461 assert!(
462 !truncate,
463 "the full library is listed, so downloads are not narrowed"
464 );
465 }
466
467 #[test]
471 fn narrowed_playlist_off_disarms_and_narrows() {
472 let areas = vec![pl_area(
473 SourceMode::Mirror,
474 &["pl"],
475 area_authoritative(true, false, true),
476 )];
477 let (can_delete, lib_auth, truncate) = verdict(&areas);
478 assert!(!can_delete, "narrowed playlist mirror is disarmed");
479 assert!(!lib_auth, "library=off leaves no library area");
480 assert!(
481 truncate,
482 "no armed deletion and no full library, so downloads narrow"
483 );
484 }
485
486 #[test]
491 fn configured_full_library_mirror_still_deletes_when_narrowed() {
492 let areas = vec![area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)];
493 let (can_delete, lib_auth, truncate) = verdict(&areas);
494 assert!(
495 can_delete,
496 "the configured full-library mirror still deletes"
497 );
498 assert!(lib_auth);
499 assert!(
500 !truncate,
501 "the full library is listed, so downloads are not narrowed"
502 );
503 }
504
505 #[test]
509 fn adoption_skips_pin_on_a_narrowed_library_off_playlist() {
510 let areas = vec![pl_area(
511 SourceMode::Mirror,
512 &["pl"],
513 area_authoritative(true, false, true),
514 )];
515 assert!(!adoption_enumerated(&areas, false));
516 }
517
518 #[test]
521 fn union_keeps_first_area_payload() {
522 let mut lib = tclip("shared");
523 lib.title = "Library".to_owned();
524 let mut pl = tclip("shared");
525 pl.title = "Playlist".to_owned();
526 let areas = vec![
527 AreaListing {
528 kind: AreaKind::Library,
529 mode: SourceMode::Copy,
530 clips: vec![lib, tclip("lib-only")],
531 authoritative_ignoring_empty: true,
532 },
533 AreaListing {
534 kind: AreaKind::Playlist {
535 id: "p".into(),
536 name: "P".into(),
537 },
538 mode: SourceMode::Mirror,
539 clips: vec![pl],
540 authoritative_ignoring_empty: true,
541 },
542 ];
543 let union = union_clips(&areas);
544 assert_eq!(union.len(), 2);
545 assert_eq!(union[0].id, "shared");
546 assert_eq!(union[0].title, "Library");
547 assert_eq!(union[1].id, "lib-only");
548 }
549
550 #[test]
551 fn a_failed_area_suppresses_deletion_for_the_run() {
552 let areas = [
553 area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
554 area(
556 AreaKind::Playlist {
557 id: "p".into(),
558 name: "P".into(),
559 },
560 SourceMode::Mirror,
561 &[],
562 false,
563 ),
564 ];
565 let sources: Vec<SourceStatus> = areas
566 .iter()
567 .map(|a| SourceStatus {
568 mode: area_mode(a, false),
569 fully_enumerated: area_enumerated(a, false),
570 })
571 .collect();
572 assert!(!deletion_allowed(&sources));
573 }
574
575 #[test]
579 fn mixed_mode_deletes_only_mirror_exclusive_orphans() {
580 let areas = vec![
581 area(AreaKind::Liked, SourceMode::Mirror, &["m-live"], true),
582 area(
583 AreaKind::Playlist {
584 id: "p".into(),
585 name: "P".into(),
586 },
587 SourceMode::Copy,
588 &["c-live"],
589 true,
590 ),
591 ];
592 let sources: Vec<SourceStatus> = areas
593 .iter()
594 .map(|a| SourceStatus {
595 mode: area_mode(a, false),
596 fully_enumerated: area_enumerated(a, false),
597 })
598 .collect();
599 assert!(deletion_allowed(&sources));
600
601 let modes = build_modes_by_id(&areas, false);
602 let union = union_clips(&areas);
603 let desired = build_desired(
604 &union.iter().collect::<Vec<_>>(),
605 AudioFormat::Flac,
606 &modes,
607 &HashMap::new(),
608 &BTreeSet::new(),
609 &BTreeSet::new(),
610 ArtifactToggles::default(),
611 &NamingConfig::default(),
612 );
613
614 let mut manifest = Manifest::new();
615 for id in ["m-live", "c-live", "m-orphan", "c-orphan"] {
617 manifest.insert(
618 id,
619 ManifestEntry {
620 path: format!("{id}.flac"),
621 format: AudioFormat::Flac,
622 size: 100,
623 preserve: id == "c-orphan",
626 ..Default::default()
627 },
628 );
629 }
630 let local: HashMap<String, LocalFile> = manifest
631 .iter()
632 .map(|(id, _)| {
633 (
634 id.clone(),
635 LocalFile {
636 exists: true,
637 size: 100,
638 },
639 )
640 })
641 .collect();
642 let plan = reconcile(&manifest, &desired, &local, &sources);
643 let deleted: Vec<&str> = plan
644 .actions
645 .iter()
646 .filter_map(|a| match a {
647 Action::Delete { clip_id, .. } => Some(clip_id.as_str()),
648 _ => None,
649 })
650 .collect();
651 assert_eq!(deleted, vec!["m-orphan"]);
652 }
653
654 #[test]
657 fn build_modes_by_id_copy_wins_and_dedups() {
658 let areas = [
659 area(AreaKind::Liked, SourceMode::Mirror, &["a", "b"], true),
660 pl_area(SourceMode::Copy, &["b", "c"], true),
661 ];
662 let map = build_modes_by_id(&areas, false);
663 assert_eq!(map["a"], vec![SourceMode::Mirror]);
664 assert_eq!(map["b"], vec![SourceMode::Mirror, SourceMode::Copy]);
665 assert_eq!(map["c"], vec![SourceMode::Copy]);
666 }
667
668 #[test]
671 fn playlists_enumerated_gates_on_every_playlist_area() {
672 assert!(playlists_enumerated(
674 &[
675 area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
676 pl_area(SourceMode::Mirror, &["b"], true),
677 ],
678 false,
679 true,
680 ));
681
682 assert!(!playlists_enumerated(
684 &[
685 area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
686 AreaListing::unresolved_playlist(SourceMode::Mirror),
687 ],
688 false,
689 true,
690 ));
691
692 assert!(!playlists_enumerated(
694 &[
695 AreaListing::failed(AreaKind::Liked, SourceMode::Mirror),
696 pl_area(SourceMode::Mirror, &["b"], true),
697 ],
698 false,
699 true,
700 ));
701
702 assert!(!playlists_enumerated(
704 &[
705 area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
706 pl_area(SourceMode::Mirror, &["b"], true),
707 ],
708 false,
709 false,
710 ));
711 }
712
713 #[test]
716 fn playlists_enumerated_ignores_library_only_sets() {
717 assert!(playlists_enumerated(
718 &[area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)],
719 false,
720 true,
721 ));
722 assert!(!playlists_enumerated(
723 &[area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)],
724 false,
725 false,
726 ));
727 assert!(playlists_enumerated(&[], false, true));
728 assert!(!playlists_enumerated(&[], false, false));
729 }
730
731 fn desired_for(clips: &[&Clip]) -> Vec<Desired> {
737 let modes: HashMap<String, Vec<SourceMode>> = clips
738 .iter()
739 .map(|c| (c.id.clone(), vec![SourceMode::Copy]))
740 .collect();
741 build_desired(
742 clips,
743 AudioFormat::Flac,
744 &modes,
745 &HashMap::new(),
746 &BTreeSet::new(),
747 &BTreeSet::new(),
748 ArtifactToggles::default(),
749 &NamingConfig::default(),
750 )
751 }
752
753 fn pl_area_named(
754 id: &str,
755 name: &str,
756 mode: SourceMode,
757 ids: &[&str],
758 authoritative: bool,
759 ) -> AreaListing {
760 area(
761 AreaKind::Playlist {
762 id: id.to_owned(),
763 name: name.to_owned(),
764 },
765 mode,
766 ids,
767 authoritative,
768 )
769 }
770
771 #[test]
772 fn additive_scoped_run_renders_selected_playlist() {
773 let ca = tclip("a");
778 let desired = desired_for(&[&ca]);
779 let areas = [pl_area_named(
780 "holiday",
781 "Holiday",
782 SourceMode::Copy,
783 &["a"],
784 true,
785 )];
786 let store = LineageStore::default();
787 let mut protected = BTreeSet::new();
788 let (rendered, delete_gate) =
789 build_scoped_playlist_desired(&areas, &desired, &store, &mut protected, false, false);
790
791 assert_eq!(rendered.len(), 1, "the selected playlist is rendered");
792 assert_eq!(rendered[0].id, "holiday");
793 assert!(!protected.contains("holiday"), "it is owned, not protected");
794 assert!(
795 !delete_gate,
796 "the delete gate still tracks members_intact (false here)"
797 );
798 }
799
800 #[test]
801 fn narrowed_scoped_run_still_protects_playlist() {
802 let ca = tclip("a");
807 let desired = desired_for(&[&ca]);
808 let narrowed = pl_area_named(
809 "holiday",
810 "Holiday",
811 SourceMode::Copy,
812 &["a"],
813 area_authoritative(true, false, true),
814 );
815 let store = LineageStore::default();
816 let mut protected = BTreeSet::new();
817 let (rendered, _) = build_scoped_playlist_desired(
818 &[narrowed],
819 &desired,
820 &store,
821 &mut protected,
822 false,
823 false,
824 );
825 assert!(rendered.is_empty(), "a narrowed area is not rendered");
826 assert!(protected.contains("holiday"), "and its id is protected");
827 }
828
829 #[test]
830 fn failed_member_listing_still_protects() {
831 let ca = tclip("a");
833 let desired = desired_for(&[&ca]);
834 let store = LineageStore::default();
835
836 let mut protected = BTreeSet::new();
838 let (rendered, _) = build_scoped_playlist_desired(
839 &[AreaListing::unresolved_playlist(SourceMode::Copy)],
840 &desired,
841 &store,
842 &mut protected,
843 false,
844 false,
845 );
846 assert!(rendered.is_empty(), "an unresolved listing renders nothing");
847
848 let mut protected = BTreeSet::new();
850 let failed = AreaListing::failed(
851 AreaKind::Playlist {
852 id: "holiday".to_owned(),
853 name: "Holiday".to_owned(),
854 },
855 SourceMode::Copy,
856 );
857 let (rendered, _) = build_scoped_playlist_desired(
858 &[failed],
859 &desired,
860 &store,
861 &mut protected,
862 false,
863 false,
864 );
865 assert!(rendered.is_empty());
866 assert!(
867 protected.contains("holiday"),
868 "a failed listing is protected"
869 );
870 }
871
872 #[test]
873 fn liked_area_follows_the_same_rule() {
874 let ca = tclip("a");
878 let desired = desired_for(&[&ca]);
879 let store = LineageStore::default();
880
881 let liked = area(AreaKind::Liked, SourceMode::Copy, &["a"], true);
882 let mut protected = BTreeSet::new();
883 let (rendered, _) =
884 build_scoped_playlist_desired(&[liked], &desired, &store, &mut protected, false, false);
885 assert_eq!(rendered.len(), 1);
886 assert_eq!(rendered[0].id, LIKED_PLAYLIST_ID);
887 assert!(!protected.contains(LIKED_PLAYLIST_ID));
888
889 let failed = AreaListing::failed(AreaKind::Liked, SourceMode::Copy);
890 let mut protected = BTreeSet::new();
891 let (rendered, _) = build_scoped_playlist_desired(
892 &[failed],
893 &desired,
894 &store,
895 &mut protected,
896 false,
897 false,
898 );
899 assert!(rendered.is_empty());
900 assert!(protected.contains(LIKED_PLAYLIST_ID));
901 }
902
903 #[test]
904 fn scoped_render_shows_missing_member_as_comment() {
905 let ca = tclip("a");
909 let desired = desired_for(&[&ca]); let areas = [pl_area_named(
911 "holiday",
912 "Holiday",
913 SourceMode::Copy,
914 &["a", "b"],
915 true,
916 )];
917 let store = LineageStore::default();
918 let mut protected = BTreeSet::new();
919 let (rendered, _) =
920 build_scoped_playlist_desired(&areas, &desired, &store, &mut protected, false, false);
921
922 assert_eq!(rendered.len(), 1);
923 let content = &rendered[0].content;
924 assert!(
925 content.contains("# (not in library)"),
926 "a missing member is a comment, not a dangling path"
927 );
928 assert!(
929 content.contains(&desired[0].path),
930 "the present member keeps its audio path"
931 );
932 }
933
934 #[test]
935 fn scoped_write_gate_decoupled_from_delete_gate() {
936 let ca = tclip("a");
940 let desired = desired_for(&[&ca]);
941 let areas = [pl_area_named(
942 "holiday",
943 "Holiday",
944 SourceMode::Copy,
945 &["a"],
946 true,
947 )];
948 let store = LineageStore::default();
949
950 let mut protected = BTreeSet::new();
951 let (rendered, gate) =
952 build_scoped_playlist_desired(&areas, &desired, &store, &mut protected, false, false);
953 assert_eq!(rendered.len(), 1, "writes with members_intact = false");
954 assert!(!gate, "but authorises no delete");
955
956 let mut protected = BTreeSet::new();
957 let (rendered, gate) =
958 build_scoped_playlist_desired(&areas, &desired, &store, &mut protected, false, true);
959 assert_eq!(rendered.len(), 1);
960 assert!(gate, "an intact, enumerated run authorises deletes");
961 }
962}