Skip to main content

asupersync/lab/
snapshot_restore.rs

1//! Snapshot/restore functionality with quiescence proof.
2//!
3//! This module provides mechanisms for saving and restoring runtime state
4//! with formal guarantees about eventual quiescence.
5//!
6//! # Quiescence Proof Sketch
7//!
8//! **Theorem**: If a snapshot S is valid, then restoring S into a fresh
9//! runtime state R and running to completion yields quiescence.
10//!
11//! **Proof sketch**:
12//!
13//! 1. **Well-formedness invariant**: A valid snapshot satisfies:
14//!    - All task IDs reference valid regions
15//!    - All obligation IDs reference valid tasks
16//!    - The region tree is acyclic (parent references valid)
17//!    - No completed regions have non-terminal children
18//!
19//! 2. **Restoration preserves invariants**: The restore procedure:
20//!    - Creates regions in topological order (parents before children)
21//!    - Creates tasks only in their owning regions
22//!    - Restores obligations only for existing tasks
23//!    - Validates structural invariants before returning
24//!
25//! 3. **Quiescence convergence**: After restoration:
26//!    - All tasks are either terminal or schedulable
27//!    - The scheduler drains runnable tasks to completion
28//!    - Cancelled tasks follow the cancellation protocol (request→drain→finalize)
29//!    - Obligations are resolved by task completion or abort
30//!    - Region close waits for all children (by construction)
31//!
32//! 4. **Termination**: The system terminates because:
33//!    - Task count is finite and monotonically decreasing
34//!    - Each poll either completes or checkpoints
35//!    - Budgets bound the number of polls
36//!    - Finalizers have bounded budgets
37//!
38//! Therefore: restore(S) + run_to_completion() ⇒ quiescence(R)
39//!
40//! # Usage
41//!
42//! ```ignore
43//! use asupersync::lab::{LabRuntime, LabConfig, SnapshotRestore};
44//!
45//! // Create and run a runtime
46//! let mut runtime = LabRuntime::new(LabConfig::new(42));
47//! // ... do work ...
48//!
49//! // Take a restorable snapshot
50//! let snapshot = runtime.state.restorable_snapshot();
51//!
52//! // Later, restore into a fresh runtime
53//! let mut restored = LabRuntime::new(LabConfig::new(42));
54//! restored.restore_from_snapshot(&snapshot)?;
55//!
56//! // Run to quiescence
57//! restored.run_until_quiescent();
58//!
59//! // Verify invariants
60//! assert!(restored.oracles.quiescence.check().is_ok());
61//! assert!(restored.oracles.obligation_leak.check().is_ok());
62//! ```
63
64use crate::runtime::RuntimeState;
65use crate::runtime::state::{
66    IdSnapshot, ObligationStateSnapshot, RegionStateSnapshot, RuntimeSnapshot, TaskSnapshot,
67    TaskStateSnapshot,
68};
69use crate::types::Time;
70use serde::{Deserialize, Serialize};
71use std::collections::{HashMap, HashSet};
72use std::fmt;
73
74/// Errors that can occur during snapshot restoration.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum RestoreError {
77    /// A task references a non-existent region.
78    OrphanTask {
79        /// The orphan task's ID.
80        task_id: u32,
81        /// The non-existent region ID referenced by the task.
82        region_id: u32,
83    },
84    /// An obligation references a non-existent task.
85    OrphanObligation {
86        /// The orphan obligation's ID.
87        obligation_id: u32,
88        /// The non-existent task ID referenced by the obligation.
89        task_id: u32,
90    },
91    /// An obligation references a non-existent owning region.
92    OrphanObligationRegion {
93        /// The orphan obligation's ID.
94        obligation_id: u32,
95        /// The non-existent region ID referenced by the obligation.
96        region_id: u32,
97    },
98    /// An obligation's owning region disagrees with its holder task's region.
99    ObligationRegionMismatch {
100        /// The obligation with inconsistent ownership.
101        obligation_id: u32,
102        /// The task holding the obligation.
103        task_id: u32,
104        /// The holder task's actual region.
105        holder_region_id: u32,
106        /// The obligation's recorded owning region.
107        owning_region_id: u32,
108    },
109    /// A region references a non-existent parent.
110    InvalidParent {
111        /// The region with the invalid parent reference.
112        region_id: u32,
113        /// The non-existent parent region ID.
114        parent_id: u32,
115    },
116    /// The region tree contains a cycle.
117    CyclicRegionTree {
118        /// The region IDs forming the cycle.
119        cycle: Vec<u32>,
120    },
121    /// A closed region has non-terminal children.
122    NonQuiescentClosure {
123        /// The closed region that violates quiescence.
124        region_id: u32,
125        /// Child regions that are still live.
126        live_children: Vec<u32>,
127        /// Tasks that are still live.
128        live_tasks: Vec<u32>,
129    },
130    /// Snapshot timestamp is inconsistent.
131    InvalidTimestamp {
132        /// The snapshot's timestamp.
133        snapshot_time: u64,
134        /// The entity's timestamp that is inconsistent.
135        entity_time: u64,
136        /// Description of the entity with inconsistent timestamp.
137        entity: String,
138    },
139    /// Duplicate entity ID detected.
140    DuplicateId {
141        /// The kind of entity (e.g., "region", "task").
142        kind: &'static str,
143        /// The duplicate ID.
144        id: u32,
145    },
146}
147
148impl fmt::Display for RestoreError {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::OrphanTask { task_id, region_id } => {
152                write!(
153                    f,
154                    "task {task_id} references non-existent region {region_id}"
155                )
156            }
157            Self::OrphanObligation {
158                obligation_id,
159                task_id,
160            } => {
161                write!(
162                    f,
163                    "obligation {obligation_id} references non-existent task {task_id}"
164                )
165            }
166            Self::OrphanObligationRegion {
167                obligation_id,
168                region_id,
169            } => {
170                write!(
171                    f,
172                    "obligation {obligation_id} references non-existent owning region {region_id}"
173                )
174            }
175            Self::ObligationRegionMismatch {
176                obligation_id,
177                task_id,
178                holder_region_id,
179                owning_region_id,
180            } => {
181                write!(
182                    f,
183                    "obligation {obligation_id} held by task {task_id} is in region \
184                     {holder_region_id}, but records owning region {owning_region_id}"
185                )
186            }
187            Self::InvalidParent {
188                region_id,
189                parent_id,
190            } => {
191                write!(
192                    f,
193                    "region {region_id} references non-existent parent {parent_id}"
194                )
195            }
196            Self::CyclicRegionTree { cycle } => {
197                write!(f, "region tree contains cycle: {cycle:?}")
198            }
199            Self::NonQuiescentClosure {
200                region_id,
201                live_children,
202                live_tasks,
203            } => {
204                write!(
205                    f,
206                    "closed region {region_id} has {} live children and {} live tasks",
207                    live_children.len(),
208                    live_tasks.len()
209                )
210            }
211            Self::InvalidTimestamp {
212                snapshot_time,
213                entity_time,
214                entity,
215            } => {
216                write!(
217                    f,
218                    "timestamp inconsistency: snapshot={snapshot_time}, {entity}={entity_time}"
219                )
220            }
221            Self::DuplicateId { kind, id } => {
222                write!(f, "duplicate {kind} ID: {id}")
223            }
224        }
225    }
226}
227
228impl std::error::Error for RestoreError {}
229
230/// Result of snapshot validation.
231#[derive(Debug, Clone)]
232pub struct ValidationResult {
233    /// Whether the snapshot is valid.
234    pub is_valid: bool,
235    /// List of validation errors (empty if valid).
236    pub errors: Vec<RestoreError>,
237    /// Structural statistics.
238    pub stats: SnapshotStats,
239}
240
241/// Statistics about a snapshot's structure.
242#[derive(Debug, Clone, Default)]
243pub struct SnapshotStats {
244    /// Number of regions.
245    pub region_count: usize,
246    /// Number of tasks.
247    pub task_count: usize,
248    /// Number of obligations.
249    pub obligation_count: usize,
250    /// Maximum region tree depth.
251    pub max_depth: usize,
252    /// Number of terminal tasks.
253    pub terminal_task_count: usize,
254    /// Number of resolved obligations.
255    pub resolved_obligation_count: usize,
256    /// Number of closed regions.
257    pub closed_region_count: usize,
258}
259
260/// A snapshot that can be restored into a runtime state.
261///
262/// Extends `RuntimeSnapshot` with validation and restoration capabilities.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct RestorableSnapshot {
265    /// The underlying runtime snapshot.
266    pub snapshot: RuntimeSnapshot,
267    /// Schema version for forward compatibility.
268    pub schema_version: u32,
269    /// Content hash for integrity verification.
270    pub content_hash: u64,
271}
272
273impl RestorableSnapshot {
274    /// Current schema version.
275    pub const SCHEMA_VERSION: u32 = 1;
276
277    /// Creates a new restorable snapshot from a runtime snapshot.
278    #[must_use]
279    pub fn new(snapshot: RuntimeSnapshot) -> Self {
280        let schema_version = Self::SCHEMA_VERSION;
281        let content_hash = Self::compute_hash(schema_version, &snapshot);
282        Self {
283            snapshot,
284            schema_version,
285            content_hash,
286        }
287    }
288
289    /// Computes a deterministic hash of the snapshot content.
290    fn compute_hash(schema_version: u32, snapshot: &RuntimeSnapshot) -> u64 {
291        // FNV-1a hash for determinism
292        const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
293        const FNV_PRIME: u64 = 0x0100_0000_01b3;
294
295        let mut hash = FNV_OFFSET;
296        for byte in schema_version.to_le_bytes() {
297            hash ^= u64::from(byte);
298            hash = hash.wrapping_mul(FNV_PRIME);
299        }
300        // Hash full snapshot content (not just counts) so semantic tampering is detected.
301        // JSON encoding is deterministic here because RuntimeSnapshot and nested fields are
302        // structs/vectors with stable field order.
303        if let Ok(encoded) = serde_json::to_vec(snapshot) {
304            for byte in encoded {
305                hash ^= u64::from(byte);
306                hash = hash.wrapping_mul(FNV_PRIME);
307            }
308        } else {
309            // Keep behavior deterministic even if serialization unexpectedly fails.
310            for byte in b"snapshot-hash-serialization-error" {
311                hash ^= u64::from(*byte);
312                hash = hash.wrapping_mul(FNV_PRIME);
313            }
314        }
315
316        hash
317    }
318
319    /// Validates the snapshot for structural consistency.
320    ///
321    /// Checks:
322    /// - All task IDs reference valid regions
323    /// - All obligation IDs reference valid tasks
324    /// - The region tree is acyclic
325    /// - Closed regions have no live children/tasks
326    /// - Timestamps are consistent
327    #[must_use]
328    #[allow(clippy::too_many_lines)]
329    pub fn validate(&self) -> ValidationResult {
330        let mut errors = Vec::new();
331        let mut stats = SnapshotStats::default();
332
333        // Referential integrity must include generations to reject stale slot reuse.
334        let region_ids: HashSet<SnapshotIdKey> = self
335            .snapshot
336            .regions
337            .iter()
338            .map(|region| snapshot_id_key(region.id))
339            .collect();
340        let task_ids: HashSet<SnapshotIdKey> = self
341            .snapshot
342            .tasks
343            .iter()
344            .map(|task| snapshot_id_key(task.id))
345            .collect();
346        let task_regions: HashMap<SnapshotIdKey, SnapshotIdKey> = self
347            .snapshot
348            .tasks
349            .iter()
350            .map(|task| (snapshot_id_key(task.id), snapshot_id_key(task.region_id)))
351            .collect();
352        let region_slots: HashSet<u32> = self
353            .snapshot
354            .regions
355            .iter()
356            .map(|region| region.id.index)
357            .collect();
358        let task_slots: HashSet<u32> = self
359            .snapshot
360            .tasks
361            .iter()
362            .map(|task| task.id.index)
363            .collect();
364        let obligation_slots: HashSet<u32> = self
365            .snapshot
366            .obligations
367            .iter()
368            .map(|obligation| obligation.id.index)
369            .collect();
370
371        stats.region_count = self.snapshot.regions.len();
372        stats.task_count = self.snapshot.tasks.len();
373        stats.obligation_count = self.snapshot.obligations.len();
374        let snapshot_time = self.snapshot.timestamp;
375
376        // Check for duplicate region IDs
377        if region_slots.len() != self.snapshot.regions.len() {
378            // Find duplicates
379            let mut seen = HashSet::new();
380            for region in &self.snapshot.regions {
381                if !seen.insert(region.id.index) {
382                    errors.push(RestoreError::DuplicateId {
383                        kind: "region",
384                        id: region.id.index,
385                    });
386                }
387            }
388        }
389
390        // Check for duplicate task IDs
391        if task_slots.len() != self.snapshot.tasks.len() {
392            let mut seen = HashSet::new();
393            for task in &self.snapshot.tasks {
394                if !seen.insert(task.id.index) {
395                    errors.push(RestoreError::DuplicateId {
396                        kind: "task",
397                        id: task.id.index,
398                    });
399                }
400            }
401        }
402
403        // Check for duplicate obligation IDs
404        if obligation_slots.len() != self.snapshot.obligations.len() {
405            let mut seen = HashSet::new();
406            for obligation in &self.snapshot.obligations {
407                if !seen.insert(obligation.id.index) {
408                    errors.push(RestoreError::DuplicateId {
409                        kind: "obligation",
410                        id: obligation.id.index,
411                    });
412                }
413            }
414        }
415
416        // Validate tasks reference valid regions
417        for task in &self.snapshot.tasks {
418            if task.created_at > snapshot_time {
419                errors.push(RestoreError::InvalidTimestamp {
420                    snapshot_time,
421                    entity_time: task.created_at,
422                    entity: format!("task {} created_at", task.id.index),
423                });
424            }
425            if !region_ids.contains(&snapshot_id_key(task.region_id)) {
426                errors.push(RestoreError::OrphanTask {
427                    task_id: task.id.index,
428                    region_id: task.region_id.index,
429                });
430            }
431            if is_task_terminal(&task.state) {
432                stats.terminal_task_count += 1;
433            }
434        }
435
436        // Validate obligations reference valid tasks
437        for obligation in &self.snapshot.obligations {
438            if obligation.created_at > snapshot_time {
439                errors.push(RestoreError::InvalidTimestamp {
440                    snapshot_time,
441                    entity_time: obligation.created_at,
442                    entity: format!("obligation {} created_at", obligation.id.index),
443                });
444            }
445            if !task_ids.contains(&snapshot_id_key(obligation.holder_task)) {
446                errors.push(RestoreError::OrphanObligation {
447                    obligation_id: obligation.id.index,
448                    task_id: obligation.holder_task.index,
449                });
450            }
451            if !region_ids.contains(&snapshot_id_key(obligation.owning_region)) {
452                errors.push(RestoreError::OrphanObligationRegion {
453                    obligation_id: obligation.id.index,
454                    region_id: obligation.owning_region.index,
455                });
456            } else if let Some(holder_region_id) =
457                task_regions.get(&snapshot_id_key(obligation.holder_task))
458            {
459                if *holder_region_id != snapshot_id_key(obligation.owning_region) {
460                    errors.push(RestoreError::ObligationRegionMismatch {
461                        obligation_id: obligation.id.index,
462                        task_id: obligation.holder_task.index,
463                        holder_region_id: holder_region_id.0,
464                        owning_region_id: obligation.owning_region.index,
465                    });
466                }
467            }
468            if is_obligation_resolved(&obligation.state) {
469                stats.resolved_obligation_count += 1;
470            }
471        }
472
473        // Validate region tree structure
474        let mut parent_map: HashMap<SnapshotIdKey, Option<SnapshotIdKey>> = HashMap::new();
475        for region in &self.snapshot.regions {
476            parent_map.insert(
477                snapshot_id_key(region.id),
478                region.parent_id.map(snapshot_id_key),
479            );
480            if let Some(parent_id) = &region.parent_id {
481                if !region_ids.contains(&snapshot_id_key(*parent_id)) {
482                    errors.push(RestoreError::InvalidParent {
483                        region_id: region.id.index,
484                        parent_id: parent_id.index,
485                    });
486                }
487            }
488            if is_region_closed(&region.state) {
489                stats.closed_region_count += 1;
490            }
491        }
492
493        // Check for cycles in region tree
494        if let Some(cycle) = detect_cycle(&parent_map) {
495            errors.push(RestoreError::CyclicRegionTree { cycle });
496        }
497
498        // Compute max depth
499        stats.max_depth = compute_max_depth(&parent_map);
500
501        // Build region → tasks and region → children maps
502        let mut region_tasks: HashMap<SnapshotIdKey, Vec<&TaskSnapshot>> = HashMap::new();
503        for task in &self.snapshot.tasks {
504            region_tasks
505                .entry(snapshot_id_key(task.region_id))
506                .or_default()
507                .push(task);
508        }
509
510        let mut region_children: HashMap<SnapshotIdKey, Vec<SnapshotIdKey>> = HashMap::new();
511        let mut closed_regions: HashSet<SnapshotIdKey> = HashSet::new();
512        for region in &self.snapshot.regions {
513            if is_region_closed(&region.state) {
514                closed_regions.insert(snapshot_id_key(region.id));
515            }
516            if let Some(parent_id) = region.parent_id {
517                region_children
518                    .entry(snapshot_id_key(parent_id))
519                    .or_default()
520                    .push(snapshot_id_key(region.id));
521            }
522        }
523
524        // Validate quiescence for closed regions
525        for region in &self.snapshot.regions {
526            if is_region_closed(&region.state) {
527                let region_id = snapshot_id_key(region.id);
528                let live_children: Vec<u32> = region_children
529                    .get(&region_id)
530                    .map(|children| {
531                        children
532                            .iter()
533                            .filter(|&&child_id| !closed_regions.contains(&child_id))
534                            .map(|&(child_index, _)| child_index)
535                            .collect()
536                    })
537                    .unwrap_or_default();
538
539                let live_tasks: Vec<u32> = region_tasks
540                    .get(&region_id)
541                    .map(|tasks| {
542                        tasks
543                            .iter()
544                            .filter(|t| !is_task_terminal(&t.state))
545                            .map(|t| t.id.index)
546                            .collect()
547                    })
548                    .unwrap_or_default();
549
550                if !live_children.is_empty() || !live_tasks.is_empty() {
551                    errors.push(RestoreError::NonQuiescentClosure {
552                        region_id: region.id.index,
553                        live_children,
554                        live_tasks,
555                    });
556                }
557            }
558        }
559
560        ValidationResult {
561            is_valid: errors.is_empty(),
562            errors,
563            stats,
564        }
565    }
566
567    /// Verifies the content hash matches.
568    #[must_use]
569    pub fn verify_integrity(&self) -> bool {
570        Self::compute_hash(self.schema_version, &self.snapshot) == self.content_hash
571    }
572
573    /// Returns the snapshot timestamp.
574    #[must_use]
575    pub fn timestamp(&self) -> Time {
576        Time::from_nanos(self.snapshot.timestamp)
577    }
578}
579
580/// Checks if a task state is terminal.
581fn is_task_terminal(state: &TaskStateSnapshot) -> bool {
582    matches!(state, TaskStateSnapshot::Completed { .. })
583}
584
585/// Checks if an obligation state is resolved.
586fn is_obligation_resolved(state: &ObligationStateSnapshot) -> bool {
587    matches!(
588        state,
589        ObligationStateSnapshot::Committed
590            | ObligationStateSnapshot::Aborted
591            | ObligationStateSnapshot::Leaked
592    )
593}
594
595/// Checks if a region state is closed.
596fn is_region_closed(state: &RegionStateSnapshot) -> bool {
597    matches!(state, RegionStateSnapshot::Closed)
598}
599
600type SnapshotIdKey = (u32, u32);
601
602fn snapshot_id_key(id: IdSnapshot) -> SnapshotIdKey {
603    (id.index, id.generation)
604}
605
606/// Detects a cycle in the parent map, returning the cycle if found.
607fn detect_cycle(parent_map: &HashMap<SnapshotIdKey, Option<SnapshotIdKey>>) -> Option<Vec<u32>> {
608    for &start in parent_map.keys() {
609        let mut visited = HashSet::new();
610        let mut path = Vec::new();
611        let mut current = Some(start);
612
613        while let Some(node) = current {
614            if visited.contains(&node) {
615                // Found a cycle - extract it
616                if let Some(pos) = path.iter().position(|&key| key == node) {
617                    return Some(path[pos..].iter().map(|(index, _)| *index).collect());
618                }
619            }
620            visited.insert(node);
621            path.push(node);
622            current = parent_map.get(&node).copied().flatten();
623        }
624    }
625    None
626}
627
628/// Computes the maximum depth of the region tree.
629fn compute_max_depth(parent_map: &HashMap<SnapshotIdKey, Option<SnapshotIdKey>>) -> usize {
630    let mut max_depth = 0;
631    for &start in parent_map.keys() {
632        let mut depth = 0;
633        let mut current = Some(start);
634        let mut visited = HashSet::new();
635        while let Some(node) = current {
636            if !visited.insert(node) {
637                // Break on cycle to keep depth computation total.
638                break;
639            }
640            depth += 1;
641            current = parent_map.get(&node).copied().flatten();
642        }
643        max_depth = max_depth.max(depth);
644    }
645    max_depth
646}
647
648/// Extension trait for creating restorable snapshots.
649pub trait SnapshotRestore {
650    /// Creates a restorable snapshot of the current state.
651    fn restorable_snapshot(&self) -> RestorableSnapshot;
652}
653
654impl SnapshotRestore for RuntimeState {
655    fn restorable_snapshot(&self) -> RestorableSnapshot {
656        RestorableSnapshot::new(self.snapshot())
657    }
658}
659
660// ─── Tests ──────────────────────────────────────────────────────────────────
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::runtime::state::IdSnapshot;
666    use crate::runtime::state::{
667        BudgetSnapshot, ObligationKindSnapshot, ObligationSnapshot, RegionSnapshot,
668    };
669
670    fn init_test(name: &str) {
671        crate::test_utils::init_test_logging();
672        crate::test_phase!(name);
673    }
674
675    fn snap_id(index: u32, generation: u32) -> IdSnapshot {
676        IdSnapshot { index, generation }
677    }
678
679    fn make_region(id: u32, parent: Option<u32>, state: RegionStateSnapshot) -> RegionSnapshot {
680        RegionSnapshot {
681            id: snap_id(id, 0),
682            parent_id: parent.map(|p| snap_id(p, 0)),
683            state,
684            budget: BudgetSnapshot {
685                deadline: None,
686                poll_quota: 1000,
687                cost_quota: None,
688                priority: 100,
689            },
690            child_count: 0,
691            task_count: 0,
692            name: None,
693        }
694    }
695
696    fn make_task(id: u32, region_id: u32, state: TaskStateSnapshot) -> TaskSnapshot {
697        TaskSnapshot {
698            id: snap_id(id, 0),
699            region_id: snap_id(region_id, 0),
700            state,
701            name: None,
702            poll_count: 0,
703            created_at: 0,
704            obligations: Vec::new(),
705        }
706    }
707
708    fn make_obligation(
709        id: u32,
710        task_id: u32,
711        state: ObligationStateSnapshot,
712    ) -> ObligationSnapshot {
713        make_obligation_in_region(id, task_id, 0, state)
714    }
715
716    fn make_obligation_in_region(
717        id: u32,
718        task_id: u32,
719        owning_region: u32,
720        state: ObligationStateSnapshot,
721    ) -> ObligationSnapshot {
722        ObligationSnapshot {
723            id: snap_id(id, 0),
724            kind: ObligationKindSnapshot::SendPermit,
725            state,
726            holder_task: snap_id(task_id, 0),
727            owning_region: snap_id(owning_region, 0),
728            created_at: 0,
729        }
730    }
731
732    fn make_snapshot(
733        regions: Vec<RegionSnapshot>,
734        tasks: Vec<TaskSnapshot>,
735        obligations: Vec<ObligationSnapshot>,
736    ) -> RestorableSnapshot {
737        RestorableSnapshot::new(RuntimeSnapshot {
738            timestamp: 1000,
739            regions,
740            tasks,
741            obligations,
742            recent_events: Vec::new(),
743        })
744    }
745
746    #[test]
747    fn empty_snapshot_is_valid() {
748        init_test("empty_snapshot_is_valid");
749        let snapshot = make_snapshot(Vec::new(), Vec::new(), Vec::new());
750        let result = snapshot.validate();
751
752        crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
753        let errors_empty = result.errors.is_empty();
754        crate::assert_with_log!(errors_empty, "errors empty", true, errors_empty);
755        crate::test_complete!("empty_snapshot_is_valid");
756    }
757
758    #[test]
759    fn single_region_is_valid() {
760        init_test("single_region_is_valid");
761        let snapshot = make_snapshot(
762            vec![make_region(0, None, RegionStateSnapshot::Open)],
763            Vec::new(),
764            Vec::new(),
765        );
766        let result = snapshot.validate();
767
768        crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
769        crate::assert_with_log!(
770            result.stats.region_count == 1,
771            "region_count",
772            1,
773            result.stats.region_count
774        );
775        crate::test_complete!("single_region_is_valid");
776    }
777
778    #[test]
779    fn task_with_valid_region_is_valid() {
780        init_test("task_with_valid_region_is_valid");
781        let snapshot = make_snapshot(
782            vec![make_region(0, None, RegionStateSnapshot::Open)],
783            vec![make_task(0, 0, TaskStateSnapshot::Running)],
784            Vec::new(),
785        );
786        let result = snapshot.validate();
787
788        crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
789        crate::assert_with_log!(
790            result.stats.task_count == 1,
791            "task_count",
792            1,
793            result.stats.task_count
794        );
795        crate::test_complete!("task_with_valid_region_is_valid");
796    }
797
798    #[test]
799    fn orphan_task_detected() {
800        init_test("orphan_task_detected");
801        let snapshot = make_snapshot(
802            vec![make_region(0, None, RegionStateSnapshot::Open)],
803            vec![make_task(0, 99, TaskStateSnapshot::Running)], // region 99 doesn't exist
804            Vec::new(),
805        );
806        let result = snapshot.validate();
807
808        let not_valid = !result.is_valid;
809        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
810        let has_error = result
811            .errors
812            .iter()
813            .any(|e| matches!(e, RestoreError::OrphanTask { .. }));
814        crate::assert_with_log!(has_error, "has OrphanTask error", true, has_error);
815        crate::test_complete!("orphan_task_detected");
816    }
817
818    #[test]
819    fn task_with_stale_region_generation_is_orphaned() {
820        init_test("task_with_stale_region_generation_is_orphaned");
821        let mut snapshot = make_snapshot(
822            vec![make_region(7, None, RegionStateSnapshot::Open)],
823            vec![make_task(0, 7, TaskStateSnapshot::Running)],
824            Vec::new(),
825        );
826        snapshot.snapshot.regions[0].id = snap_id(7, 1);
827
828        let result = snapshot.validate();
829
830        let not_valid = !result.is_valid;
831        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
832        let has_error = result.errors.iter().any(|e| {
833            matches!(
834                e,
835                RestoreError::OrphanTask {
836                    task_id: 0,
837                    region_id: 7,
838                }
839            )
840        });
841        crate::assert_with_log!(
842            has_error,
843            "generation mismatch yields OrphanTask",
844            true,
845            has_error
846        );
847        crate::test_complete!("task_with_stale_region_generation_is_orphaned");
848    }
849
850    #[test]
851    fn orphan_obligation_detected() {
852        init_test("orphan_obligation_detected");
853        let snapshot = make_snapshot(
854            vec![make_region(0, None, RegionStateSnapshot::Open)],
855            vec![make_task(0, 0, TaskStateSnapshot::Running)],
856            vec![make_obligation(0, 99, ObligationStateSnapshot::Reserved)], // task 99 doesn't exist
857        );
858        let result = snapshot.validate();
859
860        let not_valid = !result.is_valid;
861        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
862        let has_error = result
863            .errors
864            .iter()
865            .any(|e| matches!(e, RestoreError::OrphanObligation { .. }));
866        crate::assert_with_log!(has_error, "has OrphanObligation error", true, has_error);
867        crate::test_complete!("orphan_obligation_detected");
868    }
869
870    #[test]
871    fn obligation_with_stale_holder_generation_is_orphaned() {
872        init_test("obligation_with_stale_holder_generation_is_orphaned");
873        let mut snapshot = make_snapshot(
874            vec![make_region(0, None, RegionStateSnapshot::Open)],
875            vec![make_task(5, 0, TaskStateSnapshot::Running)],
876            vec![make_obligation(0, 5, ObligationStateSnapshot::Reserved)],
877        );
878        snapshot.snapshot.tasks[0].id = snap_id(5, 1);
879
880        let result = snapshot.validate();
881
882        let not_valid = !result.is_valid;
883        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
884        let has_error = result.errors.iter().any(|e| {
885            matches!(
886                e,
887                RestoreError::OrphanObligation {
888                    obligation_id: 0,
889                    task_id: 5,
890                }
891            )
892        });
893        crate::assert_with_log!(
894            has_error,
895            "generation mismatch yields OrphanObligation",
896            true,
897            has_error
898        );
899        crate::test_complete!("obligation_with_stale_holder_generation_is_orphaned");
900    }
901
902    #[test]
903    fn orphan_obligation_region_detected() {
904        init_test("orphan_obligation_region_detected");
905        let snapshot = make_snapshot(
906            vec![make_region(0, None, RegionStateSnapshot::Open)],
907            vec![make_task(0, 0, TaskStateSnapshot::Running)],
908            vec![make_obligation_in_region(
909                0,
910                0,
911                99,
912                ObligationStateSnapshot::Reserved,
913            )],
914        );
915        let result = snapshot.validate();
916
917        let not_valid = !result.is_valid;
918        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
919        let has_error = result
920            .errors
921            .iter()
922            .any(|e| matches!(e, RestoreError::OrphanObligationRegion { .. }));
923        crate::assert_with_log!(
924            has_error,
925            "has OrphanObligationRegion error",
926            true,
927            has_error
928        );
929        crate::test_complete!("orphan_obligation_region_detected");
930    }
931
932    #[test]
933    fn obligation_with_stale_owning_region_generation_is_orphaned() {
934        init_test("obligation_with_stale_owning_region_generation_is_orphaned");
935        let mut snapshot = make_snapshot(
936            vec![make_region(3, None, RegionStateSnapshot::Open)],
937            vec![make_task(0, 3, TaskStateSnapshot::Running)],
938            vec![make_obligation_in_region(
939                0,
940                0,
941                3,
942                ObligationStateSnapshot::Reserved,
943            )],
944        );
945        snapshot.snapshot.regions[0].id = snap_id(3, 1);
946        snapshot.snapshot.tasks[0].region_id = snap_id(3, 1);
947
948        let result = snapshot.validate();
949
950        let not_valid = !result.is_valid;
951        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
952        let has_error = result.errors.iter().any(|e| {
953            matches!(
954                e,
955                RestoreError::OrphanObligationRegion {
956                    obligation_id: 0,
957                    region_id: 3,
958                }
959            )
960        });
961        crate::assert_with_log!(
962            has_error,
963            "generation mismatch yields OrphanObligationRegion",
964            true,
965            has_error
966        );
967        crate::test_complete!("obligation_with_stale_owning_region_generation_is_orphaned");
968    }
969
970    #[test]
971    fn obligation_region_mismatch_detected() {
972        init_test("obligation_region_mismatch_detected");
973        let snapshot = make_snapshot(
974            vec![
975                make_region(0, None, RegionStateSnapshot::Open),
976                make_region(1, None, RegionStateSnapshot::Open),
977            ],
978            vec![make_task(0, 0, TaskStateSnapshot::Running)],
979            vec![make_obligation_in_region(
980                0,
981                0,
982                1,
983                ObligationStateSnapshot::Reserved,
984            )],
985        );
986        let result = snapshot.validate();
987
988        let not_valid = !result.is_valid;
989        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
990        let has_error = result
991            .errors
992            .iter()
993            .any(|e| matches!(e, RestoreError::ObligationRegionMismatch { .. }));
994        crate::assert_with_log!(
995            has_error,
996            "has ObligationRegionMismatch error",
997            true,
998            has_error
999        );
1000        crate::test_complete!("obligation_region_mismatch_detected");
1001    }
1002
1003    #[test]
1004    fn invalid_parent_detected() {
1005        init_test("invalid_parent_detected");
1006        let snapshot = make_snapshot(
1007            vec![
1008                make_region(0, None, RegionStateSnapshot::Open),
1009                make_region(1, Some(99), RegionStateSnapshot::Open), // parent 99 doesn't exist
1010            ],
1011            Vec::new(),
1012            Vec::new(),
1013        );
1014        let result = snapshot.validate();
1015
1016        let not_valid = !result.is_valid;
1017        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1018        let has_error = result
1019            .errors
1020            .iter()
1021            .any(|e| matches!(e, RestoreError::InvalidParent { .. }));
1022        crate::assert_with_log!(has_error, "has InvalidParent error", true, has_error);
1023        crate::test_complete!("invalid_parent_detected");
1024    }
1025
1026    #[test]
1027    fn parent_generation_mismatch_detected() {
1028        init_test("parent_generation_mismatch_detected");
1029        let mut snapshot = make_snapshot(
1030            vec![
1031                make_region(0, None, RegionStateSnapshot::Open),
1032                make_region(1, Some(0), RegionStateSnapshot::Open),
1033            ],
1034            Vec::new(),
1035            Vec::new(),
1036        );
1037        snapshot.snapshot.regions[0].id = snap_id(0, 1);
1038
1039        let result = snapshot.validate();
1040
1041        let not_valid = !result.is_valid;
1042        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1043        let has_error = result.errors.iter().any(|e| {
1044            matches!(
1045                e,
1046                RestoreError::InvalidParent {
1047                    region_id: 1,
1048                    parent_id: 0,
1049                }
1050            )
1051        });
1052        crate::assert_with_log!(
1053            has_error,
1054            "generation mismatch yields InvalidParent",
1055            true,
1056            has_error
1057        );
1058        crate::test_complete!("parent_generation_mismatch_detected");
1059    }
1060
1061    #[test]
1062    fn closed_region_with_live_task_detected() {
1063        init_test("closed_region_with_live_task_detected");
1064        let snapshot = make_snapshot(
1065            vec![make_region(0, None, RegionStateSnapshot::Closed)],
1066            vec![make_task(0, 0, TaskStateSnapshot::Running)], // task still running in closed region
1067            Vec::new(),
1068        );
1069        let result = snapshot.validate();
1070
1071        let not_valid = !result.is_valid;
1072        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1073        let has_error = result
1074            .errors
1075            .iter()
1076            .any(|e| matches!(e, RestoreError::NonQuiescentClosure { .. }));
1077        crate::assert_with_log!(has_error, "has NonQuiescentClosure error", true, has_error);
1078        crate::test_complete!("closed_region_with_live_task_detected");
1079    }
1080
1081    #[test]
1082    fn nested_regions_valid() {
1083        init_test("nested_regions_valid");
1084        let snapshot = make_snapshot(
1085            vec![
1086                make_region(0, None, RegionStateSnapshot::Open),
1087                make_region(1, Some(0), RegionStateSnapshot::Open),
1088                make_region(2, Some(1), RegionStateSnapshot::Open),
1089            ],
1090            Vec::new(),
1091            Vec::new(),
1092        );
1093        let result = snapshot.validate();
1094
1095        crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
1096        crate::assert_with_log!(
1097            result.stats.max_depth == 3,
1098            "max_depth",
1099            3,
1100            result.stats.max_depth
1101        );
1102        crate::test_complete!("nested_regions_valid");
1103    }
1104
1105    #[test]
1106    fn terminal_task_stats_computed() {
1107        init_test("terminal_task_stats_computed");
1108        let snapshot = make_snapshot(
1109            vec![make_region(0, None, RegionStateSnapshot::Open)],
1110            vec![
1111                make_task(0, 0, TaskStateSnapshot::Running),
1112                make_task(
1113                    1,
1114                    0,
1115                    TaskStateSnapshot::Completed {
1116                        outcome: crate::runtime::state::OutcomeSnapshot::Ok,
1117                    },
1118                ),
1119            ],
1120            Vec::new(),
1121        );
1122        let result = snapshot.validate();
1123
1124        crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
1125        crate::assert_with_log!(
1126            result.stats.terminal_task_count == 1,
1127            "terminal_task_count",
1128            1,
1129            result.stats.terminal_task_count
1130        );
1131        crate::test_complete!("terminal_task_stats_computed");
1132    }
1133
1134    #[test]
1135    fn content_hash_deterministic() {
1136        init_test("content_hash_deterministic");
1137        let snapshot1 = make_snapshot(
1138            vec![make_region(0, None, RegionStateSnapshot::Open)],
1139            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1140            Vec::new(),
1141        );
1142        let snapshot2 = make_snapshot(
1143            vec![make_region(0, None, RegionStateSnapshot::Open)],
1144            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1145            Vec::new(),
1146        );
1147
1148        crate::assert_with_log!(
1149            snapshot1.content_hash == snapshot2.content_hash,
1150            "hashes equal",
1151            snapshot1.content_hash,
1152            snapshot2.content_hash
1153        );
1154        crate::test_complete!("content_hash_deterministic");
1155    }
1156
1157    #[test]
1158    fn integrity_verification_works() {
1159        init_test("integrity_verification_works");
1160        let snapshot = make_snapshot(
1161            vec![make_region(0, None, RegionStateSnapshot::Open)],
1162            Vec::new(),
1163            Vec::new(),
1164        );
1165
1166        let valid = snapshot.verify_integrity();
1167        crate::assert_with_log!(valid, "integrity valid", true, valid);
1168
1169        // Tamper with hash
1170        let mut tampered = snapshot;
1171        tampered.content_hash ^= 1;
1172        let invalid = !tampered.verify_integrity();
1173        crate::assert_with_log!(invalid, "tampered invalid", true, invalid);
1174
1175        crate::test_complete!("integrity_verification_works");
1176    }
1177
1178    #[test]
1179    fn integrity_verification_detects_semantic_tampering() {
1180        init_test("integrity_verification_detects_semantic_tampering");
1181        let snapshot = make_snapshot(
1182            vec![make_region(0, None, RegionStateSnapshot::Open)],
1183            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1184            vec![make_obligation(0, 0, ObligationStateSnapshot::Reserved)],
1185        );
1186
1187        let mut tampered = snapshot;
1188        tampered.snapshot.tasks[0].state = TaskStateSnapshot::Completed {
1189            outcome: crate::runtime::state::OutcomeSnapshot::Ok,
1190        };
1191
1192        let invalid = !tampered.verify_integrity();
1193        crate::assert_with_log!(invalid, "semantic tamper invalid", true, invalid);
1194
1195        crate::test_complete!("integrity_verification_detects_semantic_tampering");
1196    }
1197
1198    #[test]
1199    fn integrity_verification_detects_schema_version_tampering() {
1200        init_test("integrity_verification_detects_schema_version_tampering");
1201        let snapshot = make_snapshot(
1202            vec![make_region(0, None, RegionStateSnapshot::Open)],
1203            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1204            Vec::new(),
1205        );
1206
1207        let mut tampered = snapshot;
1208        tampered.schema_version = tampered.schema_version.saturating_add(1);
1209
1210        let invalid = !tampered.verify_integrity();
1211        crate::assert_with_log!(invalid, "schema version tamper invalid", true, invalid);
1212
1213        crate::test_complete!("integrity_verification_detects_schema_version_tampering");
1214    }
1215
1216    #[test]
1217    fn duplicate_region_id_detected() {
1218        init_test("duplicate_region_id_detected");
1219        let snapshot = make_snapshot(
1220            vec![
1221                make_region(0, None, RegionStateSnapshot::Open),
1222                make_region(0, None, RegionStateSnapshot::Open), // duplicate
1223            ],
1224            Vec::new(),
1225            Vec::new(),
1226        );
1227        let result = snapshot.validate();
1228
1229        let not_valid = !result.is_valid;
1230        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1231        let has_error = result
1232            .errors
1233            .iter()
1234            .any(|e| matches!(e, RestoreError::DuplicateId { kind: "region", .. }));
1235        crate::assert_with_log!(has_error, "has DuplicateId error", true, has_error);
1236        crate::test_complete!("duplicate_region_id_detected");
1237    }
1238
1239    #[test]
1240    fn duplicate_obligation_id_detected() {
1241        init_test("duplicate_obligation_id_detected");
1242        let snapshot = make_snapshot(
1243            vec![make_region(0, None, RegionStateSnapshot::Open)],
1244            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1245            vec![
1246                make_obligation(7, 0, ObligationStateSnapshot::Reserved),
1247                make_obligation(7, 0, ObligationStateSnapshot::Committed), // duplicate
1248            ],
1249        );
1250        let result = snapshot.validate();
1251
1252        let not_valid = !result.is_valid;
1253        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1254        let has_error = result.errors.iter().any(|e| {
1255            matches!(
1256                e,
1257                RestoreError::DuplicateId {
1258                    kind: "obligation",
1259                    ..
1260                }
1261            )
1262        });
1263        crate::assert_with_log!(
1264            has_error,
1265            "has obligation DuplicateId error",
1266            true,
1267            has_error
1268        );
1269        crate::test_complete!("duplicate_obligation_id_detected");
1270    }
1271
1272    #[test]
1273    fn cyclic_region_tree_detected_without_depth_hang() {
1274        init_test("cyclic_region_tree_detected_without_depth_hang");
1275        let snapshot = make_snapshot(
1276            vec![
1277                make_region(0, Some(1), RegionStateSnapshot::Open),
1278                make_region(1, Some(0), RegionStateSnapshot::Open),
1279            ],
1280            Vec::new(),
1281            Vec::new(),
1282        );
1283        let result = snapshot.validate();
1284
1285        let not_valid = !result.is_valid;
1286        crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1287        let has_cycle = result
1288            .errors
1289            .iter()
1290            .any(|e| matches!(e, RestoreError::CyclicRegionTree { .. }));
1291        crate::assert_with_log!(has_cycle, "has CyclicRegionTree error", true, has_cycle);
1292        crate::assert_with_log!(
1293            result.stats.max_depth == 2,
1294            "max_depth bounded with cycle",
1295            2,
1296            result.stats.max_depth
1297        );
1298        crate::test_complete!("cyclic_region_tree_detected_without_depth_hang");
1299    }
1300
1301    #[test]
1302    fn resolved_obligation_stats_computed() {
1303        init_test("resolved_obligation_stats_computed");
1304        let snapshot = make_snapshot(
1305            vec![make_region(0, None, RegionStateSnapshot::Open)],
1306            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1307            vec![
1308                make_obligation(0, 0, ObligationStateSnapshot::Reserved),
1309                make_obligation(1, 0, ObligationStateSnapshot::Committed),
1310                make_obligation(2, 0, ObligationStateSnapshot::Aborted),
1311            ],
1312        );
1313        let result = snapshot.validate();
1314
1315        crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
1316        crate::assert_with_log!(
1317            result.stats.resolved_obligation_count == 2,
1318            "resolved_obligation_count",
1319            2,
1320            result.stats.resolved_obligation_count
1321        );
1322        crate::test_complete!("resolved_obligation_stats_computed");
1323    }
1324
1325    #[test]
1326    fn task_timestamp_after_snapshot_detected() {
1327        init_test("task_timestamp_after_snapshot_detected");
1328        let mut snapshot = make_snapshot(
1329            vec![make_region(0, None, RegionStateSnapshot::Open)],
1330            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1331            Vec::new(),
1332        );
1333        snapshot.snapshot.tasks[0].created_at = snapshot.snapshot.timestamp + 1;
1334
1335        let result = snapshot.validate();
1336        let has_error = result.errors.iter().any(|e| {
1337            matches!(
1338                e,
1339                RestoreError::InvalidTimestamp {
1340                    entity, ..
1341                } if entity.contains("task 0 created_at")
1342            )
1343        });
1344        crate::assert_with_log!(
1345            has_error,
1346            "task invalid timestamp detected",
1347            true,
1348            has_error
1349        );
1350        crate::test_complete!("task_timestamp_after_snapshot_detected");
1351    }
1352
1353    #[test]
1354    fn obligation_timestamp_after_snapshot_detected() {
1355        init_test("obligation_timestamp_after_snapshot_detected");
1356        let mut snapshot = make_snapshot(
1357            vec![make_region(0, None, RegionStateSnapshot::Open)],
1358            vec![make_task(0, 0, TaskStateSnapshot::Running)],
1359            vec![make_obligation(0, 0, ObligationStateSnapshot::Reserved)],
1360        );
1361        snapshot.snapshot.obligations[0].created_at = snapshot.snapshot.timestamp + 1;
1362
1363        let result = snapshot.validate();
1364        let has_error = result.errors.iter().any(|e| {
1365            matches!(
1366                e,
1367                RestoreError::InvalidTimestamp {
1368                    entity, ..
1369                } if entity.contains("obligation 0 created_at")
1370            )
1371        });
1372        crate::assert_with_log!(
1373            has_error,
1374            "obligation invalid timestamp detected",
1375            true,
1376            has_error
1377        );
1378        crate::test_complete!("obligation_timestamp_after_snapshot_detected");
1379    }
1380
1381    // ── derive-trait coverage (wave 73) ──────────────────────────────────
1382
1383    #[test]
1384    fn restore_error_debug_clone_eq() {
1385        let e1 = RestoreError::OrphanTask {
1386            task_id: 5,
1387            region_id: 99,
1388        };
1389        let e2 = e1.clone();
1390        assert_eq!(e1, e2);
1391        let dbg = format!("{e1:?}");
1392        assert!(dbg.contains("OrphanTask"));
1393
1394        let e3 = RestoreError::CyclicRegionTree {
1395            cycle: vec![1, 2, 3],
1396        };
1397        let e4 = e3.clone();
1398        assert_eq!(e3, e4);
1399        assert_ne!(e1, e3);
1400    }
1401
1402    #[test]
1403    fn snapshot_stats_debug_clone_default() {
1404        let s = SnapshotStats::default();
1405        assert_eq!(s.region_count, 0);
1406        assert_eq!(s.task_count, 0);
1407        assert_eq!(s.obligation_count, 0);
1408        assert_eq!(s.max_depth, 0);
1409        assert_eq!(s.terminal_task_count, 0);
1410        assert_eq!(s.resolved_obligation_count, 0);
1411        assert_eq!(s.closed_region_count, 0);
1412
1413        let s2 = s;
1414        let dbg = format!("{s2:?}");
1415        assert!(dbg.contains("SnapshotStats"));
1416    }
1417
1418    #[test]
1419    fn validation_result_debug_clone() {
1420        let vr = ValidationResult {
1421            is_valid: true,
1422            errors: vec![],
1423            stats: SnapshotStats::default(),
1424        };
1425        let vr2 = vr;
1426        assert!(vr2.is_valid);
1427        assert!(vr2.errors.is_empty());
1428        let dbg = format!("{vr2:?}");
1429        assert!(dbg.contains("ValidationResult"));
1430    }
1431}