1use std::path::PathBuf;
5
6use crate::object::{Action, ActionId, Blob, ChangeId, ContentHash, State, Tree};
7
8pub mod agent_registry;
9pub mod agent_task;
10pub mod codec;
11pub mod fs;
12pub mod liveness;
13#[cfg(any(test, feature = "memory-backend"))]
14pub mod memory;
15pub mod pack;
16pub mod shallow;
17pub mod source;
18pub mod store_compliance;
19
20pub use agent_registry::{
21 ActorChainNode, AgentEntry, AgentRegistry, AgentStatus, AgentUsageSummary, ContextQueryEntry,
22 ReserveOutcome, generate_agent_id,
23};
24pub use agent_task::{
25 AGENT_TASK_SCHEMA_VERSION, AgentTaskRecord, AgentTaskStatus, AgentTaskStore,
26 generate_agent_task_id, validate_task_id,
27};
28pub use fs::{
29 DEFAULT_PACK_INSTALL_INTENT_TTL_SECS, FsStore, PackInstallIntent, PackInstallMetricsSnapshot,
30 PackInstallPhase, PackInstallRecoverReport, install_pack_bytes_journaled,
31 pack_install_metrics_reset, pack_install_metrics_snapshot, recover_pack_install_intents,
32 recover_pack_install_intents_with_ttl,
33};
34pub use heddle_format::compression::{CompressionConfig, CompressionError, compress, decompress};
35pub use liveness::{Liveness, current_boot_id, is_owner_alive, process_alive};
36#[cfg(any(test, feature = "memory-backend"))]
37pub use memory::InMemoryStore;
38pub use pack::{PackBuilder, PackObjectId, PackReader, PackStats, StreamingPackBuilder, SyncData};
39pub use shallow::ShallowInfo;
40#[cfg(feature = "async-source")]
41pub use source::AsyncObjectSource;
42pub use source::ObjectSource;
43
44pub use crate::error::{HeddleError as StoreError, HeddleError, Result};
45
46impl From<CompressionError> for HeddleError {
47 fn from(e: CompressionError) -> Self {
48 HeddleError::Compression(e.to_string())
49 }
50}
51
52#[derive(Clone)]
64pub enum AnyStore {
65 Fs(FsStore),
66}
67
68macro_rules! any_store_dispatch {
74 ($self:ident, $method:ident ( $($arg:expr),* )) => {
75 match $self {
76 AnyStore::Fs(inner) => inner.$method($($arg),*),
77 }
78 };
79}
80
81impl ObjectStore for AnyStore {
82 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>> {
83 match self {
84 AnyStore::Fs(inner) => ObjectStore::get_blob(inner, hash),
85 }
86 }
87 fn put_blob(&self, blob: &Blob) -> Result<ContentHash> {
88 any_store_dispatch!(self, put_blob(blob))
89 }
90 fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
91 match self {
92 AnyStore::Fs(inner) => ObjectStore::get_blob_bytes(inner, hash),
93 }
94 }
95 fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
96 any_store_dispatch!(self, blob_size(hash))
97 }
98 fn loose_blob_path(&self, hash: &ContentHash) -> Option<PathBuf> {
99 any_store_dispatch!(self, loose_blob_path(hash))
100 }
101 fn promote_to_loose_uncompressed(&self, hash: &ContentHash) -> Result<bool> {
102 any_store_dispatch!(self, promote_to_loose_uncompressed(hash))
103 }
104 fn clear_recent_caches(&self) {
105 any_store_dispatch!(self, clear_recent_caches())
106 }
107 fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
108 any_store_dispatch!(self, put_blob_with_hash(blob, hash))
109 }
110 fn has_blob(&self, hash: &ContentHash) -> Result<bool> {
111 any_store_dispatch!(self, has_blob(hash))
112 }
113 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>> {
114 match self {
115 AnyStore::Fs(inner) => ObjectStore::get_tree(inner, hash),
116 }
117 }
118 fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
119 match self {
120 AnyStore::Fs(inner) => ObjectStore::get_tree_serialized(inner, hash),
121 }
122 }
123 fn put_tree(&self, tree: &Tree) -> Result<ContentHash> {
124 any_store_dispatch!(self, put_tree(tree))
125 }
126 fn has_tree(&self, hash: &ContentHash) -> Result<bool> {
127 any_store_dispatch!(self, has_tree(hash))
128 }
129 fn get_state(&self, id: &ChangeId) -> Result<Option<State>> {
130 match self {
131 AnyStore::Fs(inner) => ObjectStore::get_state(inner, id),
132 }
133 }
134 fn put_state(&self, state: &State) -> Result<()> {
135 any_store_dispatch!(self, put_state(state))
136 }
137 fn has_state(&self, id: &ChangeId) -> Result<bool> {
138 any_store_dispatch!(self, has_state(id))
139 }
140 fn list_states(&self) -> Result<Vec<ChangeId>> {
141 any_store_dispatch!(self, list_states())
142 }
143 fn get_action(&self, id: &ActionId) -> Result<Option<Action>> {
144 any_store_dispatch!(self, get_action(id))
145 }
146 fn put_action(&self, action: &mut Action) -> Result<ActionId> {
147 any_store_dispatch!(self, put_action(action))
148 }
149 fn list_actions(&self) -> Result<Vec<ActionId>> {
150 any_store_dispatch!(self, list_actions())
151 }
152 fn list_blobs(&self) -> Result<Vec<ContentHash>> {
153 any_store_dispatch!(self, list_blobs())
154 }
155 fn list_trees(&self) -> Result<Vec<ContentHash>> {
156 any_store_dispatch!(self, list_trees())
157 }
158 fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
159 any_store_dispatch!(self, put_blob_bytes_with_hash(data, hash))
160 }
161 fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
162 match self {
163 AnyStore::Fs(inner) => ObjectStore::put_tree_serialized(inner, data, hash),
164 }
165 }
166 fn put_state_serialized(&self, data: &[u8], id: ChangeId) -> Result<()> {
167 any_store_dispatch!(self, put_state_serialized(data, id))
168 }
169 fn put_action_serialized(&self, data: &[u8], id: ActionId) -> Result<()> {
170 any_store_dispatch!(self, put_action_serialized(data, id))
171 }
172 fn get_pack_object(
173 &self,
174 id: &pack::PackObjectId,
175 ) -> Result<Option<(pack::ObjectType, Vec<u8>)>> {
176 any_store_dispatch!(self, get_pack_object(id))
177 }
178 fn put_blobs_packed(&self, blobs: Vec<(ContentHash, Vec<u8>)>) -> Result<()> {
179 any_store_dispatch!(self, put_blobs_packed(blobs))
180 }
181 fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<pack::PackObjectId>> {
182 any_store_dispatch!(self, install_pack(pack_data, index_data))
183 }
184 fn install_pack_streaming(
185 &self,
186 pack_path: &std::path::Path,
187 index_path: &std::path::Path,
188 ) -> Result<Vec<pack::PackObjectId>> {
189 any_store_dispatch!(self, install_pack_streaming(pack_path, index_path))
190 }
191 fn pack_objects(&self, aggressive: bool) -> Result<(u64, u64)> {
192 any_store_dispatch!(self, pack_objects(aggressive))
193 }
194 fn prune_loose_objects(&self) -> Result<(u64, u64)> {
195 any_store_dispatch!(self, prune_loose_objects())
196 }
197 fn begin_snapshot_write_batch(&self) -> Result<()> {
198 any_store_dispatch!(self, begin_snapshot_write_batch())
199 }
200 fn flush_snapshot_write_batch(&self) -> Result<()> {
201 any_store_dispatch!(self, flush_snapshot_write_batch())
202 }
203 fn abort_snapshot_write_batch(&self) {
204 any_store_dispatch!(self, abort_snapshot_write_batch())
205 }
206 fn has_redactions_for_blob(&self, blob: &ContentHash) -> Result<bool> {
207 any_store_dispatch!(self, has_redactions_for_blob(blob))
208 }
209 fn get_redactions_bytes_for_blob(&self, blob: &ContentHash) -> Result<Option<Vec<u8>>> {
210 any_store_dispatch!(self, get_redactions_bytes_for_blob(blob))
211 }
212 fn put_redactions_bytes_for_blob(&self, blob: &ContentHash, bytes: &[u8]) -> Result<()> {
213 any_store_dispatch!(self, put_redactions_bytes_for_blob(blob, bytes))
214 }
215 fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
216 any_store_dispatch!(self, list_blobs_with_redactions())
217 }
218 fn has_state_visibility_for_state(&self, state: &ChangeId) -> Result<bool> {
219 any_store_dispatch!(self, has_state_visibility_for_state(state))
220 }
221 fn get_state_visibility_bytes_for_state(&self, state: &ChangeId) -> Result<Option<Vec<u8>>> {
222 any_store_dispatch!(self, get_state_visibility_bytes_for_state(state))
223 }
224 fn put_state_visibility_bytes_for_state(&self, state: &ChangeId, bytes: &[u8]) -> Result<()> {
225 any_store_dispatch!(self, put_state_visibility_bytes_for_state(state, bytes))
226 }
227 fn list_states_with_visibility(&self) -> Result<Vec<ChangeId>> {
228 any_store_dispatch!(self, list_states_with_visibility())
229 }
230}
231
232pub trait ObjectStore: Send + Sync {
234 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>>;
235 fn put_blob(&self, blob: &Blob) -> Result<ContentHash>;
236
237 fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
248 Ok(self
249 .get_blob(hash)?
250 .map(|blob| bytes::Bytes::from(blob.into_content())))
251 }
252
253 fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
267 Ok(self.get_blob(hash)?.map(|blob| blob.content().len() as u64))
268 }
269
270 fn loose_blob_path(&self, _hash: &ContentHash) -> Option<PathBuf> {
282 None
283 }
284
285 fn promote_to_loose_uncompressed(&self, _hash: &ContentHash) -> Result<bool> {
322 Ok(false)
323 }
324
325 fn clear_recent_caches(&self) {}
334
335 fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
336 if blob.hash() != hash {
337 return Err(HeddleError::InvalidObject("blob hash mismatch".to_string()));
338 }
339 self.put_blob(blob)
340 }
341
342 fn has_blob(&self, hash: &ContentHash) -> Result<bool>;
343 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>>;
344 fn put_tree(&self, tree: &Tree) -> Result<ContentHash>;
345 fn has_tree(&self, hash: &ContentHash) -> Result<bool>;
346 fn get_state(&self, id: &ChangeId) -> Result<Option<State>>;
347 fn put_state(&self, state: &State) -> Result<()>;
348 fn has_state(&self, id: &ChangeId) -> Result<bool>;
349 fn list_states(&self) -> Result<Vec<ChangeId>>;
350 fn get_action(&self, id: &ActionId) -> Result<Option<Action>>;
351 fn put_action(&self, action: &mut Action) -> Result<ActionId>;
352 fn list_actions(&self) -> Result<Vec<ActionId>>;
353 fn list_blobs(&self) -> Result<Vec<ContentHash>>;
354 fn list_trees(&self) -> Result<Vec<ContentHash>>;
355
356 fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
357 self.put_blob_with_hash(&Blob::from_slice(data), hash)
358 }
359
360 fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
368 Ok(self
369 .get_tree(hash)?
370 .map(|tree| rmp_serde::to_vec(&tree))
371 .transpose()?)
372 }
373
374 fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
375 let tree: Tree = rmp_serde::from_slice(data)?;
376 tree.validate()?;
377 if tree.hash() != hash {
378 return Err(HeddleError::Corruption {
379 expected: hash,
380 found: tree.hash(),
381 });
382 }
383 self.put_tree(&tree)
384 }
385
386 fn put_state_serialized(&self, data: &[u8], id: ChangeId) -> Result<()> {
387 let state: State = rmp_serde::from_slice(data)?;
388 if state.change_id != id {
389 return Err(HeddleError::InvalidObject(format!(
390 "state change_id mismatch: expected {}, found {}",
391 id, state.change_id
392 )));
393 }
394 self.put_state(&state)
395 }
396
397 fn put_action_serialized(&self, data: &[u8], id: ActionId) -> Result<()> {
398 let mut action: Action = rmp_serde::from_slice(data)?;
399 let found_id = action.compute_id();
400 if found_id != id {
401 return Err(HeddleError::InvalidObject(format!(
402 "action id mismatch: expected {}, found {}",
403 id, found_id
404 )));
405 }
406 let stored_id = self.put_action(&mut action)?;
407 if stored_id != id {
408 return Err(HeddleError::InvalidObject(format!(
409 "action id mismatch after write: expected {}, found {}",
410 id, stored_id
411 )));
412 }
413 Ok(())
414 }
415
416 fn get_pack_object(
417 &self,
418 id: &pack::PackObjectId,
419 ) -> Result<Option<(pack::ObjectType, Vec<u8>)>> {
420 match id {
421 pack::PackObjectId::Hash(hash) => {
422 if let Some(blob) = self.get_blob(hash)? {
423 return Ok(Some((pack::ObjectType::Blob, blob.content().to_vec())));
424 }
425 if let Some(tree) = self.get_tree(hash)? {
426 return Ok(Some((
427 pack::ObjectType::Tree,
428 rmp_serde::to_vec_named(&tree)?,
429 )));
430 }
431 if let Some(action) = self.get_action(&ActionId::from_hash(*hash))? {
432 return Ok(Some((
433 pack::ObjectType::Action,
434 rmp_serde::to_vec_named(&action)?,
435 )));
436 }
437 Ok(None)
438 }
439 pack::PackObjectId::ChangeId(change_id) => {
440 if let Some(state) = self.get_state(change_id)? {
441 Ok(Some((
442 pack::ObjectType::State,
443 rmp_serde::to_vec_named(&state)?,
444 )))
445 } else {
446 Ok(None)
447 }
448 }
449 }
450 }
451
452 fn put_blobs_packed(&self, blobs: Vec<(ContentHash, Vec<u8>)>) -> Result<()> {
462 for (hash, data) in blobs {
463 if !self.has_blob(&hash)? {
464 self.put_blob_bytes_with_hash(&data, hash)?;
465 }
466 }
467 Ok(())
468 }
469
470 fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<pack::PackObjectId>> {
471 let reader = pack::PackReader::from_slice(pack_data, index_data)?;
472 let ids = reader.list_ids();
473 for id in &ids {
474 let Some((obj_type, data)) = reader.get_object(id)? else {
475 continue;
476 };
477 match (id, obj_type) {
478 (pack::PackObjectId::Hash(hash), pack::ObjectType::Blob) => {
479 self.put_blob_bytes_with_hash(&data, *hash)?;
480 }
481 (pack::PackObjectId::Hash(hash), pack::ObjectType::Tree) => {
482 self.put_tree_serialized(&data, *hash)?;
483 }
484 (pack::PackObjectId::Hash(hash), pack::ObjectType::Action) => {
485 self.put_action_serialized(&data, ActionId::from_hash(*hash))?;
486 }
487 (pack::PackObjectId::ChangeId(change_id), pack::ObjectType::State) => {
488 self.put_state_serialized(&data, *change_id)?;
489 }
490 _ => {
491 return Err(HeddleError::InvalidObject(format!(
492 "unsupported native pack object: {:?} {:?}",
493 id, obj_type
494 )));
495 }
496 }
497 }
498 Ok(ids)
499 }
500
501 fn install_pack_streaming(
518 &self,
519 pack_path: &std::path::Path,
520 index_path: &std::path::Path,
521 ) -> Result<Vec<pack::PackObjectId>> {
522 let pack_data = std::fs::read(pack_path).map_err(StoreError::from)?;
523 let index_data = std::fs::read(index_path).map_err(StoreError::from)?;
524 let ids = self.install_pack(&pack_data, &index_data)?;
525 let _ = std::fs::remove_file(pack_path);
529 let _ = std::fs::remove_file(index_path);
530 Ok(ids)
531 }
532
533 fn pack_objects(&self, aggressive: bool) -> Result<(u64, u64)> {
534 let _ = aggressive;
535 Ok((0, 0))
536 }
537
538 fn prune_loose_objects(&self) -> Result<(u64, u64)> {
539 Ok((0, 0))
540 }
541
542 fn begin_snapshot_write_batch(&self) -> Result<()> {
543 Ok(())
544 }
545
546 fn flush_snapshot_write_batch(&self) -> Result<()> {
547 Ok(())
548 }
549
550 fn abort_snapshot_write_batch(&self) {}
551
552 fn has_redactions_for_blob(&self, _blob: &ContentHash) -> Result<bool> {
564 Ok(false)
565 }
566
567 fn get_redactions_bytes_for_blob(&self, _blob: &ContentHash) -> Result<Option<Vec<u8>>> {
575 Ok(None)
576 }
577
578 fn put_redactions_bytes_for_blob(&self, _blob: &ContentHash, _bytes: &[u8]) -> Result<()> {
587 Err(HeddleError::InvalidObject(
588 "this object store does not support persisting redactions".to_string(),
589 ))
590 }
591
592 fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
599 Ok(Vec::new())
600 }
601
602 fn has_state_visibility_for_state(&self, _state: &ChangeId) -> Result<bool> {
612 Ok(false)
613 }
614
615 fn get_state_visibility_bytes_for_state(&self, _state: &ChangeId) -> Result<Option<Vec<u8>>> {
621 Ok(None)
622 }
623
624 fn put_state_visibility_bytes_for_state(&self, _state: &ChangeId, _bytes: &[u8]) -> Result<()> {
629 Err(HeddleError::InvalidObject(
630 "this object store does not support persisting state visibility".to_string(),
631 ))
632 }
633
634 fn list_states_with_visibility(&self) -> Result<Vec<ChangeId>> {
638 Ok(Vec::new())
639 }
640}
641
642#[cfg(test)]
643mod any_store_tests {
644 use tempfile::TempDir;
645
646 use super::*;
647 use crate::object::{Attribution, Operation, Principal};
648
649 fn fs_any_store() -> (TempDir, AnyStore) {
650 let temp = TempDir::new().unwrap();
651 let store = FsStore::new(temp.path().join(".heddle"));
652 store.init().unwrap();
653 (temp, AnyStore::Fs(store))
654 }
655
656 #[test]
662 fn fs_variant_dispatches_every_object_store_method() {
663 let (_temp, store) = fs_any_store();
664
665 let blob = Blob::from("any-store dispatch blob");
667 let blob_hash = store.put_blob(&blob).unwrap();
668 assert_eq!(
669 ObjectStore::get_blob(&store, &blob_hash)
670 .unwrap()
671 .unwrap()
672 .content(),
673 blob.content()
674 );
675 assert!(store.has_blob(&blob_hash).unwrap());
676 assert_eq!(
677 ObjectStore::get_blob_bytes(&store, &blob_hash)
678 .unwrap()
679 .unwrap()
680 .as_ref(),
681 blob.content()
682 );
683 assert_eq!(
684 store.blob_size(&blob_hash).unwrap().unwrap(),
685 blob.content().len() as u64
686 );
687 assert!(store.loose_blob_path(&blob_hash).is_some());
688 store.promote_to_loose_uncompressed(&blob_hash).unwrap();
689 assert!(store.list_blobs().unwrap().contains(&blob_hash));
690
691 let bytes_blob = Blob::from("put-with-hash blob");
692 let bytes_hash = bytes_blob.hash();
693 assert_eq!(
694 store.put_blob_with_hash(&bytes_blob, bytes_hash).unwrap(),
695 bytes_hash
696 );
697 let raw_blob = Blob::from("raw bytes blob");
698 let raw_hash = raw_blob.hash();
699 assert_eq!(
700 store
701 .put_blob_bytes_with_hash(raw_blob.content(), raw_hash)
702 .unwrap(),
703 raw_hash
704 );
705
706 let tree = Tree::new();
708 let tree_hash = store.put_tree(&tree).unwrap();
709 assert!(ObjectStore::get_tree(&store, &tree_hash).unwrap().is_some());
710 assert!(store.has_tree(&tree_hash).unwrap());
711 assert!(store.list_trees().unwrap().contains(&tree_hash));
712 let tree2 = Tree::new();
713 let tree2_bytes = rmp_serde::to_vec_named(&tree2).unwrap();
714 assert_eq!(
715 store
716 .put_tree_serialized(&tree2_bytes, tree2.hash())
717 .unwrap(),
718 tree2.hash()
719 );
720
721 let attribution =
723 Attribution::human(Principal::new("AnyStore Test", "anystore@example.com"));
724 let state = State::new(tree_hash, vec![], attribution.clone());
725 let change_id = state.change_id;
726 store.put_state(&state).unwrap();
727 assert!(
728 ObjectStore::get_state(&store, &change_id)
729 .unwrap()
730 .is_some()
731 );
732 assert!(store.has_state(&change_id).unwrap());
733 assert!(store.list_states().unwrap().contains(&change_id));
734 let state2 = State::new(tree2.hash(), vec![], attribution.clone());
735 let state2_bytes = rmp_serde::to_vec_named(&state2).unwrap();
736 store
737 .put_state_serialized(&state2_bytes, state2.change_id)
738 .unwrap();
739
740 let mut action = Action::new(
742 None,
743 ChangeId::generate(),
744 Operation::Snapshot,
745 "any-store action",
746 attribution,
747 );
748 let action_id = store.put_action(&mut action).unwrap();
749 assert!(store.get_action(&action_id).unwrap().is_some());
750 assert!(store.list_actions().unwrap().contains(&action_id));
751 let action_bytes = rmp_serde::to_vec_named(&action).unwrap();
752 store
753 .put_action_serialized(&action_bytes, action_id)
754 .unwrap();
755
756 let packed = Blob::from("packed-via-any-store");
758 let packed_hash = packed.hash();
759 store
760 .put_blobs_packed(vec![(packed_hash, packed.into_content())])
761 .unwrap();
762 assert!(
763 store
764 .get_pack_object(&pack::PackObjectId::Hash(packed_hash))
765 .unwrap()
766 .is_some()
767 );
768 store.pack_objects(false).unwrap();
769 store.prune_loose_objects().unwrap();
770 let _ = store.install_pack(&[], &[]);
774 let _ = store.install_pack_streaming(
775 std::path::Path::new("/nonexistent/pack"),
776 std::path::Path::new("/nonexistent/idx"),
777 );
778
779 store.begin_snapshot_write_batch().unwrap();
781 store.flush_snapshot_write_batch().unwrap();
782 store.begin_snapshot_write_batch().unwrap();
783 store.abort_snapshot_write_batch();
784
785 let redaction = b"any-store redaction bytes";
787 store
788 .put_redactions_bytes_for_blob(&blob_hash, redaction)
789 .unwrap();
790 assert!(store.has_redactions_for_blob(&blob_hash).unwrap());
791 assert_eq!(
792 store
793 .get_redactions_bytes_for_blob(&blob_hash)
794 .unwrap()
795 .as_deref(),
796 Some(redaction.as_slice())
797 );
798 assert!(
799 store
800 .list_blobs_with_redactions()
801 .unwrap()
802 .contains(&blob_hash)
803 );
804
805 let state_visibility = b"any-store state visibility bytes";
807 store
808 .put_state_visibility_bytes_for_state(&change_id, state_visibility)
809 .unwrap();
810 assert!(store.has_state_visibility_for_state(&change_id).unwrap());
811 assert_eq!(
812 store
813 .get_state_visibility_bytes_for_state(&change_id)
814 .unwrap()
815 .as_deref(),
816 Some(state_visibility.as_slice())
817 );
818 assert!(
819 store
820 .list_states_with_visibility()
821 .unwrap()
822 .contains(&change_id)
823 );
824
825 store.clear_recent_caches();
827 }
828}