Skip to main content

suno_core/
area.rs

1//! The multi-area sync planner: the pure decision logic for what is
2//! authoritative and what may be deleted across a run's areas (library, liked
3//! feed, playlists). Lifted from the CLI so the deletion-authority logic is
4//! covered by the core suite, beside the leaf predicates it composes in
5//! [`crate::reconcile`].
6
7use std::collections::{BTreeSet, HashMap, HashSet};
8
9use crate::{
10    Clip, Desired, LIKED_PLAYLIST_ID, LineageStore, PlaylistDesired, PlaylistInput, SourceMode,
11    SourceStatus, area_authoritative, area_fully_enumerated, build_playlist_desired,
12    deletion_allowed,
13};
14
15/// One area's listing outcome for the multi-area planner.
16///
17/// The `authoritative_ignoring_empty` flag is the area's completeness verdict
18/// *before* the empty-mirror guard (§5), which [`area_enumerated`] applies later
19/// against the final mode, so a copy-verb override that turns a Mirror area Copy
20/// re-scores an empty area correctly. It is only ever produced by
21/// [`area_authoritative`] via [`AreaListing::listed`], so the #248 filter-loss
22/// guard cannot be bypassed by an out-of-band value.
23pub struct AreaListing {
24    kind: AreaKind,
25    /// The resolved (pre copy-override) mode for this area.
26    mode: SourceMode,
27    /// The area's downloadable clips.
28    clips: Vec<Clip>,
29    /// Completeness modulo the empty-mirror guard: `true` when the listing
30    /// drained, was not deliberately narrowed, and lost no member to the
31    /// downloadable filter.
32    authoritative_ignoring_empty: bool,
33}
34
35/// Which kind of area a listing came from, carrying playlist identity so its
36/// `.m3u8` can be maintained by id and name.
37pub enum AreaKind {
38    Library,
39    Liked,
40    Playlist { id: String, name: String },
41}
42
43impl AreaListing {
44    /// A drained listing. The authority flag is computed from the raw listing
45    /// signals via [`area_authoritative`], so the #248 guard is unbypassable
46    /// from outside the crate: the fields are private, and although in-crate
47    /// tests may construct directly, every production path goes through this
48    /// constructor.
49    pub fn listed(
50        kind: AreaKind,
51        mode: SourceMode,
52        clips: Vec<Clip>,
53        complete: bool,
54        any_filtered: bool,
55        narrowed: bool,
56    ) -> Self {
57        Self {
58            kind,
59            mode,
60            clips,
61            authoritative_ignoring_empty: area_authoritative(complete, any_filtered, narrowed),
62        }
63    }
64
65    /// A failed or empty listing: it holds no clips and is never authoritative,
66    /// so it suppresses deletion without ever vanishing from the sources (§6).
67    pub fn failed(kind: AreaKind, mode: SourceMode) -> Self {
68        Self {
69            kind,
70            mode,
71            clips: Vec::new(),
72            authoritative_ignoring_empty: false,
73        }
74    }
75
76    /// A playlist area whose listing could not be resolved or fetched (§6).
77    pub fn unresolved_playlist(mode: SourceMode) -> Self {
78        Self::failed(
79            AreaKind::Playlist {
80                id: String::new(),
81                name: String::new(),
82            },
83            mode,
84        )
85    }
86
87    /// The area's downloadable clips.
88    pub fn clips(&self) -> &[Clip] {
89        &self.clips
90    }
91}
92
93/// This area's mode after the copy-verb / force-additive override.
94pub fn area_mode(area: &AreaListing, force_copy: bool) -> SourceMode {
95    if force_copy {
96        SourceMode::Copy
97    } else {
98        area.mode
99    }
100}
101
102/// Whether this area is authoritative for deletion, applying the empty-mirror
103/// guard (§5) against the final mode.
104#[must_use]
105pub fn area_enumerated(area: &AreaListing, force_copy: bool) -> bool {
106    area_fully_enumerated(
107        area.authoritative_ignoring_empty,
108        area.clips.is_empty(),
109        area_mode(area, force_copy),
110    )
111}
112
113/// Whether a Library area is present and fully enumerated (the implicit
114/// protector counts; `library="off"` leaves no Library area, so this is false).
115#[must_use]
116pub fn library_authoritative(areas: &[AreaListing], force_copy: bool) -> bool {
117    areas
118        .iter()
119        .any(|a| matches!(a.kind, AreaKind::Library) && area_enumerated(a, force_copy))
120}
121
122/// The per-source enumeration status of every area, for the deletion verdict.
123#[must_use]
124pub fn source_statuses(areas: &[AreaListing], force_copy: bool) -> Vec<SourceStatus> {
125    areas
126        .iter()
127        .map(|area| SourceStatus {
128            mode: area_mode(area, force_copy),
129            fully_enumerated: area_enumerated(area, force_copy),
130        })
131        .collect()
132}
133
134/// Whether first-use adoption can confirm identity from this run's listing.
135///
136/// An authoritative Library is the usual anchor, but a fully-enumerated Mirror
137/// source of any kind (e.g. a playlist under `library="off"`) also arms
138/// deletion. Deleting against an account this library was never pinned to is
139/// the hole the owner pin closes (#149), so such a run is treated as enumerated:
140/// `adopt_decision` then confirms identity by clip overlap and aborts on a
141/// foreign account instead of skipping the pin.
142#[must_use]
143pub fn adoption_enumerated(areas: &[AreaListing], force_copy: bool) -> bool {
144    library_authoritative(areas, force_copy)
145        || deletion_allowed(&source_statuses(areas, force_copy))
146}
147
148/// Build the clip union across areas in canonical order, first area winning per
149/// id so the Library payload is kept (H1).
150pub fn union_clips(areas: &[AreaListing]) -> Vec<Clip> {
151    let mut seen: HashSet<String> = HashSet::new();
152    let mut union: Vec<Clip> = Vec::new();
153    for area in areas {
154        for clip in &area.clips {
155            if seen.insert(clip.id.clone()) {
156                union.push(clip.clone());
157            }
158        }
159    }
160    union
161}
162
163/// Whether the scoped `.m3u8` desired set may authorise playlist deletes: the
164/// union was not truncated (`members_intact`) and every playlist-rendering area
165/// (Playlist, Liked) fully enumerated. Library areas render no `.m3u8`, so they
166/// never gate this. Mirrors the async twin, which reports non-enumerated when a
167/// listing is partial rather than authorising a delete from a partial view (B2).
168#[must_use]
169pub fn playlists_enumerated(areas: &[AreaListing], force_copy: bool, members_intact: bool) -> bool {
170    members_intact
171        && areas.iter().all(|area| match area.kind {
172            AreaKind::Library => true,
173            AreaKind::Liked | AreaKind::Playlist { .. } => area_enumerated(area, force_copy),
174        })
175}
176
177/// Build the `.m3u8` desired state for an area-scoped run (no authoritative
178/// Library). Only the playlist and liked areas that fully enumerated their
179/// members are rendered, and only when `members_intact` (the union was not
180/// truncated by `--limit`/`--since`, so `desired` still holds every member);
181/// every other stored playlist id is protected so no `.m3u8` is rewritten or
182/// deleted from a partial view (B2/D3).
183pub fn build_scoped_playlist_desired(
184    areas: &[AreaListing],
185    desired: &[Desired],
186    store: &LineageStore,
187    protected: &mut BTreeSet<String>,
188    force_copy: bool,
189    members_intact: bool,
190) -> (Vec<PlaylistDesired>, bool) {
191    let mut owned: Vec<(String, String, Vec<Clip>)> = Vec::new();
192    for area in areas {
193        match &area.kind {
194            AreaKind::Playlist { id, name } => {
195                if members_intact && !id.is_empty() && area_enumerated(area, force_copy) {
196                    owned.push((id.clone(), name.clone(), area.clips.clone()));
197                } else if !id.is_empty() {
198                    protected.insert(id.clone());
199                }
200            }
201            AreaKind::Liked => {
202                if members_intact && area_enumerated(area, force_copy) {
203                    owned.push((
204                        LIKED_PLAYLIST_ID.to_owned(),
205                        "Liked Songs".to_owned(),
206                        area.clips.clone(),
207                    ));
208                } else {
209                    protected.insert(LIKED_PLAYLIST_ID.to_owned());
210                }
211            }
212            AreaKind::Library => {}
213        }
214    }
215    let rendered: BTreeSet<&str> = owned.iter().map(|(id, _, _)| id.as_str()).collect();
216    // Protect every stored playlist this run is not authoritatively rewriting, so
217    // a non-selected playlist's `.m3u8` is never treated as stale.
218    for id in store.playlists.keys() {
219        if !rendered.contains(id.as_str()) {
220            protected.insert(id.clone());
221        }
222    }
223    let inputs: Vec<PlaylistInput<'_>> = owned
224        .iter()
225        .map(|(id, name, members)| PlaylistInput {
226            id: id.as_str(),
227            name: name.as_str(),
228            members: members.as_slice(),
229        })
230        .collect();
231    (
232        build_playlist_desired(&inputs, desired),
233        playlists_enumerated(areas, force_copy, members_intact),
234    )
235}
236
237/// Fold every area's clips into `modes_by_id`, mapping each clip id to the
238/// deduplicated, canonical-order list of every area mode holding it.
239///
240/// `areas` is processed in canonical area order (Library, Liked, Playlists), each
241/// area's mode taken after the copy-verb override via [`area_mode`], and each
242/// clip's modes are normalised to `[Mirror, Copy]` order, mirroring
243/// `aggregate_desired` so a clip held by both a mirror and a copy area is
244/// copy-protected (SYNC-8).
245pub fn build_modes_by_id(
246    areas: &[AreaListing],
247    force_copy: bool,
248) -> HashMap<String, Vec<SourceMode>> {
249    let mut map: HashMap<String, (bool, bool)> = HashMap::new();
250    for area in areas {
251        let mode = area_mode(area, force_copy);
252        for clip in &area.clips {
253            let entry = map.entry(clip.id.clone()).or_insert((false, false));
254            match mode {
255                SourceMode::Mirror => entry.0 = true,
256                SourceMode::Copy => entry.1 = true,
257            }
258        }
259    }
260    map.into_iter()
261        .map(|(id, (mirror, copy))| {
262            let mut modes = Vec::new();
263            if mirror {
264                modes.push(SourceMode::Mirror);
265            }
266            if copy {
267                modes.push(SourceMode::Copy);
268            }
269            (id, modes)
270        })
271        .collect()
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::{
278        Action, ArtifactToggles, AudioFormat, LocalFile, Manifest, ManifestEntry, NamingConfig,
279        build_desired, narrows_downloads, reconcile,
280    };
281
282    fn tclip(id: &str) -> Clip {
283        Clip {
284            id: id.to_owned(),
285            title: "Song".to_owned(),
286            handle: "alice".to_owned(),
287            ..Default::default()
288        }
289    }
290
291    fn area(kind: AreaKind, mode: SourceMode, ids: &[&str], authoritative: bool) -> AreaListing {
292        AreaListing {
293            kind,
294            mode,
295            clips: ids.iter().map(|id| tclip(id)).collect(),
296            authoritative_ignoring_empty: authoritative,
297        }
298    }
299
300    // Test 5: an empty Mirror area is never authoritative (a legitimately empty
301    // mirror is indistinguishable from a dropped listing), so deletion is
302    // suppressed. An empty Copy area stays enumerated (it protects nothing).
303    #[test]
304    fn empty_mirror_area_is_not_enumerated() {
305        let mirror = area(AreaKind::Liked, SourceMode::Mirror, &[], true);
306        assert!(!area_enumerated(&mirror, false));
307        let copy = area(AreaKind::Liked, SourceMode::Copy, &[], true);
308        assert!(area_enumerated(&copy, false));
309        // A non-empty mirror that fully listed is authoritative.
310        let full = area(AreaKind::Liked, SourceMode::Mirror, &["x"], true);
311        assert!(area_enumerated(&full, false));
312    }
313
314    // A run under `library="off"` that mirrors a fully-enumerated playlist can
315    // delete, so first-use adoption must confirm identity (enumerated == true)
316    // rather than SkipPin into a delete against an unconfirmed account (#149).
317    #[test]
318    fn adoption_enumerated_covers_a_mirror_playlist_under_library_off() {
319        let playlist = |mode, ids: &[&str], auth| {
320            area(
321                AreaKind::Playlist {
322                    id: "p".into(),
323                    name: "P".into(),
324                },
325                mode,
326                ids,
327                auth,
328            )
329        };
330        // library="off" + a fully-enumerated Mirror playlist arms deletion.
331        assert!(adoption_enumerated(
332            &[playlist(SourceMode::Mirror, &["pl"], true)],
333            false
334        ));
335        // A copy-only run cannot delete, so identity need not be confirmed.
336        assert!(!adoption_enumerated(
337            &[playlist(SourceMode::Copy, &["pl"], true)],
338            false
339        ));
340        // An empty mirror (a dropped or ambiguous listing) is not authoritative.
341        assert!(!adoption_enumerated(
342            &[playlist(SourceMode::Mirror, &[], true)],
343            false
344        ));
345        // A partial (non-authoritative) mirror listing does not arm adoption.
346        assert!(!adoption_enumerated(
347            &[playlist(SourceMode::Mirror, &["pl"], false)],
348            false
349        ));
350        // A force-copy (additive) run never deletes, so never forces the pin.
351        assert!(!adoption_enumerated(
352            &[playlist(SourceMode::Mirror, &["pl"], true)],
353            true
354        ));
355        // The classic authoritative-library anchor still counts.
356        assert!(adoption_enumerated(
357            &[area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)],
358            false,
359        ));
360    }
361
362    // library_authoritative counts the implicit protector but is false for
363    // `library="off"` (no library area at all).
364    #[test]
365    fn library_authoritative_counts_protector_not_off() {
366        let with_protector = vec![
367            area(AreaKind::Library, SourceMode::Copy, &["lib"], true),
368            area(
369                AreaKind::Playlist {
370                    id: "p".into(),
371                    name: "P".into(),
372                },
373                SourceMode::Mirror,
374                &["pl"],
375                true,
376            ),
377        ];
378        assert!(library_authoritative(&with_protector, false));
379
380        let off = vec![area(
381            AreaKind::Playlist {
382                id: "p".into(),
383                name: "P".into(),
384            },
385            SourceMode::Mirror,
386            &["pl"],
387            true,
388        )];
389        assert!(!library_authoritative(&off, false));
390    }
391
392    /// (can_delete, library_authoritative, truncate) for a set of areas, exactly
393    /// as `run_one` computes them, for the #148 scenario traces.
394    fn verdict(areas: &[AreaListing]) -> (bool, bool, bool) {
395        let can_delete = deletion_allowed(&source_statuses(areas, false));
396        let lib_auth = library_authoritative(areas, false);
397        (
398            can_delete,
399            lib_auth,
400            narrows_downloads(can_delete, lib_auth),
401        )
402    }
403
404    fn pl_area(mode: SourceMode, ids: &[&str], authoritative: bool) -> AreaListing {
405        area(
406            AreaKind::Playlist {
407                id: "p".into(),
408                name: "P".into(),
409            },
410            mode,
411            ids,
412            authoritative,
413        )
414    }
415
416    // The #148 behaviour change at the area level: a narrowed playlist mirror
417    // neither enumerates nor arms deletion; the same listing un-narrowed does both.
418    #[test]
419    fn narrowed_playlist_mirror_disarms_deletion() {
420        let narrowed = pl_area(
421            SourceMode::Mirror,
422            &["a"],
423            area_authoritative(true, false, true),
424        );
425        assert!(!area_enumerated(&narrowed, false));
426        assert!(!deletion_allowed(&source_statuses(&[narrowed], false)));
427
428        let full = pl_area(
429            SourceMode::Mirror,
430            &["a"],
431            area_authoritative(true, false, false),
432        );
433        assert!(area_enumerated(&full, false));
434        assert!(deletion_allowed(&source_statuses(&[full], false)));
435    }
436
437    // #148 scenario (c): a narrowed playlist mirror WITH the injected full-library
438    // protector does not delete (the playlist disarms) and does not narrow
439    // downloads (the protector lists the whole library, which drives index/art).
440    #[test]
441    fn narrowed_playlist_with_protector_neither_deletes_nor_narrows() {
442        let areas = vec![
443            area(AreaKind::Library, SourceMode::Copy, &["lib"], true),
444            pl_area(
445                SourceMode::Mirror,
446                &["pl"],
447                area_authoritative(true, false, true),
448            ),
449        ];
450        let (can_delete, lib_auth, truncate) = verdict(&areas);
451        assert!(!can_delete, "narrowed playlist mirror is disarmed");
452        assert!(lib_auth, "the protector is an authoritative library");
453        assert!(
454            !truncate,
455            "the full library is listed, so downloads are not narrowed"
456        );
457    }
458
459    // #148 scenario (d): a narrowed playlist mirror under `library="off"` (no
460    // protector) does not delete and DOES narrow downloads, matching a narrowed
461    // library-only or liked run.
462    #[test]
463    fn narrowed_playlist_off_disarms_and_narrows() {
464        let areas = vec![pl_area(
465            SourceMode::Mirror,
466            &["pl"],
467            area_authoritative(true, false, true),
468        )];
469        let (can_delete, lib_auth, truncate) = verdict(&areas);
470        assert!(!can_delete, "narrowed playlist mirror is disarmed");
471        assert!(!lib_auth, "library=off leaves no library area");
472        assert!(
473            truncate,
474            "no armed deletion and no full library, so downloads narrow"
475        );
476    }
477
478    // #148 regression guard for scenario (e): a configured unfiltered
479    // `library="mirror"` lists the whole feed regardless of `--limit`/`--since`,
480    // so it stays armed and authoritative. The fix must NOT disarm it — that is
481    // the #149/D2 guarantee that closes the token-swap hole.
482    #[test]
483    fn configured_full_library_mirror_still_deletes_when_narrowed() {
484        let areas = vec![area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)];
485        let (can_delete, lib_auth, truncate) = verdict(&areas);
486        assert!(
487            can_delete,
488            "the configured full-library mirror still deletes"
489        );
490        assert!(lib_auth);
491        assert!(
492            !truncate,
493            "the full library is listed, so downloads are not narrowed"
494        );
495    }
496
497    // A narrowed `library="off"` mirror playlist cannot delete (#148), so first-use
498    // adoption skips the pin rather than confirming identity — the #149 rule that
499    // only a delete-capable run must confirm the account composes cleanly.
500    #[test]
501    fn adoption_skips_pin_on_a_narrowed_library_off_playlist() {
502        let areas = vec![pl_area(
503            SourceMode::Mirror,
504            &["pl"],
505            area_authoritative(true, false, true),
506        )];
507        assert!(!adoption_enumerated(&areas, false));
508    }
509
510    // H1: the union keeps the first area's payload per id (Library wins over a
511    // later playlist copy of the same clip).
512    #[test]
513    fn union_keeps_first_area_payload() {
514        let mut lib = tclip("shared");
515        lib.title = "Library".to_owned();
516        let mut pl = tclip("shared");
517        pl.title = "Playlist".to_owned();
518        let areas = vec![
519            AreaListing {
520                kind: AreaKind::Library,
521                mode: SourceMode::Copy,
522                clips: vec![lib, tclip("lib-only")],
523                authoritative_ignoring_empty: true,
524            },
525            AreaListing {
526                kind: AreaKind::Playlist {
527                    id: "p".into(),
528                    name: "P".into(),
529                },
530                mode: SourceMode::Mirror,
531                clips: vec![pl],
532                authoritative_ignoring_empty: true,
533            },
534        ];
535        let union = union_clips(&areas);
536        assert_eq!(union.len(), 2);
537        assert_eq!(union[0].id, "shared");
538        assert_eq!(union[0].title, "Library");
539        assert_eq!(union[1].id, "lib-only");
540    }
541
542    #[test]
543    fn a_failed_area_suppresses_deletion_for_the_run() {
544        let areas = [
545            area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
546            // Playlist listing failed: empty and non-authoritative.
547            area(
548                AreaKind::Playlist {
549                    id: "p".into(),
550                    name: "P".into(),
551                },
552                SourceMode::Mirror,
553                &[],
554                false,
555            ),
556        ];
557        let sources: Vec<SourceStatus> = areas
558            .iter()
559            .map(|a| SourceStatus {
560                mode: area_mode(a, false),
561                fully_enumerated: area_enumerated(a, false),
562            })
563            .collect();
564        assert!(!deletion_allowed(&sources));
565    }
566
567    // Test 8: with every area enumerated, a mixed Mirror + Copy selection deletes
568    // only orphans exclusive to a Mirror area; a Copy area's orphan is protected
569    // and the run remains armed.
570    #[test]
571    fn mixed_mode_deletes_only_mirror_exclusive_orphans() {
572        let areas = vec![
573            area(AreaKind::Liked, SourceMode::Mirror, &["m-live"], true),
574            area(
575                AreaKind::Playlist {
576                    id: "p".into(),
577                    name: "P".into(),
578                },
579                SourceMode::Copy,
580                &["c-live"],
581                true,
582            ),
583        ];
584        let sources: Vec<SourceStatus> = areas
585            .iter()
586            .map(|a| SourceStatus {
587                mode: area_mode(a, false),
588                fully_enumerated: area_enumerated(a, false),
589            })
590            .collect();
591        assert!(deletion_allowed(&sources));
592
593        let modes = build_modes_by_id(&areas, false);
594        let union = union_clips(&areas);
595        let desired = build_desired(
596            &union.iter().collect::<Vec<_>>(),
597            AudioFormat::Flac,
598            &modes,
599            &HashMap::new(),
600            &BTreeSet::new(),
601            ArtifactToggles::default(),
602            &NamingConfig::default(),
603        );
604
605        let mut manifest = Manifest::new();
606        // Orphans: one previously from the mirror area, one from the copy area.
607        for id in ["m-live", "c-live", "m-orphan", "c-orphan"] {
608            manifest.insert(
609                id,
610                ManifestEntry {
611                    path: format!("{id}.flac"),
612                    format: AudioFormat::Flac,
613                    size: 100,
614                    // The copy-area orphan carries the preserve marker a prior copy
615                    // run stamped, so it can never be deleted.
616                    preserve: id == "c-orphan",
617                    ..Default::default()
618                },
619            );
620        }
621        let local: HashMap<String, LocalFile> = manifest
622            .iter()
623            .map(|(id, _)| {
624                (
625                    id.clone(),
626                    LocalFile {
627                        exists: true,
628                        size: 100,
629                    },
630                )
631            })
632            .collect();
633        let plan = reconcile(&manifest, &desired, &local, &sources);
634        let deleted: Vec<&str> = plan
635            .actions
636            .iter()
637            .filter_map(|a| match a {
638                Action::Delete { clip_id, .. } => Some(clip_id.as_str()),
639                _ => None,
640            })
641            .collect();
642        assert_eq!(deleted, vec!["m-orphan"]);
643    }
644
645    // Test 7 (SYNC-8): a clip held by a Mirror and a Copy area is stamped
646    // `[Mirror, Copy]`, so build_desired carries the Copy protection.
647    #[test]
648    fn build_modes_by_id_copy_wins_and_dedups() {
649        let areas = [
650            area(AreaKind::Liked, SourceMode::Mirror, &["a", "b"], true),
651            pl_area(SourceMode::Copy, &["b", "c"], true),
652        ];
653        let map = build_modes_by_id(&areas, false);
654        assert_eq!(map["a"], vec![SourceMode::Mirror]);
655        assert_eq!(map["b"], vec![SourceMode::Mirror, SourceMode::Copy]);
656        assert_eq!(map["c"], vec![SourceMode::Copy]);
657    }
658
659    // playlists_enumerated is the scoped `.m3u8` delete gate: true only when the
660    // union is intact and every playlist-rendering area fully enumerated.
661    #[test]
662    fn playlists_enumerated_gates_on_every_playlist_area() {
663        // Every playlist/liked area enumerated and the union intact -> true.
664        assert!(playlists_enumerated(
665            &[
666                area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
667                pl_area(SourceMode::Mirror, &["b"], true),
668            ],
669            false,
670            true,
671        ));
672
673        // A failed playlist listing (empty, non-authoritative) disarms the gate.
674        assert!(!playlists_enumerated(
675            &[
676                area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
677                AreaListing::unresolved_playlist(SourceMode::Mirror),
678            ],
679            false,
680            true,
681        ));
682
683        // A failed liked listing disarms the gate too.
684        assert!(!playlists_enumerated(
685            &[
686                AreaListing::failed(AreaKind::Liked, SourceMode::Mirror),
687                pl_area(SourceMode::Mirror, &["b"], true),
688            ],
689            false,
690            true,
691        ));
692
693        // A truncated union disarms the gate even when every area enumerated.
694        assert!(!playlists_enumerated(
695            &[
696                area(AreaKind::Liked, SourceMode::Mirror, &["a"], true),
697                pl_area(SourceMode::Mirror, &["b"], true),
698            ],
699            false,
700            false,
701        ));
702    }
703
704    // Library areas render no `.m3u8`, so a library-only (or empty) set never
705    // gates the playlist delete authority: the flag just follows members_intact.
706    #[test]
707    fn playlists_enumerated_ignores_library_only_sets() {
708        assert!(playlists_enumerated(
709            &[area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)],
710            false,
711            true,
712        ));
713        assert!(!playlists_enumerated(
714            &[area(AreaKind::Library, SourceMode::Mirror, &["lib"], true)],
715            false,
716            false,
717        ));
718        assert!(playlists_enumerated(&[], false, true));
719        assert!(!playlists_enumerated(&[], false, false));
720    }
721}