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