1use async_trait::async_trait;
24use bytes::Bytes;
25use cbor2::{from_slice, to_canonical_vec};
26use serde::{Deserialize, Serialize, de::DeserializeOwned};
27use std::{future::Future, sync::Arc, time::Duration};
28
29pub use anda_db_schema::Json;
30pub use candid::Principal;
31pub use ic_oss_types::object_store::UpdateVersion;
32pub use object_store::{ObjectMeta, PutMode, PutResult, UpdateVersion as OsVersion, path::Path};
33pub use tokio_util::sync::CancellationToken;
34
35use crate::BoxError;
36use crate::model::*;
37
38pub trait AgentContext: BaseContext + CompletionFeatures {
43 fn tool_definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition>;
51
52 fn remote_tool_definitions(
61 &self,
62 endpoint: Option<&str>,
63 names: Option<&[String]>,
64 ) -> impl Future<Output = Result<Vec<FunctionDefinition>, BoxError>> + Send;
65
66 fn select_tool_resources(
68 &self,
69 name: &str,
70 resources: &mut Vec<Resource>,
71 ) -> impl Future<Output = Vec<Resource>> + Send;
72
73 fn agent_definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition>;
81
82 fn remote_agent_definitions(
91 &self,
92 endpoint: Option<&str>,
93 names: Option<&[String]>,
94 ) -> impl Future<Output = Result<Vec<FunctionDefinition>, BoxError>> + Send;
95
96 fn select_agent_resources(
98 &self,
99 name: &str,
100 resources: &mut Vec<Resource>,
101 ) -> impl Future<Output = Vec<Resource>> + Send;
102
103 fn definitions(
111 &self,
112 names: Option<&[String]>,
113 ) -> impl Future<Output = Vec<FunctionDefinition>> + Send;
114
115 fn tool_call(
123 &self,
124 args: ToolInput<Json>,
125 ) -> impl Future<Output = Result<(ToolOutput<Json>, Option<Principal>), BoxError>> + Send;
126
127 fn agent_run(
135 self,
136 args: AgentInput,
137 ) -> impl Future<Output = Result<(AgentOutput, Option<Principal>), BoxError>> + Send;
138
139 fn remote_agent_run(
148 &self,
149 endpoint: &str,
150 args: AgentInput,
151 ) -> impl Future<Output = Result<AgentOutput, BoxError>> + Send;
152}
153
154pub trait BaseContext:
162 Sized + StateFeatures + KeysFeatures + StoreFeatures + CacheFeatures + HttpFeatures
163{
164 fn remote_tool_call(
173 &self,
174 endpoint: &str,
175 args: ToolInput<Json>,
176 ) -> impl Future<Output = Result<ToolOutput<Json>, BoxError>> + Send;
177}
178
179pub trait StateFeatures: Sized {
181 fn engine_id(&self) -> &Principal;
183
184 fn engine_name(&self) -> &str;
186
187 fn caller(&self) -> &Principal;
192
193 fn meta(&self) -> &RequestMeta;
195
196 fn cancellation_token(&self) -> CancellationToken;
203
204 fn time_elapsed(&self) -> Duration;
206}
207
208pub trait KeysFeatures: Sized {
215 fn a256gcm_key(
217 &self,
218 derivation_path: Vec<Vec<u8>>,
219 ) -> impl Future<Output = Result<[u8; 32], BoxError>> + Send;
220
221 fn ed25519_sign_message(
223 &self,
224 derivation_path: Vec<Vec<u8>>,
225 message: &[u8],
226 ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
227
228 fn ed25519_verify(
230 &self,
231 derivation_path: Vec<Vec<u8>>,
232 message: &[u8],
233 signature: &[u8],
234 ) -> impl Future<Output = Result<(), BoxError>> + Send;
235
236 fn ed25519_public_key(
238 &self,
239 derivation_path: Vec<Vec<u8>>,
240 ) -> impl Future<Output = Result<[u8; 32], BoxError>> + Send;
241
242 fn secp256k1_sign_message_bip340(
244 &self,
245 derivation_path: Vec<Vec<u8>>,
246 message: &[u8],
247 ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
248
249 fn secp256k1_verify_bip340(
251 &self,
252 derivation_path: Vec<Vec<u8>>,
253 message: &[u8],
254 signature: &[u8],
255 ) -> impl Future<Output = Result<(), BoxError>> + Send;
256
257 fn secp256k1_sign_message_ecdsa(
260 &self,
261 derivation_path: Vec<Vec<u8>>,
262 message: &[u8],
263 ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
264
265 fn secp256k1_sign_digest_ecdsa(
267 &self,
268 derivation_path: Vec<Vec<u8>>,
269 message_hash: &[u8],
270 ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
271
272 fn secp256k1_verify_ecdsa(
274 &self,
275 derivation_path: Vec<Vec<u8>>,
276 message_hash: &[u8],
277 signature: &[u8],
278 ) -> impl Future<Output = Result<(), BoxError>> + Send;
279
280 fn secp256k1_public_key(
282 &self,
283 derivation_path: Vec<Vec<u8>>,
284 ) -> impl Future<Output = Result<[u8; 33], BoxError>> + Send;
285}
286
287pub trait StoreFeatures: Sized {
292 fn store_get(
294 &self,
295 path: &Path,
296 ) -> impl Future<Output = Result<(bytes::Bytes, ObjectMeta), BoxError>> + Send;
297
298 fn store_list(
304 &self,
305 prefix: Option<&Path>,
306 offset: &Path,
307 ) -> impl Future<Output = Result<Vec<ObjectMeta>, BoxError>> + Send;
308
309 fn store_put(
316 &self,
317 path: &Path,
318 mode: PutMode,
319 value: bytes::Bytes,
320 ) -> impl Future<Output = Result<PutResult, BoxError>> + Send;
321
322 fn store_rename_if_not_exists(
328 &self,
329 from: &Path,
330 to: &Path,
331 ) -> impl Future<Output = Result<(), BoxError>> + Send;
332
333 fn store_delete(&self, path: &Path) -> impl Future<Output = Result<(), BoxError>> + Send;
338}
339
340#[derive(Debug, Clone)]
342pub enum CacheExpiry {
343 TTL(Duration),
345 TTI(Duration),
347}
348
349pub trait CacheFeatures: Sized {
354 fn cache_contains(&self, key: &str) -> bool;
356
357 fn cache_get<T>(&self, key: &str) -> impl Future<Output = Result<T, BoxError>> + Send
359 where
360 T: DeserializeOwned;
361
362 fn cache_get_with<T, F>(
366 &self,
367 key: &str,
368 init: F,
369 ) -> impl Future<Output = Result<T, BoxError>> + Send
370 where
371 T: Sized + DeserializeOwned + Serialize + Send,
372 F: Future<Output = Result<(T, Option<CacheExpiry>), BoxError>> + Send + 'static;
373
374 fn cache_set<T>(
376 &self,
377 key: &str,
378 val: (T, Option<CacheExpiry>),
379 ) -> impl Future<Output = ()> + Send
380 where
381 T: Sized + Serialize + Send;
382
383 fn cache_set_if_not_exists<T>(
385 &self,
386 key: &str,
387 val: (T, Option<CacheExpiry>),
388 ) -> impl Future<Output = bool> + Send
389 where
390 T: Sized + Serialize + Send;
391
392 fn cache_delete(&self, key: &str) -> impl Future<Output = bool> + Send;
394
395 fn cache_raw_iter(
397 &self,
398 ) -> impl Iterator<Item = (Arc<String>, Arc<(Bytes, Option<CacheExpiry>)>)>;
399}
400
401pub trait HttpFeatures: Sized {
407 fn https_call(
415 &self,
416 url: &str,
417 method: http::Method,
418 headers: Option<http::HeaderMap>,
419 body: Option<Vec<u8>>, ) -> impl Future<Output = Result<reqwest::Response, BoxError>> + Send;
421
422 fn https_signed_call(
431 &self,
432 url: &str,
433 method: http::Method,
434 message_digest: [u8; 32],
435 headers: Option<http::HeaderMap>,
436 body: Option<Vec<u8>>,
437 ) -> impl Future<Output = Result<reqwest::Response, BoxError>> + Send;
438
439 fn https_signed_rpc<T>(
446 &self,
447 endpoint: &str,
448 method: &str,
449 args: impl Serialize + Send,
450 ) -> impl Future<Output = Result<T, BoxError>> + Send
451 where
452 T: DeserializeOwned;
453}
454
455#[derive(Clone, Deserialize, Serialize)]
456struct CacheStoreValue<T>(T, UpdateVersion);
457
458#[async_trait]
471pub trait CacheStoreFeatures: StoreFeatures + CacheFeatures + Send + Sync + 'static {
472 async fn cache_store_init<T, F>(&self, key: &str, init: F) -> Result<(), BoxError>
474 where
475 T: DeserializeOwned + Serialize + Send,
476 F: Future<Output = Result<T, BoxError>> + Send + 'static,
477 {
478 let p = Path::from(key);
479 match self.store_get(&p).await {
480 Ok((v, meta)) => {
481 let val: T = from_slice(&v[..])?;
482 self.cache_set(
483 key,
484 (
485 CacheStoreValue(
486 val,
487 UpdateVersion {
488 e_tag: meta.e_tag,
489 version: meta.version,
490 },
491 ),
492 None,
493 ),
494 )
495 .await;
496 Ok(())
497 }
498 Err(_) => {
499 let val: T = init.await?;
500 let data = to_canonical_vec(&val)?;
501 let res = self.store_put(&p, PutMode::Create, data.into()).await?;
502 self.cache_set(
503 key,
504 (
505 CacheStoreValue(
506 val,
507 UpdateVersion {
508 e_tag: res.e_tag,
509 version: res.version,
510 },
511 ),
512 None,
513 ),
514 )
515 .await;
516 Ok(())
517 }
518 }
519 }
520
521 async fn cache_store_get<T>(&self, key: &str) -> Result<(T, UpdateVersion), BoxError>
523 where
524 T: DeserializeOwned + Serialize + Send + Sync,
525 {
526 match self.cache_get::<CacheStoreValue<T>>(key).await {
527 Ok(CacheStoreValue(val, ver)) => Ok((val, ver)),
528 Err(_) => {
529 let p = Path::from(key);
531 let (v, meta) = self.store_get(&p).await?;
532 let val: T = from_slice(&v[..])?;
533 let version = UpdateVersion {
534 e_tag: meta.e_tag,
535 version: meta.version,
536 };
537 self.cache_set(key, (CacheStoreValue(&val, version.clone()), None))
538 .await;
539 Ok((val, version))
540 }
541 }
542 }
543
544 async fn cache_store_set<T>(
553 &self,
554 key: &str,
555 val: T,
556 version: Option<UpdateVersion>,
557 ) -> Result<UpdateVersion, BoxError>
558 where
559 T: DeserializeOwned + Serialize + Send,
560 {
561 let data = to_canonical_vec(&val)?;
562 let p = Path::from(key);
563 if let Some(ver) = version {
564 let res = self
566 .store_put(
567 &p,
568 PutMode::Update(OsVersion {
569 e_tag: ver.e_tag.clone(),
570 version: ver.version.clone(),
571 }),
572 data.into(),
573 )
574 .await?;
575 let ver = UpdateVersion {
577 e_tag: res.e_tag,
578 version: res.version,
579 };
580 self.cache_set(key, (CacheStoreValue(val, ver.clone()), None))
581 .await;
582 Ok(ver)
583 } else {
584 let res = self.store_put(&p, PutMode::Overwrite, data.into()).await?;
585 let ver = UpdateVersion {
586 e_tag: res.e_tag,
587 version: res.version,
588 };
589 self.cache_set(key, (CacheStoreValue(val, ver.clone()), None))
590 .await;
591 Ok(ver)
592 }
593 }
594
595 async fn cache_store_delete(&self, key: &str) -> Result<(), BoxError> {
603 let p = Path::from(key);
604 self.store_delete(&p).await?;
605 self.cache_delete(key).await;
606 Ok(())
607 }
608}
609
610pub fn derivation_path_with(path: &Path, derivation_path: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
612 let mut dp = Vec::with_capacity(derivation_path.len() + 1);
613 dp.push(path.as_ref().as_bytes().to_vec());
614 dp.extend(derivation_path);
615 dp
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use futures::executor::block_on;
622 use http::Extensions;
623 use std::{
624 collections::BTreeMap,
625 sync::{
626 Arc, Mutex,
627 atomic::{AtomicUsize, Ordering},
628 },
629 };
630
631 type TestCacheValue = Arc<(Bytes, Option<CacheExpiry>)>;
632 type TestCacheMap = BTreeMap<String, TestCacheValue>;
633
634 #[derive(Default)]
635 struct TestCacheStore {
636 cache: Mutex<TestCacheMap>,
637 store: Mutex<BTreeMap<String, (Bytes, UpdateVersion)>>,
638 store_gets: AtomicUsize,
639 versions: AtomicUsize,
640 }
641
642 impl TestCacheStore {
643 fn put_serialized(&self, key: &str, value: Vec<u8>, version: UpdateVersion) {
644 self.store
645 .lock()
646 .unwrap()
647 .insert(key.to_string(), (value.into(), version));
648 }
649
650 fn next_version(&self) -> UpdateVersion {
651 let version = self.versions.fetch_add(1, Ordering::SeqCst) + 1;
652 UpdateVersion {
653 e_tag: Some(format!("etag-{version}")),
654 version: Some(version.to_string()),
655 }
656 }
657 }
658
659 impl CacheFeatures for TestCacheStore {
660 fn cache_contains(&self, key: &str) -> bool {
661 self.cache.lock().unwrap().contains_key(key)
662 }
663
664 async fn cache_get<T>(&self, key: &str) -> Result<T, BoxError>
665 where
666 T: DeserializeOwned,
667 {
668 let value = self
669 .cache
670 .lock()
671 .unwrap()
672 .get(key)
673 .cloned()
674 .ok_or_else(|| format!("key {key} not found"))?;
675 from_slice(&value.0[..]).map_err(|err| err.into())
676 }
677
678 async fn cache_get_with<T, F>(&self, key: &str, init: F) -> Result<T, BoxError>
679 where
680 T: Sized + DeserializeOwned + Serialize + Send,
681 F: Future<Output = Result<(T, Option<CacheExpiry>), BoxError>> + Send + 'static,
682 {
683 if let Some(value) = self.cache.lock().unwrap().get(key).cloned() {
684 return from_slice(&value.0[..]).map_err(|err| err.into());
685 }
686
687 let (value, expiry) = init.await?;
688 let data = to_canonical_vec(&value)?;
689 self.cache
690 .lock()
691 .unwrap()
692 .insert(key.to_string(), Arc::new((data.into(), expiry)));
693 Ok(value)
694 }
695
696 async fn cache_set<T>(&self, key: &str, val: (T, Option<CacheExpiry>))
697 where
698 T: Sized + Serialize + Send,
699 {
700 let data = to_canonical_vec(&val.0).unwrap();
701 self.cache
702 .lock()
703 .unwrap()
704 .insert(key.to_string(), Arc::new((data.into(), val.1)));
705 }
706
707 async fn cache_set_if_not_exists<T>(&self, key: &str, val: (T, Option<CacheExpiry>)) -> bool
708 where
709 T: Sized + Serialize + Send,
710 {
711 let mut cache = self.cache.lock().unwrap();
712 if cache.contains_key(key) {
713 return false;
714 }
715
716 let data = to_canonical_vec(&val.0).unwrap();
717 cache.insert(key.to_string(), Arc::new((data.into(), val.1)));
718 true
719 }
720
721 async fn cache_delete(&self, key: &str) -> bool {
722 self.cache.lock().unwrap().remove(key).is_some()
723 }
724
725 fn cache_raw_iter(
726 &self,
727 ) -> impl Iterator<Item = (Arc<String>, Arc<(Bytes, Option<CacheExpiry>)>)> {
728 self.cache
729 .lock()
730 .unwrap()
731 .iter()
732 .map(|(key, value)| (Arc::new(key.clone()), value.clone()))
733 .collect::<Vec<_>>()
734 .into_iter()
735 }
736 }
737
738 impl StoreFeatures for TestCacheStore {
739 async fn store_get(&self, path: &Path) -> Result<(bytes::Bytes, ObjectMeta), BoxError> {
740 self.store_gets.fetch_add(1, Ordering::SeqCst);
741 let (value, version) = self
742 .store
743 .lock()
744 .unwrap()
745 .get(path.as_ref())
746 .cloned()
747 .ok_or_else(|| format!("path {path} not found"))?;
748
749 Ok((
750 value.clone(),
751 ObjectMeta {
752 location: path.clone(),
753 last_modified: chrono::Utc::now(),
754 size: value.len() as u64,
755 e_tag: version.e_tag,
756 version: version.version,
757 },
758 ))
759 }
760
761 async fn store_list(
762 &self,
763 _prefix: Option<&Path>,
764 _offset: &Path,
765 ) -> Result<Vec<ObjectMeta>, BoxError> {
766 Ok(Vec::new())
767 }
768
769 async fn store_put(
770 &self,
771 path: &Path,
772 mode: PutMode,
773 value: bytes::Bytes,
774 ) -> Result<PutResult, BoxError> {
775 let key = path.as_ref().to_string();
776 let mut store = self.store.lock().unwrap();
777 match mode {
778 PutMode::Create if store.contains_key(&key) => {
779 return Err(format!("path {path} already exists").into());
780 }
781 PutMode::Update(expected) => {
782 let Some((_, current)) = store.get(&key) else {
783 return Err(format!("path {path} not found").into());
784 };
785 if current.e_tag != expected.e_tag || current.version != expected.version {
786 return Err(format!("path {path} version mismatch").into());
787 }
788 }
789 _ => {}
790 }
791
792 let version = self.next_version();
793 store.insert(key, (value, version.clone()));
794 Ok(PutResult {
795 e_tag: version.e_tag,
796 version: version.version,
797 extensions: Extensions::default(),
798 })
799 }
800
801 async fn store_rename_if_not_exists(&self, from: &Path, to: &Path) -> Result<(), BoxError> {
802 let mut store = self.store.lock().unwrap();
803 let to = to.as_ref().to_string();
804 if store.contains_key(&to) {
805 return Err(format!("path {to} already exists").into());
806 }
807 let value = store
808 .remove(from.as_ref())
809 .ok_or_else(|| format!("path {from} not found"))?;
810 store.insert(to, value);
811 Ok(())
812 }
813
814 async fn store_delete(&self, path: &Path) -> Result<(), BoxError> {
815 self.store.lock().unwrap().remove(path.as_ref());
816 Ok(())
817 }
818 }
819
820 impl CacheStoreFeatures for TestCacheStore {}
821
822 #[test]
823 fn cache_store_get_populates_cache_without_second_store_read() {
824 let ctx = TestCacheStore::default();
825 let stored_version = UpdateVersion {
826 e_tag: Some("etag-stored".to_string()),
827 version: Some("1".to_string()),
828 };
829 let data = to_canonical_vec(&123_u32).unwrap();
830 ctx.put_serialized("answer", data, stored_version.clone());
831
832 let (value, version) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
833 assert_eq!(value, 123);
834 assert_eq!(version.e_tag, stored_version.e_tag);
835 assert_eq!(version.version, stored_version.version);
836 assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 1);
837
838 let (value, _) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
839 assert_eq!(value, 123);
840 assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 1);
841 }
842
843 #[test]
844 fn cache_store_set_overwrite_updates_cache() {
845 let ctx = TestCacheStore::default();
846
847 let version = block_on(ctx.cache_store_set("answer", 42_u32, None)).unwrap();
848 assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 0);
849
850 let (value, cached_version) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
851 assert_eq!(value, 42);
852 assert_eq!(cached_version.e_tag, version.e_tag);
853 assert_eq!(cached_version.version, version.version);
854 assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 0);
855 }
856
857 #[test]
858 fn cache_store_init_loads_existing_value_and_skips_initializer() {
859 let ctx = TestCacheStore::default();
860 let stored_version = UpdateVersion {
861 e_tag: Some("etag-existing".to_string()),
862 version: Some("7".to_string()),
863 };
864 let data = to_canonical_vec(&"stored".to_string()).unwrap();
865 ctx.put_serialized("message", data, stored_version.clone());
866
867 block_on(ctx.cache_store_init("message", async {
868 Err::<String, BoxError>("initializer should not run".into())
869 }))
870 .unwrap();
871
872 let (value, version) = block_on(ctx.cache_store_get::<String>("message")).unwrap();
873 assert_eq!(value, "stored");
874 assert_eq!(version.e_tag, stored_version.e_tag);
875 assert_eq!(version.version, stored_version.version);
876 assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 1);
877 }
878
879 #[test]
880 fn cache_store_init_creates_missing_value_and_delete_clears_layers() {
881 let ctx = TestCacheStore::default();
882
883 block_on(ctx.cache_store_init("message", async {
884 Ok::<_, BoxError>("created".to_string())
885 }))
886 .unwrap();
887 assert!(ctx.cache_contains("message"));
888 assert!(ctx.store.lock().unwrap().contains_key("message"));
889
890 let (value, _) = block_on(ctx.cache_store_get::<String>("message")).unwrap();
891 assert_eq!(value, "created");
892
893 block_on(ctx.cache_store_delete("message")).unwrap();
894 assert!(!ctx.cache_contains("message"));
895 assert!(!ctx.store.lock().unwrap().contains_key("message"));
896 }
897
898 #[test]
899 fn cache_store_set_update_enforces_expected_version() {
900 let ctx = TestCacheStore::default();
901
902 let version = block_on(ctx.cache_store_set("answer", 1_u32, None)).unwrap();
903 let updated = block_on(ctx.cache_store_set("answer", 2_u32, Some(version))).unwrap();
904 let (value, cached_version) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
905 assert_eq!(value, 2);
906 assert_eq!(cached_version.version, updated.version);
907
908 let err = block_on(ctx.cache_store_set(
909 "answer",
910 3_u32,
911 Some(UpdateVersion {
912 e_tag: Some("wrong".to_string()),
913 version: Some("wrong".to_string()),
914 }),
915 ))
916 .unwrap_err();
917 assert!(err.to_string().contains("version mismatch"));
918 }
919
920 #[test]
921 fn cache_and_store_mock_helpers_cover_absent_existing_and_error_paths() {
922 let ctx = TestCacheStore::default();
923
924 let ttl = CacheExpiry::TTL(Duration::from_secs(5));
925 block_on(ctx.cache_set("ttl", ("one".to_string(), Some(ttl.clone()))));
926 assert!(ctx.cache_contains("ttl"));
927
928 let existing = block_on(ctx.cache_set_if_not_exists(
929 "ttl",
930 (
931 "two".to_string(),
932 Some(CacheExpiry::TTI(Duration::from_secs(9))),
933 ),
934 ));
935 assert!(!existing);
936
937 let inserted = block_on(ctx.cache_set_if_not_exists(
938 "tti",
939 (
940 "three".to_string(),
941 Some(CacheExpiry::TTI(Duration::from_secs(9))),
942 ),
943 ));
944 assert!(inserted);
945
946 let mut seen = ctx
947 .cache_raw_iter()
948 .map(|(key, value)| (key.to_string(), value.1.clone()))
949 .collect::<Vec<_>>();
950 seen.sort_by(|a, b| a.0.cmp(&b.0));
951 assert_eq!(seen.len(), 2);
952 let ttl_expiry = seen
953 .iter()
954 .find(|(key, _)| key == "ttl")
955 .and_then(|(_, expiry)| expiry.as_ref())
956 .unwrap();
957 let tti_expiry = seen
958 .iter()
959 .find(|(key, _)| key == "tti")
960 .and_then(|(_, expiry)| expiry.as_ref())
961 .unwrap();
962 match ttl_expiry {
963 CacheExpiry::TTL(duration) => assert_eq!(*duration, Duration::from_secs(5)),
964 CacheExpiry::TTI(_) => panic!("expected ttl"),
965 }
966 match tti_expiry {
967 CacheExpiry::TTI(duration) => assert_eq!(*duration, Duration::from_secs(9)),
968 CacheExpiry::TTL(_) => panic!("expected tti"),
969 }
970
971 let value =
972 block_on(ctx.cache_get_with("lazy", async { Ok::<_, BoxError>((99_u32, None)) }))
973 .unwrap();
974 assert_eq!(value, 99);
975 let cached =
976 block_on(ctx.cache_get_with("lazy", async { Ok::<_, BoxError>((100_u32, None)) }))
977 .unwrap();
978 assert_eq!(cached, 99);
979
980 let first =
981 block_on(ctx.store_put(&Path::from("created"), PutMode::Create, Bytes::from("a")))
982 .unwrap();
983 assert!(first.version.is_some());
984 let err =
985 block_on(ctx.store_put(&Path::from("created"), PutMode::Create, Bytes::from("b")))
986 .unwrap_err();
987 assert!(err.to_string().contains("already exists"));
988
989 block_on(ctx.store_rename_if_not_exists(&Path::from("created"), &Path::from("renamed")))
990 .unwrap();
991 assert!(ctx.store.lock().unwrap().contains_key("renamed"));
992 let err = block_on(
993 ctx.store_rename_if_not_exists(&Path::from("missing"), &Path::from("renamed")),
994 )
995 .unwrap_err();
996 assert!(err.to_string().contains("already exists"));
997 let err = block_on(
998 ctx.store_rename_if_not_exists(&Path::from("missing"), &Path::from("new-destination")),
999 )
1000 .unwrap_err();
1001 assert!(err.to_string().contains("not found"));
1002
1003 let listed =
1004 block_on(ctx.store_list(Some(&Path::from("r")), &Path::from("renamed"))).unwrap();
1005 assert!(listed.is_empty());
1006 }
1007
1008 #[test]
1009 fn derivation_path_with_prefixes_current_path() {
1010 let path = Path::from("agent/main");
1011 let derivation_path = derivation_path_with(&path, vec![b"child".to_vec()]);
1012 assert_eq!(
1013 derivation_path,
1014 vec![b"agent/main".to_vec(), b"child".to_vec()]
1015 );
1016 }
1017}