Skip to main content

nexo_memory_snapshot/
meta.rs

1//! Operator-facing summaries returned by [`crate::snapshotter::MemorySnapshotter`].
2//!
3//! These types appear in CLI JSON output, in NATS lifecycle event payloads,
4//! and (for [`SnapshotMeta`]) in admin-ui responses. They are kept small,
5//! `Serialize`-only on the report side, and free of internal handles.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::id::{AgentId, SnapshotId};
12use crate::manifest::SchemaVersions;
13
14/// Summary of a stored bundle. Built from the manifest at `list()` /
15/// `snapshot()` time so callers do not have to unpack the body.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SnapshotMeta {
18    pub id: SnapshotId,
19    pub agent_id: AgentId,
20    pub tenant: String,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub label: Option<String>,
23    pub created_at_ms: i64,
24    pub bundle_path: PathBuf,
25    pub bundle_size_bytes: u64,
26    pub bundle_sha256: String,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub git_oid: Option<String>,
29    pub schema_versions: SchemaVersions,
30    pub encrypted: bool,
31    pub redactions_applied: bool,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct RestoreReport {
36    pub agent_id: AgentId,
37    pub from: SnapshotId,
38    /// `Some` whenever an auto-pre-snapshot was captured (default).
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub pre_snapshot: Option<SnapshotId>,
41    /// HEAD oid the memdir was reset to, if git restore happened.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub git_reset_oid: Option<String>,
44    pub sqlite_restored_dbs: Vec<String>,
45    pub state_files_restored: Vec<String>,
46    pub workers_restarted: bool,
47    /// `true` when the call exited without mutating live state.
48    pub dry_run: bool,
49}
50
51#[derive(Debug, Clone, Serialize)]
52pub struct GitDiffSummary {
53    pub commits_between: u32,
54    pub files_changed: u32,
55    pub insertions: u64,
56    pub deletions: u64,
57}
58
59#[derive(Debug, Clone, Serialize)]
60pub struct SqliteDiffSummary {
61    pub long_term_rows_added: i64,
62    pub long_term_rows_removed: i64,
63    pub vector_rows_added: i64,
64    pub vector_rows_removed: i64,
65    pub concepts_rows_added: i64,
66    pub concepts_rows_removed: i64,
67    pub compactions_added: i64,
68}
69
70#[derive(Debug, Clone, Serialize)]
71pub struct StateDiffSummary {
72    pub extract_cursor_changed: bool,
73    pub last_dream_run_changed: bool,
74}
75
76#[derive(Debug, Clone, Serialize)]
77pub struct SnapshotDiff {
78    pub a: SnapshotId,
79    pub b: SnapshotId,
80    pub git_summary: GitDiffSummary,
81    pub sqlite_summary: SqliteDiffSummary,
82    pub state_summary: StateDiffSummary,
83}
84
85#[derive(Debug, Clone, Serialize)]
86pub struct VerifyReport {
87    pub bundle: PathBuf,
88    pub manifest_ok: bool,
89    pub bundle_sha256_ok: bool,
90    pub per_artifact_ok: bool,
91    pub schema_versions: SchemaVersions,
92    pub age_protected: bool,
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::id::SnapshotId;
99
100    #[test]
101    fn snapshot_meta_round_trips_via_json() {
102        let m = SnapshotMeta {
103            id: SnapshotId::new(),
104            agent_id: "ana".into(),
105            tenant: "default".into(),
106            label: Some("manual".into()),
107            created_at_ms: 1_700_000_000_000,
108            bundle_path: PathBuf::from("/tmp/x.tar.zst"),
109            bundle_size_bytes: 1024,
110            bundle_sha256: "ab".repeat(32),
111            git_oid: Some("deadbeef".into()),
112            schema_versions: SchemaVersions::CURRENT,
113            encrypted: false,
114            redactions_applied: true,
115        };
116        let s = serde_json::to_string(&m).unwrap();
117        let back: SnapshotMeta = serde_json::from_str(&s).unwrap();
118        assert_eq!(back.id, m.id);
119        assert_eq!(back.bundle_size_bytes, 1024);
120        assert!(back.redactions_applied);
121    }
122
123    #[test]
124    fn restore_report_omits_none_fields() {
125        let r = RestoreReport {
126            agent_id: "ana".into(),
127            from: SnapshotId::new(),
128            pre_snapshot: None,
129            git_reset_oid: None,
130            sqlite_restored_dbs: vec![],
131            state_files_restored: vec![],
132            workers_restarted: true,
133            dry_run: false,
134        };
135        let s = serde_json::to_string(&r).unwrap();
136        assert!(!s.contains("\"pre_snapshot\""));
137        assert!(!s.contains("\"git_reset_oid\""));
138    }
139
140    #[test]
141    fn diff_summary_serializes_zero_deltas() {
142        let d = SnapshotDiff {
143            a: SnapshotId::new(),
144            b: SnapshotId::new(),
145            git_summary: GitDiffSummary {
146                commits_between: 0,
147                files_changed: 0,
148                insertions: 0,
149                deletions: 0,
150            },
151            sqlite_summary: SqliteDiffSummary {
152                long_term_rows_added: 0,
153                long_term_rows_removed: 0,
154                vector_rows_added: 0,
155                vector_rows_removed: 0,
156                concepts_rows_added: 0,
157                concepts_rows_removed: 0,
158                compactions_added: 0,
159            },
160            state_summary: StateDiffSummary {
161                extract_cursor_changed: false,
162                last_dream_run_changed: false,
163            },
164        };
165        let s = serde_json::to_string(&d).unwrap();
166        assert!(s.contains("\"commits_between\":0"));
167    }
168
169    #[test]
170    fn verify_report_round_trip_serialize_only() {
171        let v = VerifyReport {
172            bundle: PathBuf::from("x"),
173            manifest_ok: true,
174            bundle_sha256_ok: true,
175            per_artifact_ok: true,
176            schema_versions: SchemaVersions::CURRENT,
177            age_protected: false,
178        };
179        let s = serde_json::to_string(&v).unwrap();
180        assert!(s.contains("\"manifest_ok\":true"));
181    }
182}