Skip to main content

asupersync/
monitor.rs

1//! Process monitors and deterministic down notifications.
2//!
3//! Monitors allow a task to observe the termination of another task (the
4//! "monitored" process). When the monitored process terminates, a
5//! [`DownNotification`] is delivered to the watcher.
6//!
7//! # Deterministic Ordering
8//!
9//! Down notifications follow the contracts specified in
10//! `docs/spork_deterministic_ordering.md`:
11//!
12//! - **DOWN-ORDER**: Notifications are sorted by
13//!   `(completion_vt, monitored_tid, monitor_ref)`.
14//! - **DOWN-BATCH**: When multiple notifications become ready in a single
15//!   scheduler step, they are sorted before delivery.
16//! - **DOWN-CONTENT**: Each notification carries the monitored TaskId, reason,
17//!   and the MonitorRef returned when the monitor was established.
18//! - **DOWN-CLEANUP**: Region close releases all monitors held by tasks in
19//!   that region.
20//!
21//! # Example
22//!
23//! ```rust,ignore
24//! // Establish a monitor
25//! let mon_ref = monitor_set.establish(watcher_id, watcher_region, target_id);
26//!
27//! // When target terminates, generate notifications
28//! let watchers = monitor_set.watchers_of(target_id);
29//! let mut batch = DownBatch::new();
30//! for (mref, watcher) in &watchers {
31//!     batch.push(completion_vt, DownNotification {
32//!         monitored: target_id,
33//!         reason: DownReason::from_task_outcome(&outcome),
34//!         monitor_ref: *mref,
35//!     });
36//! }
37//! let ordered = batch.into_sorted();
38//! ```
39
40use std::collections::BTreeMap;
41
42use serde::{Deserialize, Serialize};
43
44use crate::types::cancel::CancelReason;
45use crate::types::outcome::PanicPayload;
46use crate::types::{Outcome, RegionId, TaskId, Time};
47
48// ============================================================================
49// MonitorRef
50// ============================================================================
51
52/// Opaque reference to an established monitor.
53///
54/// Returned by [`MonitorSet::establish`] and carried in [`DownNotification`].
55/// Unique within a single runtime instance.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct MonitorRef(u64);
58
59impl MonitorRef {
60    /// Allocates a monitor reference from a runtime-local sequence.
61    #[inline]
62    fn new(id: u64) -> Self {
63        Self(id)
64    }
65
66    /// Creates a `MonitorRef` with a specific id (for testing only).
67    #[cfg(test)]
68    fn from_raw(id: u64) -> Self {
69        Self(id)
70    }
71
72    /// Creates a `MonitorRef` for integration testing purposes.
73    #[doc(hidden)]
74    #[must_use]
75    #[inline]
76    pub const fn new_for_test(id: u64) -> Self {
77        Self(id)
78    }
79
80    /// Returns the underlying numeric identifier.
81    #[must_use]
82    #[inline]
83    pub fn id(self) -> u64 {
84        self.0
85    }
86}
87
88impl std::fmt::Display for MonitorRef {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(f, "MonitorRef({})", self.0)
91    }
92}
93
94// ============================================================================
95// DownReason
96// ============================================================================
97
98/// Reason a monitored process terminated.
99///
100/// Maps from the runtime's [`Outcome`] type to a monitor-specific enum
101/// that can be pattern-matched by watchers.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub enum DownReason {
104    /// Process completed successfully (`Outcome::Ok`).
105    Normal,
106    /// Process terminated with an application error (`Outcome::Err`).
107    Error(String),
108    /// Process was cancelled (`Outcome::Cancelled`).
109    Cancelled(CancelReason),
110    /// Process panicked (`Outcome::Panicked`).
111    Panicked(PanicPayload),
112}
113
114impl DownReason {
115    /// Converts a task outcome to a down reason.
116    #[must_use]
117    #[inline]
118    pub fn from_task_outcome(outcome: &Outcome<(), crate::error::Error>) -> Self {
119        match outcome {
120            Outcome::Ok(()) => Self::Normal,
121            Outcome::Err(e) => Self::Error(format!("{e}")),
122            Outcome::Cancelled(r) => Self::Cancelled(r.clone()),
123            Outcome::Panicked(p) => Self::Panicked(p.clone()),
124        }
125    }
126
127    /// Returns `true` if the process terminated normally.
128    #[must_use]
129    #[inline]
130    pub fn is_normal(&self) -> bool {
131        matches!(self, Self::Normal)
132    }
133
134    /// Returns `true` if the process terminated with an error.
135    #[must_use]
136    #[inline]
137    pub fn is_error(&self) -> bool {
138        matches!(self, Self::Error(_))
139    }
140
141    /// Returns `true` if the process was cancelled.
142    #[must_use]
143    #[inline]
144    pub fn is_cancelled(&self) -> bool {
145        matches!(self, Self::Cancelled(_))
146    }
147
148    /// Returns `true` if the process panicked.
149    #[must_use]
150    #[inline]
151    pub fn is_panicked(&self) -> bool {
152        matches!(self, Self::Panicked(_))
153    }
154}
155
156impl std::fmt::Display for DownReason {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            Self::Normal => write!(f, "normal"),
160            Self::Error(e) => write!(f, "error: {e}"),
161            Self::Cancelled(r) => write!(f, "cancelled: {r:?}"),
162            Self::Panicked(p) => write!(f, "panicked: {p}"),
163        }
164    }
165}
166
167// ============================================================================
168// DownNotification
169// ============================================================================
170
171/// Notification delivered when a monitored process terminates.
172///
173/// **Contract (DOWN-CONTENT)**:
174/// - `monitored` is the `TaskId` of the terminated process.
175/// - `reason` is the termination outcome mapped to [`DownReason`].
176/// - `monitor_ref` is the reference returned by [`MonitorSet::establish`].
177#[derive(Debug, Clone)]
178pub struct DownNotification {
179    /// The task that terminated.
180    pub monitored: TaskId,
181    /// Why it terminated.
182    pub reason: DownReason,
183    /// The monitor reference from establishment.
184    pub monitor_ref: MonitorRef,
185}
186
187// ============================================================================
188// MonitorRecord (internal)
189// ============================================================================
190
191/// Internal record of an active monitor.
192#[derive(Debug, Clone)]
193struct MonitorRecord {
194    /// The task watching for termination.
195    watcher: TaskId,
196    /// The region owning the watcher (for region-close cleanup).
197    watcher_region: RegionId,
198    /// The task being monitored.
199    monitored: TaskId,
200}
201
202// ============================================================================
203// MonitorSet
204// ============================================================================
205
206/// Collection of active monitors with deterministic iteration order.
207///
208/// All internal data structures use [`BTreeMap`] to ensure no dependence on
209/// `HashMap` iteration order, satisfying the **REG-NOHASH** contract.
210///
211/// # Indexes
212///
213/// Three indexes are maintained for efficient lookup:
214/// - `by_ref`: MonitorRef → MonitorRecord (primary)
215/// - `by_monitored`: TaskId → Vec<MonitorRef> (find watchers of a terminated task)
216/// - `by_watcher_region`: RegionId → Vec<MonitorRef> (region-close cleanup)
217#[derive(Debug)]
218#[allow(clippy::struct_field_names)]
219pub struct MonitorSet {
220    by_ref: BTreeMap<MonitorRef, MonitorRecord>,
221    by_monitored: BTreeMap<TaskId, Vec<MonitorRef>>,
222    by_watcher_region: BTreeMap<RegionId, Vec<MonitorRef>>,
223    next_monitor_ref: u64,
224}
225
226impl Default for MonitorSet {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232impl MonitorSet {
233    /// Creates an empty monitor set.
234    #[must_use]
235    pub fn new() -> Self {
236        Self {
237            by_ref: BTreeMap::new(),
238            by_monitored: BTreeMap::new(),
239            by_watcher_region: BTreeMap::new(),
240            next_monitor_ref: 1,
241        }
242    }
243
244    #[inline]
245    fn alloc_monitor_ref(&mut self) -> MonitorRef {
246        let next = self.next_monitor_ref;
247        self.next_monitor_ref = self
248            .next_monitor_ref
249            .checked_add(1)
250            .expect("monitor ref space exhausted");
251        MonitorRef::new(next)
252    }
253
254    /// Establishes a monitor: `watcher` will be notified when `monitored` terminates.
255    ///
256    /// Returns a [`MonitorRef`] that uniquely identifies this monitor relationship.
257    /// The same watcher can monitor the same target multiple times; each call
258    /// returns a distinct `MonitorRef` and will produce a separate notification.
259    pub fn establish(
260        &mut self,
261        watcher: TaskId,
262        watcher_region: RegionId,
263        monitored: TaskId,
264    ) -> MonitorRef {
265        let monitor_ref = self.alloc_monitor_ref();
266        let record = MonitorRecord {
267            watcher,
268            watcher_region,
269            monitored,
270        };
271
272        self.by_ref.insert(monitor_ref, record);
273        self.by_monitored
274            .entry(monitored)
275            .or_default()
276            .push(monitor_ref);
277        self.by_watcher_region
278            .entry(watcher_region)
279            .or_default()
280            .push(monitor_ref);
281
282        monitor_ref
283    }
284
285    /// Removes a specific monitor. Returns `true` if it existed.
286    pub fn demonitor(&mut self, monitor_ref: MonitorRef) -> bool {
287        let Some(record) = self.by_ref.remove(&monitor_ref) else {
288            return false;
289        };
290        if let Some(refs) = self.by_monitored.get_mut(&record.monitored) {
291            refs.retain(|r| *r != monitor_ref);
292            if refs.is_empty() {
293                self.by_monitored.remove(&record.monitored);
294            }
295        }
296        if let Some(refs) = self.by_watcher_region.get_mut(&record.watcher_region) {
297            refs.retain(|r| *r != monitor_ref);
298            if refs.is_empty() {
299                self.by_watcher_region.remove(&record.watcher_region);
300            }
301        }
302        true
303    }
304
305    /// Returns all `(MonitorRef, watcher_TaskId)` pairs watching the given task.
306    ///
307    /// Used when a task terminates to generate [`DownNotification`]s.
308    #[must_use]
309    pub fn watchers_of(&self, monitored: TaskId) -> Vec<(MonitorRef, TaskId)> {
310        let Some(refs) = self.by_monitored.get(&monitored) else {
311            return Vec::new();
312        };
313        refs.iter()
314            .filter_map(|mref| self.by_ref.get(mref).map(|rec| (*mref, rec.watcher)))
315            .collect()
316    }
317
318    /// Removes all monitors watching a specific task and returns removed refs.
319    ///
320    /// Called after a task terminates and all notifications have been generated.
321    pub fn remove_monitored(&mut self, monitored: TaskId) -> Vec<MonitorRef> {
322        let Some(refs) = self.by_monitored.remove(&monitored) else {
323            return Vec::new();
324        };
325        let mut removed = Vec::with_capacity(refs.len());
326        for mref in refs {
327            if let Some(record) = self.by_ref.remove(&mref) {
328                if let Some(region_refs) = self.by_watcher_region.get_mut(&record.watcher_region) {
329                    region_refs.retain(|r| *r != mref);
330                    if region_refs.is_empty() {
331                        self.by_watcher_region.remove(&record.watcher_region);
332                    }
333                }
334                removed.push(mref);
335            }
336        }
337        removed
338    }
339
340    /// Removes all monitors held by tasks in the given region.
341    ///
342    /// **Contract (DOWN-CLEANUP)**: When a region closes, all monitors
343    /// established by tasks in that region are released. No further
344    /// down notifications are delivered to tasks in the region.
345    pub fn cleanup_region(&mut self, region: RegionId) -> Vec<MonitorRef> {
346        let Some(refs) = self.by_watcher_region.remove(&region) else {
347            return Vec::new();
348        };
349        let mut removed = Vec::with_capacity(refs.len());
350        for mref in refs {
351            if let Some(record) = self.by_ref.remove(&mref) {
352                if let Some(monitored_refs) = self.by_monitored.get_mut(&record.monitored) {
353                    monitored_refs.retain(|r| *r != mref);
354                    if monitored_refs.is_empty() {
355                        self.by_monitored.remove(&record.monitored);
356                    }
357                }
358                removed.push(mref);
359            }
360        }
361        removed
362    }
363
364    /// Returns the number of active monitors.
365    #[must_use]
366    pub fn len(&self) -> usize {
367        self.by_ref.len()
368    }
369
370    /// Returns `true` if there are no active monitors.
371    #[must_use]
372    pub fn is_empty(&self) -> bool {
373        self.by_ref.is_empty()
374    }
375
376    /// Returns the watcher for a given monitor ref, if it exists.
377    #[must_use]
378    pub fn watcher_of(&self, monitor_ref: MonitorRef) -> Option<TaskId> {
379        self.by_ref.get(&monitor_ref).map(|r| r.watcher)
380    }
381
382    /// Returns the monitored task for a given monitor ref, if it exists.
383    #[must_use]
384    pub fn monitored_of(&self, monitor_ref: MonitorRef) -> Option<TaskId> {
385        self.by_ref.get(&monitor_ref).map(|r| r.monitored)
386    }
387}
388
389// ============================================================================
390// DownBatch — deterministic delivery ordering
391// ============================================================================
392
393/// A batch of down notifications pending delivery, with deterministic sort.
394///
395/// **Contract (DOWN-ORDER)**: Notifications are sorted by
396/// `(completion_vt, monitored_tid, monitor_ref)` — virtual time first, then
397/// `TaskId`, then `MonitorRef` to fully order duplicate monitors on the same
398/// target in the same quantum.
399///
400/// **Contract (DOWN-BATCH)**: When multiple down notifications become ready
401/// in a single scheduler step, they are sorted before enqueue. The watcher
402/// receives them in sorted order.
403#[derive(Debug, Default)]
404pub struct DownBatch {
405    entries: Vec<DownBatchEntry>,
406}
407
408/// Internal entry pairing a notification with its sort key.
409#[derive(Debug, Clone)]
410struct DownBatchEntry {
411    /// Virtual time when the monitored task completed.
412    completion_vt: Time,
413    /// The notification to deliver.
414    notification: DownNotification,
415}
416
417impl DownBatch {
418    /// Creates an empty batch.
419    #[must_use]
420    pub fn new() -> Self {
421        Self::default()
422    }
423
424    /// Adds a notification to the batch with its completion virtual time.
425    pub fn push(&mut self, completion_vt: Time, notification: DownNotification) {
426        self.entries.push(DownBatchEntry {
427            completion_vt,
428            notification,
429        });
430    }
431
432    /// Returns the number of notifications in the batch.
433    #[must_use]
434    pub fn len(&self) -> usize {
435        self.entries.len()
436    }
437
438    /// Returns `true` if the batch is empty.
439    #[must_use]
440    pub fn is_empty(&self) -> bool {
441        self.entries.is_empty()
442    }
443
444    /// Sorts by `(completion_vt, monitored_tid, monitor_ref)` and returns notifications
445    /// in deterministic delivery order.
446    ///
447    /// This consumes the batch. The sort is stable, so notifications with
448    /// identical `(vt, tid, monitor_ref)` keys preserve insertion order.
449    #[must_use]
450    pub fn into_sorted(mut self) -> Vec<DownNotification> {
451        self.entries.sort_by(|a, b| {
452            let vt_cmp = a.completion_vt.cmp(&b.completion_vt);
453            vt_cmp
454                .then_with(|| a.notification.monitored.cmp(&b.notification.monitored))
455                .then_with(|| a.notification.monitor_ref.cmp(&b.notification.monitor_ref))
456        });
457        self.entries.into_iter().map(|e| e.notification).collect()
458    }
459}
460
461// ============================================================================
462// Tests
463// ============================================================================
464
465#[cfg(test)]
466mod tests {
467    #![allow(
468        clippy::pedantic,
469        clippy::nursery,
470        clippy::expect_fun_call,
471        clippy::map_unwrap_or,
472        clippy::cast_possible_wrap,
473        clippy::future_not_send
474    )]
475    use super::*;
476
477    fn test_task_id(index: u32, generation: u32) -> TaskId {
478        TaskId::new_for_test(index, generation)
479    }
480
481    fn test_region_id(index: u32, generation: u32) -> RegionId {
482        RegionId::new_for_test(index, generation)
483    }
484
485    // ── MonitorRef ──────────────────────────────────────────────────────
486
487    #[test]
488    fn monitor_ref_uniqueness() {
489        let mut set = MonitorSet::new();
490        let region = test_region_id(0, 0);
491        let target = test_task_id(2, 0);
492        let r1 = set.establish(test_task_id(10, 0), region, target);
493        let r2 = set.establish(test_task_id(11, 0), region, target);
494        assert_ne!(r1, r2);
495        assert!(r1 < r2); // monotonically increasing
496    }
497
498    #[test]
499    fn fresh_monitor_sets_restart_ref_sequence() {
500        let region = test_region_id(0, 0);
501        let target = test_task_id(2, 0);
502
503        let mut first = MonitorSet::new();
504        let first_a = first.establish(test_task_id(10, 0), region, target);
505        let first_b = first.establish(test_task_id(11, 0), region, target);
506
507        let mut second = MonitorSet::new();
508        let second_a = second.establish(test_task_id(20, 0), region, target);
509        let second_b = second.establish(test_task_id(21, 0), region, target);
510
511        assert_eq!(first_a.id(), 1);
512        assert_eq!(first_b.id(), 2);
513        assert_eq!(second_a.id(), 1);
514        assert_eq!(second_b.id(), 2);
515    }
516
517    #[test]
518    fn monitor_ref_display() {
519        let r = MonitorRef::from_raw(42);
520        assert_eq!(format!("{r}"), "MonitorRef(42)");
521    }
522
523    #[test]
524    fn monitor_ref_ordering() {
525        let r1 = MonitorRef::from_raw(1);
526        let r2 = MonitorRef::from_raw(2);
527        let r3 = MonitorRef::from_raw(3);
528        assert!(r1 < r2);
529        assert!(r2 < r3);
530    }
531
532    // ── DownReason ──────────────────────────────────────────────────────
533
534    #[test]
535    fn down_reason_predicates() {
536        assert!(DownReason::Normal.is_normal());
537        assert!(!DownReason::Normal.is_error());
538
539        assert!(DownReason::Error("oops".into()).is_error());
540        assert!(!DownReason::Error("oops".into()).is_normal());
541
542        assert!(DownReason::Cancelled(CancelReason::default()).is_cancelled());
543        assert!(DownReason::Panicked(PanicPayload::new("boom")).is_panicked());
544    }
545
546    #[test]
547    fn down_reason_display() {
548        assert_eq!(format!("{}", DownReason::Normal), "normal");
549        assert!(format!("{}", DownReason::Error("fail".into())).contains("fail"));
550        assert!(format!("{}", DownReason::Panicked(PanicPayload::new("boom"))).contains("boom"));
551    }
552
553    #[test]
554    fn down_reason_from_task_outcome_ok() {
555        let outcome: Outcome<(), crate::error::Error> = Outcome::ok(());
556        let reason = DownReason::from_task_outcome(&outcome);
557        assert!(reason.is_normal());
558    }
559
560    #[test]
561    fn down_reason_from_task_outcome_cancelled() {
562        let outcome: Outcome<(), crate::error::Error> = Outcome::cancelled(CancelReason::default());
563        let reason = DownReason::from_task_outcome(&outcome);
564        assert!(reason.is_cancelled());
565    }
566
567    #[test]
568    fn down_reason_from_task_outcome_panicked() {
569        let outcome: Outcome<(), crate::error::Error> =
570            Outcome::panicked(PanicPayload::new("test"));
571        let reason = DownReason::from_task_outcome(&outcome);
572        assert!(reason.is_panicked());
573    }
574
575    // ── MonitorSet: establish / demonitor ────────────────────────────────
576
577    #[test]
578    fn establish_creates_monitor() {
579        let mut set = MonitorSet::new();
580        let watcher = test_task_id(1, 0);
581        let region = test_region_id(0, 0);
582        let target = test_task_id(2, 0);
583
584        let mref = set.establish(watcher, region, target);
585        assert_eq!(set.len(), 1);
586        assert_eq!(set.watcher_of(mref), Some(watcher));
587        assert_eq!(set.monitored_of(mref), Some(target));
588    }
589
590    #[test]
591    fn establish_multiple_monitors_same_target() {
592        let mut set = MonitorSet::new();
593        let w1 = test_task_id(1, 0);
594        let w2 = test_task_id(2, 0);
595        let region = test_region_id(0, 0);
596        let target = test_task_id(3, 0);
597
598        let m1 = set.establish(w1, region, target);
599        let m2 = set.establish(w2, region, target);
600        assert_ne!(m1, m2);
601        assert_eq!(set.len(), 2);
602
603        let watchers = set.watchers_of(target);
604        assert_eq!(watchers.len(), 2);
605    }
606
607    #[test]
608    fn establish_same_watcher_twice_yields_distinct_refs() {
609        let mut set = MonitorSet::new();
610        let watcher = test_task_id(1, 0);
611        let region = test_region_id(0, 0);
612        let target = test_task_id(2, 0);
613
614        let m1 = set.establish(watcher, region, target);
615        let m2 = set.establish(watcher, region, target);
616        assert_ne!(m1, m2);
617        assert_eq!(set.len(), 2);
618    }
619
620    #[test]
621    fn demonitor_removes_monitor() {
622        let mut set = MonitorSet::new();
623        let watcher = test_task_id(1, 0);
624        let region = test_region_id(0, 0);
625        let target = test_task_id(2, 0);
626
627        let mref = set.establish(watcher, region, target);
628        assert!(set.demonitor(mref));
629        assert_eq!(set.len(), 0);
630        assert!(set.watchers_of(target).is_empty());
631    }
632
633    #[test]
634    fn demonitor_nonexistent_returns_false() {
635        let mut set = MonitorSet::new();
636        assert!(!set.demonitor(MonitorRef::from_raw(999)));
637    }
638
639    #[test]
640    fn demonitor_only_removes_specific_monitor() {
641        let mut set = MonitorSet::new();
642        let w1 = test_task_id(1, 0);
643        let w2 = test_task_id(2, 0);
644        let region = test_region_id(0, 0);
645        let target = test_task_id(3, 0);
646
647        let m1 = set.establish(w1, region, target);
648        let _m2 = set.establish(w2, region, target);
649
650        set.demonitor(m1);
651        assert_eq!(set.len(), 1);
652        assert_eq!(set.watchers_of(target).len(), 1);
653    }
654
655    // ── MonitorSet: watchers_of ────────────────────────────────────────
656
657    #[test]
658    fn watchers_of_empty() {
659        let set = MonitorSet::new();
660        assert!(set.watchers_of(test_task_id(99, 0)).is_empty());
661    }
662
663    #[test]
664    fn watchers_of_returns_all_watchers() {
665        let mut set = MonitorSet::new();
666        let region = test_region_id(0, 0);
667        let target = test_task_id(10, 0);
668
669        let w1 = test_task_id(1, 0);
670        let w2 = test_task_id(2, 0);
671        let w3 = test_task_id(3, 0);
672
673        let m1 = set.establish(w1, region, target);
674        let m2 = set.establish(w2, region, target);
675        let m3 = set.establish(w3, region, target);
676
677        let watchers = set.watchers_of(target);
678        assert_eq!(watchers.len(), 3);
679
680        let mrefs: Vec<MonitorRef> = watchers.iter().map(|(r, _)| *r).collect();
681        assert!(mrefs.contains(&m1));
682        assert!(mrefs.contains(&m2));
683        assert!(mrefs.contains(&m3));
684
685        let tids: Vec<TaskId> = watchers.iter().map(|(_, t)| *t).collect();
686        assert!(tids.contains(&w1));
687        assert!(tids.contains(&w2));
688        assert!(tids.contains(&w3));
689    }
690
691    // ── MonitorSet: remove_monitored ───────────────────────────────────
692
693    #[test]
694    fn remove_monitored_clears_all_watchers() {
695        let mut set = MonitorSet::new();
696        let region = test_region_id(0, 0);
697        let target = test_task_id(10, 0);
698
699        set.establish(test_task_id(1, 0), region, target);
700        set.establish(test_task_id(2, 0), region, target);
701
702        let removed = set.remove_monitored(target);
703        assert_eq!(removed.len(), 2);
704        assert!(set.is_empty());
705        assert!(set.watchers_of(target).is_empty());
706    }
707
708    #[test]
709    fn remove_monitored_preserves_other_monitors() {
710        let mut set = MonitorSet::new();
711        let region = test_region_id(0, 0);
712        let t1 = test_task_id(10, 0);
713        let t2 = test_task_id(20, 0);
714        let watcher = test_task_id(1, 0);
715
716        set.establish(watcher, region, t1);
717        set.establish(watcher, region, t2);
718
719        set.remove_monitored(t1);
720        assert_eq!(set.len(), 1);
721        assert_eq!(set.watchers_of(t2).len(), 1);
722    }
723
724    // ── MonitorSet: cleanup_region (DOWN-CLEANUP) ─────────────────────
725
726    #[test]
727    fn cleanup_region_removes_all_monitors_in_region() {
728        let mut set = MonitorSet::new();
729        let r1 = test_region_id(1, 0);
730        let r2 = test_region_id(2, 0);
731        let target = test_task_id(10, 0);
732
733        // Watcher in region 1
734        set.establish(test_task_id(1, 0), r1, target);
735        // Watcher in region 2
736        set.establish(test_task_id(2, 0), r2, target);
737
738        let removed = set.cleanup_region(r1);
739        assert_eq!(removed.len(), 1);
740        assert_eq!(set.len(), 1);
741        // Only region 2's monitor remains
742        assert_eq!(set.watchers_of(target).len(), 1);
743    }
744
745    #[test]
746    fn cleanup_region_empty_is_noop() {
747        let mut set = MonitorSet::new();
748        let removed = set.cleanup_region(test_region_id(99, 0));
749        assert!(removed.is_empty());
750    }
751
752    #[test]
753    fn cleanup_region_cleans_monitored_index() {
754        let mut set = MonitorSet::new();
755        let region = test_region_id(1, 0);
756        let target = test_task_id(10, 0);
757
758        set.establish(test_task_id(1, 0), region, target);
759        set.cleanup_region(region);
760
761        // The monitored_index should also be cleaned
762        assert!(set.watchers_of(target).is_empty());
763    }
764
765    // ── DownBatch: deterministic ordering (DOWN-ORDER + DOWN-BATCH) ───
766
767    #[test]
768    fn down_batch_empty() {
769        let batch = DownBatch::new();
770        assert!(batch.is_empty());
771        assert_eq!(batch.len(), 0);
772        assert!(batch.into_sorted().is_empty());
773    }
774
775    #[test]
776    fn down_batch_single_item() {
777        let mut batch = DownBatch::new();
778        let notif = DownNotification {
779            monitored: test_task_id(1, 0),
780            reason: DownReason::Normal,
781            monitor_ref: MonitorRef::from_raw(1),
782        };
783        batch.push(Time::from_nanos(100), notif);
784        assert_eq!(batch.len(), 1);
785
786        let sorted = batch.into_sorted();
787        assert_eq!(sorted.len(), 1);
788        assert_eq!(sorted[0].monitored, test_task_id(1, 0));
789    }
790
791    #[test]
792    fn down_batch_sorts_by_virtual_time() {
793        let mut batch = DownBatch::new();
794
795        // Insert in reverse vt order
796        batch.push(
797            Time::from_nanos(300),
798            DownNotification {
799                monitored: test_task_id(1, 0),
800                reason: DownReason::Normal,
801                monitor_ref: MonitorRef::from_raw(1),
802            },
803        );
804        batch.push(
805            Time::from_nanos(100),
806            DownNotification {
807                monitored: test_task_id(2, 0),
808                reason: DownReason::Normal,
809                monitor_ref: MonitorRef::from_raw(2),
810            },
811        );
812        batch.push(
813            Time::from_nanos(200),
814            DownNotification {
815                monitored: test_task_id(3, 0),
816                reason: DownReason::Normal,
817                monitor_ref: MonitorRef::from_raw(3),
818            },
819        );
820
821        let sorted = batch.into_sorted();
822        assert_eq!(sorted[0].monitored, test_task_id(2, 0)); // vt=100
823        assert_eq!(sorted[1].monitored, test_task_id(3, 0)); // vt=200
824        assert_eq!(sorted[2].monitored, test_task_id(1, 0)); // vt=300
825    }
826
827    #[test]
828    fn down_batch_tie_breaks_by_task_id() {
829        let mut batch = DownBatch::new();
830        let same_vt = Time::from_nanos(100);
831
832        // Same vt, different task IDs — should sort by TaskId (ArenaIndex order)
833        batch.push(
834            same_vt,
835            DownNotification {
836                monitored: test_task_id(5, 0),
837                reason: DownReason::Normal,
838                monitor_ref: MonitorRef::from_raw(1),
839            },
840        );
841        batch.push(
842            same_vt,
843            DownNotification {
844                monitored: test_task_id(1, 0),
845                reason: DownReason::Normal,
846                monitor_ref: MonitorRef::from_raw(2),
847            },
848        );
849        batch.push(
850            same_vt,
851            DownNotification {
852                monitored: test_task_id(3, 0),
853                reason: DownReason::Normal,
854                monitor_ref: MonitorRef::from_raw(3),
855            },
856        );
857
858        let sorted = batch.into_sorted();
859        assert_eq!(sorted[0].monitored, test_task_id(1, 0));
860        assert_eq!(sorted[1].monitored, test_task_id(3, 0));
861        assert_eq!(sorted[2].monitored, test_task_id(5, 0));
862    }
863
864    #[test]
865    fn down_batch_tie_breaks_duplicate_target_by_monitor_ref() {
866        let mut batch = DownBatch::new();
867        let same_vt = Time::from_nanos(100);
868        let same_target = test_task_id(7, 0);
869
870        batch.push(
871            same_vt,
872            DownNotification {
873                monitored: same_target,
874                reason: DownReason::Normal,
875                monitor_ref: MonitorRef::from_raw(3),
876            },
877        );
878        batch.push(
879            same_vt,
880            DownNotification {
881                monitored: same_target,
882                reason: DownReason::Normal,
883                monitor_ref: MonitorRef::from_raw(1),
884            },
885        );
886        batch.push(
887            same_vt,
888            DownNotification {
889                monitored: same_target,
890                reason: DownReason::Normal,
891                monitor_ref: MonitorRef::from_raw(2),
892            },
893        );
894
895        let sorted = batch.into_sorted();
896        let refs: Vec<u64> = sorted.into_iter().map(|n| n.monitor_ref.id()).collect();
897        assert_eq!(refs, vec![1, 2, 3]);
898    }
899
900    #[test]
901    fn down_batch_tie_breaks_by_generation_then_slot() {
902        let mut batch = DownBatch::new();
903        let same_vt = Time::from_nanos(100);
904
905        // TaskId comparison: generation first, then slot (ArenaIndex ordering)
906        // TaskId(slot=1, gen=2) vs TaskId(slot=2, gen=1)
907        // ArenaIndex sorts by (generation, index) via derived Ord
908        batch.push(
909            same_vt,
910            DownNotification {
911                monitored: test_task_id(1, 2), // gen=2
912                reason: DownReason::Normal,
913                monitor_ref: MonitorRef::from_raw(1),
914            },
915        );
916        batch.push(
917            same_vt,
918            DownNotification {
919                monitored: test_task_id(2, 1), // gen=1
920                reason: DownReason::Normal,
921                monitor_ref: MonitorRef::from_raw(2),
922            },
923        );
924
925        let sorted = batch.into_sorted();
926        // The ordering depends on ArenaIndex's Ord implementation.
927        // TaskId wraps ArenaIndex which is (index, generation) — we need to verify.
928        // Both are valid orderings; what matters is determinism.
929        assert_eq!(sorted.len(), 2);
930        // The sort is deterministic: same input always produces same output.
931        let first = sorted[0].monitored;
932        let second = sorted[1].monitored;
933        assert_ne!(first, second);
934    }
935
936    #[test]
937    fn down_batch_mixed_vt_and_tid_ordering() {
938        let mut batch = DownBatch::new();
939
940        // Interleaved: some same vt, some different
941        batch.push(
942            Time::from_nanos(200),
943            DownNotification {
944                monitored: test_task_id(3, 0),
945                reason: DownReason::Normal,
946                monitor_ref: MonitorRef::from_raw(1),
947            },
948        );
949        batch.push(
950            Time::from_nanos(100),
951            DownNotification {
952                monitored: test_task_id(5, 0),
953                reason: DownReason::Error("err".into()),
954                monitor_ref: MonitorRef::from_raw(2),
955            },
956        );
957        batch.push(
958            Time::from_nanos(100),
959            DownNotification {
960                monitored: test_task_id(2, 0),
961                reason: DownReason::Cancelled(CancelReason::default()),
962                monitor_ref: MonitorRef::from_raw(3),
963            },
964        );
965        batch.push(
966            Time::from_nanos(200),
967            DownNotification {
968                monitored: test_task_id(1, 0),
969                reason: DownReason::Panicked(PanicPayload::new("boom")),
970                monitor_ref: MonitorRef::from_raw(4),
971            },
972        );
973
974        let sorted = batch.into_sorted();
975        // vt=100: tid=2 before tid=5
976        assert_eq!(sorted[0].monitored, test_task_id(2, 0));
977        assert_eq!(sorted[1].monitored, test_task_id(5, 0));
978        // vt=200: tid=1 before tid=3
979        assert_eq!(sorted[2].monitored, test_task_id(1, 0));
980        assert_eq!(sorted[3].monitored, test_task_id(3, 0));
981    }
982
983    // ── Integration: MonitorSet + DownBatch ─────────────────────────────
984
985    #[test]
986    fn end_to_end_monitor_to_notification() {
987        let mut set = MonitorSet::new();
988        let region = test_region_id(0, 0);
989        let watcher = test_task_id(1, 0);
990        let target1 = test_task_id(10, 0);
991        let target2 = test_task_id(20, 0);
992
993        let m1 = set.establish(watcher, region, target1);
994        let m2 = set.establish(watcher, region, target2);
995
996        // Both targets terminate at the same virtual time
997        let completion_vt = Time::from_nanos(500);
998        let mut batch = DownBatch::new();
999
1000        for (mref, _watcher_tid) in set.watchers_of(target1) {
1001            batch.push(
1002                completion_vt,
1003                DownNotification {
1004                    monitored: target1,
1005                    reason: DownReason::Normal,
1006                    monitor_ref: mref,
1007                },
1008            );
1009        }
1010        for (mref, _watcher_tid) in set.watchers_of(target2) {
1011            batch.push(
1012                completion_vt,
1013                DownNotification {
1014                    monitored: target2,
1015                    reason: DownReason::Error("fail".into()),
1016                    monitor_ref: mref,
1017                },
1018            );
1019        }
1020
1021        let sorted = batch.into_sorted();
1022        assert_eq!(sorted.len(), 2);
1023        // target1 (tid=10) before target2 (tid=20) at same vt
1024        assert_eq!(sorted[0].monitored, target1);
1025        assert_eq!(sorted[0].monitor_ref, m1);
1026        assert!(sorted[0].reason.is_normal());
1027
1028        assert_eq!(sorted[1].monitored, target2);
1029        assert_eq!(sorted[1].monitor_ref, m2);
1030        assert!(sorted[1].reason.is_error());
1031
1032        // Cleanup
1033        set.remove_monitored(target1);
1034        set.remove_monitored(target2);
1035        assert!(set.is_empty());
1036    }
1037
1038    #[test]
1039    fn region_cleanup_prevents_stale_notifications() {
1040        let mut set = MonitorSet::new();
1041        let region = test_region_id(1, 0);
1042        let watcher = test_task_id(1, 0);
1043        let target = test_task_id(10, 0);
1044
1045        set.establish(watcher, region, target);
1046
1047        // Region closes before target terminates
1048        set.cleanup_region(region);
1049
1050        // No watchers remain — no notifications should be generated
1051        assert!(set.watchers_of(target).is_empty());
1052        assert!(set.is_empty());
1053    }
1054
1055    // ---------------------------------------------------------------
1056    // Conformance tests (bd-1hkxo)
1057    //
1058    // - Multiple watchers on same target
1059    // - Multiple simultaneous downs (deterministic batch ordering)
1060    // - Cancellation interaction (region cleanup consistency)
1061    // - Monotone severity preservation in Down notifications
1062    // ---------------------------------------------------------------
1063
1064    /// Conformance: multiple watchers receive independent Down notifications
1065    /// when the monitored task terminates. Each watcher gets its own
1066    /// notification with its unique MonitorRef.
1067    #[test]
1068    fn conformance_multiple_watchers_independent_notifications() {
1069        let mut set = MonitorSet::new();
1070        let region = test_region_id(0, 0);
1071        let target = test_task_id(100, 0);
1072
1073        let w1 = test_task_id(1, 0);
1074        let w2 = test_task_id(2, 0);
1075        let w3 = test_task_id(3, 0);
1076        let w4 = test_task_id(4, 0);
1077
1078        let m1 = set.establish(w1, region, target);
1079        let m2 = set.establish(w2, region, target);
1080        let m3 = set.establish(w3, region, target);
1081        let m4 = set.establish(w4, region, target);
1082
1083        // Target terminates
1084        let watchers = set.watchers_of(target);
1085        assert_eq!(watchers.len(), 4);
1086
1087        let completion_vt = Time::from_nanos(1000);
1088        let mut batch = DownBatch::new();
1089        for (mref, _watcher) in &watchers {
1090            batch.push(
1091                completion_vt,
1092                DownNotification {
1093                    monitored: target,
1094                    reason: DownReason::Error("crash".into()),
1095                    monitor_ref: *mref,
1096                },
1097            );
1098        }
1099
1100        let sorted = batch.into_sorted();
1101        assert_eq!(sorted.len(), 4, "each watcher must receive a notification");
1102
1103        // All notifications reference the same target
1104        for notif in &sorted {
1105            assert_eq!(notif.monitored, target);
1106            assert!(notif.reason.is_error());
1107        }
1108
1109        // Each notification has a unique MonitorRef
1110        let mrefs: Vec<MonitorRef> = sorted.iter().map(|n| n.monitor_ref).collect();
1111        assert!(mrefs.contains(&m1));
1112        assert!(mrefs.contains(&m2));
1113        assert!(mrefs.contains(&m3));
1114        assert!(mrefs.contains(&m4));
1115    }
1116
1117    /// Conformance: multiple simultaneous downs are delivered in deterministic
1118    /// order. When N targets terminate at the same virtual time, notifications
1119    /// are sorted by (vt, monitored_tid).
1120    #[test]
1121    fn conformance_simultaneous_downs_deterministic_order() {
1122        let mut set = MonitorSet::new();
1123        let region = test_region_id(0, 0);
1124        let watcher = test_task_id(1, 0);
1125
1126        // Watcher monitors 5 targets
1127        let targets: Vec<TaskId> = (10..15).map(|i| test_task_id(i, 0)).collect();
1128        let mrefs: Vec<MonitorRef> = targets
1129            .iter()
1130            .map(|t| set.establish(watcher, region, *t))
1131            .collect();
1132
1133        // All 5 targets terminate at the SAME virtual time
1134        let same_vt = Time::from_nanos(500);
1135        let mut batch = DownBatch::new();
1136
1137        // Insert in reverse order to test that sorting overrides insertion order
1138        for i in (0..5).rev() {
1139            batch.push(
1140                same_vt,
1141                DownNotification {
1142                    monitored: targets[i],
1143                    reason: DownReason::Error(format!("error_{i}")),
1144                    monitor_ref: mrefs[i],
1145                },
1146            );
1147        }
1148
1149        let sorted = batch.into_sorted();
1150        assert_eq!(sorted.len(), 5);
1151
1152        // Sorted by TaskId since all vt are equal
1153        // targets[0]=tid(10), targets[1]=tid(11), ..., targets[4]=tid(14)
1154        for (i, notif) in sorted.iter().enumerate() {
1155            assert_eq!(
1156                notif.monitored,
1157                targets[i],
1158                "notification {i} should be for target tid({})",
1159                10 + i
1160            );
1161        }
1162
1163        // Run this 10 times to verify stability
1164        for _trial in 0..10 {
1165            let mut batch2 = DownBatch::new();
1166            for i in (0..5).rev() {
1167                batch2.push(
1168                    same_vt,
1169                    DownNotification {
1170                        monitored: targets[i],
1171                        reason: DownReason::Error(format!("error_{i}")),
1172                        monitor_ref: mrefs[i],
1173                    },
1174                );
1175            }
1176            let sorted2 = batch2.into_sorted();
1177            for (i, notif) in sorted2.iter().enumerate() {
1178                assert_eq!(notif.monitored, targets[i]);
1179            }
1180        }
1181    }
1182
1183    /// Conformance: mixed virtual times produce correct interleaved ordering.
1184    /// Multiple targets terminate at different times; ordering respects vt first,
1185    /// then tid for tie-breaking.
1186    #[test]
1187    fn conformance_mixed_vt_deterministic_interleaving() {
1188        let mut set = MonitorSet::new();
1189        let region = test_region_id(0, 0);
1190        let watcher = test_task_id(1, 0);
1191
1192        let t_a = test_task_id(5, 0);
1193        let t_b = test_task_id(3, 0);
1194        let t_c = test_task_id(8, 0);
1195        let t_d = test_task_id(2, 0);
1196
1197        let m_a = set.establish(watcher, region, t_a);
1198        let m_b = set.establish(watcher, region, t_b);
1199        let m_c = set.establish(watcher, region, t_c);
1200        let m_d = set.establish(watcher, region, t_d);
1201
1202        let mut batch = DownBatch::new();
1203        // Different vt values; some share the same vt
1204        batch.push(
1205            Time::from_nanos(200),
1206            DownNotification {
1207                monitored: t_a,
1208                reason: DownReason::Error("a".into()),
1209                monitor_ref: m_a,
1210            },
1211        );
1212        batch.push(
1213            Time::from_nanos(100),
1214            DownNotification {
1215                monitored: t_b,
1216                reason: DownReason::Panicked(PanicPayload::new("b")),
1217                monitor_ref: m_b,
1218            },
1219        );
1220        batch.push(
1221            Time::from_nanos(200),
1222            DownNotification {
1223                monitored: t_c,
1224                reason: DownReason::Normal,
1225                monitor_ref: m_c,
1226            },
1227        );
1228        batch.push(
1229            Time::from_nanos(100),
1230            DownNotification {
1231                monitored: t_d,
1232                reason: DownReason::Cancelled(CancelReason::default()),
1233                monitor_ref: m_d,
1234            },
1235        );
1236
1237        let sorted = batch.into_sorted();
1238        // vt=100: tid(2) before tid(3)
1239        assert_eq!(sorted[0].monitored, t_d); // tid(2), vt=100
1240        assert_eq!(sorted[1].monitored, t_b); // tid(3), vt=100
1241        assert_eq!(sorted[2].monitored, t_a); // vt=200: tid(5) before tid(8)
1242        assert_eq!(sorted[3].monitored, t_c); // tid(8), vt=200
1243    }
1244
1245    /// Conformance: region cleanup prevents stale Down delivery across
1246    /// multiple regions. Watchers in closed regions don't receive notifications;
1247    /// watchers in open regions still do.
1248    #[test]
1249    fn conformance_cancellation_cleanup_cross_region() {
1250        let mut set = MonitorSet::new();
1251        let r_closing = test_region_id(1, 0);
1252        let r_open = test_region_id(2, 0);
1253        let target = test_task_id(100, 0);
1254
1255        let w_closing = test_task_id(1, 0);
1256        let w_open = test_task_id(2, 0);
1257
1258        set.establish(w_closing, r_closing, target);
1259        let m_open = set.establish(w_open, r_open, target);
1260
1261        // Cancel region 1: w_closing's monitors are released
1262        let removed = set.cleanup_region(r_closing);
1263        assert_eq!(removed.len(), 1);
1264
1265        // Target terminates: only w_open should receive notification
1266        let watchers = set.watchers_of(target);
1267        assert_eq!(watchers.len(), 1);
1268        assert_eq!(watchers[0].0, m_open);
1269        assert_eq!(watchers[0].1, w_open);
1270
1271        // Build notification batch — only one notification
1272        let mut batch = DownBatch::new();
1273        for (mref, _) in &watchers {
1274            batch.push(
1275                Time::from_nanos(500),
1276                DownNotification {
1277                    monitored: target,
1278                    reason: DownReason::Error("target died".into()),
1279                    monitor_ref: *mref,
1280                },
1281            );
1282        }
1283
1284        let sorted = batch.into_sorted();
1285        assert_eq!(
1286            sorted.len(),
1287            1,
1288            "only the open-region watcher gets notified"
1289        );
1290        assert_eq!(sorted[0].monitor_ref, m_open);
1291    }
1292
1293    /// Conformance: after region cleanup, indexes are fully consistent.
1294    /// No dangling references in by_ref, by_monitored, or by_watcher_region.
1295    #[test]
1296    fn conformance_cleanup_index_consistency() {
1297        let mut set = MonitorSet::new();
1298        let r1 = test_region_id(1, 0);
1299        let r2 = test_region_id(2, 0);
1300
1301        let t1 = test_task_id(1, 0);
1302        let t2 = test_task_id(2, 0);
1303        let t3 = test_task_id(3, 0);
1304        let target = test_task_id(100, 0);
1305
1306        // Three watchers across two regions
1307        set.establish(t1, r1, target);
1308        set.establish(t2, r1, target);
1309        let m3 = set.establish(t3, r2, target);
1310
1311        // Cleanup region 1
1312        set.cleanup_region(r1);
1313
1314        // Only m3 remains
1315        assert_eq!(set.len(), 1);
1316        assert_eq!(set.watchers_of(target).len(), 1);
1317        assert_eq!(set.watcher_of(m3), Some(t3));
1318        assert_eq!(set.monitored_of(m3), Some(target));
1319
1320        // Cleanup region 2
1321        set.cleanup_region(r2);
1322        assert!(set.is_empty());
1323        assert!(set.watchers_of(target).is_empty());
1324    }
1325
1326    /// Conformance: monotone severity — Down notifications carry the exact
1327    /// DownReason from the task outcome. All four severity levels are preserved.
1328    #[test]
1329    fn conformance_monotone_severity_in_down() {
1330        let outcomes = vec![
1331            ("Normal", DownReason::Normal),
1332            ("Error", DownReason::Error("fail".into())),
1333            ("Cancelled", DownReason::Cancelled(CancelReason::default())),
1334            ("Panicked", DownReason::Panicked(PanicPayload::new("boom"))),
1335        ];
1336
1337        for (name, reason) in outcomes {
1338            let notif = DownNotification {
1339                monitored: test_task_id(1, 0),
1340                reason: reason.clone(),
1341                monitor_ref: MonitorRef::from_raw(1),
1342            };
1343
1344            // The notification carries the EXACT reason — no downgrade
1345            match name {
1346                "Normal" => assert!(notif.reason.is_normal()),
1347                "Error" => assert!(notif.reason.is_error()),
1348                "Cancelled" => assert!(notif.reason.is_cancelled()),
1349                "Panicked" => assert!(notif.reason.is_panicked()),
1350                _ => unreachable!(),
1351            }
1352        }
1353    }
1354
1355    /// Conformance: remove_monitored + cleanup_region applied in sequence
1356    /// produces a clean, empty set. No leaked internal state.
1357    #[test]
1358    fn conformance_sequential_cleanup_no_leaks() {
1359        let mut set = MonitorSet::new();
1360        let r1 = test_region_id(1, 0);
1361        let r2 = test_region_id(2, 0);
1362
1363        let w1 = test_task_id(1, 0);
1364        let w2 = test_task_id(2, 0);
1365        let t1 = test_task_id(10, 0);
1366        let t2 = test_task_id(20, 0);
1367
1368        // w1 (r1) monitors t1 and t2
1369        set.establish(w1, r1, t1);
1370        set.establish(w1, r1, t2);
1371        // w2 (r2) monitors t1
1372        set.establish(w2, r2, t1);
1373
1374        assert_eq!(set.len(), 3);
1375
1376        // t1 terminates: remove its monitors
1377        set.remove_monitored(t1);
1378        assert_eq!(set.len(), 1); // only w1 -> t2 remains
1379
1380        // Region 1 closes: remove remaining monitors
1381        set.cleanup_region(r1);
1382        assert!(set.is_empty());
1383
1384        // All queries return empty
1385        assert!(set.watchers_of(t1).is_empty());
1386        assert!(set.watchers_of(t2).is_empty());
1387        assert_eq!(set.len(), 0);
1388    }
1389
1390    /// Conformance: demonitor prevents Down delivery for the specific monitor
1391    /// while leaving other monitors on the same target intact.
1392    #[test]
1393    fn conformance_demonitor_selective_cancellation() {
1394        let mut set = MonitorSet::new();
1395        let region = test_region_id(0, 0);
1396        let target = test_task_id(100, 0);
1397
1398        let w1 = test_task_id(1, 0);
1399        let w2 = test_task_id(2, 0);
1400        let w3 = test_task_id(3, 0);
1401
1402        let m1 = set.establish(w1, region, target);
1403        let _m2 = set.establish(w2, region, target);
1404        let _m3 = set.establish(w3, region, target);
1405
1406        // Demonitor w1 only
1407        assert!(set.demonitor(m1));
1408
1409        // Only w2 and w3 remain as watchers
1410        let watchers = set.watchers_of(target);
1411        assert_eq!(watchers.len(), 2);
1412
1413        let watcher_tids: Vec<TaskId> = watchers.iter().map(|(_, t)| *t).collect();
1414        assert!(
1415            !watcher_tids.contains(&w1),
1416            "demonitored watcher must not appear"
1417        );
1418        assert!(watcher_tids.contains(&w2));
1419        assert!(watcher_tids.contains(&w3));
1420    }
1421
1422    #[test]
1423    fn monitor_ref_debug_clone_copy_eq_hash_ord() {
1424        use std::collections::HashSet;
1425
1426        let r = MonitorRef::from_raw(42);
1427        let dbg = format!("{r:?}");
1428        assert!(dbg.contains("MonitorRef"));
1429
1430        let r2 = r;
1431        assert_eq!(r, r2);
1432
1433        // Copy
1434        let r3 = r;
1435        assert_eq!(r, r3);
1436
1437        // Ord
1438        let r4 = MonitorRef::from_raw(100);
1439        assert!(r < r4);
1440
1441        // Hash
1442        let mut set = HashSet::new();
1443        set.insert(r);
1444        set.insert(r4);
1445        assert_eq!(set.len(), 2);
1446    }
1447
1448    #[test]
1449    fn down_reason_debug_clone_eq() {
1450        let d = DownReason::Normal;
1451        let dbg = format!("{d:?}");
1452        assert!(dbg.contains("Normal"));
1453
1454        let d2 = d.clone();
1455        assert_eq!(d, d2);
1456
1457        let d3 = DownReason::Error("oops".into());
1458        assert_ne!(d, d3);
1459    }
1460}
1461
1462// ============================================================================
1463// Conformance Tests
1464// ============================================================================
1465
1466#[cfg(test)]
1467#[path = "monitor_conformance_tests.rs"]
1468mod monitor_conformance_tests;
1469
1470#[cfg(test)]
1471mod conformance_integration {
1472    use super::monitor_conformance_tests::{MonitorConformanceHarness, TestVerdict};
1473
1474    #[test]
1475    fn monitor_conformance_suite() {
1476        crate::test_utils::init_test_logging();
1477
1478        let mut harness = MonitorConformanceHarness::new();
1479
1480        // Run the full conformance test suite
1481        let results = harness.run_full_suite();
1482
1483        let mut failures = Vec::new();
1484        let mut passes = 0;
1485
1486        for result in results {
1487            match result.verdict {
1488                TestVerdict::Pass => {
1489                    passes += 1;
1490                }
1491                TestVerdict::Fail(reason) => {
1492                    failures.push(format!("{}: {}", result.test_name, reason));
1493                }
1494            }
1495        }
1496
1497        assert!(
1498            failures.is_empty(),
1499            "Monitor conformance failures:\n{}",
1500            failures.join("\n")
1501        );
1502
1503        assert!(
1504            passes > 0,
1505            "No conformance tests passed - harness may be broken"
1506        );
1507
1508        crate::test_complete!("monitor_conformance_suite");
1509    }
1510}