lean_ctx/core/context_snapshot/
mod.rs1pub mod builder;
22pub mod digest;
23pub mod publish;
24pub mod restore;
25pub mod signing;
26pub mod timeline;
27pub mod types;
28
29pub use builder::{SnapshotOptions, build, create};
30pub use digest::{canonical_body, compute_id, finalize_id};
31pub use publish::{ImportOutcome, PublishOptions, PublishOutcome, import, publish};
32pub use restore::{GitRestore, RestoreOptions, RestoreOutcome, SessionMerge, restore};
33pub use signing::{sign_snapshot, verify_snapshot};
34pub use timeline::{
35 TimelineEntry, head_id, load_entries, read_snapshot, resolve_id, snapshots_dir, write_snapshot,
36};
37pub use types::{
38 ContextSnapshotV1, GitAnchorV1, MAX_SNAPSHOT_LEDGER_ITEMS, MAX_SNAPSHOT_LINEAGE_ITEMS,
39 MAX_SNAPSHOT_SESSION_LIST, SnapshotLedgerItemV1, SnapshotLedgerV1, SnapshotLineageItemV1,
40 SnapshotLineageV1, SnapshotProjectV1, SnapshotRoiV1, SnapshotSessionV1, SnapshotSignatureV1,
41};
42
43#[cfg(test)]
44mod tests {
45 use super::types::*;
46 use crate::core::contracts::CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION;
47
48 fn full_snapshot() -> ContextSnapshotV1 {
50 ContextSnapshotV1 {
51 schema_version: CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION,
52 snapshot_id: "id".repeat(32),
53 parent_id: Some("p".repeat(64)),
54 created_at: "2026-06-28T12:00:00Z".into(),
55 lean_ctx_version: "3.9.0".into(),
56 git: GitAnchorV1 {
57 commit: Some("abc1234".into()),
58 branch: Some("feat/context-time-machine".into()),
59 dirty: true,
60 },
61 project: SnapshotProjectV1 {
62 root_hash: Some("r".repeat(64)),
63 identity_hash: Some("i".repeat(64)),
64 },
65 roi: SnapshotRoiV1 {
66 input_tokens: 1000,
67 output_tokens: 200,
68 tokens_saved: 800,
69 compression_rate: 0.444_44,
70 },
71 lineage: SnapshotLineageV1 {
72 items_recorded: 42,
73 items: vec![SnapshotLineageItemV1 {
74 seq: 1,
75 kind: "read".into(),
76 tool: "ctx_read".into(),
77 path: Some("src/main.rs".into()),
78 input_tokens: 500,
79 output_tokens: 13,
80 compression_ratio: 0.974,
81 content_hash: Some("c".repeat(64)),
82 }],
83 },
84 ledger: SnapshotLedgerV1 {
85 window_size: 8000,
86 total_tokens_sent: 1200,
87 total_tokens_saved: 800,
88 items: vec![SnapshotLedgerItemV1 {
89 path: "src/main.rs".into(),
90 state: "pinned".into(),
91 phi: Some(0.91),
92 sent_tokens: 13,
93 original_tokens: 500,
94 }],
95 },
96 session: Some(SnapshotSessionV1 {
97 session_id: Some("sess-1".into()),
98 task: Some("Implement Context Time Machine".into()),
99 decisions: vec!["Use ed25519 for snapshot signing".into()],
100 files_touched: vec!["rust/src/core/context_snapshot/mod.rs".into()],
101 progress_pct: Some(20),
102 }),
103 signature: Some(SnapshotSignatureV1 {
104 algorithm: "ed25519".into(),
105 public_key: "a".repeat(64),
106 value: "b".repeat(128),
107 }),
108 }
109 }
110
111 #[test]
112 fn serde_roundtrip_preserves_every_field() {
113 let original = full_snapshot();
114 let json = serde_json::to_string(&original).expect("serialize");
115 let restored: ContextSnapshotV1 = serde_json::from_str(&json).expect("deserialize");
116 assert_eq!(original, restored);
117 }
118
119 #[test]
120 fn new_uses_current_schema_version_and_empty_slices() {
121 let snap = ContextSnapshotV1::new("2026-06-28T12:00:00Z".into(), "3.9.0".into());
122 assert_eq!(snap.schema_version, CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION);
123 assert!(snap.snapshot_id.is_empty());
124 assert!(snap.signature.is_none());
125 assert!(snap.lineage.items.is_empty());
126 assert!(snap.ledger.items.is_empty());
127 }
128
129 #[test]
130 fn sign_then_serde_roundtrip_still_verifies() {
131 use ed25519_dalek::SigningKey;
132 let mut snap = ContextSnapshotV1::new("2026-06-28T12:00:00Z".into(), "3.9.0".into());
133 super::sign_snapshot(&mut snap, &SigningKey::from_bytes(&[9u8; 32])).expect("sign");
134
135 let json = serde_json::to_string(&snap).expect("serialize");
136 let restored: ContextSnapshotV1 = serde_json::from_str(&json).expect("deserialize");
137 assert_eq!(snap, restored);
138 assert!(super::verify_snapshot(&restored).expect("verify"));
139 }
140}