1use std::collections::{BTreeMap, BTreeSet};
10use std::sync::Mutex;
11
12use rustev_contract::Document;
13use rustev_contract::cache_entry::{
14 CacheEntry, DecisionPayload, EntryBody, EntryGenerations, EntryKind, EntryPayload,
15 RequestPayload, ReusedRequestPayload,
16};
17use rustev_contract::canonical::CanonicalError;
18use rustev_contract::ids::ContentDigest;
19use rustev_contract::optimization::{OptimizationPolicy, PersistentCachePolicy, StoreContract};
20use rustev_contract::output::RawOutput;
21use rustev_contract::schema;
22use rustev_contract::time::DurationMs;
23use rustev_contract::time::Timestamp;
24use rustev_core::seams::{BoxFuture, CancelSignal};
25
26use crate::Inner;
27use crate::clock::Instant;
28use crate::optimization::RequestKey;
29use crate::optimization::decision::{DecisionEntry, ReusedRequest, ReusedRequests};
30
31#[derive(Debug, Clone)]
34pub struct StoreCall {
35 pub remaining_ms: Option<u64>,
37 pub cancel: CancelSignal,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Stored {
44 pub bytes: Option<Vec<u8>>,
45 pub generations: EntryGenerations,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct PutTerms {
51 pub scope: String,
53 pub generations: EntryGenerations,
55 pub max_scope_bytes: u64,
56 pub max_namespace_bytes: u64,
57 pub expires: Timestamp,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum PutAnswer {
65 Stored,
66 GenerationAdvanced,
68 OverBudget,
70 Conflict,
73}
74
75impl PutAnswer {
76 pub fn diagnostic(self) -> Option<&'static str> {
78 match self {
79 PutAnswer::Stored => None,
80 PutAnswer::GenerationAdvanced => Some("generation_advanced"),
81 PutAnswer::OverBudget => Some("over_budget"),
82 PutAnswer::Conflict => Some("store_conflict"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum StoreSelector {
90 Namespace,
91 Scope(String),
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Invalidated {
98 pub generation: u64,
99 pub removed: u64,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct StoreFailure {
106 pub detail: String,
107}
108
109pub trait RuntimeStore: Send + Sync {
112 fn contract(&self) -> &StoreContract;
114
115 fn get<'a>(
118 &'a self,
119 namespace: &'a str,
120 scope: &'a str,
121 key_id: &'a str,
122 call: StoreCall,
123 ) -> BoxFuture<'a, Result<Stored, StoreFailure>>;
124
125 fn put<'a>(
127 &'a self,
128 namespace: &'a str,
129 key_id: &'a str,
130 bytes: Vec<u8>,
131 terms: PutTerms,
132 call: StoreCall,
133 ) -> BoxFuture<'a, Result<PutAnswer, StoreFailure>>;
134
135 fn delete<'a>(
137 &'a self,
138 namespace: &'a str,
139 key_id: &'a str,
140 call: StoreCall,
141 ) -> BoxFuture<'a, Result<(), StoreFailure>>;
142
143 fn invalidate<'a>(
146 &'a self,
147 namespace: &'a str,
148 selector: StoreSelector,
149 call: StoreCall,
150 ) -> BoxFuture<'a, Result<Invalidated, StoreFailure>>;
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub(crate) enum EntryMiss {
156 SchemaUnknown,
158 Corrupt,
161 Collision,
165 GenerationAdvanced,
167 Expired,
168 FromFuture,
170}
171
172impl EntryMiss {
173 pub(crate) fn diagnostic(self) -> &'static str {
177 match self {
178 EntryMiss::SchemaUnknown => "entry_schema_unknown",
179 EntryMiss::Corrupt => "entry_corrupt",
180 EntryMiss::Collision => "entry_collision",
181 EntryMiss::GenerationAdvanced => "generation_advanced",
182 EntryMiss::Expired => "entry_expired",
183 EntryMiss::FromFuture => "entry_from_future",
184 }
185 }
186
187 pub(crate) fn deletes(self) -> bool {
190 self == EntryMiss::Corrupt
191 }
192}
193
194pub(crate) struct Expected<'a> {
196 pub kind: EntryKind,
197 pub key: &'a RequestKey,
198 pub generations: EntryGenerations,
200 pub now: Timestamp,
202}
203
204#[derive(Debug, Clone, PartialEq)]
206pub(crate) struct Served {
207 pub payload: EntryPayload,
208 pub created: Timestamp,
209 pub age_ms: u64,
210 pub evidence: ContentDigest,
211}
212
213pub(crate) fn read_entry(bytes: &[u8], e: Expected<'_>) -> Result<Served, EntryMiss> {
216 let doc = serde_json::from_slice::<serde_json::Value>(bytes).map_err(|_| EntryMiss::Corrupt)?;
220 if doc.get("schema").and_then(|s| s.as_str()) != Some(schema::CACHE_ENTRY) {
221 return Err(EntryMiss::SchemaUnknown);
222 }
223 let entry = CacheEntry::parse(bytes).map_err(|_| EntryMiss::Corrupt)?;
224 if !entry.intact() {
225 return Err(EntryMiss::Corrupt);
226 }
227 let body = entry.entry;
228 if !matches!(
230 (body.kind, &body.payload),
231 (EntryKind::Request, EntryPayload::Request(_))
232 | (EntryKind::Decision, EntryPayload::Decision(_))
233 ) {
234 return Err(EntryMiss::Corrupt);
235 }
236 if body.kind != e.kind || body.key_material != hex(&e.key.material) {
238 return Err(EntryMiss::Collision);
239 }
240 if body.key_id != e.key.id {
243 return Err(EntryMiss::Corrupt);
244 }
245 if body.generations != e.generations {
246 return Err(EntryMiss::GenerationAdvanced);
247 }
248 let (now, created) = (e.now.as_ms(), body.created.as_ms());
249 if now < created {
250 return Err(EntryMiss::FromFuture);
251 }
252 let age_ms = u64::try_from(now - created).unwrap_or(u64::MAX);
253 if age_ms > body.ttl_ms {
254 return Err(EntryMiss::Expired);
255 }
256 Ok(Served {
257 payload: body.payload,
258 created: body.created,
259 age_ms,
260 evidence: body.evidence,
261 })
262}
263
264pub(crate) struct EntryWrite<'a> {
266 pub key: &'a RequestKey,
267 pub policy: &'a OptimizationPolicy,
268 pub generations: EntryGenerations,
269 pub created: Timestamp,
270 pub ttl_ms: u64,
271 pub payload: EntryPayload,
272 pub evidence: ContentDigest,
273}
274
275pub(crate) fn write_entry(w: EntryWrite<'_>) -> Result<Vec<u8>, CanonicalError> {
277 let entry = CacheEntry::seal(EntryBody {
278 kind: match w.payload {
279 EntryPayload::Request(_) => EntryKind::Request,
280 EntryPayload::Decision(_) => EntryKind::Decision,
281 },
282 key_material: hex(&w.key.material),
283 key_id: w.key.id.clone(),
284 key_schema_version: w.policy.key_schema_version,
285 namespace_version: w.policy.cache_namespace_version,
286 runtime_version: env!("CARGO_PKG_VERSION").into(),
287 generations: w.generations,
288 created: w.created,
289 ttl_ms: w.ttl_ms,
290 payload: w.payload,
291 evidence: w.evidence,
292 })?;
293 entry.record_canonical()
294}
295
296pub(crate) struct PendingWrite {
299 pub step: String,
300 pub instance: Vec<String>,
301 pub key: RequestKey,
302 pub output: RawOutput,
303 pub source_attempt_id: String,
304}
305
306#[derive(Default)]
309pub(crate) struct Pending {
310 captured: Mutex<BTreeMap<String, EntryGenerations>>,
311 writes: Mutex<Vec<PendingWrite>>,
312}
313
314impl Pending {
315 pub(crate) fn capture(&self, key_id: &str, generations: EntryGenerations) {
317 lock(&self.captured)
318 .entry(key_id.to_string())
319 .or_insert(generations);
320 }
321
322 pub(crate) fn queue(&self, write: PendingWrite) {
323 lock(&self.writes).push(write);
324 }
325}
326
327fn lock<S>(m: &Mutex<S>) -> std::sync::MutexGuard<'_, S> {
328 m.lock().unwrap_or_else(|e| e.into_inner())
329}
330
331pub(crate) type WriteDiagnostics = Vec<((String, Vec<String>), String)>;
334
335pub(crate) struct Writing<'a> {
337 pub rt: &'a Inner,
338 pub store: &'a dyn RuntimeStore,
339 pub policy: &'a OptimizationPolicy,
340 pub persistent: &'a PersistentCachePolicy,
341 pub cancel: &'a CancelSignal,
342 pub deadline: Instant,
343 pub created: Timestamp,
344 pub evidence: Option<ContentDigest>,
347}
348
349pub(crate) struct StoreCtx<'a> {
353 pub rt: &'a Inner,
354 pub store: &'a dyn RuntimeStore,
355 pub persistent: &'a PersistentCachePolicy,
356 pub cancel: &'a CancelSignal,
357 pub deadline: Instant,
358}
359
360impl Writing<'_> {
361 fn calls(&self) -> StoreCtx<'_> {
362 StoreCtx {
363 rt: self.rt,
364 store: self.store,
365 persistent: self.persistent,
366 cancel: self.cancel,
367 deadline: self.deadline,
368 }
369 }
370}
371
372async fn put_entry(p: &StoreCtx<'_>, e: EntryWrite<'_>) -> Result<(), String> {
377 let key = e.key;
378 if p.rt.persistent_unavailable(&key.namespace, &key.scope_id) {
379 return Err(crate::driver::UNAVAILABLE.into());
380 }
381 let (created, ttl_ms, generations) = (e.created, e.ttl_ms, e.generations);
382 let bytes =
383 write_entry(e).map_err(|_| "persistent cache entry has no canonical form".to_string())?;
384 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > p.persistent.max_entry_bytes {
385 return Err("over_budget: entry exceeds max_entry_bytes".into());
386 }
387 let expires = created.checked_add(DurationMs(ttl_ms)).unwrap_or_else(|_| {
390 Timestamp::from_ms(rustev_contract::time::MAX_TIMESTAMP_MS)
391 .expect("the last representable time is a timestamp")
392 });
393 let terms = PutTerms {
394 scope: key.scope_id.clone(),
395 generations,
396 max_scope_bytes: p.persistent.max_scope_bytes,
397 max_namespace_bytes: p.persistent.max_namespace_bytes,
398 expires,
399 };
400 let answer = crate::driver::store_call(p.rt, p.cancel, p.deadline, |call| {
401 p.store.put(&key.namespace, &key.id, bytes, terms, call)
402 })
403 .await;
404 match answer {
405 Ok(answer) => answer.diagnostic().map_or(Ok(()), |d| Err(d.into())),
406 Err(failure) => Err(format!("store_failed: {}", failure.detail)),
407 }
408}
409
410pub(crate) async fn write_pending(pending: Pending, w: Writing<'_>) -> WriteDiagnostics {
416 let captured = pending
417 .captured
418 .into_inner()
419 .unwrap_or_else(|e| e.into_inner());
420 let writes = pending
421 .writes
422 .into_inner()
423 .unwrap_or_else(|e| e.into_inner());
424 let mut written = BTreeSet::new();
425 let mut diagnostics = vec![];
426 for write in writes {
427 let Some(&generations) = captured.get(&write.key.id) else {
428 continue;
429 };
430 if !written.insert(write.key.id.clone()) {
431 continue;
432 }
433 let at = (write.step.clone(), write.instance.clone());
434 let Some(evidence) = w.evidence.clone() else {
435 diagnostics.push((
436 at,
437 "persistent cache entry not written: the evidence has no canonical form".into(),
438 ));
439 continue;
440 };
441 let entry = EntryWrite {
442 key: &write.key,
443 policy: w.policy,
444 generations,
445 created: w.created,
446 ttl_ms: w.persistent.ttl_ms,
447 payload: EntryPayload::Request(RequestPayload {
448 target: 0,
449 output: write.output,
450 source_attempt_id: write.source_attempt_id,
451 }),
452 evidence,
453 };
454 if let Err(d) = put_entry(&w.calls(), entry).await {
455 diagnostics.push((at, d));
456 }
457 }
458 diagnostics
459}
460
461pub(crate) enum DecisionRead {
463 Hit {
467 entry: DecisionEntry,
468 age_ms: u64,
469 generations: EntryGenerations,
470 },
471 Miss {
474 captured: Option<EntryGenerations>,
475 diagnostic: String,
476 },
477 Failed(String),
479}
480
481pub(crate) async fn read_decision(
485 p: &StoreCtx<'_>,
486 key: &RequestKey,
487 now: Timestamp,
488) -> DecisionRead {
489 if p.rt.persistent_unavailable(&key.namespace, &key.scope_id) {
490 return DecisionRead::Miss {
491 captured: None,
492 diagnostic: crate::driver::UNAVAILABLE.into(),
493 };
494 }
495 let stored = crate::driver::store_call(p.rt, p.cancel, p.deadline, |call| {
496 p.store.get(&key.namespace, &key.scope_id, &key.id, call)
497 })
498 .await;
499 let stored = match stored {
500 Ok(stored) => stored,
501 Err(failure) => return DecisionRead::Failed(format!("store_failed: {}", failure.detail)),
502 };
503 let captured = Some(stored.generations);
504 let Some(bytes) = stored.bytes else {
505 return DecisionRead::Miss {
506 captured,
507 diagnostic: "persistent decision cache miss".into(),
508 };
509 };
510 let read = read_entry(
511 &bytes,
512 Expected {
513 kind: EntryKind::Decision,
514 key,
515 generations: stored.generations,
516 now,
517 },
518 );
519 let miss = match read {
520 Ok(Served {
521 payload: EntryPayload::Decision(payload),
522 created,
523 age_ms,
524 ..
525 }) => match decision_entry(key, payload, created) {
526 Some(entry) => {
527 return DecisionRead::Hit {
528 entry,
529 age_ms,
530 generations: stored.generations,
531 };
532 }
533 None => EntryMiss::Corrupt,
534 },
535 Ok(_) => EntryMiss::Collision,
537 Err(miss) => miss,
538 };
539 let mut diagnostic = miss.diagnostic().to_string();
540 if miss.deletes()
541 && let Err(failure) = crate::driver::store_call(p.rt, p.cancel, p.deadline, |call| {
542 p.store.delete(&key.namespace, &key.id, call)
543 })
544 .await
545 {
546 diagnostic = format!("{diagnostic}; store_failed: {}", failure.detail);
547 }
548 DecisionRead::Miss {
549 captured,
550 diagnostic,
551 }
552}
553
554fn decision_entry(
557 key: &RequestKey,
558 payload: DecisionPayload,
559 created: Timestamp,
560) -> Option<DecisionEntry> {
561 let mut requests = ReusedRequests::new();
562 for r in payload.requests {
563 let reused = ReusedRequest {
564 key: RequestKey::from_material(
565 unhex(&r.key_material)?,
566 key.scope_id.clone(),
567 key.namespace.clone(),
568 ),
569 output: r.output,
570 source_attempt_id: r.source_attempt_id,
571 };
572 if requests.insert((r.step, r.instance), reused).is_some() {
573 return None;
574 }
575 }
576 Some(DecisionEntry {
577 requests,
578 volatile: payload.volatile,
579 source_run_record: payload.run_record,
580 created,
581 })
582}
583
584pub(crate) struct DecisionWrite<'a> {
586 pub key: &'a RequestKey,
587 pub policy: &'a OptimizationPolicy,
588 pub requests: &'a ReusedRequests,
589 pub volatile: Vec<rustev_contract::snapshot::Entry>,
591 pub judgment: ContentDigest,
592 pub run_record: ContentDigest,
593 pub evidence: ContentDigest,
594 pub generations: EntryGenerations,
596 pub created: Timestamp,
597 pub ttl_ms: u64,
599}
600
601pub(crate) async fn write_decision(p: &StoreCtx<'_>, w: DecisionWrite<'_>) -> Result<(), String> {
604 let requests = w
605 .requests
606 .iter()
607 .map(|((step, instance), r)| ReusedRequestPayload {
608 step: step.clone(),
609 instance: instance.clone(),
610 key_material: hex(&r.key.material),
611 output: r.output.clone(),
612 source_attempt_id: r.source_attempt_id.clone(),
613 })
614 .collect();
615 let entry = EntryWrite {
616 key: w.key,
617 policy: w.policy,
618 generations: w.generations,
619 created: w.created,
620 ttl_ms: w.ttl_ms,
621 payload: EntryPayload::Decision(DecisionPayload {
622 requests,
623 volatile: w.volatile,
624 judgment: w.judgment,
625 run_record: w.run_record,
626 }),
627 evidence: w.evidence,
628 };
629 put_entry(p, entry).await
630}
631
632fn unhex(s: &str) -> Option<Vec<u8>> {
633 let digit = |c: u8| match c {
634 b'0'..=b'9' => Some(c - b'0'),
635 b'a'..=b'f' => Some(c - b'a' + 10),
636 _ => None,
637 };
638 let s = s.as_bytes();
639 if !s.len().is_multiple_of(2) {
640 return None;
641 }
642 s.chunks(2)
643 .map(|pair| Some(digit(pair[0])? << 4 | digit(pair[1])?))
644 .collect()
645}
646
647fn hex(bytes: &[u8]) -> String {
648 const DIGITS: &[u8; 16] = b"0123456789abcdef";
649 let mut s = String::with_capacity(bytes.len() * 2);
650 for b in bytes {
651 s.push(DIGITS[usize::from(b >> 4)] as char);
652 s.push(DIGITS[usize::from(b & 0xf)] as char);
653 }
654 s
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use rustev_contract::canonical::record_canonical_bytes;
661 use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
662 use rustev_contract::optimization::{ExpiryBasis, OptimizationAdmission};
663 use rustev_contract::output::RawOutput;
664
665 use crate::optimization::{KeyParts, request_key};
666
667 fn digest(c: char) -> String {
668 format!("sha256:{}", c.to_string().repeat(64))
669 }
670
671 fn policy() -> OptimizationPolicy {
672 OptimizationPolicy {
673 key_schema_version: 1,
674 cache_namespace_version: 1,
675 admission: OptimizationAdmission::Refuse,
676 expiry: ExpiryBasis::InjectedRuntimeTime,
677 batch: None,
678 memory_cache: None,
679 shared_calls: None,
680 persistent_cache: None,
681 decision_cache: None,
682 }
683 }
684
685 fn key(principal: &[u8]) -> RequestKey {
686 let plan = PlanId::parse(&digest('a')).unwrap();
687 let artifact = ArtifactId::parse(&digest('b')).unwrap();
688 let descriptor = DescriptorId::parse(&digest('c')).unwrap();
689 request_key(KeyParts {
690 plan_id: &plan,
691 profile: "none",
692 policy: &policy(),
693 principal,
694 backend_artifact: &artifact,
695 backend_descriptor: &descriptor,
696 step: "topic",
697 instance: &[],
698 projection: b"{}",
699 })
700 }
701
702 const GENS: EntryGenerations = EntryGenerations {
703 namespace: 3,
704 scope: 7,
705 };
706
707 fn at(ms: i64) -> Timestamp {
708 Timestamp::from_ms(ms).unwrap()
709 }
710
711 fn entry(k: &RequestKey) -> Vec<u8> {
712 write_entry(EntryWrite {
713 key: k,
714 policy: &policy(),
715 generations: GENS,
716 created: at(1_000),
717 ttl_ms: 500,
718 payload: EntryPayload::Request(RequestPayload {
719 target: 0,
720 output: RawOutput::Label("billing".into()),
721 source_attempt_id: "att-1".into(),
722 }),
723 evidence: ContentDigest::parse(&digest('d')).unwrap(),
724 })
725 .unwrap()
726 }
727
728 fn read(
729 bytes: &[u8],
730 k: &RequestKey,
731 gens: EntryGenerations,
732 now: i64,
733 ) -> Result<u64, EntryMiss> {
734 read_entry(
735 bytes,
736 Expected {
737 kind: EntryKind::Request,
738 key: k,
739 generations: gens,
740 now: at(now),
741 },
742 )
743 .map(|s| s.age_ms)
744 }
745
746 #[test]
747 fn an_intact_current_unexpired_entry_is_served_with_its_age() {
748 let k = key(b"tenant-a");
749 let bytes = entry(&k);
750 let served = read_entry(
751 &bytes,
752 Expected {
753 kind: EntryKind::Request,
754 key: &k,
755 generations: GENS,
756 now: at(1_200),
757 },
758 )
759 .unwrap();
760 assert_eq!(served.age_ms, 200);
761 assert_eq!(served.created, at(1_000));
762 let EntryPayload::Request(payload) = served.payload else {
763 panic!("a request payload");
764 };
765 assert_eq!(payload.output, RawOutput::Label("billing".into()));
766 assert_eq!(payload.source_attempt_id, "att-1");
767 assert_eq!(read(&bytes, &k, GENS, 1_500), Ok(500));
769 let doc = CacheEntry::parse(&bytes).unwrap();
771 assert_eq!(record_canonical_bytes(&doc).unwrap(), bytes);
772 assert!(doc.intact());
773 }
774
775 #[test]
778 fn every_failed_check_is_a_miss_with_its_diagnostic() {
779 let k = key(b"tenant-a");
780 let bytes = entry(&k);
781 let miss = |r: Result<u64, EntryMiss>| r.unwrap_err();
782
783 let other = String::from_utf8(bytes.clone())
785 .unwrap()
786 .replace(schema::CACHE_ENTRY, "rustev.cache-entry/9");
787 let m = miss(read(other.as_bytes(), &k, GENS, 1_200));
788 assert_eq!(m, EntryMiss::SchemaUnknown);
789 assert!(!m.deletes());
790 assert_eq!(m.diagnostic(), "entry_schema_unknown");
791 assert_eq!(
792 miss(read(br#"{"version":1}"#, &k, GENS, 1_200)),
793 EntryMiss::SchemaUnknown
794 );
795 assert_eq!(
796 miss(read(b"\x00garbage", &k, GENS, 1_200)),
797 EntryMiss::Corrupt
798 );
799
800 let altered = String::from_utf8(bytes.clone())
802 .unwrap()
803 .replace("billing", "account");
804 let m = miss(read(altered.as_bytes(), &k, GENS, 1_200));
805 assert_eq!(m, EntryMiss::Corrupt);
806 assert!(m.deletes());
807 assert_eq!(m.diagnostic(), "entry_corrupt");
808
809 let mut forged = k.clone();
812 forged.material.push(0);
813 let m = miss(read(&bytes, &forged, GENS, 1_200));
814 assert_eq!(m, EntryMiss::Collision);
815 assert_eq!(m.diagnostic(), "entry_collision");
816 assert!(!m.deletes());
817 assert_eq!(
818 miss(read(&bytes, &key(b"tenant-b"), GENS, 1_200)),
819 EntryMiss::Collision
820 );
821 let mut misnamed = k.clone();
824 misnamed.id = format!("sha256:{}", "0".repeat(64));
825 let m = miss(read(&bytes, &misnamed, GENS, 1_200));
826 assert_eq!(m, EntryMiss::Corrupt);
827 assert!(m.deletes());
828
829 for gens in [
831 EntryGenerations {
832 namespace: 4,
833 ..GENS
834 },
835 EntryGenerations { scope: 8, ..GENS },
836 ] {
837 let m = miss(read(&bytes, &k, gens, 1_200));
838 assert_eq!(m, EntryMiss::GenerationAdvanced);
839 assert_eq!(m.diagnostic(), "generation_advanced");
840 assert!(!m.deletes());
841 }
842
843 let m = miss(read(&bytes, &k, GENS, 1_501));
845 assert_eq!(m, EntryMiss::Expired);
846 assert_eq!(m.diagnostic(), "entry_expired");
847 assert!(!m.deletes());
848 let m = miss(read(&bytes, &k, GENS, 999));
849 assert_eq!(m, EntryMiss::FromFuture);
850 assert_eq!(m.diagnostic(), "entry_from_future");
851 assert!(!m.deletes());
852 }
853
854 #[test]
859 fn a_decision_entry_reads_back_and_checks_its_kind() {
860 let decision = key(b"tenant-a");
861 let reused = key(b"tenant-a");
862 let payload = DecisionPayload {
863 requests: vec![ReusedRequestPayload {
864 step: "topic".into(),
865 instance: vec![],
866 key_material: hex(&reused.material),
867 output: RawOutput::Label("billing".into()),
868 source_attempt_id: "att-1".into(),
869 }],
870 volatile: vec![],
871 judgment: ContentDigest::parse(&digest('e')).unwrap(),
872 run_record: ContentDigest::parse(&digest('f')).unwrap(),
873 };
874 let write = |payload: EntryPayload| {
875 write_entry(EntryWrite {
876 key: &decision,
877 policy: &policy(),
878 generations: GENS,
879 created: at(1_000),
880 ttl_ms: 500,
881 payload,
882 evidence: ContentDigest::parse(&digest('d')).unwrap(),
883 })
884 .unwrap()
885 };
886 let bytes = write(EntryPayload::Decision(payload.clone()));
887 assert!(String::from_utf8_lossy(&bytes).contains("\"kind\":\"decision\""));
888 let expected = |kind| Expected {
889 kind,
890 key: &decision,
891 generations: GENS,
892 now: at(1_100),
893 };
894 let served = read_entry(&bytes, expected(EntryKind::Decision)).unwrap();
895 let EntryPayload::Decision(read) = served.payload else {
896 panic!("a decision payload");
897 };
898 assert_eq!(read, payload);
899 let held = decision_entry(&decision, read, served.created).unwrap();
900 assert_eq!(held.created, at(1_000));
901 assert_eq!(held.source_run_record, payload.run_record);
902 let r = &held.requests[&("topic".to_string(), vec![])];
903 assert_eq!(r.key, reused);
904 assert_eq!(r.source_attempt_id, "att-1");
905 assert_eq!(
907 read_entry(&bytes, expected(EntryKind::Request)).unwrap_err(),
908 EntryMiss::Collision
909 );
910 assert_eq!(
911 read_entry(&entry(&decision), expected(EntryKind::Decision)).unwrap_err(),
912 EntryMiss::Collision
913 );
914 let mut doc = CacheEntry::parse(&bytes).unwrap();
916 doc.entry.kind = EntryKind::Request;
917 let mislabelled = CacheEntry::seal(doc.entry)
918 .unwrap()
919 .record_canonical()
920 .unwrap();
921 assert_eq!(
922 read_entry(&mislabelled, expected(EntryKind::Request)).unwrap_err(),
923 EntryMiss::Corrupt
924 );
925 let mut odd = payload.clone();
927 odd.requests[0].key_material.push('0');
928 assert!(decision_entry(&decision, odd, at(1_000)).is_none());
929 let mut upper = payload.clone();
930 upper.requests[0].key_material = upper.requests[0].key_material.to_uppercase();
931 assert!(decision_entry(&decision, upper, at(1_000)).is_none());
932 let mut twice = payload;
933 twice.requests.push(twice.requests[0].clone());
934 assert!(decision_entry(&decision, twice, at(1_000)).is_none());
935 }
936
937 #[test]
938 fn put_answers_name_their_diagnostics() {
939 assert_eq!(PutAnswer::Stored.diagnostic(), None);
940 assert_eq!(
941 PutAnswer::GenerationAdvanced.diagnostic(),
942 Some("generation_advanced")
943 );
944 assert_eq!(PutAnswer::OverBudget.diagnostic(), Some("over_budget"));
945 assert_eq!(PutAnswer::Conflict.diagnostic(), Some("store_conflict"));
946 }
947
948 #[test]
949 fn hex_is_lowercase_and_two_digits_per_byte() {
950 assert_eq!(hex(&[0x00, 0x0f, 0xa0, 0xff]), "000fa0ff");
951 assert_eq!(hex(&[]), "");
952 assert_eq!(unhex("000fa0ff"), Some(vec![0x00, 0x0f, 0xa0, 0xff]));
953 assert_eq!(unhex(""), Some(vec![]));
954 for bad in ["0", "0g", "0F", "+1"] {
955 assert_eq!(unhex(bad), None, "{bad}");
956 }
957 }
958}