1use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::id::{AgentId, SnapshotId};
12use crate::manifest::SchemaVersions;
13
14#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub pre_snapshot: Option<SnapshotId>,
41 #[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 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}