Skip to main content

rsynko_manager/
laws.rs

1//! Law scenarios for the manager, stated once over the capabilities.
2//!
3//! A scenario authors its own collection through the capabilities it is bound to, so it constrains
4//! any interpreter of the manager sorts and a runner names it without supplying anything. One
5//! bundle states one module's meaning, and its bounds name exactly the capabilities that meaning
6//! is derived from.
7
8use crate::*;
9use alux_ext::ext;
10use anyhow::{Result, bail};
11use rsynko_media::*;
12use rsynko_session::Wanted;
13use std::ffi::OsStr;
14use std::fmt::Debug;
15use std::path::{Path, PathBuf};
16use std::time::Duration;
17
18/// Supplies the collection observation a downloads scenario cannot author for itself.
19pub trait DownloadsLawFixture: ManagerSorts {
20    /// Observes the inputs one downloads collection holds, in collection order.
21    fn law_collected_sources(&self, downloads: &Self::Downloads) -> Vec<String>;
22}
23
24/// Authors the collection every stateful manager scenario starts from.
25#[ext(name = ManagerLawAuthoring)]
26pub impl<This> This
27where
28    This: NavigationStateAlg
29        + QueueCatalogAlg
30        + QueueAppendAlg
31        + DraftStateAlg
32        + ManagerStatusAlg
33        + SourceRequestAlg
34        + SubmissionAlg
35        + MediaOptionsAlg
36        + OutputChoiceAlg,
37    This::Id: Copy,
38{
39    /// Authors the stated number of media requests and observes the identities they were given.
40    ///
41    /// The requests are stated rather than submitted: what a line reads as is what one
42    /// interpreter recognizes, and a scenario about the collection is not about that.
43    fn author_law_queue(&mut self, count: usize) -> Vec<This::Id> {
44        let requests = (0..count)
45            .map(|index| self.source(law_source(index), self.suggested_output(), self.progressive()))
46            .collect();
47        let ids = self.append_sources(requests);
48        // Where the cursor rests after adding is what submitting states; a scenario about
49        // anything else starts from the collection already focused.
50        self.set_selected_queue_id(ids.first().copied());
51        ids
52    }
53}
54
55/// Names the source one authored entry submits.
56fn law_source(index: usize) -> String {
57    format!("law://source-{index}")
58}
59
60/// Authors the collection-navigation laws.
61#[ext(name = NavigationLaws)]
62pub impl<This> This
63where
64    This: NavigationStateAlg
65        + QueueCatalogAlg
66        + QueueAppendAlg
67        + QueueRemoveAlg
68        + DraftStateAlg
69        + ManagerStatusAlg
70        + SourceRequestAlg
71        + SubmissionAlg
72        + MediaOptionsAlg
73        + OutputChoiceAlg,
74    This::Entry: QueueEntryAlg + RequestOptionsAlg,
75    This::Id: Copy + Eq + Debug,
76{
77    /// Checks that selection, expansion, and removal preserve stable queue identity.
78    ///
79    /// The laws checked are:
80    ///
81    /// 1. adding sources focuses the first new identity;
82    /// 2. selection advances in collection order and wraps at both ends;
83    /// 3. expanded details name the selected identity and move with it;
84    /// 4. removing the selected entry chooses a remaining neighbor;
85    /// 5. removing an expanded entry denotes the collection again;
86    /// 6. breadcrumbs are rooted at the collection and name the expanded identity.
87    ///
88    /// # Errors
89    ///
90    /// Returns the first violated law.
91    fn navigation_laws(&mut self) -> Result<()> {
92        let ids = self.author_law_queue(3);
93        if ids.len() != 3 {
94            bail!("authoring three sources produced {} entries", ids.len());
95        }
96        self.select_relative(true);
97        if self.selected_queue_id() != ids.get(1).copied() {
98            bail!("selecting next did not advance in collection order");
99        }
100        self.select_relative(false);
101        self.select_relative(false);
102        if self.selected_queue_id() != ids.last().copied() {
103            bail!("selecting previous did not wrap to the last identity");
104        }
105        self.select_relative(true);
106        if self.selected_queue_id() != ids.first().copied() {
107            bail!("selecting next did not wrap to the first identity");
108        }
109
110        self.set_page(ManagerPage::Details(ids[0]));
111        self.select_relative(true);
112        if self.page() != ManagerPage::Details(ids[1]) {
113            bail!("expanded details did not move with the selection");
114        }
115        if self.selected_queue_id() != Some(ids[1]) {
116            bail!("expanding details displaced the collection selection");
117        }
118
119        self.remove_selected();
120        let remaining: Vec<This::Id> = self.queue_ids().collect();
121        if remaining != [ids[0], ids[2]] {
122            bail!("removal did not preserve the surviving collection order: {remaining:?}");
123        }
124        if self.selected_queue_id() != Some(ids[2]) {
125            bail!("removing the selected entry did not choose a remaining neighbor");
126        }
127        if self.page() != ManagerPage::Collection {
128            bail!("removing the expanded entry did not denote the collection again");
129        }
130
131        let rooted = self.breadcrumbs();
132        if rooted.len() != 1 {
133            bail!("the collection rests under {} pages", rooted.len() - 1);
134        }
135        self.set_page(ManagerPage::Details(ids[2]));
136        let expanded = self.breadcrumbs();
137        if expanded.first() != rooted.first() {
138            bail!("expanded details are not rooted at the collection");
139        }
140        let named = self.queue_entry(ids[2]).map(QueueEntryAlg::label);
141        if expanded.last().map(|crumb| crumb.label.as_str()) != named {
142            bail!("the breadcrumb of an expanded entry does not name what it denotes");
143        }
144        Ok(())
145    }
146}
147
148/// Authors the editor-draft laws.
149#[ext(name = DraftLaws)]
150pub impl<This> This
151where
152    This: NavigationStateAlg
153        + QueueCatalogAlg
154        + QueueAppendAlg
155        + DraftStateAlg
156        + OutputDraftAlg
157        + InputDraftAlg
158        + ManagerStatusAlg
159        + SourceRequestAlg
160        + SubmissionAlg
161        + MediaOptionsAlg
162        + OutputChoiceAlg,
163    This::Entry: QueueEntryAlg,
164    This::Id: Copy + Eq + Debug,
165{
166    /// Checks that a draft is observed back and submits exactly the sources it names.
167    ///
168    /// The laws checked are:
169    ///
170    /// 1. a draft is observed back exactly as it was replaced;
171    /// 2. the source draft and the output-name draft are independent;
172    /// 3. submitting adds the non-blank trimmed lines in draft order and nothing else;
173    /// 4. submitting clears the draft and denotes the collection;
174    /// 5. submitting a draft naming no source adds nothing and moves no focus.
175    ///
176    /// # Errors
177    ///
178    /// Returns the first violated law.
179    fn draft_laws(&mut self) -> Result<()> {
180        self.set_draft("first\nsecond".to_owned());
181        if self.draft() != "first\nsecond" {
182            bail!("a replaced draft is not the one observed back");
183        }
184        self.set_output_draft("name.mp4".to_owned());
185        if self.draft() != "first\nsecond" {
186            bail!("replacing the output-name draft disturbed the source draft");
187        }
188        if self.output_draft() != "name.mp4" {
189            bail!("a replaced output-name draft is not the one observed back");
190        }
191
192        self.set_draft(String::new());
193        self.submit_draft();
194        if self.queue_ids().next().is_some() {
195            bail!("submitting a draft naming no source still added an entry");
196        }
197
198        self.set_page(ManagerPage::AddSources);
199        self.set_draft(format!("  {}  \n\n{}\n", law_source(0), law_source(1)));
200        self.submit_draft();
201        let submitted: Vec<String> =
202            self.queue_ids().filter_map(|id| self.queue_entry(id)).map(|entry| entry.label().to_owned()).collect();
203        if submitted != [law_source(0), law_source(1)] {
204            bail!("submitting did not add exactly the sources the draft names: {submitted:?}");
205        }
206        if !self.draft().is_empty() {
207            bail!("submitting did not clear the draft");
208        }
209        if self.page() != ManagerPage::Collection {
210            bail!("submitting did not denote the collection again");
211        }
212
213        let focused = self.selected_queue_id();
214        self.set_draft("   ".to_owned());
215        self.submit_draft();
216        if self.selected_queue_id() != focused {
217            bail!("submitting a draft naming no source moved the focus");
218        }
219        Ok(())
220    }
221}
222
223/// Authors the text-editing laws.
224#[ext(name = TextLaws)]
225pub impl<This> This
226where
227    This: NavigationStateAlg + TextEditorStateAlg,
228{
229    /// Checks that editing moves by Unicode scalar and never splits one.
230    ///
231    /// The laws checked are:
232    ///
233    /// 1. the cursor moves by whole scalars in both directions;
234    /// 2. insertion advances past exactly the inserted bytes;
235    /// 3. deleting before and at the cursor removes one whole scalar;
236    /// 4. every observed cursor rests on a UTF-8 boundary.
237    ///
238    /// # Errors
239    ///
240    /// Returns the first violated law.
241    fn text_editor_laws(&mut self) -> Result<()> {
242        // An editor exists on the page that offers one, so the scenario opens one first.
243        self.set_page(ManagerPage::AddSources);
244        self.set_active_text_editor("a😀b".to_owned(), "a😀".len());
245        self.move_cursor_left();
246        self.check_law_editor("a😀b", 1)?;
247        self.move_cursor_right();
248        self.check_law_editor("a😀b", "a😀".len())?;
249        self.delete_before_cursor();
250        self.check_law_editor("ab", 1)?;
251
252        self.insert_text("😀");
253        self.check_law_editor("a😀b", "a😀".len())?;
254        self.move_cursor_home();
255        self.check_law_editor("a😀b", 0)?;
256        self.delete_at_cursor();
257        self.check_law_editor("😀b", 0)?;
258        self.move_cursor_end();
259        self.check_law_editor("😀b", "😀b".len())
260    }
261
262    /// Checks the observed text and cursor against the meaning one edit denotes.
263    ///
264    /// # Errors
265    ///
266    /// Returns the violated law.
267    fn check_law_editor(&self, text: &str, cursor: usize) -> Result<()> {
268        let Some((observed, position)) = self.active_text_editor() else {
269            bail!("the editor an edit was applied to observes no text");
270        };
271        if observed != text || position != cursor {
272            bail!("editing denoted {observed:?} at {position} rather than {text:?} at {cursor}");
273        }
274        if !observed.is_char_boundary(position) {
275            bail!("the observed cursor {position} splits a Unicode scalar in {observed:?}");
276        }
277        Ok(())
278    }
279}
280
281/// Authors the keyed-transfer laws.
282#[ext(name = QueueLaws)]
283pub impl<This> This
284where
285    This: NavigationStateAlg
286        + QueueCatalogAlg
287        + QueueAppendAlg
288        + TransferStateAlg
289        + SourceMetadataAlg
290        + DraftStateAlg
291        + ManagerStatusAlg
292        + SourceRequestAlg
293        + SubmissionAlg
294        + MediaOptionsAlg
295        + OutputChoiceAlg,
296    This::Entry: QueueEntryAlg + TransferViewAlg,
297    This::Id: Copy + Eq + Debug,
298{
299    /// Checks that observations are keyed and that derived progress follows only from them.
300    ///
301    /// The laws checked are:
302    ///
303    /// 1. an observation updates exactly the identity it addresses;
304    /// 2. byte rate, remaining time, and completed share are derived from bytes and elapsed time;
305    /// 3. full byte progress denotes publication, while only terminal success denotes completion;
306    /// 4. the submitted source is the label until extraction supplies a title.
307    ///
308    /// # Errors
309    ///
310    /// Returns the first violated law.
311    fn queue_laws(&mut self) -> Result<()> {
312        let ids = self.author_law_queue(2);
313        let destination = PathBuf::from("law.bin");
314
315        self.apply_transfer_event(
316            ids[0],
317            TransferObservationOp::Progress {
318                destination: destination.clone(),
319                downloaded: 4_000_000,
320                total: Some(10_000_000),
321            },
322        );
323        self.apply_transfer_event(ids[0], TransferObservationOp::Elapsed { elapsed: Duration::from_secs(2) });
324        if self.law_transferred(ids[1]) != Some(0) {
325            bail!("a keyed observation reached an identity it does not address");
326        }
327
328        let observed = self.law_entry(ids[0])?;
329        if observed.bytes_per_second() != Some(2_000_000) {
330            bail!("byte rate is not derived from observed bytes and elapsed time");
331        }
332        if observed.estimated_remaining() != Some(Duration::from_secs(3)) {
333            bail!("remaining time is not derived from observed bytes and elapsed time");
334        }
335        if observed.percent() != Some(40) {
336            bail!("the completed share is not derived from observed bytes and expected size");
337        }
338
339        self.apply_transfer_event(
340            ids[0],
341            TransferObservationOp::Progress {
342                destination: destination.clone(),
343                downloaded: 10_000_000,
344                total: Some(10_000_000),
345            },
346        );
347        let observed = self.law_entry(ids[0])?;
348        if observed.percent() != Some(100) {
349            bail!("full byte progress is not observed as a complete share");
350        }
351        if observed.transfer_complete() {
352            bail!("full byte progress denoted completion before terminal success");
353        }
354        self.apply_transfer_event(ids[0], TransferObservationOp::Completed { destination, bytes: 10_000_000 });
355        if !self.law_entry(ids[0])?.transfer_complete() {
356            bail!("terminal success did not denote completion");
357        }
358
359        if self.law_entry(ids[1])?.label() != law_source(1) {
360            bail!("the submitted source is not the label before extraction supplies a title");
361        }
362        self.apply_source_metadata(ids[1], "law".to_owned(), Some("Law Title".to_owned()));
363        if self.law_entry(ids[1])?.label() != "Law Title" {
364            bail!("an extracted title is not the observed label");
365        }
366        Ok(())
367    }
368
369    /// Observes one entry by stable identity.
370    ///
371    /// # Errors
372    ///
373    /// Returns a violation when the collection holds no such identity.
374    fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
375        match self.queue_entry(id) {
376            Some(entry) => Ok(entry),
377            None => bail!("the collection holds no entry for {id:?}"),
378        }
379    }
380
381    /// Observes the bytes one identity has retrieved, when it names an entry.
382    fn law_transferred(&self, id: This::Id) -> Option<u64> {
383        self.queue_entry(id).map(TransferViewAlg::transferred)
384    }
385}
386
387/// Authors the derived-transition laws.
388#[ext(name = TransitionLaws)]
389pub impl<This> This
390where
391    This: NavigationStateAlg
392        + QueueCatalogAlg
393        + QueueAppendAlg
394        + QueuePauseAlg
395        + QueueDuplicateAlg
396        + QueueOutputAlg
397        + QueueSourceAlg
398        + TransferStateAlg
399        + SourceMetadataAlg
400        + DetailSelectionAlg
401        + DraftStateAlg
402        + OutputDraftAlg
403        + InputDraftAlg
404        + ManagerStatusAlg
405        + SourceRequestAlg
406        + SubmissionAlg
407        + MediaOptionsAlg
408        + OutputChoiceAlg,
409    This::Entry: QueueEntryAlg,
410    This::Id: Copy + Eq + Debug,
411{
412    /// Checks that Space, restart, duplication, and output editing denote their transitions.
413    ///
414    /// The laws checked are:
415    ///
416    /// 1. Space schedules exactly the selected ready entry and fixes its request;
417    /// 2. pause is offered only while the active interpreter advertises that capability;
418    /// 3. a failed request offers restart and restarts without becoming editable again;
419    /// 4. duplication creates a fresh editable identity with its own transfer state;
420    /// 5. an extracted title is the default output stem;
421    /// 6. an output name is editable before first start and normalized when applied.
422    ///
423    /// # Errors
424    ///
425    /// Returns the first violated law.
426    fn transition_laws(&mut self) -> Result<()> {
427        let ids = self.author_law_queue(2);
428        for (index, id) in ids.iter().copied().enumerate() {
429            self.apply_source_metadata(id, format!("law-{index}"), Some(format!("Law {index}")));
430        }
431        if self.law_space(ids[0])? != Some(SpaceAction::Start) {
432            bail!("a ready entry naming an output does not offer Start");
433        }
434        let derived = self.law_entry(ids[0])?.output().map(Path::to_owned);
435        let stem = derived.as_deref().and_then(Path::file_stem).and_then(OsStr::to_str);
436        if stem != Some("Law 0") {
437            bail!("the extracted title is not the default output stem: {stem:?}");
438        }
439
440        self.set_selected_queue_id(Some(ids[0]));
441        self.set_page(ManagerPage::Output(ids[0]));
442        self.set_output_draft("My: copy".to_owned());
443        self.submit_output();
444        let named = self.law_entry(ids[0])?;
445        let Some(output) = named.output() else {
446            bail!("applying an output-name draft named no output");
447        };
448        if output.components().count() != 1 || output.to_string_lossy().contains(':') {
449            bail!("an applied output name is not one portable file component: {}", output.display());
450        }
451
452        self.apply_selected_space();
453        if self.law_entry(ids[0])?.is_editable() {
454            bail!("first start did not fix the request options");
455        }
456        if self.law_space(ids[1])? != Some(SpaceAction::Start) {
457            bail!("scheduling one entry disturbed another entry's Space meaning");
458        }
459        if self.law_space(ids[0])?.is_some() {
460            bail!("a waiting entry offers a Space action");
461        }
462
463        self.apply_transfer_event(ids[0], TransferObservationOp::Started {});
464        if self.law_space(ids[0])?.is_some() {
465            bail!("an active transfer offers pause although no interpreter advertises it");
466        }
467        self.apply_transfer_event(ids[0], TransferObservationOp::PauseCapability { supported: true });
468        if self.law_space(ids[0])? != Some(SpaceAction::Pause) {
469            bail!("an advertised pause capability does not offer pause");
470        }
471        self.apply_selected_space();
472        if self.law_space(ids[0])? != Some(SpaceAction::Resume) {
473            bail!("a paused transfer does not offer resumption");
474        }
475        self.apply_selected_space();
476        if self.law_space(ids[0])? != Some(SpaceAction::Pause) {
477            bail!("a resumed transfer does not offer pause again");
478        }
479
480        self.apply_transfer_event(
481            ids[0],
482            TransferObservationOp::ProgramFailed {
483                summary: "no matching format".to_owned(),
484                detail: "no directly retrievable format matches the request".to_owned(),
485            },
486        );
487        if !self.law_entry(ids[0])?.detail_controls().contains(&DetailControl::Restart) {
488            bail!("a failed request offers no restart");
489        }
490        self.restart_selected();
491        if self.law_entry(ids[0])?.is_editable() {
492            bail!("restarting a failed request made it editable again");
493        }
494
495        self.duplicate_selected();
496        let Some(duplicate) = self.selected_queue_id() else {
497            bail!("duplication focused no identity");
498        };
499        if duplicate == ids[0] {
500            bail!("duplication reused the identity it duplicates");
501        }
502        let fresh = self.law_entry(duplicate)?;
503        if !fresh.is_editable() {
504            bail!("a duplicate is not editable");
505        }
506        if fresh.space_action() != Some(SpaceAction::Start) {
507            bail!("a duplicate does not start from a ready transfer state");
508        }
509        if fresh.label() != self.law_entry(ids[0])?.label() {
510            bail!("duplication did not preserve what the source denotes");
511        }
512        Ok(())
513    }
514
515    /// Observes one entry by stable identity.
516    ///
517    /// # Errors
518    ///
519    /// Returns a violation when the collection holds no such identity.
520    fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
521        match self.queue_entry(id) {
522            Some(entry) => Ok(entry),
523            None => bail!("the collection holds no entry for {id:?}"),
524        }
525    }
526
527    /// Observes the Space meaning one identity currently offers.
528    ///
529    /// # Errors
530    ///
531    /// Returns a violation when the collection holds no such identity.
532    fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
533        Ok(self.law_entry(id)?.space_action())
534    }
535}
536
537/// Authors the menu-availability laws.
538#[ext(name = MenuLaws)]
539pub impl<This> This
540where
541    This: NavigationStateAlg
542        + QueueCatalogAlg
543        + QueueAppendAlg
544        + QueuePauseAlg
545        + QueueDuplicateAlg
546        + TransferStateAlg
547        + SourceMetadataAlg
548        + DetailSelectionAlg
549        + DraftStateAlg
550        + OutputDraftAlg
551        + InputDraftAlg
552        + TextEditorStateAlg
553        + ManagerStatusAlg
554        + SourceRequestAlg
555        + SubmissionAlg
556        + MediaOptionsAlg
557        + OutputChoiceAlg,
558    This::Entry: QueueEntryAlg + RequestOptionsAlg,
559    This::Id: Copy + Eq + Debug,
560{
561    /// Checks that availability is derived from page, selection, and queue observations.
562    ///
563    /// The laws checked are:
564    ///
565    /// 1. an empty collection enables only the actions that still have meaning;
566    /// 2. a selected ready entry enables Space, Remove, and activation;
567    /// 3. cursor movement is enabled exactly when the page holds another value to move to;
568    /// 4. activation on the add-sources page is enabled exactly when the draft names a source;
569    /// 5. a disabled action denotes no operation, so applying it changes nothing observable.
570    ///
571    /// # Errors
572    ///
573    /// Returns the first violated law.
574    fn menu_laws(&mut self) -> Result<()> {
575        for (action, expected) in [
576            (ManagerAction::Space, ActionAvailability::Disabled),
577            (ManagerAction::Remove, ActionAvailability::Disabled),
578            (ManagerAction::Activate, ActionAvailability::Disabled),
579            (ManagerAction::Next, ActionAvailability::Disabled),
580            (ManagerAction::AddSources, ActionAvailability::Enabled),
581            (ManagerAction::Exit, ActionAvailability::Enabled),
582        ] {
583            if self.action_availability(action) != expected {
584                bail!("an empty collection denotes {action:?} as {expected:?} nowhere");
585            }
586        }
587
588        let ids = self.author_law_queue(1);
589        self.apply_source_metadata(ids[0], "law".to_owned(), Some("Law".to_owned()));
590        for action in [ManagerAction::Space, ManagerAction::Remove, ManagerAction::Activate] {
591            if self.action_availability(action) != ActionAvailability::Enabled {
592                bail!("a selected ready entry does not enable {action:?}");
593            }
594        }
595        if self.action_availability(ManagerAction::Next) != ActionAvailability::Disabled {
596            bail!("a collection of one entry offers cursor movement");
597        }
598        let before = self.law_space(ids[0])?;
599        self.author_law_queue(1);
600        if self.action_availability(ManagerAction::Next) != ActionAvailability::Enabled {
601            bail!("a collection of two entries does not offer cursor movement");
602        }
603
604        self.set_page(ManagerPage::AddSources);
605        self.set_draft("   ".to_owned());
606        if self.action_availability(ManagerAction::Activate) != ActionAvailability::Disabled {
607            bail!("a draft naming no source enables activation");
608        }
609        if self.action_availability(ManagerAction::Back) != ActionAvailability::Enabled {
610            bail!("a page below the collection does not enable returning to it");
611        }
612        self.set_draft(law_source(2));
613        if self.action_availability(ManagerAction::Activate) != ActionAvailability::Enabled {
614            bail!("a draft naming a source does not enable activation");
615        }
616
617        // Space has no meaning away from the collection, so applying it must deny that meaning.
618        if self.action_availability(ManagerAction::Space) != ActionAvailability::Disabled {
619            bail!("the add-sources page enables Space");
620        }
621        self.set_selected_queue_id(Some(ids[0]));
622        self.apply_selected_space();
623        if self.law_space(ids[0])? != before {
624            bail!("a disabled action denoted an operation when it was applied");
625        }
626        Ok(())
627    }
628
629    /// Observes one entry's Space meaning by stable identity.
630    ///
631    /// # Errors
632    ///
633    /// Returns a violation when the collection holds no such identity.
634    fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
635        match self.queue_entry(id) {
636            Some(entry) => Ok(entry.space_action()),
637            None => bail!("the collection holds no entry for {id:?}"),
638        }
639    }
640}
641
642/// Authors the downloads-collection laws.
643#[ext(name = DownloadsLaws)]
644pub impl<This, Source> This
645where
646    This: ManagerSorts<Source = Source, Downloads: DownloadCollectionAlg<Source = Source>>
647        + DownloadsAlg
648        + SourceRequestAlg
649        + SubmissionAlg
650        + MediaOptionsAlg
651        + OutputChoiceAlg
652        + DownloadsLawFixture,
653{
654    /// Checks that a collection holds exactly the sources it was given, in the order given.
655    ///
656    /// The laws checked are:
657    ///
658    /// 1. the empty collection holds nothing;
659    /// 2. a collection holds its sources in declaration order;
660    /// 3. adding appends after what the collection already holds.
661    ///
662    /// # Errors
663    ///
664    /// Returns the first violated law.
665    fn downloads_laws(&self) -> Result<()> {
666        let empty = self.empty_downloads();
667        if !self.law_collected_sources(&empty).is_empty() {
668            bail!("the empty collection holds a source");
669        }
670        let stated = [law_source(0), law_source(1)];
671        let collected = self.progressive_downloads(stated.clone());
672        if self.law_collected_sources(&collected) != stated {
673            bail!("a collection does not hold its sources in declaration order");
674        }
675        let appended = collected.add_sources([self.source(law_source(2), self.suggested_output(), self.progressive())]);
676        if self.law_collected_sources(&appended) != [stated[0].clone(), stated[1].clone(), law_source(2)] {
677            bail!("adding did not append after what the collection already holds");
678        }
679        Ok(())
680    }
681}
682
683/// Authors the download-record laws.
684#[ext(name = LogLaws)]
685pub impl<This> This
686where
687    This: NavigationStateAlg
688        + QueueCatalogAlg
689        + QueueAppendAlg
690        + FormatCatalogStateAlg
691        + QueueDryRunAlg
692        + RehearsalStateAlg
693        + DownloadLogAlg
694        + DetailSelectionAlg
695        + DraftStateAlg
696        + ManagerStatusAlg
697        + SourceRequestAlg
698        + SubmissionAlg
699        + MediaOptionsAlg
700        + OutputChoiceAlg,
701    This::Entry: QueueEntryAlg,
702    This::Id: Copy + Eq + Debug,
703{
704    /// Checks that the record is keyed, ordered, and reachable from the entry it describes.
705    ///
706    /// The laws checked are:
707    ///
708    /// 1. a note reaches exactly the identity it addresses;
709    /// 2. notes are observed in the order they were stated;
710    /// 3. every entry offers its record among its details controls;
711    /// 4. activating that control denotes the record of that identity.
712    ///
713    /// # Errors
714    ///
715    /// Returns the first violated law.
716    fn log_laws(&mut self) -> Result<()> {
717        let ids = self.author_law_queue(2);
718        let unaddressed: Vec<String> = self.law_notes(ids[1])?;
719        self.note_download(ids[0], "first note".to_owned());
720        self.note_download(ids[0], "second note".to_owned());
721
722        if self.law_notes(ids[1])? != unaddressed {
723            bail!("a note reached an identity it does not address");
724        }
725        let stated = self.law_notes(ids[0])?;
726        let tail: Vec<&String> = stated.iter().rev().take(2).collect();
727        if tail != [&"second note".to_owned(), &"first note".to_owned()] {
728            bail!("notes are not observed in the order they were stated: {stated:?}");
729        }
730
731        let controls = self.queue_entry(ids[0]).map(QueueEntryAlg::detail_controls).unwrap_or_default();
732        if !controls.contains(&DetailControl::Log) {
733            bail!("an entry offers no record among its controls: {controls:?}");
734        }
735        self.set_selected_queue_id(Some(ids[0]));
736        self.set_page(ManagerPage::Details(ids[0]));
737        self.set_selected_detail_control(Some(DetailControl::Log));
738        self.open_log();
739        if self.page() != ManagerPage::Log(ids[0]) {
740            bail!("activating the record control did not denote that record");
741        }
742        Ok(())
743    }
744
745    /// Observes the notes stated about one identity.
746    ///
747    /// # Errors
748    ///
749    /// Returns a violation when the collection holds no such identity.
750    fn law_notes(&self, id: This::Id) -> Result<Vec<String>> {
751        match self.queue_entry(id) {
752            Some(entry) => Ok(entry.download_log().map(str::to_owned).collect()),
753            None => bail!("the collection holds no entry for {id:?}"),
754        }
755    }
756}
757
758/// Supplies the sources a submission scenario cannot author for itself.
759///
760/// Which texts name a folder and which name a media item is what an interpreter recognizes; that
761/// there are two kinds, and what each becomes, is what this specification states.
762pub trait SubmissionLawFixture {
763    /// Names one line this interpreter reads as a transfer, and the two ends it names.
764    fn law_folder_submission(&self) -> (String, String, String);
765
766    /// Names one line naming where a transfer comes from, and where it should come to rest.
767    fn law_lone_submission(&self) -> (String, String);
768
769    /// Names one line this interpreter reads as a media item.
770    fn law_media_source(&self) -> String;
771
772    /// Names one line no source recognizes, which is therefore one path to transfer.
773    fn law_unrecognized_source(&self) -> String;
774}
775
776/// Authors the submission laws.
777#[ext(name = SubmissionLaws)]
778pub impl<This> This
779where
780    This: NavigationStateAlg
781        + QueueCatalogAlg
782        + QueueAppendAlg
783        + DraftStateAlg
784        + TextEditorStateAlg
785        + ManagerStatusAlg
786        + SourceRequestAlg
787        + SubmissionAlg
788        + MediaOptionsAlg
789        + OutputChoiceAlg
790        + SourceRecognitionAlg
791        + SubmissionLawFixture,
792    This::Entry: QueueEntryAlg + RequestOptionsAlg,
793    This::Id: Copy + Eq + Debug,
794{
795    /// Checks that what a source names decides what transferring it means.
796    ///
797    /// The laws checked are:
798    ///
799    /// 1. a line naming two ends states one request from the first end into the second;
800    /// 2. a folder chooses no media role, and a media item does;
801    /// 3. a folder offers the ways it may be transferred, and always transfers one of them;
802    /// 4. a folder states a rehearsal mode, begins with it on, and Space rehearses it;
803    /// 5. a media item states no rehearsal mode, and Space starts it once it names an output;
804    /// 6. a request performed by naming a program states that command, and one fetched does not;
805    /// 7. a transfer names where it would come to rest without anything being extracted first,
806    ///    and names it after what is being transferred rather than after where anybody stands;
807    /// 8. submitting sources focuses the first of them;
808    /// 9. a line no source recognizes is transferred, whatever else it looks like;
809    /// 10. exactly the requests performed by naming a program state the command they run.
810    ///
811    /// # Errors
812    ///
813    /// Returns the first violated law.
814    fn submission_laws(&mut self) -> Result<()> {
815        let (folder, input, output) = self.law_folder_submission();
816        let media = self.law_media_source();
817        self.set_draft(format!("{folder}\n{media}"));
818        self.submit_draft();
819        let ids = self.queue_ids().collect::<Vec<_>>();
820        let [folder_id, media_id] = ids[..] else {
821            bail!("submitting two sources did not append two requests");
822        };
823
824        if self.selected_queue_id() != Some(folder_id) {
825            bail!("submitting sources did not focus the first of them");
826        }
827
828        let submitted = self.submission_law_entry(folder_id)?;
829        if submitted.source() != input {
830            bail!("a transfer does not come from the end its line named");
831        }
832        if submitted.output().map(Path::to_path_buf) != Some(PathBuf::from(&output)) {
833            bail!("a transfer does not come to rest at the end its line named");
834        }
835        if submitted.performer() != Performer::Program {
836            bail!("a transfer is not performed by naming a program");
837        }
838        if submitted.media_streams().is_some() {
839            bail!("a folder chose a media role");
840        }
841        // A folder chooses no representation, and chooses one way of transferring instead.
842        if submitted.selectable_choices().next().is_none() {
843            bail!("a folder offers no way of transferring itself");
844        }
845        if submitted.chosen_choice().is_none() {
846            bail!("a folder transfers no stated way");
847        }
848        if submitted.selectable_choices().any(|choice| submitted.choice_summary(choice).is_none()) {
849            bail!("a way of transferring does not say what it does");
850        }
851        if submitted.dry_run() != Some(true) {
852            bail!("a folder does not begin by stating what it would do");
853        }
854        if submitted.stated_command().is_none() {
855            bail!("a folder does not state the command that would perform it");
856        }
857        if submitted.space_action() != Some(SpaceAction::Rehearse) {
858            bail!("Space does not rehearse a folder nobody has rehearsed");
859        }
860
861        self.check_unrecognized_laws()?;
862        for id in [folder_id, media_id] {
863            let submitted = self.submission_law_entry(id)?;
864            // What performs a request decides whether there is a command to read: a program is
865            // named and read back, and a retrieval is not named at all.
866            let names_a_program = submitted.performer() == Performer::Program;
867            if submitted.stated_command().is_some() != names_a_program {
868                bail!("what performs a request and whether it states a command disagree");
869            }
870        }
871
872        let submitted = self.submission_law_entry(media_id)?;
873        if submitted.performer() != Performer::Retrieval {
874            bail!("a line a source claimed is not retrieved");
875        }
876        if submitted.media_streams().is_none() {
877            bail!("a media item chose no media role");
878        }
879        if submitted.dry_run().is_some() {
880            bail!("a media item states a rehearsal mode it cannot have");
881        }
882        if submitted.chosen_choice().is_some() {
883            bail!("a media item fixed a representation nobody chose");
884        }
885        if submitted.stated_command().is_some() {
886            bail!("a media item states a command, and it is fetched rather than run");
887        }
888        Ok(())
889    }
890
891    /// Checks that a line nobody claimed is a path rather than something to fetch.
892    ///
893    /// # Errors
894    ///
895    /// Returns the first violated law.
896    fn check_unrecognized_laws(&mut self) -> Result<()> {
897        let line = self.law_unrecognized_source();
898        if self.recognizes_source(&line) {
899            bail!("the line authored as unrecognized is recognized by a source");
900        }
901        self.set_draft(line.clone());
902        self.submit_draft();
903        let Some(id) = self.selected_queue_id() else {
904            bail!("submitting {line} appended nothing");
905        };
906        let submitted = self.submission_law_entry(id)?;
907        if submitted.source() != line {
908            bail!("a transfer does not come from the path its line named");
909        }
910        if submitted.performer() != Performer::Program {
911            bail!("a line no source recognizes is not performed by naming a program");
912        }
913        if submitted.stated_command().is_none() {
914            bail!("a line no source recognizes states no command to perform it");
915        }
916        Ok(())
917    }
918
919    /// Observes the entry one identity denotes, or that the collection no longer holds it.
920    fn submission_law_entry(&self, id: This::Id) -> Result<&This::Entry> {
921        let Some(entry) = self.queue_entry(id) else {
922            bail!("the collection no longer holds {id:?}");
923        };
924        Ok(entry)
925    }
926}
927/// Supplies the changes a rehearsal scenario cannot author for itself.
928pub trait RehearsalLawFixture: ManagerSorts {
929    /// Defines one change a rehearsal would make to a path.
930    fn law_change(&self, path: &str, kind: ChangeKind, size: Option<u64>) -> Self::Change;
931
932    /// Turns the rehearsal mode on for one request, the way a folder source would.
933    fn law_offer_rehearsal(&mut self, id: Self::Id);
934}
935
936/// Authors the rehearsal laws.
937#[ext(name = RehearsalLaws)]
938pub impl<This> This
939where
940    This: NavigationStateAlg
941        + QueueCatalogAlg
942        + QueueAppendAlg
943        + QueueDryRunAlg
944        + QueueDuplicateAlg
945        + QueueOutputAlg
946        + QueueSourceAlg
947        + RehearsalStateAlg
948        + DetailSelectionAlg
949        + DraftStateAlg
950        + ManagerStatusAlg
951        + SourceRequestAlg
952        + SubmissionAlg
953        + MediaOptionsAlg
954        + OutputChoiceAlg
955        + RehearsalLawFixture,
956    This::Entry: QueueEntryAlg + RehearsalViewAlg<Change: PlannedChangeAlg>,
957    This::Id: Copy + Eq + Debug,
958{
959    /// Checks that stating what a transfer would do is not doing it.
960    ///
961    /// The laws checked are:
962    ///
963    /// 1. a request states a rehearsal mode exactly when it offers the control turning it;
964    /// 2. the mode decides what Space means, and nothing else about the request;
965    /// 3. a rehearsal leaves the request exactly as editable as it found it;
966    /// 4. a reported rehearsal states every change it was given, and offers its report;
967    /// 5. a rehearsed request still permits its input to change, and states that it does;
968    /// 6. changing what would be run forgets the report describing what would have been;
969    /// 7. the mode changes what the request states would be run;
970    /// 8. a failed rehearsal is not a failed request: Space still means something afterward;
971    /// 9. a duplicate keeps both ends and is not armed, however armed the first one was.
972    ///
973    /// # Errors
974    ///
975    /// Returns the first violated law.
976    fn rehearsal_laws(&mut self) -> Result<()> {
977        let ids = self.author_law_queue(1);
978        let id = ids[0];
979        if self.rehearsal_law_entry(id)?.dry_run().is_some() {
980            bail!("a request states a rehearsal mode it was never given");
981        }
982        if self.rehearsal_law_controls(id)?.contains(&DetailControl::DryRun) {
983            bail!("a request with no rehearsal mode offers the control turning it");
984        }
985        self.law_offer_rehearsal(id);
986        // A folder transfer names the folder it would write into, the same as any other request.
987        self.set_queue_output(id, PathBuf::from("law-destination"));
988        if self.rehearsal_law_entry(id)?.dry_run() != Some(true) {
989            bail!("a folder request does not begin by stating what it would do");
990        }
991        if !self.rehearsal_law_controls(id)?.contains(&DetailControl::DryRun) {
992            bail!("a request with a rehearsal mode does not offer the control turning it");
993        }
994        if self.rehearsal_law_controls(id)?.contains(&DetailControl::Report) {
995            bail!("an unrehearsed request offers a report of what it would do");
996        }
997        if self.rehearsal_law_entry(id)?.space_action() != Some(SpaceAction::Rehearse) {
998            bail!("Space does not rehearse while the rehearsal mode is on");
999        }
1000
1001        let editable = self.rehearsal_law_entry(id)?.is_editable();
1002        self.apply_rehearsal_event(id, RehearsalObservationOp::Started {});
1003        if self.rehearsal_law_entry(id)?.phase() != TransferPhase::Rehearsing {
1004            bail!("a started rehearsal is not observable as one");
1005        }
1006        let changes = vec![
1007            self.law_change("kept.txt", ChangeKind::Unchanged, Some(2)),
1008            self.law_change("new.txt", ChangeKind::Create, Some(4)),
1009        ];
1010        self.apply_rehearsal_event(id, RehearsalObservationOp::Reported { changes });
1011        if self.rehearsal_law_entry(id)?.is_editable() != editable {
1012            bail!("a rehearsal changed what the request permits");
1013        }
1014        if self.rehearsal_law_entry(id)?.phase() != TransferPhase::Ready {
1015            bail!("a finished rehearsal left the request in the phase that ran it");
1016        }
1017        let reported = self
1018            .rehearsal_law_entry(id)?
1019            .planned_changes()
1020            .map(|change| (change.change_path().to_owned(), change.change_kind()))
1021            .collect::<Vec<_>>();
1022        if reported != vec![("kept.txt".to_owned(), ChangeKind::Unchanged), ("new.txt".to_owned(), ChangeKind::Create)]
1023        {
1024            bail!("a report does not state the changes the rehearsal stated: {reported:?}");
1025        }
1026        if !self.rehearsal_law_controls(id)?.contains(&DetailControl::Report) {
1027            bail!("a rehearsed request does not offer its report");
1028        }
1029
1030        self.check_open_input_laws(id)?;
1031        let rehearsed = self.rehearsal_law_entry(id)?.stated_command();
1032        self.set_queue_dry_run(id, false);
1033        let armed = self.rehearsal_law_entry(id)?.stated_command();
1034        if rehearsed.is_some() && rehearsed == armed {
1035            bail!("turning the rehearsal mode off did not change what would be run");
1036        }
1037        if self.rehearsal_law_entry(id)?.space_action() != Some(SpaceAction::Start) {
1038            bail!("Space does not transfer once the rehearsal mode is off");
1039        }
1040        if !self.rehearsal_law_controls(id)?.contains(&DetailControl::DryRun) {
1041            bail!("turning the rehearsal mode off took away the control turning it back on");
1042        }
1043        self.apply_rehearsal_event(id, RehearsalObservationOp::Failed { message: "the far end refused".to_owned() });
1044        if self.rehearsal_law_entry(id)?.space_action().is_none() {
1045            bail!("a failed rehearsal left the request with nothing Space can do");
1046        }
1047        self.check_duplicate_laws(id)
1048    }
1049
1050    /// Checks that a request duplicated from an armed one is not itself armed.
1051    ///
1052    /// Duplicating states a fresh request, and a fresh request states what it would do before it
1053    /// does it, however it came to exist. A transfer is the pair of ends it names, so both are
1054    /// kept: a duplicate nobody can run without naming them again is not a duplicate.
1055    ///
1056    /// # Errors
1057    ///
1058    /// Returns the first violated law.
1059    fn check_duplicate_laws(&mut self, id: This::Id) -> Result<()> {
1060        let armed = self.rehearsal_law_entry(id)?;
1061        let (source, output) = (armed.source().to_owned(), armed.output().map(Path::to_path_buf));
1062        let Some(duplicate) = self.duplicate_queue_entry(id) else {
1063            bail!("a request that may be duplicated was not");
1064        };
1065        let fresh = self.rehearsal_law_entry(duplicate)?;
1066        if fresh.dry_run() != Some(true) {
1067            bail!("a duplicate of an armed request is armed itself");
1068        }
1069        if fresh.source() != source {
1070            bail!("a duplicated transfer does not come from where the first one did");
1071        }
1072        if fresh.output().map(Path::to_path_buf) != output {
1073            bail!("a duplicated transfer does not come to rest where the first one did");
1074        }
1075        Ok(())
1076    }
1077
1078    /// Checks that a rehearsed request still permits its input to change.
1079    ///
1080    /// A rehearsal is not a start, so everything the request states is still open afterward.
1081    ///
1082    /// # Errors
1083    ///
1084    /// Returns the first violated law.
1085    fn check_open_input_laws(&mut self, id: This::Id) -> Result<()> {
1086        let replacement = "law://replaced-input";
1087        self.set_queue_source(id, replacement.to_owned());
1088        if self.rehearsal_law_entry(id)?.source() != replacement {
1089            bail!("a rehearsed request refused to change its input");
1090        }
1091        if !self.rehearsal_law_controls(id)?.contains(&DetailControl::Input) {
1092            bail!("a request that permits its input to change does not offer to change it");
1093        }
1094        Ok(())
1095    }
1096
1097    /// Observes the entry one identity denotes, or that the collection no longer holds it.
1098    fn rehearsal_law_entry(&self, id: This::Id) -> Result<&This::Entry> {
1099        let Some(entry) = self.queue_entry(id) else {
1100            bail!("the collection no longer holds {id:?}");
1101        };
1102        Ok(entry)
1103    }
1104
1105    /// Observes the controls one identity offers.
1106    fn rehearsal_law_controls(&self, id: This::Id) -> Result<Vec<DetailControl>> {
1107        Ok(self.rehearsal_law_entry(id)?.detail_controls())
1108    }
1109}
1110
1111/// Authors the attendance laws.
1112#[ext(name = AttentionLaws)]
1113pub impl<This> This
1114where
1115    This: NavigationStateAlg
1116        + QueueCatalogAlg
1117        + QueueAppendAlg
1118        + QueueRemoveAlg
1119        + QueuePauseAlg
1120        + QueueOutputAlg
1121        + DraftStateAlg
1122        + ManagerStatusAlg
1123        + SafeExitAlg
1124        + SourceRequestAlg
1125        + SubmissionAlg
1126        + MediaOptionsAlg
1127        + OutputChoiceAlg
1128        + TransferStateAlg
1129        + RehearsalStateAlg,
1130    This::Entry: QueueEntryAlg,
1131    This::Change: PlannedChangeAlg,
1132    This::Id: Copy + Eq + Debug,
1133{
1134    /// Checks what the collection states to whoever is doing the work it asks for.
1135    ///
1136    /// The laws checked are:
1137    ///
1138    /// 1. exactly the requests waiting for an interpreter want work done;
1139    /// 2. a request whose work has begun is no longer waiting for it;
1140    /// 3. a request being worked on wants its run, a paused one wants it held, and a removed one
1141    ///    wants none;
1142    /// 4. once the reader is leaving, no request wants its run;
1143    /// 5. a destination is taken exactly while another request is writing to it.
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns the first violated law.
1148    fn attention_laws(&mut self) -> Result<()> {
1149        let ids = self.author_law_queue(2);
1150        // Both name one destination, which is the situation the collection has to state.
1151        let destination = PathBuf::from("law-destination");
1152        for id in &ids {
1153            self.set_queue_output(*id, destination.clone());
1154        }
1155        if !self.wanting_work().is_empty() {
1156            bail!("a request nobody asked for wants work done");
1157        }
1158        self.set_waiting(ids[0]);
1159        if self.wanting_work() != vec![ids[0]] {
1160            bail!("the requests wanting work are not the requests waiting for it");
1161        }
1162        if self.destination_is_taken(ids[1]) {
1163            bail!("a destination nobody is writing to is taken");
1164        }
1165
1166        self.begun(ids[0], true);
1167        if !self.wanting_work().is_empty() {
1168            bail!("a request whose work began is still waiting for it to begin");
1169        }
1170        if self.wanted(ids[0]) != Wanted::Running {
1171            bail!("a request being worked on does not want its run");
1172        }
1173        if !self.destination_is_taken(ids[1]) {
1174            bail!("a destination another request is writing to is free");
1175        }
1176        if self.destination_is_taken(ids[0]) {
1177            bail!("a request takes its own destination from itself");
1178        }
1179
1180        self.toggle_queue_pause(ids[0]);
1181        if self.wanted(ids[0]) != Wanted::Held {
1182            bail!("a paused request does not want its run held still");
1183        }
1184        if !self.destination_is_taken(ids[1]) {
1185            bail!("a destination a held request still holds open is free");
1186        }
1187        self.toggle_queue_pause(ids[0]);
1188        if self.wanted(ids[0]) != Wanted::Running {
1189            bail!("a resumed request does not want its run to carry on");
1190        }
1191
1192        self.remove_queue_entry(ids[0]);
1193        if self.wanted(ids[0]) != Wanted::Unwanted {
1194            bail!("a removed request still wants its run");
1195        }
1196        if self.destination_is_taken(ids[1]) {
1197            bail!("a removed request still holds its destination");
1198        }
1199
1200        self.set_waiting(ids[1]);
1201        self.request_safe_exit();
1202        if self.wanted(ids[1]) != Wanted::Unwanted {
1203            bail!("a request still wants its run once the reader is leaving");
1204        }
1205        Ok(())
1206    }
1207}
1208
1209/// Authors the request-options laws.
1210#[ext(name = OptionsLaws)]
1211pub impl<This, Format> This
1212where
1213    This: ManagerSorts<Format = Format>
1214        + MediaSorts<Format = Format>
1215        + MetadataAlg
1216        + FormatAlg
1217        + FormatPredicateAlg
1218        + FormatPredicateMatchAlg
1219        + NavigationStateAlg
1220        + QueueCatalogAlg
1221        + QueueAppendAlg
1222        + QueueFormatEditAlg
1223        + FormatCatalogStateAlg
1224        + QueueDryRunAlg
1225        + RehearsalStateAlg
1226        + SourceMetadataAlg
1227        + TransferStateAlg
1228        + DraftStateAlg
1229        + ManagerStatusAlg
1230        + SourceRequestAlg
1231        + SubmissionAlg
1232        + MediaOptionsAlg
1233        + OutputChoiceAlg,
1234    This::Entry: QueueEntryAlg + RequestOptionsAlg,
1235    This::Id: Copy + Eq + Debug,
1236{
1237    /// Checks that one selector states the preferred roles and every discovered identity.
1238    ///
1239    /// The laws checked are:
1240    ///
1241    /// 1. the predicate one role denotes accepts exactly the formats carrying those streams;
1242    /// 2. a discovery observation reaches exactly the identity it addresses;
1243    /// 3. every discovered format is selectable, in discovery order;
1244    /// 4. selection walks the offered roles first and the discovered identities after;
1245    /// 5. preferring a role releases any identity, and choosing an identity fixes exactly it;
1246    /// 6. a request that is no longer editable keeps the choice it was started with.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Returns the first violated law.
1251    fn options_laws(&mut self) -> Result<()> {
1252        let described = [("progressive", true, true), ("audio", true, false), ("video", false, true)]
1253            .map(|(id, audio, video)| self.law_format(id, audio, video));
1254        for (role, expected) in
1255            [(MediaStreams::AudioVideo, "progressive"), (MediaStreams::Audio, "audio"), (MediaStreams::Video, "video")]
1256        {
1257            let predicate = self.stream_role_format(role);
1258            let accepted: Vec<&str> = ["progressive", "audio", "video"]
1259                .into_iter()
1260                .zip(&described)
1261                .filter(|(_, format)| self.format_matches(&predicate, format))
1262                .map(|(id, _)| id)
1263                .collect();
1264            if accepted != [expected] {
1265                bail!("the predicate {role:?} denotes accepts {accepted:?} rather than {expected}");
1266            }
1267        }
1268
1269        let ids = self.author_law_queue(2);
1270        self.apply_format_catalog_event(
1271            ids[0],
1272            FormatDiscoveryOp::Available { formats: described.into_iter().collect() },
1273        );
1274        if self.law_selectable(ids[1])?.next().is_some() {
1275            bail!("a discovery observation reached an identity it does not address");
1276        }
1277        let discovered = ["progressive", "audio", "video"];
1278        let selectable: Vec<String> = self.law_selectable(ids[0])?.map(str::to_owned).collect();
1279        if selectable != discovered {
1280            bail!("the selectable formats are not the discovered ones: {selectable:?}");
1281        }
1282
1283        // Selection walks one list: every offered role, then every discovered identity.
1284        let mut walked = Vec::new();
1285        for _ in 0..MediaStreams::OFFERED.len() + discovered.len() {
1286            let entry = self.law_entry(ids[0])?;
1287            walked.push(entry.chosen_choice().map(str::to_owned));
1288            self.cycle_queue_format(ids[0], true);
1289        }
1290        let roles = walked.iter().take(MediaStreams::OFFERED.len()).filter(|chosen| chosen.is_none()).count();
1291        if roles != MediaStreams::OFFERED.len() {
1292            bail!("selection does not begin with the offered roles: {walked:?}");
1293        }
1294        let identities: Vec<String> = walked.into_iter().flatten().collect();
1295        if identities != discovered {
1296            bail!("selection does not continue through the discovered identities: {identities:?}");
1297        }
1298
1299        // Stepping back onto a role releases whatever identity was fixed.
1300        self.cycle_queue_format(ids[0], true);
1301        if self.law_entry(ids[0])?.chosen_choice().is_some() {
1302            bail!("preferring a role kept the identity it was meant to release");
1303        }
1304
1305        self.apply_source_metadata(ids[0], "law".to_owned(), Some("Law".to_owned()));
1306        self.set_waiting(ids[0]);
1307        let fixed = self.law_entry(ids[0])?.media_streams();
1308        self.cycle_queue_format(ids[0], true);
1309        if self.law_entry(ids[0])?.media_streams() != fixed {
1310            bail!("a started request changed the choice it was started with");
1311        }
1312        Ok(())
1313    }
1314
1315    /// Defines one described format carrying the stated streams.
1316    fn law_format(&self, id: &str, audio: bool, video: bool) -> Format {
1317        self.format(
1318            id,
1319            self.metadata([
1320                (FORMAT_HAS_AUDIO.to_owned(), self.boolean_metadata(audio)),
1321                (FORMAT_HAS_VIDEO.to_owned(), self.boolean_metadata(video)),
1322                (FORMAT_EXTENSION.to_owned(), self.string_metadata("mp4")),
1323            ]),
1324        )
1325    }
1326
1327    /// Observes one entry by stable identity.
1328    ///
1329    /// # Errors
1330    ///
1331    /// Returns a violation when the collection holds no such identity.
1332    fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
1333        match self.queue_entry(id) {
1334            Some(entry) => Ok(entry),
1335            None => bail!("the collection holds no entry for {id:?}"),
1336        }
1337    }
1338
1339    /// Observes the identities one entry currently offers.
1340    ///
1341    /// # Errors
1342    ///
1343    /// Returns a violation when the collection holds no such identity.
1344    fn law_selectable(&self, id: This::Id) -> Result<impl Iterator<Item = &str>> {
1345        Ok(self.law_entry(id)?.selectable_choices())
1346    }
1347}
1348
1349/// Authors the intent-stream laws.
1350#[ext(name = IntentLaws)]
1351pub impl<This> This
1352where
1353    This: NavigationStateAlg
1354        + QueueCatalogAlg
1355        + QueueAppendAlg
1356        + QueueRemoveAlg
1357        + QueuePauseAlg
1358        + QueueDuplicateAlg
1359        + QueueFormatEditAlg
1360        + FormatCatalogStateAlg
1361        + QueueDryRunAlg
1362        + RehearsalStateAlg
1363        + TransferStateAlg
1364        + DetailSelectionAlg
1365        + DraftStateAlg
1366        + OutputDraftAlg
1367        + InputDraftAlg
1368        + QueueOutputAlg
1369        + QueueSourceAlg
1370        + SourceMetadataAlg
1371        + TextEditorStateAlg
1372        + ManagerStatusAlg
1373        + SafeExitAlg
1374        + SourceRequestAlg
1375        + SubmissionAlg
1376        + MediaOptionsAlg
1377        + OutputChoiceAlg,
1378    This::Entry: QueueEntryAlg + RequestOptionsAlg,
1379    This::Id: Copy + Eq + Debug,
1380{
1381    /// Checks that each intention denotes exactly the meaning derived for it.
1382    ///
1383    /// The laws checked are:
1384    ///
1385    /// 1. the selection intentions denote derived collection navigation;
1386    /// 2. expanding and returning is the identity on page and selection;
1387    /// 3. the editing intentions denote the derived cursor operations;
1388    /// 4. an intention addressed to one identity leaves the others unobserved.
1389    ///
1390    /// # Errors
1391    ///
1392    /// Returns the first violated law.
1393    fn intent_laws(&mut self) -> Result<()> {
1394        let ids = self.author_law_queue(3);
1395        self.apply_manager_event(ManagerIntentOp::SelectNext {});
1396        let intended = self.selected_queue_id();
1397        self.select_relative(false);
1398        self.select_relative(true);
1399        if self.selected_queue_id() != intended {
1400            bail!("the select-next intention does not denote derived collection navigation");
1401        }
1402
1403        let selected = self.selected_queue_id();
1404        self.apply_manager_event(ManagerIntentOp::OpenSelected {});
1405        if self.page() != ManagerPage::Details(ids[1]) {
1406            bail!("opening details did not expand the selected identity");
1407        }
1408        self.apply_manager_event(ManagerIntentOp::Back {});
1409        if self.page() != ManagerPage::Collection || self.selected_queue_id() != selected {
1410            bail!("expanding and returning is not the identity on page and selection");
1411        }
1412
1413        self.apply_manager_event(ManagerIntentOp::OpenAddSources {});
1414        self.apply_manager_event(ManagerIntentOp::InsertText { text: "héllo".to_owned() });
1415        self.apply_manager_event(ManagerIntentOp::MoveCursorLeft {});
1416        self.apply_manager_event(ManagerIntentOp::DeleteBeforeCursor {});
1417        if self.draft() != "hélo" {
1418            bail!("the editing intentions do not denote the derived cursor operations: {}", self.draft());
1419        }
1420
1421        self.apply_manager_event(ManagerIntentOp::Back {});
1422        let unaddressed = self.law_entry_shape(ids[2]);
1423        self.apply_manager_event(ManagerIntentOp::Transfer {
1424            id: ids[0],
1425            event: TransferObservationOp::Progress {
1426                destination: PathBuf::from("law.bin"),
1427                downloaded: 5,
1428                total: Some(10),
1429            },
1430        });
1431        if self.law_entry_shape(ids[2]) != unaddressed {
1432            bail!("an intention addressed to one identity reached another");
1433        }
1434        Ok(())
1435    }
1436
1437    /// Observes one entry through exactly what the specification exposes about it.
1438    fn law_entry_shape(&self, id: This::Id) -> Option<(Vec<DetailControl>, Option<SpaceAction>)> {
1439        self.queue_entry(id).map(|entry| (entry.detail_controls(), entry.space_action()))
1440    }
1441}