1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Arc, OnceLock, RwLock};
4use std::time::{Instant, SystemTime, UNIX_EPOCH};
5
6use serde::{Deserialize, Serialize};
7
8use super::types::{OclaError, OclaResult};
9use crate::core::ocla_bus::{self, OclaEvent};
10
11static NEXT_FORK_ID: AtomicU64 = AtomicU64::new(1);
12static NEXT_SNAPSHOT_ID: AtomicU64 = AtomicU64::new(1);
13static GLOBAL_CAPSULE_STORE: OnceLock<CapsuleStore> = OnceLock::new();
14
15#[must_use]
16pub fn global_capsule_store() -> &'static CapsuleStore {
17 GLOBAL_CAPSULE_STORE.get_or_init(CapsuleStore::new)
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct Delta {
22 pub offset: usize,
23 pub data: Vec<u8>,
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct CapsuleSnapshot {
29 pub snapshot_id: String,
30 pub capsule_ref: String,
31 pub content: Vec<u8>,
32 pub delta_count: usize,
33 pub created_at_ms: u64,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct HandoffDelta {
39 pub from_ref: String,
40 pub to_ref: String,
41 pub operations: Vec<DeltaOp>,
42 pub compressed_size: usize,
43 pub original_size: usize,
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize)]
48pub enum DeltaOp {
49 Keep { offset: usize, len: usize },
50 Insert { offset: usize, data: Vec<u8> },
51 Delete { offset: usize, len: usize },
52}
53
54#[derive(Clone, Debug)]
55pub struct CapsuleEntry {
56 pub parent_ref: Option<String>,
57 pub data: Vec<u8>,
58 pub deltas: Vec<Delta>,
59 pub budget_tokens: u64,
60 pub created_at: Instant,
61}
62
63#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
64pub struct CapsuleStats {
65 pub total_entries: usize,
66 pub total_bytes: usize,
67 pub max_depth: usize,
68}
69
70#[derive(Clone, Debug, Default)]
71struct CapsuleStoreState {
72 entries: HashMap<String, CapsuleEntry>,
73 snapshots: Vec<CapsuleSnapshot>,
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct CapsuleStore {
78 state: Arc<RwLock<CapsuleStoreState>>,
79}
80
81impl CapsuleStore {
82 #[must_use]
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn register(&self, data: &[u8]) -> String {
88 let capsule_ref = format!("capsule:{}", blake3::hash(data).to_hex());
89 let entry = CapsuleEntry {
90 parent_ref: None,
91 data: data.to_vec(),
92 deltas: Vec::new(),
93 budget_tokens: 0,
94 created_at: Instant::now(),
95 };
96 if let Ok(mut state) = self.state.write() {
97 state.entries.insert(capsule_ref.clone(), entry);
98 }
99 capsule_ref
100 }
101
102 pub fn fork(&self, parent_ref: &str, budget_tokens: u64) -> OclaResult<String> {
103 let mut state = self
104 .state
105 .write()
106 .map_err(|_| invalid("capsule store lock poisoned"))?;
107 if !state.entries.contains_key(parent_ref) {
108 return Err(invalid(format!("unknown parent capsule: {parent_ref}")));
109 }
110
111 let fork_id = NEXT_FORK_ID.fetch_add(1, Ordering::Relaxed);
112 let identity = format!("{parent_ref}\0{budget_tokens}\0{fork_id}");
113 let capsule_ref = format!("capsule:{}", blake3::hash(identity.as_bytes()).to_hex());
114 state.entries.insert(
115 capsule_ref.clone(),
116 CapsuleEntry {
117 parent_ref: Some(parent_ref.to_string()),
118 data: Vec::new(),
119 deltas: Vec::new(),
120 budget_tokens,
121 created_at: Instant::now(),
122 },
123 );
124 Ok(capsule_ref)
125 }
126
127 pub fn resolve(&self, capsule_ref: &str) -> OclaResult<Vec<u8>> {
128 let state = self
129 .state
130 .read()
131 .map_err(|_| invalid("capsule store lock poisoned"))?;
132 resolve_entries(&state.entries, capsule_ref)
133 }
134
135 #[cfg(test)]
136 pub(crate) fn budget_tokens(&self, capsule_ref: &str) -> OclaResult<u64> {
137 let state = self
138 .state
139 .read()
140 .map_err(|_| invalid("capsule store lock poisoned"))?;
141 state
142 .entries
143 .get(capsule_ref)
144 .map(|entry| entry.budget_tokens)
145 .ok_or_else(|| invalid(format!("unknown capsule: {capsule_ref}")))
146 }
147
148 pub fn apply_delta(&self, capsule_ref: &str, delta: Delta) -> OclaResult<()> {
149 let mut state = self
150 .state
151 .write()
152 .map_err(|_| invalid("capsule store lock poisoned"))?;
153 let current = resolve_entries(&state.entries, capsule_ref)?;
154 let entry = state
155 .entries
156 .get_mut(capsule_ref)
157 .ok_or_else(|| invalid(format!("unknown capsule: {capsule_ref}")))?;
158 if entry.parent_ref.is_none() {
159 return Err(invalid("deltas can only be applied to forked capsules"));
160 }
161 if delta.offset > current.len() {
162 return Err(invalid("capsule delta starts beyond materialized content"));
163 }
164 entry.deltas.push(delta);
165 Ok(())
166 }
167
168 pub fn merge_back(&self, child_ref: &str) -> OclaResult<()> {
169 let mut state = self
170 .state
171 .write()
172 .map_err(|_| invalid("capsule store lock poisoned"))?;
173 resolve_entries(&state.entries, child_ref)?;
174 let (parent_ref, deltas) = {
175 let child = state
176 .entries
177 .get(child_ref)
178 .ok_or_else(|| invalid(format!("unknown capsule: {child_ref}")))?;
179 (
180 child
181 .parent_ref
182 .clone()
183 .ok_or_else(|| invalid("root capsules cannot merge back"))?,
184 child.deltas.clone(),
185 )
186 };
187 let parent = state
188 .entries
189 .get_mut(&parent_ref)
190 .ok_or_else(|| invalid(format!("unknown parent capsule: {parent_ref}")))?;
191 parent.deltas.extend(deltas);
192 state
193 .entries
194 .get_mut(child_ref)
195 .ok_or_else(|| invalid(format!("unknown capsule: {child_ref}")))?
196 .deltas
197 .clear();
198 Ok(())
199 }
200
201 pub fn snapshot(&self, capsule_ref: &str) -> OclaResult<CapsuleSnapshot> {
203 let mut state = self
204 .state
205 .write()
206 .map_err(|_| invalid("capsule store lock poisoned"))?;
207 let content = resolve_entries(&state.entries, capsule_ref)?;
208 let delta_count = state
209 .entries
210 .get(capsule_ref)
211 .ok_or_else(|| invalid(format!("unknown capsule: {capsule_ref}")))?
212 .deltas
213 .len();
214 let snapshot_number = NEXT_SNAPSHOT_ID.fetch_add(1, Ordering::Relaxed);
215 let snapshot_id = format!(
216 "snapshot:{}",
217 blake3::hash(format!("{capsule_ref}\\0{snapshot_number}").as_bytes()).to_hex()
218 );
219 let snapshot = CapsuleSnapshot {
220 snapshot_id,
221 capsule_ref: capsule_ref.to_string(),
222 content,
223 delta_count,
224 created_at_ms: unix_time_ms(),
225 };
226 state.snapshots.push(snapshot.clone());
227 Ok(snapshot)
228 }
229
230 pub fn rollback(&self, capsule_ref: &str, snapshot_id: &str) -> OclaResult<()> {
232 let mut state = self
233 .state
234 .write()
235 .map_err(|_| invalid("capsule store lock poisoned"))?;
236 let snapshot = state
237 .snapshots
238 .iter()
239 .find(|snapshot| {
240 snapshot.snapshot_id == snapshot_id && snapshot.capsule_ref == capsule_ref
241 })
242 .cloned()
243 .ok_or_else(|| invalid(format!("unknown capsule snapshot: {snapshot_id}")))?;
244 let entry = state
245 .entries
246 .get_mut(capsule_ref)
247 .ok_or_else(|| invalid(format!("unknown capsule: {capsule_ref}")))?;
248 entry.parent_ref = None;
249 entry.data = snapshot.content;
250 entry.deltas.clear();
251 ocla_bus::emit(OclaEvent::AgentChainEvent {
252 agent_id: capsule_ref.to_string(),
253 action: "capsule_rollback".to_string(),
254 parent_agent: Some(snapshot_id.to_string()),
255 });
256 Ok(())
257 }
258
259 pub fn compute_handoff_delta(&self, from_ref: &str, to_ref: &str) -> OclaResult<HandoffDelta> {
261 let state = self
262 .state
263 .read()
264 .map_err(|_| invalid("capsule store lock poisoned"))?;
265 let from = resolve_entries(&state.entries, from_ref)?;
266 let to = resolve_entries(&state.entries, to_ref)?;
267 let operations = compute_delta_operations(&from, &to);
268 Ok(HandoffDelta {
269 from_ref: from_ref.to_string(),
270 to_ref: to_ref.to_string(),
271 compressed_size: handoff_delta_size(&operations),
272 original_size: to.len(),
273 operations,
274 })
275 }
276
277 pub fn apply_handoff_delta(&self, base_ref: &str, delta: &HandoffDelta) -> OclaResult<String> {
279 if delta.from_ref != base_ref {
280 return Err(invalid("handoff delta base reference does not match"));
281 }
282 let mut state = self
283 .state
284 .write()
285 .map_err(|_| invalid("capsule store lock poisoned"))?;
286 let base = resolve_entries(&state.entries, base_ref)?;
287 let content = apply_handoff_operations(&base, &delta.operations)?;
288 let handoff_id = NEXT_FORK_ID.fetch_add(1, Ordering::Relaxed);
289 let identity = format!(
290 "handoff\\0{base_ref}\\0{}\\0{handoff_id}",
291 blake3::hash(&content)
292 );
293 let capsule_ref = format!("capsule:{}", blake3::hash(identity.as_bytes()).to_hex());
294 state.entries.insert(
295 capsule_ref.clone(),
296 CapsuleEntry {
297 parent_ref: None,
298 data: content,
299 deltas: Vec::new(),
300 budget_tokens: 0,
301 created_at: Instant::now(),
302 },
303 );
304 Ok(capsule_ref)
305 }
306
307 #[must_use]
308 pub fn stats(&self) -> CapsuleStats {
309 let Ok(state) = self.state.read() else {
310 return CapsuleStats::default();
311 };
312 let total_bytes = state.entries.values().fold(0_usize, |total, entry| {
313 total.saturating_add(entry.data.len()).saturating_add(
314 entry
315 .deltas
316 .iter()
317 .map(|delta| delta.data.len())
318 .sum::<usize>(),
319 )
320 });
321 let max_depth = state
322 .entries
323 .keys()
324 .map(|capsule_ref| depth_of(&state.entries, capsule_ref))
325 .max()
326 .unwrap_or(0);
327 CapsuleStats {
328 total_entries: state.entries.len(),
329 total_bytes,
330 max_depth,
331 }
332 }
333}
334
335fn resolve_entries(
336 entries: &HashMap<String, CapsuleEntry>,
337 capsule_ref: &str,
338) -> OclaResult<Vec<u8>> {
339 let mut current = capsule_ref;
340 let mut visited = HashSet::new();
341 let mut layers = Vec::new();
342 loop {
343 if !visited.insert(current) {
344 return Err(invalid("capsule parent cycle detected"));
345 }
346 let entry = entries
347 .get(current)
348 .ok_or_else(|| invalid(format!("unknown capsule: {capsule_ref}")))?;
349 layers.push(entry.deltas.clone());
350 if let Some(parent_ref) = entry.parent_ref.as_deref() {
351 current = parent_ref;
352 } else {
353 let mut data = entry.data.clone();
354 for layer in layers.iter().rev() {
355 for delta in layer {
356 apply_patch(&mut data, delta)?;
357 }
358 }
359 return Ok(data);
360 }
361 }
362}
363
364fn apply_patch(data: &mut Vec<u8>, delta: &Delta) -> OclaResult<()> {
365 let end = delta
366 .offset
367 .checked_add(delta.data.len())
368 .ok_or_else(|| invalid("capsule delta range overflow"))?;
369 if delta.offset > data.len() {
370 return Err(invalid("capsule delta starts beyond materialized content"));
371 }
372 if end > data.len() {
373 data.resize(end, 0);
374 }
375 data[delta.offset..end].copy_from_slice(&delta.data);
376 Ok(())
377}
378
379fn compute_delta_operations(from: &[u8], to: &[u8]) -> Vec<DeltaOp> {
380 if from == to {
381 return Vec::new();
382 }
383
384 let prefix_len = from
385 .iter()
386 .zip(to)
387 .take_while(|(left, right)| left == right)
388 .count();
389 let suffix_len = from[prefix_len..]
390 .iter()
391 .rev()
392 .zip(to[prefix_len..].iter().rev())
393 .take_while(|(left, right)| left == right)
394 .count();
395 let from_middle_end = from.len() - suffix_len;
396 let to_middle_end = to.len() - suffix_len;
397 let mut operations = Vec::with_capacity(4);
398
399 if prefix_len > 0 {
400 operations.push(DeltaOp::Keep {
401 offset: 0,
402 len: prefix_len,
403 });
404 }
405 if from_middle_end > prefix_len {
406 operations.push(DeltaOp::Delete {
407 offset: prefix_len,
408 len: from_middle_end - prefix_len,
409 });
410 }
411 if to_middle_end > prefix_len {
412 operations.push(DeltaOp::Insert {
413 offset: from_middle_end,
414 data: to[prefix_len..to_middle_end].to_vec(),
415 });
416 }
417 if suffix_len > 0 {
418 operations.push(DeltaOp::Keep {
419 offset: from_middle_end,
420 len: suffix_len,
421 });
422 }
423 operations
424}
425
426fn apply_handoff_operations(base: &[u8], operations: &[DeltaOp]) -> OclaResult<Vec<u8>> {
427 if operations.is_empty() {
428 return Ok(base.to_vec());
429 }
430
431 let mut cursor = 0;
432 let mut output = Vec::with_capacity(base.len());
433 for operation in operations {
434 match operation {
435 DeltaOp::Keep { offset, len } => {
436 validate_handoff_offset(*offset, cursor)?;
437 let end = cursor
438 .checked_add(*len)
439 .ok_or_else(|| invalid("handoff keep range overflow"))?;
440 let bytes = base
441 .get(cursor..end)
442 .ok_or_else(|| invalid("handoff keep extends beyond base content"))?;
443 output.extend_from_slice(bytes);
444 cursor = end;
445 }
446 DeltaOp::Insert { offset, data } => {
447 validate_handoff_offset(*offset, cursor)?;
448 output.extend_from_slice(data);
449 }
450 DeltaOp::Delete { offset, len } => {
451 validate_handoff_offset(*offset, cursor)?;
452 cursor = cursor
453 .checked_add(*len)
454 .ok_or_else(|| invalid("handoff delete range overflow"))?;
455 if cursor > base.len() {
456 return Err(invalid("handoff delete extends beyond base content"));
457 }
458 }
459 }
460 }
461 if cursor != base.len() {
462 return Err(invalid("handoff delta does not consume base content"));
463 }
464 Ok(output)
465}
466
467fn validate_handoff_offset(offset: usize, cursor: usize) -> OclaResult<()> {
468 if offset != cursor {
469 return Err(invalid("handoff operation offset is out of order"));
470 }
471 Ok(())
472}
473
474fn handoff_delta_size(operations: &[DeltaOp]) -> usize {
475 operations.iter().fold(0_usize, |size, operation| {
476 let operation_size = match operation {
477 DeltaOp::Keep { .. } | DeltaOp::Delete { .. } => 2 * std::mem::size_of::<usize>(),
478 DeltaOp::Insert { data, .. } => 2 * std::mem::size_of::<usize>() + data.len(),
479 };
480 size.saturating_add(operation_size)
481 })
482}
483
484fn unix_time_ms() -> u64 {
485 SystemTime::now()
486 .duration_since(UNIX_EPOCH)
487 .unwrap_or_default()
488 .as_millis()
489 .try_into()
490 .unwrap_or(u64::MAX)
491}
492
493fn depth_of(entries: &HashMap<String, CapsuleEntry>, capsule_ref: &str) -> usize {
494 let mut depth = 0;
495 let mut current = capsule_ref;
496 let mut visited = HashSet::new();
497 while visited.insert(current) {
498 let Some(entry) = entries.get(current) else {
499 break;
500 };
501 let Some(parent_ref) = entry.parent_ref.as_deref() else {
502 break;
503 };
504 depth += 1;
505 current = parent_ref;
506 }
507 depth
508}
509
510fn invalid(message: impl Into<String>) -> OclaError {
511 OclaError::InvalidRequest(message.into())
512}
513
514#[cfg(test)]
515mod tests {
516 use super::{CapsuleStore, Delta, DeltaOp, global_capsule_store};
517
518 fn test_delta() -> Delta {
519 Delta {
520 offset: 1,
521 data: vec![b'a'],
522 }
523 }
524
525 #[test]
526 fn global_store_registers_capsule() {
527 let capsule_ref = global_capsule_store().register(b"global capsule");
528 assert_eq!(
529 global_capsule_store()
530 .resolve(&capsule_ref)
531 .expect("global resolves"),
532 b"global capsule"
533 );
534 }
535
536 #[test]
537 fn register_resolves_original_data() {
538 let store = CapsuleStore::new();
539 let capsule_ref = store.register(b"hello");
540 assert_eq!(
541 store.resolve(&capsule_ref).expect("root resolves"),
542 b"hello"
543 );
544 }
545 #[test]
546 fn fork_resolves_parent_data() {
547 let store = CapsuleStore::new();
548 let parent_ref = store.register(b"hello");
549 let child_ref = store.fork(&parent_ref, 100).expect("fork succeeds");
550 assert_eq!(store.resolve(&child_ref).expect("child resolves"), b"hello");
551 assert_eq!(store.budget_tokens(&child_ref).expect("budget exists"), 100);
552 }
553 #[test]
554 fn fork_delta_resolves_patched_data() {
555 let store = CapsuleStore::new();
556 let parent_ref = store.register(b"hello");
557 let child_ref = store.fork(&parent_ref, 100).expect("fork succeeds");
558 store
559 .apply_delta(&child_ref, test_delta())
560 .expect("delta applies");
561 assert_eq!(store.resolve(&child_ref).expect("child resolves"), b"hallo");
562 }
563
564 #[test]
565 fn later_overlapping_delta_wins_within_layer() {
566 let store = CapsuleStore::new();
567 let parent_ref = store.register(b"hello");
568 let fork_ref = store.fork(&parent_ref, 100).expect("fork succeeds");
569
570 store
571 .apply_delta(
572 &fork_ref,
573 Delta {
574 offset: 1,
575 data: vec![b'a'],
576 },
577 )
578 .expect("first delta applies");
579 store
580 .apply_delta(
581 &fork_ref,
582 Delta {
583 offset: 1,
584 data: vec![b'u'],
585 },
586 )
587 .expect("second delta applies");
588
589 assert_eq!(store.resolve(&fork_ref).expect("fork resolves"), b"hullo");
590 }
591
592 #[test]
593 fn child_delta_overrides_parent_delta() {
594 let store = CapsuleStore::new();
595 let root_ref = store.register(b"hello");
596 let parent_ref = store.fork(&root_ref, 100).expect("parent fork succeeds");
597 store
598 .apply_delta(
599 &parent_ref,
600 Delta {
601 offset: 1,
602 data: vec![b'a'],
603 },
604 )
605 .expect("parent delta applies");
606 let child_ref = store.fork(&parent_ref, 100).expect("child fork succeeds");
607 store
608 .apply_delta(
609 &child_ref,
610 Delta {
611 offset: 1,
612 data: vec![b'u'],
613 },
614 )
615 .expect("child delta applies");
616
617 assert_eq!(store.resolve(&child_ref).expect("child resolves"), b"hullo");
618 }
619
620 #[test]
621 fn merge_back_preserves_chronological_delta_order() {
622 let store = CapsuleStore::new();
623 let root_ref = store.register(b"hello");
624 let parent_ref = store.fork(&root_ref, 100).expect("parent fork succeeds");
625 store
626 .apply_delta(
627 &parent_ref,
628 Delta {
629 offset: 1,
630 data: vec![b'a'],
631 },
632 )
633 .expect("parent delta applies");
634 let child_ref = store.fork(&parent_ref, 100).expect("child fork succeeds");
635 store
636 .apply_delta(
637 &child_ref,
638 Delta {
639 offset: 1,
640 data: vec![b'u'],
641 },
642 )
643 .expect("child delta applies");
644
645 store.merge_back(&child_ref).expect("merge succeeds");
646
647 assert_eq!(
648 store.resolve(&parent_ref).expect("parent resolves"),
649 b"hullo"
650 );
651 assert_eq!(store.resolve(&child_ref).expect("child resolves"), b"hullo");
652 }
653 #[test]
654 fn merge_back_projects_deltas_to_parent() {
655 let store = CapsuleStore::new();
656 let parent_ref = store.register(b"hello");
657 let child_ref = store.fork(&parent_ref, 100).expect("fork succeeds");
658 store
659 .apply_delta(&child_ref, test_delta())
660 .expect("delta applies");
661 store.merge_back(&child_ref).expect("merge succeeds");
662 assert_eq!(
663 store.resolve(&parent_ref).expect("parent resolves"),
664 b"hallo"
665 );
666 assert_eq!(store.resolve(&child_ref).expect("child resolves"), b"hallo");
667 }
668 #[test]
669 fn stats_report_entries_storage_and_depth() {
670 let store = CapsuleStore::new();
671 let parent_ref = store.register(b"hello");
672 let child_ref = store.fork(&parent_ref, 100).expect("fork succeeds");
673 store
674 .apply_delta(&child_ref, test_delta())
675 .expect("delta applies");
676 let stats = store.stats();
677 assert_eq!(stats.total_entries, 2);
678 assert_eq!(stats.total_bytes, 6);
679 assert_eq!(stats.max_depth, 1);
680 }
681
682 #[test]
683 fn snapshot_rollback_restores_snapshot_content() {
684 let store = CapsuleStore::new();
685 let root_ref = store.register(b"hello");
686 let child_ref = store.fork(&root_ref, 100).expect("fork succeeds");
687 store
688 .apply_delta(
689 &child_ref,
690 Delta {
691 offset: 1,
692 data: vec![b'a'],
693 },
694 )
695 .expect("first delta applies");
696 let snapshot = store.snapshot(&child_ref).expect("snapshot succeeds");
697 store
698 .apply_delta(
699 &child_ref,
700 Delta {
701 offset: 1,
702 data: vec![b'u'],
703 },
704 )
705 .expect("second delta applies");
706
707 store
708 .rollback(&child_ref, &snapshot.snapshot_id)
709 .expect("rollback succeeds");
710
711 assert_eq!(store.resolve(&child_ref).expect("child resolves"), b"hallo");
712 }
713
714 #[test]
715 fn handoff_delta_for_identical_capsules_is_empty() {
716 let store = CapsuleStore::new();
717 let first_ref = store.register(b"unchanged");
718 let second_ref = store.register(b"unchanged");
719
720 let delta = store
721 .compute_handoff_delta(&first_ref, &second_ref)
722 .expect("delta computes");
723
724 assert!(delta.operations.is_empty());
725 assert_eq!(delta.compressed_size, 0);
726 }
727
728 #[test]
729 fn handoff_delta_for_different_capsules_has_insert_and_delete() {
730 let store = CapsuleStore::new();
731 let from_ref = store.register(b"hello");
732 let to_ref = store.register(b"halo!");
733
734 let delta = store
735 .compute_handoff_delta(&from_ref, &to_ref)
736 .expect("delta computes");
737
738 assert!(
739 delta
740 .operations
741 .iter()
742 .any(|operation| matches!(operation, DeltaOp::Insert { .. }))
743 );
744 assert!(
745 delta
746 .operations
747 .iter()
748 .any(|operation| matches!(operation, DeltaOp::Delete { .. }))
749 );
750 }
751
752 #[test]
753 fn apply_handoff_delta_round_trip_matches_target() {
754 let store = CapsuleStore::new();
755 let from_ref = store.register(b"goodbye");
756 let to_ref = store.register(b"good day!");
757 let delta = store
758 .compute_handoff_delta(&from_ref, &to_ref)
759 .expect("delta computes");
760
761 let applied_ref = store
762 .apply_handoff_delta(&from_ref, &delta)
763 .expect("delta applies");
764
765 assert_eq!(
766 store.resolve(&applied_ref).expect("applied resolves"),
767 store.resolve(&to_ref).expect("target resolves")
768 );
769 }
770
771 #[test]
772 fn rollback_with_invalid_snapshot_id_errors() {
773 let store = CapsuleStore::new();
774 let capsule_ref = store.register(b"hello");
775
776 assert!(store.rollback(&capsule_ref, "snapshot:missing").is_err());
777 }
778}