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