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