1use std::fmt;
32use std::time::Duration;
33
34use serde::{Deserialize, Serialize};
35
36use crate::types::{CancelReason, Outcome, RegionId, TaskId};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum Subsystem {
45 Supervision,
47 Registry,
49 Link,
51 Monitor,
53}
54
55impl fmt::Display for Subsystem {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::Supervision => write!(f, "supervision"),
59 Self::Registry => write!(f, "registry"),
60 Self::Link => write!(f, "link"),
61 Self::Monitor => write!(f, "monitor"),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
70pub enum Verdict {
71 Restart,
74 Stop,
76 Escalate,
78
79 Accept,
82 Reject,
84 Release,
86 Abort,
88
89 Propagate,
92 Suppress,
94
95 Deliver,
98 Drop,
100}
101
102impl fmt::Display for Verdict {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::Restart => write!(f, "RESTART"),
106 Self::Stop => write!(f, "STOP"),
107 Self::Escalate => write!(f, "ESCALATE"),
108 Self::Accept => write!(f, "ACCEPT"),
109 Self::Reject => write!(f, "REJECT"),
110 Self::Release => write!(f, "RELEASE"),
111 Self::Abort => write!(f, "ABORT"),
112 Self::Propagate => write!(f, "PROPAGATE"),
113 Self::Suppress => write!(f, "SUPPRESS"),
114 Self::Deliver => write!(f, "DELIVER"),
115 Self::Drop => write!(f, "DROP"),
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub enum EvidenceDetail {
130 Supervision(SupervisionDetail),
132 Registry(RegistryDetail),
134 Link(LinkDetail),
136 Monitor(MonitorDetail),
138}
139
140impl Eq for EvidenceDetail {}
141
142impl fmt::Display for EvidenceDetail {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::Supervision(d) => write!(f, "{d}"),
146 Self::Registry(d) => write!(f, "{d}"),
147 Self::Link(d) => write!(f, "{d}"),
148 Self::Monitor(d) => write!(f, "{d}"),
149 }
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub enum SupervisionDetail {
161 MonotoneSeverity {
163 outcome_kind: String,
165 },
166 ExplicitStop,
168 ExplicitEscalate,
170 EscalateWithoutParent,
172 RestartAllowed {
174 attempt: u32,
176 delay: Option<Duration>,
178 },
179 WindowExhausted {
181 max_restarts: u32,
183 window: Duration,
185 },
186 BudgetRefused {
188 constraint: String,
190 },
191}
192
193impl Eq for SupervisionDetail {}
194
195impl fmt::Display for SupervisionDetail {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::MonotoneSeverity { outcome_kind } => {
199 write!(f, "monotone severity: {outcome_kind} is not restartable")
200 }
201 Self::ExplicitStop => write!(f, "strategy is Stop"),
202 Self::ExplicitEscalate => write!(f, "strategy is Escalate"),
203 Self::EscalateWithoutParent => {
204 write!(f, "strategy is Escalate but no parent region exists")
205 }
206 Self::RestartAllowed { attempt, delay } => match delay {
207 Some(d) => write!(f, "restart allowed (attempt {attempt}, delay {d:?})"),
208 None => write!(f, "restart allowed (attempt {attempt})"),
209 },
210 Self::WindowExhausted {
211 max_restarts,
212 window,
213 } => write!(f, "window exhausted: {max_restarts} restarts in {window:?}"),
214 Self::BudgetRefused { constraint } => {
215 write!(f, "budget refused: {constraint}")
216 }
217 }
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub enum RegistryDetail {
226 NameAvailable,
228 NameCollision {
230 existing_holder: TaskId,
232 },
233 RegionClosed {
235 region: RegionId,
237 },
238 LeaseCommitted,
240 LeaseCancelled {
242 reason: CancelReason,
244 },
245 LeaseCleanedUp {
247 region: RegionId,
249 },
250 TaskCleanedUp {
252 task: TaskId,
254 },
255}
256
257impl fmt::Display for RegistryDetail {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 match self {
260 Self::NameAvailable => write!(f, "name available"),
261 Self::NameCollision { existing_holder } => {
262 write!(f, "name collision: held by {existing_holder:?}")
263 }
264 Self::RegionClosed { region } => {
265 write!(f, "region closed: {region:?}")
266 }
267 Self::LeaseCommitted => write!(f, "lease committed (normal release)"),
268 Self::LeaseCancelled { reason } => {
269 write!(f, "lease cancelled: {reason}")
270 }
271 Self::LeaseCleanedUp { region } => {
272 write!(f, "lease cleaned up (region {region:?} closing)")
273 }
274 Self::TaskCleanedUp { task } => {
275 write!(f, "lease cleaned up (task {task:?} terminating)")
276 }
277 }
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub enum LinkDetail {
286 ExitPropagated {
288 source: TaskId,
290 reason: Outcome<(), ()>,
292 },
293 TrapExit {
295 source: TaskId,
297 },
298 Unlinked,
300 RegionCleanup {
302 region: RegionId,
304 },
305}
306
307impl Eq for LinkDetail {}
308
309impl fmt::Display for LinkDetail {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 match self {
312 Self::ExitPropagated { source, reason } => {
313 write!(f, "exit propagated from {source:?} ({reason:?})")
314 }
315 Self::TrapExit { source } => {
316 write!(f, "exit trapped from {source:?}")
317 }
318 Self::Unlinked => write!(f, "unlinked before failure"),
319 Self::RegionCleanup { region } => {
320 write!(f, "link cleaned up (region {region:?} closing)")
321 }
322 }
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330pub enum MonitorDetail {
331 DownDelivered {
333 monitored: TaskId,
335 reason: Outcome<(), ()>,
337 },
338 WatcherRegionClosed {
340 region: RegionId,
342 },
343 Demonitored,
345 RegionCleanup {
347 region: RegionId,
349 count: usize,
351 },
352}
353
354impl Eq for MonitorDetail {}
355
356impl fmt::Display for MonitorDetail {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 match self {
359 Self::DownDelivered { monitored, reason } => {
360 write!(f, "down delivered for {monitored:?} ({reason:?})")
361 }
362 Self::WatcherRegionClosed { region } => {
363 write!(f, "watcher region {region:?} closed")
364 }
365 Self::Demonitored => write!(f, "demonitored before termination"),
366 Self::RegionCleanup { region, count } => {
367 write!(f, "region {region:?} cleanup released {count} monitor(s)")
368 }
369 }
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383pub struct EvidenceRecord {
384 pub timestamp: u64,
386 pub task_id: TaskId,
388 pub region_id: RegionId,
390 pub subsystem: Subsystem,
392 pub verdict: Verdict,
394 pub detail: EvidenceDetail,
396}
397
398impl EvidenceRecord {
399 #[must_use]
405 pub fn render(&self) -> String {
406 format!(
407 "[{}] {} {}: {}",
408 self.timestamp, self.subsystem, self.verdict, self.detail
409 )
410 }
411
412 #[must_use]
420 pub fn to_card(&self) -> EvidenceCard {
421 let (rule, substitution, intuition) =
422 evidence_card_triple(self.subsystem, self.verdict, &self.detail);
423
424 EvidenceCard {
425 timestamp: self.timestamp,
426 task_id: self.task_id,
427 region_id: self.region_id,
428 subsystem: self.subsystem,
429 verdict: self.verdict,
430 rule,
431 substitution,
432 intuition,
433 }
434 }
435
436 #[must_use]
438 pub fn render_card(&self) -> String {
439 self.to_card().render()
440 }
441}
442
443impl fmt::Display for EvidenceRecord {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 write!(
446 f,
447 "[{}] {} {}: {}",
448 self.timestamp, self.subsystem, self.verdict, self.detail
449 )
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
466pub struct EvidenceCard {
467 pub timestamp: u64,
469 pub task_id: TaskId,
471 pub region_id: RegionId,
473 pub subsystem: Subsystem,
475 pub verdict: Verdict,
477 pub rule: String,
479 pub substitution: String,
481 pub intuition: String,
483}
484
485impl EvidenceCard {
486 #[must_use]
497 pub fn render(&self) -> String {
498 format!(
499 "[{}] {} {} task={:?} region={:?}\nrule: {}\nsubstitution: {}\nintuition: {}\n",
500 self.timestamp,
501 self.subsystem,
502 self.verdict,
503 self.task_id,
504 self.region_id,
505 self.rule,
506 self.substitution,
507 self.intuition
508 )
509 }
510}
511
512impl fmt::Display for EvidenceCard {
513 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 f.write_str(&self.render())
515 }
516}
517
518fn supervision_card_triple(detail: &SupervisionDetail) -> (String, String, String) {
519 match detail {
520 SupervisionDetail::MonotoneSeverity { outcome_kind } => (
521 "If outcome severity is not restartable, the supervisor must STOP.".to_string(),
522 format!("outcome_kind={outcome_kind} => STOP"),
523 format!(
524 "Outcome {outcome_kind} is terminal for supervision; stopping preserves monotone severity."
525 ),
526 ),
527 SupervisionDetail::ExplicitStop => (
528 "If supervision strategy is Stop, the supervisor must STOP.".to_string(),
529 "strategy=Stop => STOP".to_string(),
530 "Strategy is Stop; no restart is attempted.".to_string(),
531 ),
532 SupervisionDetail::ExplicitEscalate => (
533 "If supervision strategy is Escalate, the supervisor must ESCALATE.".to_string(),
534 "strategy=Escalate => ESCALATE".to_string(),
535 "Strategy is Escalate; failure is propagated to the parent region.".to_string(),
536 ),
537 SupervisionDetail::EscalateWithoutParent => (
538 "If supervision strategy is Escalate but no parent region exists, the supervisor must STOP."
539 .to_string(),
540 "strategy=Escalate,parent=None => STOP".to_string(),
541 "Root escalation has no parent target; stopping preserves a total supervision decision."
542 .to_string(),
543 ),
544 SupervisionDetail::RestartAllowed { attempt, delay } => (
545 "If restart window and budget allow, the supervisor may RESTART.".to_string(),
546 delay.as_ref().map_or_else(
547 || format!("attempt={attempt} => RESTART"),
548 |d| format!("attempt={attempt}, delay={d:?} => RESTART"),
549 ),
550 "Restart attempt is permitted; any configured delay is applied deterministically."
551 .to_string(),
552 ),
553 SupervisionDetail::WindowExhausted {
554 max_restarts,
555 window,
556 } => (
557 "If restarts in the intensity window exceed the limit, the supervisor must STOP."
558 .to_string(),
559 format!("max_restarts={max_restarts}, window={window:?} => STOP"),
560 "Restart intensity exceeded; stopping prevents an unbounded crash loop.".to_string(),
561 ),
562 SupervisionDetail::BudgetRefused { constraint } => (
563 "If restart would violate budget constraints, the supervisor must STOP.".to_string(),
564 format!("{constraint} => STOP"),
565 "Budget would be exceeded; stopping is deterministic and cancel-correct.".to_string(),
566 ),
567 }
568}
569
570fn registry_card_triple(detail: &RegistryDetail) -> (String, String, String) {
571 match detail {
572 RegistryDetail::NameAvailable => (
573 "If the name is unheld, registration is ACCEPTED.".to_string(),
574 "name is available => ACCEPT".to_string(),
575 "No collision; a name lease is created and must be resolved linearly.".to_string(),
576 ),
577 RegistryDetail::NameCollision { existing_holder } => (
578 "If the name is already held, registration is REJECTED.".to_string(),
579 format!("existing_holder={existing_holder:?} => REJECT"),
580 "Collision prevents ambiguous ownership; reject to preserve determinism.".to_string(),
581 ),
582 RegistryDetail::RegionClosed { region } => (
583 "If the owning region is closed, registration is REJECTED.".to_string(),
584 format!("region={region:?} is closed => REJECT"),
585 "Closed regions cannot accept new obligations; reject avoids orphaned leases."
586 .to_string(),
587 ),
588 RegistryDetail::LeaseCommitted => (
589 "If the lease obligation is committed, the name is RELEASED.".to_string(),
590 "lease committed => RELEASE".to_string(),
591 "Normal lifecycle release; name becomes available again.".to_string(),
592 ),
593 RegistryDetail::LeaseCancelled { reason } => (
594 "If cancellation occurs, the lease is ABORTED.".to_string(),
595 format!("cancel_reason={reason} => ABORT"),
596 "Cancellation triggers cleanup; abort avoids stale names.".to_string(),
597 ),
598 RegistryDetail::LeaseCleanedUp { region } => (
599 "If region cleanup runs, the lease is ABORTED.".to_string(),
600 format!("cleanup_region={region:?} => ABORT"),
601 "Region close implies quiescence; leases are aborted during cleanup.".to_string(),
602 ),
603 RegistryDetail::TaskCleanedUp { task } => (
604 "If task cleanup runs, the lease is ABORTED.".to_string(),
605 format!("cleanup_task={task:?} => ABORT"),
606 "Task termination must not leave names held; abort releases the lease.".to_string(),
607 ),
608 }
609}
610
611fn link_card_triple(detail: &LinkDetail) -> (String, String, String) {
612 match detail {
613 LinkDetail::ExitPropagated { source, reason } => (
614 "If a linked task exits and exits are not trapped, the signal is PROPAGATED."
615 .to_string(),
616 format!("source={source:?}, reason={reason:?} => PROPAGATE"),
617 "Linked failures propagate to preserve OTP-style failure semantics.".to_string(),
618 ),
619 LinkDetail::TrapExit { source } => (
620 "If the target traps exits, the signal is SUPPRESSED.".to_string(),
621 format!("source={source:?}, trap_exit=true => SUPPRESS"),
622 "Target traps exits, so failure is converted into a message instead of killing."
623 .to_string(),
624 ),
625 LinkDetail::Unlinked => (
626 "If the link was removed, no propagation occurs.".to_string(),
627 "link already removed => SUPPRESS".to_string(),
628 "No active link exists; nothing to propagate.".to_string(),
629 ),
630 LinkDetail::RegionCleanup { region } => (
631 "If region cleanup runs, links are cleaned up without propagation.".to_string(),
632 format!("cleanup_region={region:?} => SUPPRESS"),
633 "Region close implies quiescence; cleanup suppresses further signals.".to_string(),
634 ),
635 }
636}
637
638fn monitor_card_triple(detail: &MonitorDetail) -> (String, String, String) {
639 match detail {
640 MonitorDetail::DownDelivered { monitored, reason } => (
641 "If a monitored task terminates, a DOWN is DELIVERED to the watcher.".to_string(),
642 format!("monitored={monitored:?}, reason={reason:?} => DELIVER"),
643 "Monitors provide observation without coupling; DOWN is delivered deterministically."
644 .to_string(),
645 ),
646 MonitorDetail::WatcherRegionClosed { region } => (
647 "If the watcher region is closed, DOWN delivery is DROPPED.".to_string(),
648 format!("watcher_region={region:?} closed => DROP"),
649 "Watcher cannot receive messages after cleanup; drop avoids resurrecting work."
650 .to_string(),
651 ),
652 MonitorDetail::Demonitored => (
653 "If the monitor was removed, no DOWN is delivered.".to_string(),
654 "demonitored => DROP".to_string(),
655 "No active monitor exists; nothing to deliver.".to_string(),
656 ),
657 MonitorDetail::RegionCleanup { region, count } => (
658 "If region cleanup runs, monitors are DROPPED.".to_string(),
659 format!("cleanup_region={region:?}, released={count} => DROP"),
660 "Region close releases monitor obligations; dropping is deterministic cleanup."
661 .to_string(),
662 ),
663 }
664}
665
666fn evidence_card_triple(
667 _subsystem: Subsystem,
668 _verdict: Verdict,
669 detail: &EvidenceDetail,
670) -> (String, String, String) {
671 match detail {
672 EvidenceDetail::Supervision(d) => supervision_card_triple(d),
673 EvidenceDetail::Registry(d) => registry_card_triple(d),
674 EvidenceDetail::Link(d) => link_card_triple(d),
675 EvidenceDetail::Monitor(d) => monitor_card_triple(d),
676 }
677}
678
679#[derive(Debug, Clone, Default, Serialize, Deserialize)]
689pub struct GeneralizedLedger {
690 entries: Vec<EvidenceRecord>,
691}
692
693impl GeneralizedLedger {
694 #[must_use]
696 pub fn new() -> Self {
697 Self {
698 entries: Vec::new(),
699 }
700 }
701
702 pub fn push(&mut self, record: EvidenceRecord) {
704 self.entries.push(record);
705 }
706
707 #[must_use]
709 pub fn entries(&self) -> &[EvidenceRecord] {
710 &self.entries
711 }
712
713 #[must_use]
715 pub fn len(&self) -> usize {
716 self.entries.len()
717 }
718
719 #[must_use]
721 pub fn is_empty(&self) -> bool {
722 self.entries.is_empty()
723 }
724
725 pub fn for_task(&self, task_id: TaskId) -> impl Iterator<Item = &EvidenceRecord> {
727 self.entries.iter().filter(move |e| e.task_id == task_id)
728 }
729
730 pub fn for_subsystem(&self, subsystem: Subsystem) -> impl Iterator<Item = &EvidenceRecord> {
732 self.entries
733 .iter()
734 .filter(move |e| e.subsystem == subsystem)
735 }
736
737 pub fn with_verdict(&self, verdict: Verdict) -> impl Iterator<Item = &EvidenceRecord> {
739 self.entries.iter().filter(move |e| e.verdict == verdict)
740 }
741
742 pub fn filter<F>(&self, predicate: F) -> impl Iterator<Item = &EvidenceRecord>
744 where
745 F: Fn(&EvidenceRecord) -> bool,
746 {
747 self.entries.iter().filter(move |e| predicate(e))
748 }
749
750 pub fn clear(&mut self) {
752 self.entries.clear();
753 }
754
755 #[must_use]
760 pub fn render(&self) -> String {
761 let mut out = String::new();
762 for entry in &self.entries {
763 out.push_str(&entry.render());
764 out.push('\n');
765 }
766 out
767 }
768
769 #[must_use]
773 pub fn render_cards(&self) -> String {
774 let mut out = String::new();
775 for entry in &self.entries {
776 out.push_str(&entry.render_card());
777 out.push('\n');
778 }
779 out
780 }
781}
782
783impl fmt::Display for GeneralizedLedger {
784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785 for entry in &self.entries {
786 writeln!(f, "{entry}")?;
787 }
788 Ok(())
789 }
790}
791
792#[cfg(test)]
797mod tests {
798 use super::*;
799 use crate::types::PanicPayload;
800 use crate::util::ArenaIndex;
801
802 fn test_task_id() -> TaskId {
803 TaskId::from_arena(ArenaIndex::new(0, 1))
804 }
805
806 fn test_task_id_2() -> TaskId {
807 TaskId::from_arena(ArenaIndex::new(0, 2))
808 }
809
810 fn test_region_id() -> RegionId {
811 RegionId::from_arena(ArenaIndex::new(0, 0))
812 }
813
814 fn init_test(name: &str) {
815 crate::test_utils::init_test_logging();
816 crate::test_phase!(name);
817 }
818
819 #[test]
820 fn evidence_record_render_supervision_restart() {
821 init_test("evidence_record_render_supervision_restart");
822
823 let record = EvidenceRecord {
824 timestamp: 1_000_000_000,
825 task_id: test_task_id(),
826 region_id: test_region_id(),
827 subsystem: Subsystem::Supervision,
828 verdict: Verdict::Restart,
829 detail: EvidenceDetail::Supervision(SupervisionDetail::RestartAllowed {
830 attempt: 2,
831 delay: Some(Duration::from_millis(200)),
832 }),
833 };
834
835 let rendered = record.render();
836 assert!(rendered.contains("supervision RESTART"));
837 assert!(rendered.contains("restart allowed (attempt 2, delay 200ms)"));
838
839 crate::test_complete!("evidence_record_render_supervision_restart");
840 }
841
842 #[test]
843 fn evidence_record_render_supervision_stop() {
844 init_test("evidence_record_render_supervision_stop");
845
846 let record = EvidenceRecord {
847 timestamp: 2_000_000_000,
848 task_id: test_task_id(),
849 region_id: test_region_id(),
850 subsystem: Subsystem::Supervision,
851 verdict: Verdict::Stop,
852 detail: EvidenceDetail::Supervision(SupervisionDetail::WindowExhausted {
853 max_restarts: 3,
854 window: Duration::from_secs(60),
855 }),
856 };
857
858 let rendered = record.render();
859 assert!(rendered.contains("supervision STOP"));
860 assert!(rendered.contains("window exhausted: 3 restarts in 60s"));
861
862 crate::test_complete!("evidence_record_render_supervision_stop");
863 }
864
865 #[test]
866 fn evidence_record_render_registry_accept() {
867 init_test("evidence_record_render_registry_accept");
868
869 let record = EvidenceRecord {
870 timestamp: 500,
871 task_id: test_task_id(),
872 region_id: test_region_id(),
873 subsystem: Subsystem::Registry,
874 verdict: Verdict::Accept,
875 detail: EvidenceDetail::Registry(RegistryDetail::NameAvailable),
876 };
877
878 assert_eq!(record.render(), "[500] registry ACCEPT: name available");
879
880 crate::test_complete!("evidence_record_render_registry_accept");
881 }
882
883 #[test]
884 fn evidence_record_render_registry_reject_collision() {
885 init_test("evidence_record_render_registry_reject_collision");
886
887 let record = EvidenceRecord {
888 timestamp: 600,
889 task_id: test_task_id(),
890 region_id: test_region_id(),
891 subsystem: Subsystem::Registry,
892 verdict: Verdict::Reject,
893 detail: EvidenceDetail::Registry(RegistryDetail::NameCollision {
894 existing_holder: test_task_id_2(),
895 }),
896 };
897
898 let rendered = record.render();
899 assert!(rendered.contains("registry REJECT"));
900 assert!(rendered.contains("name collision"));
901
902 crate::test_complete!("evidence_record_render_registry_reject_collision");
903 }
904
905 #[test]
906 fn evidence_record_render_link_propagate() {
907 init_test("evidence_record_render_link_propagate");
908
909 let record = EvidenceRecord {
910 timestamp: 700,
911 task_id: test_task_id(),
912 region_id: test_region_id(),
913 subsystem: Subsystem::Link,
914 verdict: Verdict::Propagate,
915 detail: EvidenceDetail::Link(LinkDetail::ExitPropagated {
916 source: test_task_id_2(),
917 reason: Outcome::Err(()),
918 }),
919 };
920
921 let rendered = record.render();
922 assert!(rendered.contains("link PROPAGATE"));
923 assert!(rendered.contains("exit propagated"));
924
925 crate::test_complete!("evidence_record_render_link_propagate");
926 }
927
928 #[test]
929 fn evidence_record_render_monitor_deliver() {
930 init_test("evidence_record_render_monitor_deliver");
931
932 let record = EvidenceRecord {
933 timestamp: 800,
934 task_id: test_task_id(),
935 region_id: test_region_id(),
936 subsystem: Subsystem::Monitor,
937 verdict: Verdict::Deliver,
938 detail: EvidenceDetail::Monitor(MonitorDetail::DownDelivered {
939 monitored: test_task_id_2(),
940 reason: Outcome::Panicked(PanicPayload::new("oops")),
941 }),
942 };
943
944 let rendered = record.render();
945 assert!(rendered.contains("monitor DELIVER"));
946 assert!(rendered.contains("down delivered"));
947
948 crate::test_complete!("evidence_record_render_monitor_deliver");
949 }
950
951 #[test]
952 fn generalized_ledger_push_and_query() {
953 init_test("generalized_ledger_push_and_query");
954
955 let mut ledger = GeneralizedLedger::new();
956 assert!(ledger.is_empty());
957
958 ledger.push(EvidenceRecord {
960 timestamp: 100,
961 task_id: test_task_id(),
962 region_id: test_region_id(),
963 subsystem: Subsystem::Supervision,
964 verdict: Verdict::Restart,
965 detail: EvidenceDetail::Supervision(SupervisionDetail::RestartAllowed {
966 attempt: 1,
967 delay: None,
968 }),
969 });
970
971 ledger.push(EvidenceRecord {
973 timestamp: 200,
974 task_id: test_task_id_2(),
975 region_id: test_region_id(),
976 subsystem: Subsystem::Registry,
977 verdict: Verdict::Accept,
978 detail: EvidenceDetail::Registry(RegistryDetail::NameAvailable),
979 });
980
981 ledger.push(EvidenceRecord {
983 timestamp: 300,
984 task_id: test_task_id(),
985 region_id: test_region_id(),
986 subsystem: Subsystem::Supervision,
987 verdict: Verdict::Stop,
988 detail: EvidenceDetail::Supervision(SupervisionDetail::ExplicitStop),
989 });
990
991 assert_eq!(ledger.len(), 3);
992
993 assert_eq!(ledger.for_subsystem(Subsystem::Supervision).count(), 2);
995
996 assert_eq!(ledger.for_subsystem(Subsystem::Registry).count(), 1);
997
998 assert_eq!(ledger.with_verdict(Verdict::Restart).count(), 1);
1000
1001 assert_eq!(ledger.with_verdict(Verdict::Stop).count(), 1);
1002
1003 assert_eq!(ledger.for_task(test_task_id()).count(), 2);
1005
1006 assert_eq!(ledger.for_task(test_task_id_2()).count(), 1);
1007
1008 crate::test_complete!("generalized_ledger_push_and_query");
1009 }
1010
1011 #[test]
1012 fn generalized_ledger_render_deterministic() {
1013 init_test("generalized_ledger_render_deterministic");
1014
1015 let mut ledger_a = GeneralizedLedger::new();
1016 let mut ledger_b = GeneralizedLedger::new();
1017
1018 let records = vec![
1019 EvidenceRecord {
1020 timestamp: 100,
1021 task_id: test_task_id(),
1022 region_id: test_region_id(),
1023 subsystem: Subsystem::Supervision,
1024 verdict: Verdict::Restart,
1025 detail: EvidenceDetail::Supervision(SupervisionDetail::RestartAllowed {
1026 attempt: 1,
1027 delay: None,
1028 }),
1029 },
1030 EvidenceRecord {
1031 timestamp: 200,
1032 task_id: test_task_id(),
1033 region_id: test_region_id(),
1034 subsystem: Subsystem::Supervision,
1035 verdict: Verdict::Stop,
1036 detail: EvidenceDetail::Supervision(SupervisionDetail::MonotoneSeverity {
1037 outcome_kind: "Panicked".to_string(),
1038 }),
1039 },
1040 ];
1041
1042 for r in &records {
1043 ledger_a.push(r.clone());
1044 ledger_b.push(r.clone());
1045 }
1046
1047 assert_eq!(ledger_a.render(), ledger_b.render());
1049
1050 assert_eq!(format!("{ledger_a}"), ledger_a.render());
1052
1053 crate::test_complete!("generalized_ledger_render_deterministic");
1054 }
1055
1056 #[test]
1057 fn generalized_ledger_clear() {
1058 init_test("generalized_ledger_clear");
1059
1060 let mut ledger = GeneralizedLedger::new();
1061 ledger.push(EvidenceRecord {
1062 timestamp: 100,
1063 task_id: test_task_id(),
1064 region_id: test_region_id(),
1065 subsystem: Subsystem::Supervision,
1066 verdict: Verdict::Stop,
1067 detail: EvidenceDetail::Supervision(SupervisionDetail::ExplicitStop),
1068 });
1069
1070 assert_eq!(ledger.len(), 1);
1071 ledger.clear();
1072 assert!(ledger.is_empty());
1073
1074 crate::test_complete!("generalized_ledger_clear");
1075 }
1076
1077 #[test]
1078 fn subsystem_display() {
1079 init_test("subsystem_display");
1080
1081 assert_eq!(format!("{}", Subsystem::Supervision), "supervision");
1082 assert_eq!(format!("{}", Subsystem::Registry), "registry");
1083 assert_eq!(format!("{}", Subsystem::Link), "link");
1084 assert_eq!(format!("{}", Subsystem::Monitor), "monitor");
1085
1086 crate::test_complete!("subsystem_display");
1087 }
1088
1089 #[test]
1090 fn verdict_display() {
1091 init_test("verdict_display");
1092
1093 assert_eq!(format!("{}", Verdict::Restart), "RESTART");
1094 assert_eq!(format!("{}", Verdict::Stop), "STOP");
1095 assert_eq!(format!("{}", Verdict::Accept), "ACCEPT");
1096 assert_eq!(format!("{}", Verdict::Reject), "REJECT");
1097 assert_eq!(format!("{}", Verdict::Propagate), "PROPAGATE");
1098 assert_eq!(format!("{}", Verdict::Deliver), "DELIVER");
1099
1100 crate::test_complete!("verdict_display");
1101 }
1102
1103 #[test]
1104 fn registry_detail_display_variants() {
1105 init_test("registry_detail_display_variants");
1106
1107 let details = vec![
1108 (RegistryDetail::NameAvailable, "name available"),
1109 (
1110 RegistryDetail::LeaseCommitted,
1111 "lease committed (normal release)",
1112 ),
1113 ];
1114
1115 for (detail, expected) in details {
1116 assert_eq!(format!("{detail}"), expected);
1117 }
1118
1119 crate::test_complete!("registry_detail_display_variants");
1120 }
1121
1122 #[test]
1123 fn link_detail_display_variants() {
1124 init_test("link_detail_display_variants");
1125
1126 assert_eq!(
1127 format!("{}", LinkDetail::Unlinked),
1128 "unlinked before failure"
1129 );
1130
1131 crate::test_complete!("link_detail_display_variants");
1132 }
1133
1134 #[test]
1135 fn monitor_detail_display_variants() {
1136 init_test("monitor_detail_display_variants");
1137
1138 assert_eq!(
1139 format!("{}", MonitorDetail::Demonitored),
1140 "demonitored before termination"
1141 );
1142
1143 crate::test_complete!("monitor_detail_display_variants");
1144 }
1145
1146 #[test]
1147 fn generalized_ledger_filter_predicate() {
1148 init_test("generalized_ledger_filter_predicate");
1149
1150 let mut ledger = GeneralizedLedger::new();
1151 for i in 0u64..5 {
1152 ledger.push(EvidenceRecord {
1153 timestamp: i * 100,
1154 task_id: test_task_id(),
1155 region_id: test_region_id(),
1156 subsystem: Subsystem::Supervision,
1157 verdict: if i < 3 {
1158 Verdict::Restart
1159 } else {
1160 Verdict::Stop
1161 },
1162 detail: EvidenceDetail::Supervision(if i < 3 {
1163 SupervisionDetail::RestartAllowed {
1164 attempt: (i as u32) + 1,
1165 delay: None,
1166 }
1167 } else {
1168 SupervisionDetail::WindowExhausted {
1169 max_restarts: 3,
1170 window: Duration::from_secs(60),
1171 }
1172 }),
1173 });
1174 }
1175
1176 assert_eq!(ledger.filter(|e| e.timestamp > 200).count(), 2);
1178
1179 crate::test_complete!("generalized_ledger_filter_predicate");
1180 }
1181
1182 #[test]
1183 fn evidence_record_render_card_supervision_restart() {
1184 init_test("evidence_record_render_card_supervision_restart");
1185
1186 let record = EvidenceRecord {
1187 timestamp: 1_000_000_001,
1188 task_id: test_task_id(),
1189 region_id: test_region_id(),
1190 subsystem: Subsystem::Supervision,
1191 verdict: Verdict::Restart,
1192 detail: EvidenceDetail::Supervision(SupervisionDetail::RestartAllowed {
1193 attempt: 1,
1194 delay: Some(Duration::from_millis(10)),
1195 }),
1196 };
1197
1198 let rendered = record.render_card();
1199 assert!(rendered.contains("supervision RESTART"));
1200 assert!(rendered.contains("rule: If restart window and budget allow"));
1201 assert!(rendered.contains("substitution: attempt=1, delay=10ms => RESTART"));
1202 assert!(rendered.contains("intuition: Restart attempt is permitted"));
1203
1204 crate::test_complete!("evidence_record_render_card_supervision_restart");
1205 }
1206
1207 #[test]
1208 fn generalized_ledger_render_cards_deterministic() {
1209 init_test("generalized_ledger_render_cards_deterministic");
1210
1211 let mut ledger_a = GeneralizedLedger::new();
1212 let mut ledger_b = GeneralizedLedger::new();
1213
1214 for ledger in [&mut ledger_a, &mut ledger_b] {
1215 ledger.push(EvidenceRecord {
1216 timestamp: 10,
1217 task_id: test_task_id(),
1218 region_id: test_region_id(),
1219 subsystem: Subsystem::Registry,
1220 verdict: Verdict::Reject,
1221 detail: EvidenceDetail::Registry(RegistryDetail::NameCollision {
1222 existing_holder: test_task_id_2(),
1223 }),
1224 });
1225 ledger.push(EvidenceRecord {
1226 timestamp: 11,
1227 task_id: test_task_id(),
1228 region_id: test_region_id(),
1229 subsystem: Subsystem::Supervision,
1230 verdict: Verdict::Stop,
1231 detail: EvidenceDetail::Supervision(SupervisionDetail::WindowExhausted {
1232 max_restarts: 2,
1233 window: Duration::from_secs(1),
1234 }),
1235 });
1236 }
1237
1238 assert_eq!(ledger_a.render_cards(), ledger_b.render_cards());
1240
1241 crate::test_complete!("generalized_ledger_render_cards_deterministic");
1242 }
1243
1244 #[test]
1247 fn subsystem_debug_copy_hash() {
1248 use std::collections::HashSet;
1249 let s = Subsystem::Supervision;
1250 let dbg = format!("{s:?}");
1251 assert!(dbg.contains("Supervision"));
1252
1253 let s2 = s;
1255 assert_eq!(s, s2);
1256
1257 let mut set = HashSet::new();
1259 set.insert(Subsystem::Supervision);
1260 set.insert(Subsystem::Registry);
1261 set.insert(Subsystem::Link);
1262 set.insert(Subsystem::Monitor);
1263 assert_eq!(set.len(), 4);
1264 assert!(set.contains(&Subsystem::Link));
1265 }
1266
1267 #[test]
1268 fn verdict_debug_copy_hash() {
1269 use std::collections::HashSet;
1270 let v = Verdict::Restart;
1271 let dbg = format!("{v:?}");
1272 assert!(dbg.contains("Restart"));
1273
1274 let v2 = v;
1276 assert_eq!(v, v2);
1277
1278 let mut set = HashSet::new();
1280 for v in [
1281 Verdict::Restart,
1282 Verdict::Stop,
1283 Verdict::Escalate,
1284 Verdict::Accept,
1285 Verdict::Reject,
1286 Verdict::Release,
1287 Verdict::Abort,
1288 Verdict::Propagate,
1289 Verdict::Suppress,
1290 Verdict::Deliver,
1291 Verdict::Drop,
1292 ] {
1293 set.insert(v);
1294 }
1295 assert_eq!(set.len(), 11);
1296 }
1297
1298 #[test]
1299 fn evidence_detail_debug_clone_eq() {
1300 let detail = EvidenceDetail::Supervision(SupervisionDetail::ExplicitStop);
1301 let dbg = format!("{detail:?}");
1302 assert!(dbg.contains("Supervision"));
1303 assert!(dbg.contains("ExplicitStop"));
1304
1305 let cloned = detail.clone();
1306 assert_eq!(detail, cloned);
1307
1308 let other = EvidenceDetail::Registry(RegistryDetail::NameAvailable);
1310 assert_ne!(detail, other);
1311 }
1312
1313 #[test]
1314 fn supervision_detail_debug_clone() {
1315 let detail = SupervisionDetail::RestartAllowed {
1316 attempt: 3,
1317 delay: Some(Duration::from_millis(100)),
1318 };
1319 let dbg = format!("{detail:?}");
1320 assert!(dbg.contains("RestartAllowed"));
1321 assert!(dbg.contains('3'));
1322
1323 let cloned = detail.clone();
1324 assert_eq!(detail, cloned);
1325 }
1326
1327 #[test]
1328 fn evidence_record_debug_clone_eq() {
1329 let record = EvidenceRecord {
1330 timestamp: 42,
1331 task_id: test_task_id(),
1332 region_id: test_region_id(),
1333 subsystem: Subsystem::Registry,
1334 verdict: Verdict::Accept,
1335 detail: EvidenceDetail::Registry(RegistryDetail::NameAvailable),
1336 };
1337 let dbg = format!("{record:?}");
1338 assert!(dbg.contains("EvidenceRecord"));
1339 assert!(dbg.contains("42"));
1340
1341 let cloned = record.clone();
1342 assert_eq!(record, cloned);
1343 }
1344
1345 #[test]
1346 fn evidence_card_debug_clone() {
1347 let record = EvidenceRecord {
1348 timestamp: 100,
1349 task_id: test_task_id(),
1350 region_id: test_region_id(),
1351 subsystem: Subsystem::Supervision,
1352 verdict: Verdict::Stop,
1353 detail: EvidenceDetail::Supervision(SupervisionDetail::ExplicitStop),
1354 };
1355 let card = record.to_card();
1356 let dbg = format!("{card:?}");
1357 assert!(dbg.contains("EvidenceCard"));
1358
1359 let cloned = card.clone();
1360 assert_eq!(card, cloned);
1361 }
1362
1363 #[test]
1364 fn generalized_ledger_debug_clone_default() {
1365 let ledger = GeneralizedLedger::default();
1366 assert!(ledger.is_empty());
1367 assert_eq!(ledger.len(), 0);
1368
1369 let dbg = format!("{ledger:?}");
1370 assert!(dbg.contains("GeneralizedLedger"));
1371
1372 let mut ledger2 = GeneralizedLedger::new();
1373 ledger2.push(EvidenceRecord {
1374 timestamp: 1,
1375 task_id: test_task_id(),
1376 region_id: test_region_id(),
1377 subsystem: Subsystem::Monitor,
1378 verdict: Verdict::Deliver,
1379 detail: EvidenceDetail::Monitor(MonitorDetail::Demonitored),
1380 });
1381 let cloned = ledger2.clone();
1382 assert_eq!(cloned.len(), 1);
1383 assert_eq!(cloned.entries()[0].timestamp, 1);
1384 }
1385}