Skip to main content

nexo_memory_snapshot/
error.rs

1//! Typed errors for snapshot operations.
2
3use crate::id::{AgentId, SnapshotId};
4
5/// Failure modes surfaced by every public method on
6/// [`crate::snapshotter::MemorySnapshotter`]. Each variant maps to a stable
7/// CLI exit code + Prometheus `outcome` label so operators and tests can
8/// branch on the failure class without parsing strings.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum SnapshotError {
12    /// Another snapshot is already being captured for this agent. Holders
13    /// release the lock on completion or after the configured timeout.
14    #[error("snapshot already in progress for agent {0}")]
15    Concurrent(AgentId),
16
17    /// The agent id has no on-disk state under this tenant root.
18    #[error("agent not found: {0}")]
19    UnknownAgent(AgentId),
20
21    /// A bundle path resolved outside its tenant root. Always a
22    /// programming error or a hostile bundle name; never a transient
23    /// failure.
24    #[error("path escapes tenant root")]
25    CrossTenant,
26
27    /// Either the manifest's whole-bundle hash or one of the
28    /// per-artifact hashes did not match the recomputed value.
29    #[error("manifest checksum mismatch")]
30    ChecksumMismatch,
31
32    /// Bundle's `manifest_version` is newer than this binary supports.
33    /// Operators must upgrade the runtime; older bundles are accepted
34    /// because the codec is forwards-compatible.
35    #[error("bundle schema version newer than runtime ({bundle} > {runtime})")]
36    SchemaTooNew { bundle: u32, runtime: u32 },
37
38    /// A required artifact named in the manifest is missing from the
39    /// bundle body.
40    #[error("bundle missing artifact: {0}")]
41    MissingArtifact(String),
42
43    /// Restore preconditions failed (e.g. workers refused to pause,
44    /// pre-snapshot failed, target dir not empty without `--force`).
45    #[error("restore refused: {0}")]
46    RestoreRefused(String),
47
48    /// Encryption layer failed (key rejected, ciphertext malformed,
49    /// `snapshot-encryption` feature off when bundle is age-wrapped).
50    #[error("encryption: {0}")]
51    Encryption(String),
52
53    /// Retention sweep refused to delete (e.g. `keep_count` would drop
54    /// below 1, or restore-in-progress on the candidate).
55    #[error("retention violation: {0}")]
56    Retention(String),
57
58    /// A snapshot id exists in the manifest layout but the file under
59    /// the tenant root is gone.
60    #[error("snapshot {0} not found")]
61    NotFound(SnapshotId),
62
63    #[error("io: {0}")]
64    Io(#[from] std::io::Error),
65
66    #[error("git: {0}")]
67    Git(#[from] git2::Error),
68
69    #[error("serde_json: {0}")]
70    SerdeJson(#[from] serde_json::Error),
71
72    #[error("sqlx: {0}")]
73    Sqlx(#[from] sqlx::Error),
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::id::SnapshotId;
80    use uuid::Uuid;
81
82    #[test]
83    fn display_concurrent_includes_agent() {
84        let err = SnapshotError::Concurrent("ana".into());
85        assert!(format!("{err}").contains("ana"));
86    }
87
88    #[test]
89    fn display_schema_too_new_includes_versions() {
90        let err = SnapshotError::SchemaTooNew {
91            bundle: 5,
92            runtime: 2,
93        };
94        let msg = format!("{err}");
95        assert!(msg.contains("5"));
96        assert!(msg.contains("2"));
97    }
98
99    #[test]
100    fn display_not_found_includes_id() {
101        let id = SnapshotId(Uuid::nil());
102        let err = SnapshotError::NotFound(id);
103        assert!(format!("{err}").contains("00000000"));
104    }
105
106    #[test]
107    fn io_error_round_trip_via_from() {
108        let raw = std::io::Error::new(std::io::ErrorKind::NotFound, "x");
109        let err: SnapshotError = raw.into();
110        assert!(matches!(err, SnapshotError::Io(_)));
111    }
112}